python-sql 1.4.3__tar.gz → 1.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. {python-sql-1.4.3 → python_sql-1.5.0}/.gitlab-ci.yml +2 -2
  2. {python-sql-1.4.3 → python_sql-1.5.0}/.hgtags +2 -0
  3. {python-sql-1.4.3 → python_sql-1.5.0}/CHANGELOG +7 -0
  4. {python-sql-1.4.3 → python_sql-1.5.0}/PKG-INFO +1 -1
  5. {python-sql-1.4.3 → python_sql-1.5.0}/python_sql.egg-info/PKG-INFO +1 -1
  6. {python-sql-1.4.3 → python_sql-1.5.0}/python_sql.egg-info/SOURCES.txt +1 -0
  7. {python-sql-1.4.3 → python_sql-1.5.0}/sql/__init__.py +473 -12
  8. {python-sql-1.4.3 → python_sql-1.5.0}/sql/functions.py +5 -2
  9. {python-sql-1.4.3 → python_sql-1.5.0}/sql/operators.py +2 -2
  10. python_sql-1.5.0/sql/tests/test_insert.py +198 -0
  11. python_sql-1.5.0/sql/tests/test_merge.py +111 -0
  12. {python-sql-1.4.3 → python_sql-1.5.0}/sql/tests/test_operators.py +12 -12
  13. {python-sql-1.4.3 → python_sql-1.5.0}/sql/tests/test_select.py +47 -1
  14. {python-sql-1.4.3 → python_sql-1.5.0}/tox.ini +2 -2
  15. python-sql-1.4.3/sql/tests/test_insert.py +0 -105
  16. {python-sql-1.4.3 → python_sql-1.5.0}/.flake8 +0 -0
  17. {python-sql-1.4.3 → python_sql-1.5.0}/.isort.cfg +0 -0
  18. {python-sql-1.4.3 → python_sql-1.5.0}/COPYRIGHT +0 -0
  19. {python-sql-1.4.3 → python_sql-1.5.0}/MANIFEST.in +0 -0
  20. {python-sql-1.4.3 → python_sql-1.5.0}/README.rst +0 -0
  21. {python-sql-1.4.3 → python_sql-1.5.0}/python_sql.egg-info/dependency_links.txt +0 -0
  22. {python-sql-1.4.3 → python_sql-1.5.0}/python_sql.egg-info/top_level.txt +0 -0
  23. {python-sql-1.4.3 → python_sql-1.5.0}/setup.cfg +0 -0
  24. {python-sql-1.4.3 → python_sql-1.5.0}/setup.py +0 -0
  25. {python-sql-1.4.3 → python_sql-1.5.0}/sql/aggregate.py +0 -0
  26. {python-sql-1.4.3 → python_sql-1.5.0}/sql/conditionals.py +0 -0
  27. {python-sql-1.4.3 → python_sql-1.5.0}/sql/tests/__init__.py +0 -0
  28. {python-sql-1.4.3 → python_sql-1.5.0}/sql/tests/test_aggregate.py +0 -0
  29. {python-sql-1.4.3 → python_sql-1.5.0}/sql/tests/test_alias.py +0 -0
  30. {python-sql-1.4.3 → python_sql-1.5.0}/sql/tests/test_as.py +0 -0
  31. {python-sql-1.4.3 → python_sql-1.5.0}/sql/tests/test_cast.py +0 -0
  32. {python-sql-1.4.3 → python_sql-1.5.0}/sql/tests/test_collate.py +0 -0
  33. {python-sql-1.4.3 → python_sql-1.5.0}/sql/tests/test_column.py +0 -0
  34. {python-sql-1.4.3 → python_sql-1.5.0}/sql/tests/test_combining_query.py +0 -0
  35. {python-sql-1.4.3 → python_sql-1.5.0}/sql/tests/test_conditionals.py +0 -0
  36. {python-sql-1.4.3 → python_sql-1.5.0}/sql/tests/test_delete.py +0 -0
  37. {python-sql-1.4.3 → python_sql-1.5.0}/sql/tests/test_for.py +0 -0
  38. {python-sql-1.4.3 → python_sql-1.5.0}/sql/tests/test_functions.py +0 -0
  39. {python-sql-1.4.3 → python_sql-1.5.0}/sql/tests/test_join.py +0 -0
  40. {python-sql-1.4.3 → python_sql-1.5.0}/sql/tests/test_lateral.py +0 -0
  41. {python-sql-1.4.3 → python_sql-1.5.0}/sql/tests/test_literal.py +0 -0
  42. {python-sql-1.4.3 → python_sql-1.5.0}/sql/tests/test_order.py +0 -0
  43. {python-sql-1.4.3 → python_sql-1.5.0}/sql/tests/test_table.py +0 -0
  44. {python-sql-1.4.3 → python_sql-1.5.0}/sql/tests/test_update.py +0 -0
  45. {python-sql-1.4.3 → python_sql-1.5.0}/sql/tests/test_values.py +0 -0
  46. {python-sql-1.4.3 → python_sql-1.5.0}/sql/tests/test_window.py +0 -0
  47. {python-sql-1.4.3 → python_sql-1.5.0}/sql/tests/test_with.py +0 -0
@@ -54,7 +54,7 @@ test-tox-python:
54
54
  extends: .test-tox
55
55
  image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/python:${PYTHON_VERSION}
56
56
  script:
57
- - tox -e "py${PYTHON_VERSION/./}" -vv -- -v --output-file junit.xml
57
+ - tox -e "py${PYTHON_VERSION/./}" -- -v --output-file junit.xml
58
58
  parallel:
59
59
  matrix:
60
60
  - PYTHON_VERSION: ["3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
@@ -63,4 +63,4 @@ test-tox-pypy:
63
63
  extends: .test-tox
64
64
  image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/pypy:3
65
65
  script:
66
- - tox -e pypy3 -vv -- -v --output-file junit.xml
66
+ - tox -e pypy3 -- -v --output-file junit.xml
@@ -16,3 +16,5 @@ edc03ee84f0ac96d403d8f984d59fffa3274cd2f 1.3.0
16
16
  a317c40a4d60089ba9e465fbd64b78df24f9e890 1.4.0
17
17
  e71bbae3398cb6a0e72f97a0cada9fcdee2bddea 1.4.1
18
18
  fcb64787b51db2068061eb4aa13825abc1134916 1.4.2
19
+ 111e3e86865360f83a65c04fa48c55f3d2957ee3 1.4.3
20
+ 6f9066b83fe3a8c4699a8555ad1bc406f18974ff 1.5.0
@@ -1,3 +1,10 @@
1
+ Version 1.5.0 - 2024-05-13
2
+ * Skip alias on INSERT without ON CONFLICT or RETURNING
3
+ * Add MERGE
4
+ * Support UPSERT
5
+ * Remove default escape char on LIKE and ILIKE
6
+ * Add GROUPING SETS, CUBE, and ROLLUP
7
+
1
8
  Version 1.4.3 - 2023-12-30
2
9
  * Render common table expression in combining query
3
10
  * Add support for Python 3.12
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-sql
3
- Version: 1.4.3
3
+ Version: 1.5.0
4
4
  Summary: Library to write SQL queries
5
5
  Home-page: https://pypi.org/project/python-sql/
6
6
  Download-URL: https://downloads.tryton.org/python-sql/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-sql
3
- Version: 1.4.3
3
+ Version: 1.5.0
4
4
  Summary: Library to write SQL queries
5
5
  Home-page: https://pypi.org/project/python-sql/
6
6
  Download-URL: https://downloads.tryton.org/python-sql/
@@ -33,6 +33,7 @@ sql/tests/test_insert.py
33
33
  sql/tests/test_join.py
34
34
  sql/tests/test_lateral.py
35
35
  sql/tests/test_literal.py
36
+ sql/tests/test_merge.py
36
37
  sql/tests/test_operators.py
37
38
  sql/tests/test_order.py
38
39
  sql/tests/test_select.py
@@ -7,9 +7,13 @@ from collections import defaultdict
7
7
  from itertools import chain
8
8
  from threading import current_thread, local
9
9
 
10
- __version__ = '1.4.3'
11
- __all__ = ['Flavor', 'Table', 'Values', 'Literal', 'Column', 'Join',
12
- 'Asc', 'Desc', 'NullsFirst', 'NullsLast', 'format2numeric']
10
+ __version__ = '1.5.0'
11
+ __all__ = [
12
+ 'Flavor', 'Table', 'Values', 'Literal', 'Column', 'Grouping', 'Conflict',
13
+ 'Matched', 'MatchedUpdate', 'MatchedDelete',
14
+ 'NotMatched', 'NotMatchedInsert',
15
+ 'Rollup', 'Cube', 'Excluded', 'Join', 'Asc', 'Desc', 'NullsFirst',
16
+ 'NullsLast', 'format2numeric']
13
17
 
14
18
 
15
19
  def _escape_identifier(name):
@@ -663,17 +667,20 @@ class Select(FromItem, SelectQuery):
663
667
 
664
668
 
665
669
  class Insert(WithQuery):
666
- __slots__ = ('_table', '_columns', '_values', '_returning')
670
+ __slots__ = ('_table', '_columns', '_values', '_on_conflict', '_returning')
667
671
 
668
- def __init__(self, table, columns=None, values=None, returning=None,
669
- **kwargs):
672
+ def __init__(
673
+ self, table, columns=None, values=None, returning=None,
674
+ on_conflict=None, **kwargs):
670
675
  self._table = None
671
676
  self._columns = None
672
677
  self._values = None
678
+ self._on_conflict = None
673
679
  self._returning = None
674
680
  self.table = table
675
681
  self.columns = columns
676
682
  self.values = values
683
+ self.on_conflict = on_conflict
677
684
  self.returning = returning
678
685
  super(Insert, self).__init__(**kwargs)
679
686
 
@@ -709,6 +716,17 @@ class Insert(WithQuery):
709
716
  value = Values(value)
710
717
  self._values = value
711
718
 
719
+ @property
720
+ def on_conflict(self):
721
+ return self._on_conflict
722
+
723
+ @on_conflict.setter
724
+ def on_conflict(self, value):
725
+ if value is not None:
726
+ assert isinstance(value, Conflict)
727
+ assert value.table == self.table
728
+ self._on_conflict = value
729
+
712
730
  @property
713
731
  def returning(self):
714
732
  return self._returning
@@ -743,13 +761,20 @@ class Insert(WithQuery):
743
761
  # TODO manage DEFAULT
744
762
  elif self.values is None:
745
763
  values = ' DEFAULT VALUES'
764
+ on_conflict = ''
765
+ if self.on_conflict:
766
+ on_conflict = ' %s' % self.on_conflict
746
767
  returning = ''
747
768
  if self.returning:
748
769
  returning = ' RETURNING ' + ', '.join(
749
770
  map(self._format, self.returning))
771
+ if on_conflict or returning:
772
+ table = '%s AS "%s"' % (self.table, self.table.alias)
773
+ else:
774
+ table = str(self.table)
750
775
  return (self._with_str()
751
- + 'INSERT INTO %s AS "%s"' % (self.table, self.table.alias)
752
- + columns + values + returning)
776
+ + 'INSERT INTO %s' % table
777
+ + columns + values + on_conflict + returning)
753
778
 
754
779
  @property
755
780
  def params(self):
@@ -757,12 +782,149 @@ class Insert(WithQuery):
757
782
  p.extend(self._with_params())
758
783
  if isinstance(self.values, Query):
759
784
  p.extend(self.values.params)
785
+ if self.on_conflict:
786
+ p.extend(self.on_conflict.params)
760
787
  if self.returning:
761
788
  for exp in self.returning:
762
789
  p.extend(exp.params)
763
790
  return tuple(p)
764
791
 
765
792
 
793
+ class Conflict(object):
794
+ __slots__ = (
795
+ '_table', '_indexed_columns', '_index_where', '_columns', '_values',
796
+ '_where')
797
+
798
+ def __init__(
799
+ self, table, indexed_columns=None, index_where=None,
800
+ columns=None, values=None, where=None):
801
+ self._table = None
802
+ self._indexed_columns = None
803
+ self._index_where = None
804
+ self._columns = None
805
+ self._values = None
806
+ self._where = None
807
+ self.table = table
808
+ self.indexed_columns = indexed_columns
809
+ self.index_where = index_where
810
+ self.columns = columns
811
+ self.values = values
812
+ self.where = where
813
+
814
+ @property
815
+ def table(self):
816
+ return self._table
817
+
818
+ @table.setter
819
+ def table(self, value):
820
+ assert isinstance(value, Table)
821
+ self._table = value
822
+
823
+ @property
824
+ def indexed_columns(self):
825
+ return self._indexed_columns
826
+
827
+ @indexed_columns.setter
828
+ def indexed_columns(self, value):
829
+ if value is not None:
830
+ assert all(isinstance(col, Column) for col in value)
831
+ assert all(col.table == self.table for col in value)
832
+ self._indexed_columns = value
833
+
834
+ @property
835
+ def index_where(self):
836
+ return self._index_where
837
+
838
+ @index_where.setter
839
+ def index_where(self, value):
840
+ from sql.operators import And, Or
841
+ if value is not None:
842
+ assert isinstance(value, (Expression, And, Or))
843
+ self._index_where = value
844
+
845
+ @property
846
+ def columns(self):
847
+ return self._columns
848
+
849
+ @columns.setter
850
+ def columns(self, value):
851
+ if value is not None:
852
+ assert all(isinstance(col, Column) for col in value)
853
+ assert all(col.table == self.table for col in value)
854
+ self._columns = value
855
+
856
+ @property
857
+ def values(self):
858
+ return self._values
859
+
860
+ @values.setter
861
+ def values(self, value):
862
+ if value is not None:
863
+ assert isinstance(value, (list, Select))
864
+ if isinstance(value, list):
865
+ value = Values([value])
866
+ self._values = value
867
+
868
+ @property
869
+ def where(self):
870
+ return self._where
871
+
872
+ @where.setter
873
+ def where(self, value):
874
+ from sql.operators import And, Or
875
+ if value is not None:
876
+ assert isinstance(value, (Expression, And, Or))
877
+ self._where = value
878
+
879
+ def __str__(self):
880
+ indexed_columns = ''
881
+ if self.indexed_columns:
882
+ assert all(c.table == self.table for c in self.indexed_columns)
883
+ # Get columns without alias
884
+ indexed_columns = ', '.join(
885
+ c.column_name for c in self.indexed_columns)
886
+ indexed_columns = ' (' + indexed_columns + ')'
887
+ if self.index_where:
888
+ indexed_columns += ' WHERE ' + str(self.index_where)
889
+ else:
890
+ assert not self.index_where
891
+ do = ''
892
+ if not self.columns:
893
+ assert not self.values
894
+ assert not self.where
895
+ do = 'NOTHING'
896
+ else:
897
+ assert all(c.table == self.table for c in self.columns)
898
+ # Get columns without alias
899
+ do = ', '.join(c.column_name for c in self.columns)
900
+ # TODO manage DEFAULT
901
+ values = str(self.values)
902
+ if values.startswith('VALUES'):
903
+ values = values[len('VALUES'):]
904
+ else:
905
+ values = ' (' + values + ')'
906
+ if len(self.columns) == 1:
907
+ # PostgreSQL would require ROW expression
908
+ # with single column with parenthesis
909
+ do = 'UPDATE SET ' + do + ' =' + values
910
+ else:
911
+ do = 'UPDATE SET (' + do + ') =' + values
912
+ if self.where:
913
+ do += ' WHERE %s' % self.where
914
+ return 'ON CONFLICT' + indexed_columns + ' DO ' + do
915
+
916
+ @property
917
+ def params(self):
918
+ p = []
919
+ if self.index_where:
920
+ p.extend(self.index_where.params)
921
+ if self.values:
922
+ p.extend(self.values.params)
923
+ if self.where:
924
+ p.extend(self.where.params)
925
+ return p
926
+
927
+
766
928
  class Update(Insert):
767
929
  __slots__ = ('_where', '_values', 'from_')
768
930
 
@@ -919,6 +1081,207 @@ class Delete(WithQuery):
919
1081
  return tuple(p)
920
1082
 
921
1083
 
1084
+ class Merge(WithQuery):
1085
+ __slots__ = ('_target', '_source', '_condition', '_whens')
1086
+
1087
+ def __init__(self, target, source, condition, *whens, **kwargs):
1088
+ self._target = None
1089
+ self._source = None
1090
+ self._condition = None
1091
+ self._whens = None
1092
+ self.target = target
1093
+ self.source = source
1094
+ self.condition = condition
1095
+ self.whens = whens
1096
+ super().__init__(**kwargs)
1097
+
1098
+ @property
1099
+ def target(self):
1100
+ return self._target
1101
+
1102
+ @target.setter
1103
+ def target(self, value):
1104
+ assert isinstance(value, Table)
1105
+ self._target = value
1106
+
1107
+ @property
1108
+ def source(self):
1109
+ return self._source
1110
+
1111
+ @source.setter
1112
+ def source(self, value):
1113
+ assert isinstance(value, (Table, SelectQuery, Values))
1114
+ self._source = value
1115
+
1116
+ @property
1117
+ def condition(self):
1118
+ return self._condition
1119
+
1120
+ @condition.setter
1121
+ def condition(self, value):
1122
+ assert isinstance(value, Expression)
1123
+ self._condition = value
1124
+
1125
+ @property
1126
+ def whens(self):
1127
+ return self._whens
1128
+
1129
+ @whens.setter
1130
+ def whens(self, value):
1131
+ assert all(isinstance(w, Matched) for w in value)
1132
+ self._whens = tuple(value)
1133
+
1134
+ def __str__(self):
1135
+ with AliasManager():
1136
+ if isinstance(self.source, (Select, Values)):
1137
+ source = '(%s)' % self.source
1138
+ else:
1139
+ source = self.source
1140
+ if self.condition:
1141
+ condition = 'ON %s' % self.condition
1142
+ else:
1143
+ condition = ''
1144
+ return (self._with_str()
1145
+ + 'MERGE INTO %s AS "%s" ' % (self.target, self.target.alias)
1146
+ + 'USING %s AS "%s" ' % (source, self.source.alias)
1147
+ + condition + ' ' + ' '.join(map(str, self.whens)))
1148
+
1149
+ @property
1150
+ def params(self):
1151
+ p = []
1152
+ p.extend(self._with_params())
1153
+ if isinstance(self.source, (SelectQuery, Values)):
1154
+ p.extend(self.source.params)
1155
+ if self.condition:
1156
+ p.extend(self.condition.params)
1157
+ for match in self.whens:
1158
+ p.extend(match.params)
1159
+ return tuple(p)
1160
+
1161
+
1162
+ class Matched(object):
1163
+ __slots__ = ('_condition',)
1164
+ _when = 'MATCHED'
1165
+
1166
+ def __init__(self, condition=None):
1167
+ self._condition = None
1168
+ self.condition = condition
1169
+
1170
+ @property
1171
+ def condition(self):
1172
+ return self._condition
1173
+
1174
+ @condition.setter
1175
+ def condition(self, value):
1176
+ if value is not None:
1177
+ assert isinstance(value, Expression)
1178
+ self._condition = value
1179
+
1180
+ def _then_str(self):
1181
+ return 'DO NOTHING'
1182
+
1183
+ def __str__(self):
1184
+ if self.condition is not None:
1185
+ condition = ' AND ' + str(self.condition)
1186
+ else:
1187
+ condition = ''
1188
+ return 'WHEN ' + self._when + condition + ' THEN ' + self._then_str()
1189
+
1190
+ @property
1191
+ def params(self):
1192
+ p = []
1193
+ if self.condition:
1194
+ p.extend(self.condition.params)
1195
+ return tuple(p)
1196
+
1197
+
1198
+ class _MatchedValues(Matched):
1199
+ __slots__ = ('_columns', '_values')
1200
+
1201
+ def __init__(self, columns, values, **kwargs):
1202
+ self._columns = columns
1203
+ self._values = values
1204
+ self.columns = columns
1205
+ self.values = values
1206
+ super().__init__(**kwargs)
1207
+
1208
+ @property
1209
+ def columns(self):
1210
+ return self._columns
1211
+
1212
+ @columns.setter
1213
+ def columns(self, value):
1214
+ assert all(isinstance(col, Column) for col in value)
1215
+ self._columns = value
1216
+
1217
+
1218
+ class MatchedUpdate(_MatchedValues, Matched):
1219
+ __slots__ = ()
1220
+
1221
+ @property
1222
+ def values(self):
1223
+ return self._values
1224
+
1225
+ @values.setter
1226
+ def values(self, value):
1227
+ self._values = value
1228
+
1229
+ def _then_str(self):
1230
+ columns = [c.column_name for c in self.columns]
1231
+ return 'UPDATE SET ' + ', '.join(
1232
+ '%s = %s' % (c, Update._format(v))
1233
+ for c, v in zip(columns, self.values))
1234
+
1235
+ @property
1236
+ def params(self):
1237
+ p = list(super().params)
1238
+ for value in self.values:
1239
+ if isinstance(value, (Expression, Select)):
1240
+ p.extend(value.params)
1241
+ else:
1242
+ p.append(value)
1243
+ return tuple(p)
1244
+
1245
+
1246
+ class MatchedDelete(Matched):
1247
+ __slots__ = ()
1248
+
1249
+ def _then_str(self):
1250
+ return 'DELETE'
1251
+
1252
+
1253
+ class NotMatched(Matched):
1254
+ __slots__ = ()
1255
+ _when = 'NOT MATCHED'
1256
+
1257
+
1258
+ class NotMatchedInsert(_MatchedValues, NotMatched):
1259
+ __slots__ = ()
1260
+
1261
+ @property
1262
+ def values(self):
1263
+ return self._values
1264
+
1265
+ @values.setter
1266
+ def values(self, value):
1267
+ self._values = Values([value])
1268
+
1269
+ def _then_str(self):
1270
+ columns = ', '.join(c.column_name for c in self.columns)
1271
+ columns = '(' + columns + ')'
1272
+ if self.values is None:
1273
+ values = ' DEFAULT VALUES '
1274
+ else:
1275
+ values = ' ' + str(self.values)
1276
+ return 'INSERT ' + columns + values
1277
+
1278
+ @property
1279
+ def params(self):
1280
+ p = list(super().params)
1281
+ p.extend(self.values.params)
1282
+ return tuple(p)
1283
+
1284
+
922
1285
  class CombiningQuery(FromItem, SelectQuery):
923
1286
  __slots__ = ('queries', 'all_')
924
1287
  _operator = ''
@@ -989,9 +1352,11 @@ class Table(FromItem):
989
1352
  def params(self):
990
1353
  return ()
991
1354
 
992
- def insert(self, columns=None, values=None, returning=None, with_=None):
1355
+ def insert(
1356
+ self, columns=None, values=None, returning=None, with_=None,
1357
+ on_conflict=None):
993
1358
  return Insert(self, columns=columns, values=values,
994
- returning=returning, with_=with_)
1359
+ on_conflict=on_conflict, returning=returning, with_=with_)
995
1360
 
996
1361
  def update(self, columns, values, from_=None, where=None, returning=None,
997
1362
  with_=None):
@@ -1003,6 +1368,25 @@ class Table(FromItem):
1003
1368
  return Delete(self, only=only, using=using, where=where,
1004
1369
  returning=returning, with_=with_)
1005
1370
 
1371
+ def merge(self, source, condition, *whens, with_=None):
1372
+ return Merge(self, source, condition, *whens, with_=with_)
1373
+
1374
+
1375
+ class _Excluded(Table):
1376
+ def __init__(self):
1377
+ super().__init__('EXCLUDED')
1378
+
1379
+ @property
1380
+ def alias(self):
1381
+ return 'EXCLUDED'
1382
+
1383
+ @property
1384
+ def has_alias(self):
1385
+ return False
1386
+
1387
+
1388
+ Excluded = _Excluded()
1389
+
1006
1390
 
1007
1391
  class Join(FromItem):
1008
1392
  __slots__ = ('_left', '_right', '_condition', '_type_')
@@ -1070,10 +1454,14 @@ class Join(FromItem):
1070
1454
  def params(self):
1071
1455
  p = []
1072
1456
  for item in (self.left, self.right):
1073
- if hasattr(item, 'params'):
1457
+ try:
1074
1458
  p.extend(item.params)
1075
- if hasattr(self.condition, 'params'):
1459
+ except AttributeError:
1460
+ pass
1461
+ try:
1076
1462
  p.extend(self.condition.params)
1463
+ except AttributeError:
1464
+ pass
1077
1465
  return tuple(p)
1078
1466
 
1079
1467
  @property
@@ -1442,6 +1830,79 @@ class Collate(Expression):
1442
1830
  return (self.expression,)
1443
1831
 
1444
1832
 
1833
+ class Grouping(Expression):
1834
+ __slots__ = ('_sets',)
1835
+
1836
+ def __init__(self, *sets):
1837
+ super().__init__()
1838
+ self.sets = sets
1839
+
1840
+ @property
1841
+ def sets(self):
1842
+ return self._sets
1843
+
1844
+ @sets.setter
1845
+ def sets(self, value):
1846
+ assert all(
1847
+ isinstance(col, Expression) for cols in value for col in cols)
1848
+ self._sets = tuple(tuple(cols) for cols in value)
1849
+
1850
+ def __str__(self):
1851
+ return 'GROUPING SETS (%s)' % (
1852
+ ', '.join(
1853
+ '(%s)' % ', '.join(str(col) for col in cols)
1854
+ for cols in self.sets))
1855
+
1856
+ @property
1857
+ def params(self):
1858
+ return sum((col.params for cols in self.sets for col in cols), ())
1859
+
1860
+
1861
+ class Rollup(Expression):
1862
+ __slots__ = ('_expressions',)
1863
+
1864
+ def __init__(self, *expressions):
1865
+ super().__init__()
1866
+ self.expressions = expressions
1867
+
1868
+ @property
1869
+ def expressions(self):
1870
+ return self._expressions
1871
+
1872
+ @expressions.setter
1873
+ def expressions(self, value):
1874
+ assert all(
1875
+ isinstance(col, Expression)
1876
+ or all(isinstance(c, Expression) for c in col)
1877
+ for col in value)
1878
+ self._expressions = tuple(value)
1879
+
1880
+ def __str__(self):
1881
+ def format(col):
1882
+ if isinstance(col, Expression):
1883
+ return str(col)
1884
+ else:
1885
+ return '(%s)' % ', '.join(str(c) for c in col)
1886
+ return '%s (%s)' % (
1887
+ self.__class__.__name__.upper(),
1888
+ ', '.join(format(col) for col in self.expressions))
1889
+
1890
+ @property
1891
+ def params(self):
1892
+ p = []
1893
+ for col in self.expressions:
1894
+ if isinstance(col, Expression):
1895
+ p.extend(col.params)
1896
+ else:
1897
+ for c in col:
1898
+ p.extend(c.params)
1899
+ return tuple(p)
1900
+
1901
+
1902
+ class Cube(Rollup):
1903
+ pass
1904
+
1905
+
1445
1906
  class Window(object):
1446
1907
  __slots__ = (
1447
1908
  '_partition', '_order_by', '_frame', '_start', '_end', '_exclude')
@@ -315,8 +315,11 @@ class Trim(Function):
315
315
  for arg in (self.characters, self.string):
316
316
  if isinstance(arg, str):
317
317
  p.append(arg)
318
- elif hasattr(arg, 'params'):
319
- p.extend(arg.params)
318
+ else:
319
+ try:
320
+ p.extend(arg.params)
321
+ except AttributeError:
322
+ pass
320
323
  return tuple(p)
321
324
 
322
325
 
@@ -377,7 +377,7 @@ class Like(BinaryOperator):
377
377
  __slots__ = 'escape'
378
378
  _operator = 'LIKE'
379
379
 
380
- def __init__(self, left, right, escape='\\'):
380
+ def __init__(self, left, right, escape=None):
381
381
  super().__init__(left, right)
382
382
  assert not escape or len(escape) == 1
383
383
  self.escape = escape
@@ -386,7 +386,7 @@ class Like(BinaryOperator):
386
386
  def params(self):
387
387
  params = super().params
388
388
  if self.escape or Flavor().get().escape_empty:
389
- params += (self.escape,)
389
+ params += (self.escape or '',)
390
390
  return params
391
391
 
392
392
  def __str__(self):