python-sql 1.4.2__py3-none-any.whl → 1.5.0__py3-none-any.whl

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-sql
3
- Version: 1.4.2
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/
@@ -23,6 +23,7 @@ Classifier: Programming Language :: Python :: 3.8
23
23
  Classifier: Programming Language :: Python :: 3.9
24
24
  Classifier: Programming Language :: Python :: 3.10
25
25
  Classifier: Programming Language :: Python :: 3.11
26
+ Classifier: Programming Language :: Python :: 3.12
26
27
  Classifier: Topic :: Database
27
28
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
28
29
  Requires-Python: >=3.5
@@ -1,13 +1,8 @@
1
- sql/__init__.py,sha256=7K5UBv_YAtQJgPsHtA2haUcXXj1ARZ4wkcOnRqhMDfg,47369
1
+ sql/__init__.py,sha256=QVWSm0kbssgK1QXk5m4Zg1ykv8_md_liRNd0QwOM1I4,59923
2
2
  sql/aggregate.py,sha256=Dmog1JIuFZ8fFT8RjeGbWGU7kCdJcMrNlwTSO1HIwc8,5241
3
3
  sql/conditionals.py,sha256=xJY6ffEBeES6CiGKErXSa2dK2FXaEaR_QQl_CKILP30,2541
4
- sql/functions.py,sha256=MEd0EHMAwYlOWbLXSLhN-FHDryzKgWE9kjdF-z6LBUM,12307
5
- sql/operators.py,sha256=xKQF-uzV8lNnQQwabJqXPht3ZTkThOPfZDpq4HGUuAw,10782
6
- sql/__pycache__/__init__.cpython-39.pyc,sha256=BlJWuvgStja33Lcn3WPzflsHCJJ-5Ddja7R5j1fYenY,54412
7
- sql/__pycache__/aggregate.cpython-39.pyc,sha256=6yGP5PBW49MbIQHiKl7qfWdKyG01CL-iJaMHhFddWoY,6155
8
- sql/__pycache__/conditionals.cpython-39.pyc,sha256=wSI0Vk5XDr6gVs2DSoUZX8WT21bNr3DfRy1QV-BtEo0,2961
9
- sql/__pycache__/functions.cpython-39.pyc,sha256=SOd0flReMnTi_2hTrm3nrLXTYD9dBUX4FHkdVqIkgf4,17408
10
- sql/__pycache__/operators.cpython-39.pyc,sha256=LuFoz-JMNGcm0sEh2-zqEuJSatOpbcUKO5ANvWE7ujs,14996
4
+ sql/functions.py,sha256=qdmLZ5eoqIsc7erlPsdByecUey7rexhVdUTSB_RnOZ4,12373
5
+ sql/operators.py,sha256=cuh2pvRDNbWbWwE4su9dwsuDxPlJ_S9gn3U-iVvrbKE,10788
11
6
  sql/tests/__init__.py,sha256=oHBEqcjoFLDwTiSIDwYNLS8Vj1lkq8-O5DsTKCh1BJw,675
12
7
  sql/tests/test_aggregate.py,sha256=7KDN81zxzkVjjoHHLpQMvB-yVqwkKdPoKuppqmWfDYI,2592
13
8
  sql/tests/test_alias.py,sha256=AcMrKVvFPsR84BRGc_dN6t3O5vAqNjBeWicb6I6EuBI,1884
@@ -15,24 +10,25 @@ sql/tests/test_as.py,sha256=OfXDu0CnJ2paRKR29dMdx7q_Q9TFY60MWlvib6MR5LY,926
15
10
  sql/tests/test_cast.py,sha256=ejrqHQZwA3967UhszYR3caZYvH3waPcF6li4rnmxGi4,653
16
11
  sql/tests/test_collate.py,sha256=LWJhwcmVBsbJpEtaBRu-BUkBufXktUgCHS4XR7LbAYA,842
17
12
  sql/tests/test_column.py,sha256=_GkBkBzLW_fDoJy5yUWDtHTSyka1s5Drd4gDuvtIM_8,866
18
- sql/tests/test_combining_query.py,sha256=wZRi1rcWCWfpA6-RJ3zSsOllLmolNcLEHBmW7aHINok,1809
13
+ sql/tests/test_combining_query.py,sha256=SyVHnIUiikGwc7ViAEalCKwgYRRfayNmsBD1NhCWoNM,2277
19
14
  sql/tests/test_conditionals.py,sha256=kNkgsjOatXpJnF9AED03ANNd0zWIwWTrnWdi4PWfl3E,2705
20
15
  sql/tests/test_delete.py,sha256=0xEllZmXVc9YH6lGJI0_DVXCpB1CTc-e62GK0_fnoM4,1591
21
16
  sql/tests/test_for.py,sha256=6e4n7NbKTdlEp_z6MrNT_eM8rwXR94gANtBmWzhxcjk,550
22
17
  sql/tests/test_functions.py,sha256=CM-a0c1ey9uEhw3wFL8PWETB3Kpl5wtkGNLt44r_0EE,5377
23
- sql/tests/test_insert.py,sha256=PbhBySrLog4H0GNG9X5WOPRzeKkbyjXNTE467HhKjqU,3972
18
+ sql/tests/test_insert.py,sha256=1jt2nNrNRdxH7VykkQCwjiVK-LJmmjaGiDaqUJWeh9U,7378
24
19
  sql/tests/test_join.py,sha256=OWFqe2Zbb-uDTzg9b0GcH2p2xzhinuvy6E_ANMCOD-Y,1891
25
20
  sql/tests/test_lateral.py,sha256=f6hlapGKJ-ZWobMPktN4mSNXgfyNr7O5ZwZASb_oo30,1046
26
21
  sql/tests/test_literal.py,sha256=ht-VLhGdpUm_rbtOzpQgbX3YQup_xp7F9va0RSWY0Ts,1010
27
- sql/tests/test_operators.py,sha256=9r9fCmlWjx9kj1KqMwX3JO0kyHRUnv3qTP_haWTXiNQ,15639
22
+ sql/tests/test_merge.py,sha256=CWF7grhpfqlgNUlafDr9Xbx-EY9uGMFbJRPGoLf5pd8,4074
23
+ sql/tests/test_operators.py,sha256=6j6AEK5_xKJq-WC7Q9fXDE5YxDrwW7Z3LhJwkLo1tI8,15542
28
24
  sql/tests/test_order.py,sha256=VXfs5ZMQQiLqrSVFn-EykjJ3cHgJAiStiUELjlbZefY,1996
29
- sql/tests/test_select.py,sha256=5eUqSjP58tr5I2ylMbrYHQgUr1Zw1nvfhDbgdf3xXn4,18053
25
+ sql/tests/test_select.py,sha256=njjy2gyyKXczDHc8XpcaaWGekSEC_iz8hpTNyPaGLsk,19882
30
26
  sql/tests/test_table.py,sha256=ZF6VzLcx54ulj69Qnt0hdQ7_XamAqpofyTKs0WiGB4E,758
31
27
  sql/tests/test_update.py,sha256=ki2O9PkLbAd5kX4TJkmttB7OicSWhlYbQQXKWHOR2gg,3490
32
28
  sql/tests/test_values.py,sha256=qKvaFSDEQmVWajWhMZgqD2-rQcWJeW_nsevVMsV3jD8,1053
33
29
  sql/tests/test_window.py,sha256=jexNAwjbB88hvPOuSN5suVthLwC88LgTO0diICPU8bE,2383
34
30
  sql/tests/test_with.py,sha256=A8jwi8FctjFIKI3ppGv3sO9EV2rmJ9pcP3HFmhx7YiY,2256
35
- python_sql-1.4.2.dist-info/METADATA,sha256=uopRjmLolv5Br3hleDN0dpXf7FRiWIODVb1x2GWGJO0,8463
36
- python_sql-1.4.2.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
37
- python_sql-1.4.2.dist-info/top_level.txt,sha256=hwJHmWfbRP5a_XNfqgHwi2zrjAAzAJk24QZoTocslow,4
38
- python_sql-1.4.2.dist-info/RECORD,,
31
+ python_sql-1.5.0.dist-info/METADATA,sha256=8CwIidhUWbq1rQMBL_B6iqhmilWaAi8hdnxikysosuk,8514
32
+ python_sql-1.5.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
33
+ python_sql-1.5.0.dist-info/top_level.txt,sha256=hwJHmWfbRP5a_XNfqgHwi2zrjAAzAJk24QZoTocslow,4
34
+ python_sql-1.5.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.40.0)
2
+ Generator: bdist_wheel (0.43.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
sql/__init__.py CHANGED
@@ -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.2'
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 = ''
@@ -932,17 +1295,21 @@ class CombiningQuery(FromItem, SelectQuery):
932
1295
  def __str__(self):
933
1296
  with AliasManager():
934
1297
  operator = ' %s %s' % (self._operator, 'ALL ' if self.all_ else '')
935
- return (operator.join(map(str, self.queries)) + self._order_by_str
1298
+ return (
1299
+ self._with_str()
1300
+ + operator.join(map(str, self.queries)) + self._order_by_str
936
1301
  + self._limit_offset_str)
937
1302
 
938
1303
  @property
939
1304
  def params(self):
940
1305
  p = []
941
- for q in self.queries:
942
- p.extend(q.params)
943
- if self.order_by:
944
- for expression in self.order_by:
945
- p.extend(expression.params)
1306
+ with AliasManager():
1307
+ p.extend(self._with_params())
1308
+ for q in self.queries:
1309
+ p.extend(q.params)
1310
+ if self.order_by:
1311
+ for expression in self.order_by:
1312
+ p.extend(expression.params)
946
1313
  return tuple(p)
947
1314
 
948
1315
 
@@ -985,9 +1352,11 @@ class Table(FromItem):
985
1352
  def params(self):
986
1353
  return ()
987
1354
 
988
- 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):
989
1358
  return Insert(self, columns=columns, values=values,
990
- returning=returning, with_=with_)
1359
+ on_conflict=on_conflict, returning=returning, with_=with_)
991
1360
 
992
1361
  def update(self, columns, values, from_=None, where=None, returning=None,
993
1362
  with_=None):
@@ -999,6 +1368,25 @@ class Table(FromItem):
999
1368
  return Delete(self, only=only, using=using, where=where,
1000
1369
  returning=returning, with_=with_)
1001
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
+
1002
1390
 
1003
1391
  class Join(FromItem):
1004
1392
  __slots__ = ('_left', '_right', '_condition', '_type_')
@@ -1066,10 +1454,14 @@ class Join(FromItem):
1066
1454
  def params(self):
1067
1455
  p = []
1068
1456
  for item in (self.left, self.right):
1069
- if hasattr(item, 'params'):
1457
+ try:
1070
1458
  p.extend(item.params)
1071
- if hasattr(self.condition, 'params'):
1459
+ except AttributeError:
1460
+ pass
1461
+ try:
1072
1462
  p.extend(self.condition.params)
1463
+ except AttributeError:
1464
+ pass
1073
1465
  return tuple(p)
1074
1466
 
1075
1467
  @property
@@ -1438,6 +1830,79 @@ class Collate(Expression):
1438
1830
  return (self.expression,)
1439
1831
 
1440
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
+
1441
1906
  class Window(object):
1442
1907
  __slots__ = (
1443
1908
  '_partition', '_order_by', '_frame', '_start', '_end', '_exclude')
sql/functions.py CHANGED
@@ -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
 
sql/operators.py CHANGED
@@ -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):
@@ -2,7 +2,7 @@
2
2
  # this repository contains the full copyright notices and license terms.
3
3
  import unittest
4
4
 
5
- from sql import Table, Union
5
+ from sql import Table, Union, With
6
6
 
7
7
 
8
8
  class TestUnion(unittest.TestCase):
@@ -21,6 +21,18 @@ class TestUnion(unittest.TestCase):
21
21
  'SELECT * FROM "t1" AS "a" UNION SELECT * FROM "t2" AS "b"')
22
22
  self.assertEqual(tuple(query.params), ())
23
23
 
24
+ def test_union_with(self):
25
+ table = Table('t')
26
+ with_ = With()
27
+ with_.query = table.select(table.id, where=table.id == 1)
28
+ query = Union(self.q1, self.q2, with_=with_)
29
+
30
+ self.assertEqual(str(query),
31
+ 'WITH "a" AS ('
32
+ 'SELECT "b"."id" FROM "t" AS "b" WHERE ("b"."id" = %s)) '
33
+ 'SELECT * FROM "t1" AS "c" UNION SELECT * FROM "t2" AS "d"')
34
+ self.assertEqual(tuple(query.params), (1,))
35
+
24
36
  def test_union3(self):
25
37
  query = Union(self.q1, self.q2, self.q3)
26
38
  self.assertEqual(str(query),
sql/tests/test_insert.py CHANGED
@@ -2,7 +2,7 @@
2
2
  # this repository contains the full copyright notices and license terms.
3
3
  import unittest
4
4
 
5
- from sql import Table, With
5
+ from sql import Conflict, Excluded, Table, With
6
6
  from sql.functions import Abs
7
7
 
8
8
 
@@ -11,21 +11,21 @@ class TestInsert(unittest.TestCase):
11
11
 
12
12
  def test_insert_default(self):
13
13
  query = self.table.insert()
14
- self.assertEqual(str(query), 'INSERT INTO "t" AS "a" DEFAULT VALUES')
14
+ self.assertEqual(str(query), 'INSERT INTO "t" DEFAULT VALUES')
15
15
  self.assertEqual(tuple(query.params), ())
16
16
 
17
17
  def test_insert_values(self):
18
18
  query = self.table.insert([self.table.c1, self.table.c2],
19
19
  [['foo', 'bar']])
20
20
  self.assertEqual(str(query),
21
- 'INSERT INTO "t" AS "a" ("c1", "c2") VALUES (%s, %s)')
21
+ 'INSERT INTO "t" ("c1", "c2") VALUES (%s, %s)')
22
22
  self.assertEqual(tuple(query.params), ('foo', 'bar'))
23
23
 
24
24
  def test_insert_many_values(self):
25
25
  query = self.table.insert([self.table.c1, self.table.c2],
26
26
  [['foo', 'bar'], ['spam', 'eggs']])
27
27
  self.assertEqual(str(query),
28
- 'INSERT INTO "t" AS "a" ("c1", "c2") VALUES (%s, %s), (%s, %s)')
28
+ 'INSERT INTO "t" ("c1", "c2") VALUES (%s, %s), (%s, %s)')
29
29
  self.assertEqual(tuple(query.params), ('foo', 'bar', 'spam', 'eggs'))
30
30
 
31
31
  def test_insert_subselect(self):
@@ -34,14 +34,14 @@ class TestInsert(unittest.TestCase):
34
34
  subquery = t2.select(t2.c1, t2.c2)
35
35
  query = t1.insert([t1.c1, t1.c2], subquery)
36
36
  self.assertEqual(str(query),
37
- 'INSERT INTO "t1" AS "b" ("c1", "c2") '
37
+ 'INSERT INTO "t1" ("c1", "c2") '
38
38
  'SELECT "a"."c1", "a"."c2" FROM "t2" AS "a"')
39
39
  self.assertEqual(tuple(query.params), ())
40
40
 
41
41
  def test_insert_function(self):
42
42
  query = self.table.insert([self.table.c], [[Abs(-1)]])
43
43
  self.assertEqual(str(query),
44
- 'INSERT INTO "t" AS "a" ("c") VALUES (ABS(%s))')
44
+ 'INSERT INTO "t" ("c") VALUES (ABS(%s))')
45
45
  self.assertEqual(tuple(query.params), (-1,))
46
46
 
47
47
  def test_insert_returning(self):
@@ -74,7 +74,7 @@ class TestInsert(unittest.TestCase):
74
74
  values=w.select())
75
75
  self.assertEqual(str(query),
76
76
  'WITH "a" AS (SELECT * FROM "t1" AS "b") '
77
- 'INSERT INTO "t" AS "c" ("c1") SELECT * FROM "a" AS "a"')
77
+ 'INSERT INTO "t" ("c1") SELECT * FROM "a" AS "a"')
78
78
  self.assertEqual(tuple(query.params), ())
79
79
 
80
80
  def test_insert_in_with(self):
@@ -101,5 +101,98 @@ class TestInsert(unittest.TestCase):
101
101
  query = t1.insert([t1.c1], [['foo']])
102
102
 
103
103
  self.assertEqual(str(query),
104
- 'INSERT INTO "default"."t1" AS "a" ("c1") VALUES (%s)')
104
+ 'INSERT INTO "default"."t1" ("c1") VALUES (%s)')
105
105
  self.assertEqual(tuple(query.params), ('foo',))
106
+
107
+ def test_upsert_nothing(self):
108
+ query = self.table.insert(
109
+ [self.table.c1], [['foo']],
110
+ on_conflict=Conflict(self.table))
111
+
112
+ self.assertEqual(str(query),
113
+ 'INSERT INTO "t" AS "a" ("c1") VALUES (%s) '
114
+ 'ON CONFLICT DO NOTHING')
115
+ self.assertEqual(tuple(query.params), ('foo',))
116
+
117
+ def test_upsert_indexed_column(self):
118
+ query = self.table.insert(
119
+ [self.table.c1], [['foo']],
120
+ on_conflict=Conflict(
121
+ self.table,
122
+ indexed_columns=[self.table.c1, self.table.c2]))
123
+
124
+ self.assertEqual(str(query),
125
+ 'INSERT INTO "t" AS "a" ("c1") VALUES (%s) '
126
+ 'ON CONFLICT ("c1", "c2") DO NOTHING')
127
+ self.assertEqual(tuple(query.params), ('foo',))
128
+
129
+ def test_upsert_indexed_column_index_where(self):
130
+ query = self.table.insert(
131
+ [self.table.c1], [['foo']],
132
+ on_conflict=Conflict(
133
+ self.table,
134
+ indexed_columns=[self.table.c1],
135
+ index_where=self.table.c2 == 'bar'))
136
+
137
+ self.assertEqual(str(query),
138
+ 'INSERT INTO "t" AS "a" ("c1") VALUES (%s) '
139
+ 'ON CONFLICT ("c1") WHERE ("a"."c2" = %s) DO NOTHING')
140
+ self.assertEqual(tuple(query.params), ('foo', 'bar'))
141
+
142
+ def test_upsert_update(self):
143
+ query = self.table.insert(
144
+ [self.table.c1], [['baz']],
145
+ on_conflict=Conflict(
146
+ self.table,
147
+ columns=[self.table.c1, self.table.c2],
148
+ values=['foo', 'bar']))
149
+
150
+ self.assertEqual(str(query),
151
+ 'INSERT INTO "t" AS "a" ("c1") VALUES (%s) '
152
+ 'ON CONFLICT DO UPDATE SET ("c1", "c2") = (%s, %s)')
153
+ self.assertEqual(tuple(query.params), ('baz', 'foo', 'bar'))
154
+
155
+ def test_upsert_update_where(self):
156
+ query = self.table.insert(
157
+ [self.table.c1], [['baz']],
158
+ on_conflict=Conflict(
159
+ self.table,
160
+ columns=[self.table.c1],
161
+ values=['foo'],
162
+ where=self.table.c2 == 'bar'))
163
+
164
+ self.assertEqual(str(query),
165
+ 'INSERT INTO "t" AS "a" ("c1") VALUES (%s) '
166
+ 'ON CONFLICT DO UPDATE SET "c1" = (%s) '
167
+ 'WHERE ("a"."c2" = %s)')
168
+ self.assertEqual(tuple(query.params), ('baz', 'foo', 'bar'))
169
+
170
+ def test_upsert_update_subquery(self):
171
+ t1 = Table('t1')
172
+ t2 = Table('t2')
173
+ subquery = t2.select(t2.c1, t2.c2)
174
+ query = t1.insert(
175
+ [t1.c1], [['baz']],
176
+ on_conflict=Conflict(
177
+ t1,
178
+ columns=[t1.c1, t1.c2],
179
+ values=subquery))
180
+
181
+ self.assertEqual(str(query),
182
+ 'INSERT INTO "t1" AS "b" ("c1") VALUES (%s) '
183
+ 'ON CONFLICT DO UPDATE SET ("c1", "c2") = '
184
+ '(SELECT "a"."c1", "a"."c2" FROM "t2" AS "a")')
185
+ self.assertEqual(tuple(query.params), ('baz',))
186
+
187
+ def test_upsert_update_excluded(self):
188
+ query = self.table.insert(
189
+ [self.table.c1], [[1]],
190
+ on_conflict=Conflict(
191
+ self.table,
192
+ columns=[self.table.c1],
193
+ values=[Excluded.c1 + 2]))
194
+
195
+ self.assertEqual(str(query),
196
+ 'INSERT INTO "t" AS "a" ("c1") VALUES (%s) '
197
+ 'ON CONFLICT DO UPDATE SET "c1" = (("EXCLUDED"."c1" + %s))')
198
+ self.assertEqual(tuple(query.params), (1, 2))
@@ -0,0 +1,111 @@
1
+ # This file is part of python-sql. The COPYRIGHT file at the top level of
2
+ # this repository contains the full copyright notices and license terms.
3
+
4
+ import unittest
5
+
6
+ from sql import (
7
+ Matched, MatchedDelete, MatchedUpdate, NotMatched, NotMatchedInsert, Table,
8
+ With)
9
+
10
+
11
+ class TestMerge(unittest.TestCase):
12
+ target = Table('t')
13
+ source = Table('s')
14
+
15
+ def test_merge(self):
16
+ query = self.target.merge(
17
+ self.source, self.target.c1 == self.source.c2, Matched())
18
+ self.assertEqual(
19
+ str(query),
20
+ 'MERGE INTO "t" AS "a" USING "s" AS "b" '
21
+ 'ON ("a"."c1" = "b"."c2") '
22
+ 'WHEN MATCHED THEN DO NOTHING')
23
+ self.assertEqual(query.params, ())
24
+
25
+ def test_condition(self):
26
+ query = self.target.merge(
27
+ self.source,
28
+ (self.target.c1 == self.source.c2) & (self.target.c3 == 42),
29
+ Matched())
30
+ self.assertEqual(
31
+ str(query),
32
+ 'MERGE INTO "t" AS "a" USING "s" AS "b" '
33
+ 'ON (("a"."c1" = "b"."c2") AND ("a"."c3" = %s)) '
34
+ 'WHEN MATCHED THEN DO NOTHING')
35
+ self.assertEqual(query.params, (42,))
36
+
37
+ def test_matched(self):
38
+ query = self.target.merge(
39
+ self.source, self.target.c1 == self.source.c2,
40
+ Matched((self.source.c3 == 42)
41
+ & (self.target.c4 == self.source.c5)))
42
+ self.assertEqual(
43
+ str(query),
44
+ 'MERGE INTO "t" AS "a" USING "s" AS "b" '
45
+ 'ON ("a"."c1" = "b"."c2") '
46
+ 'WHEN MATCHED '
47
+ 'AND (("b"."c3" = %s) AND ("a"."c4" = "b"."c5")) '
48
+ 'THEN DO NOTHING')
49
+ self.assertEqual(query.params, (42,))
50
+
51
+ def test_matched_update(self):
52
+ query = self.target.merge(
53
+ self.source, self.target.c1 == self.source.c2,
54
+ MatchedUpdate([self.target.c1], [self.target.c1 + self.source.c2]))
55
+ self.assertEqual(
56
+ str(query),
57
+ 'MERGE INTO "t" AS "a" USING "s" AS "b" '
58
+ 'ON ("a"."c1" = "b"."c2") '
59
+ 'WHEN MATCHED THEN '
60
+ 'UPDATE SET "c1" = ("a"."c1" + "b"."c2")')
61
+ self.assertEqual(query.params, ())
62
+
63
+ def test_matched_delete(self):
64
+ query = self.target.merge(
65
+ self.source, self.target.c1 == self.source.c2, MatchedDelete())
66
+ self.assertEqual(
67
+ str(query),
68
+ 'MERGE INTO "t" AS "a" USING "s" AS "b" '
69
+ 'ON ("a"."c1" = "b"."c2") '
70
+ 'WHEN MATCHED THEN DELETE')
71
+ self.assertEqual(query.params, ())
72
+
73
+ def test_not_matched(self):
74
+ query = self.target.merge(
75
+ self.source, self.target.c1 == self.source.c2, NotMatched())
76
+ self.assertEqual(
77
+ str(query),
78
+ 'MERGE INTO "t" AS "a" USING "s" AS "b" '
79
+ 'ON ("a"."c1" = "b"."c2") '
80
+ 'WHEN NOT MATCHED THEN DO NOTHING')
81
+ self.assertEqual(query.params, ())
82
+
83
+ def test_not_matched_insert(self):
84
+ query = self.target.merge(
85
+ self.source, self.target.c1 == self.source.c2,
86
+ NotMatchedInsert(
87
+ [self.target.c1, self.target.c2],
88
+ [self.source.c3, self.source.c4]))
89
+ self.assertEqual(
90
+ str(query),
91
+ 'MERGE INTO "t" AS "a" USING "s" AS "b" '
92
+ 'ON ("a"."c1" = "b"."c2") '
93
+ 'WHEN NOT MATCHED THEN '
94
+ 'INSERT ("c1", "c2") VALUES ("b"."c3", "b"."c4")')
95
+ self.assertEqual(query.params, ())
96
+
97
+ def test_with(self):
98
+ t1 = Table('t1')
99
+ w = With(query=t1.select(where=t1.c2 == 42))
100
+ source = w.select()
101
+
102
+ query = self.target.merge(
103
+ source, self.target.c1 == source.c2, Matched(), with_=[w])
104
+ self.assertEqual(
105
+ str(query),
106
+ 'WITH "a" AS (SELECT * FROM "t1" AS "d" WHERE ("d"."c2" = %s)) '
107
+ 'MERGE INTO "t" AS "b" '
108
+ 'USING (SELECT * FROM "a" AS "a") AS "c" '
109
+ 'ON ("b"."c1" = "c"."c2") '
110
+ 'WHEN MATCHED THEN DO NOTHING')
111
+ self.assertEqual(query.params, (42,))
@@ -287,8 +287,8 @@ class TestOperators(unittest.TestCase):
287
287
  self.table.c1.like('foo'),
288
288
  ~NotLike(self.table.c1, 'foo'),
289
289
  ~~Like(self.table.c1, 'foo')]:
290
- self.assertEqual(str(like), '("c1" LIKE %s ESCAPE %s)')
291
- self.assertEqual(like.params, ('foo', '\\'))
290
+ self.assertEqual(str(like), '("c1" LIKE %s)')
291
+ self.assertEqual(like.params, ('foo',))
292
292
 
293
293
  def test_like_escape(self):
294
294
  like = Like(self.table.c1, 'foo', escape='$')
@@ -299,7 +299,7 @@ class TestOperators(unittest.TestCase):
299
299
  flavor = Flavor(escape_empty=False)
300
300
  Flavor.set(flavor)
301
301
  try:
302
- like = Like(self.table.c1, 'foo', escape='')
302
+ like = Like(self.table.c1, 'foo')
303
303
  self.assertEqual(str(like), '("c1" LIKE %s)')
304
304
  self.assertEqual(like.params, ('foo',))
305
305
  finally:
@@ -309,7 +309,7 @@ class TestOperators(unittest.TestCase):
309
309
  flavor = Flavor(escape_empty=True)
310
310
  Flavor.set(flavor)
311
311
  try:
312
- like = Like(self.table.c1, 'foo', escape='')
312
+ like = Like(self.table.c1, 'foo')
313
313
  self.assertEqual(str(like), '("c1" LIKE %s ESCAPE %s)')
314
314
  self.assertEqual(like.params, ('foo', ''))
315
315
  finally:
@@ -322,8 +322,8 @@ class TestOperators(unittest.TestCase):
322
322
  for like in [ILike(self.table.c1, 'foo'),
323
323
  self.table.c1.ilike('foo'),
324
324
  ~NotILike(self.table.c1, 'foo')]:
325
- self.assertEqual(str(like), '("c1" ILIKE %s ESCAPE %s)')
326
- self.assertEqual(like.params, ('foo', '\\'))
325
+ self.assertEqual(str(like), '("c1" ILIKE %s)')
326
+ self.assertEqual(like.params, ('foo',))
327
327
  finally:
328
328
  Flavor.set(Flavor())
329
329
 
@@ -332,8 +332,8 @@ class TestOperators(unittest.TestCase):
332
332
  try:
333
333
  like = ILike(self.table.c1, 'foo')
334
334
  self.assertEqual(
335
- str(like), '(UPPER("c1") LIKE UPPER(%s) ESCAPE %s)')
336
- self.assertEqual(like.params, ('foo', '\\'))
335
+ str(like), '(UPPER("c1") LIKE UPPER(%s))')
336
+ self.assertEqual(like.params, ('foo',))
337
337
  finally:
338
338
  Flavor.set(Flavor())
339
339
 
@@ -343,8 +343,8 @@ class TestOperators(unittest.TestCase):
343
343
  try:
344
344
  for like in [NotILike(self.table.c1, 'foo'),
345
345
  ~self.table.c1.ilike('foo')]:
346
- self.assertEqual(str(like), '("c1" NOT ILIKE %s ESCAPE %s)')
347
- self.assertEqual(like.params, ('foo', '\\'))
346
+ self.assertEqual(str(like), '("c1" NOT ILIKE %s)')
347
+ self.assertEqual(like.params, ('foo',))
348
348
  finally:
349
349
  Flavor.set(Flavor())
350
350
 
@@ -353,8 +353,8 @@ class TestOperators(unittest.TestCase):
353
353
  try:
354
354
  like = NotILike(self.table.c1, 'foo')
355
355
  self.assertEqual(
356
- str(like), '(UPPER("c1") NOT LIKE UPPER(%s) ESCAPE %s)')
357
- self.assertEqual(like.params, ('foo', '\\'))
356
+ str(like), '(UPPER("c1") NOT LIKE UPPER(%s))')
357
+ self.assertEqual(like.params, ('foo',))
358
358
  finally:
359
359
  Flavor.set(Flavor())
360
360
 
sql/tests/test_select.py CHANGED
@@ -4,7 +4,9 @@ import unittest
4
4
  import warnings
5
5
  from copy import deepcopy
6
6
 
7
- from sql import Flavor, For, Join, Literal, Select, Table, Union, Window, With
7
+ from sql import (
8
+ Cube, Flavor, For, Grouping, Join, Literal, Rollup, Select, Table, Union,
9
+ Window, With)
8
10
  from sql.aggregate import Max, Min
9
11
  from sql.functions import DatePart, Function, Now, Rank
10
12
 
@@ -185,6 +187,50 @@ class TestSelect(unittest.TestCase):
185
187
  'SELECT %s FROM "t" AS "a" GROUP BY %s')
186
188
  self.assertEqual(tuple(query.params), ('foo', 'foo'))
187
189
 
190
+ def test_select_group_by_grouping_sets(self):
191
+ query = self.table.select(
192
+ Literal('*'),
193
+ group_by=Grouping((self.table.a, self.table.b), (Literal('foo'),)))
194
+ self.assertEqual(str(query),
195
+ 'SELECT %s FROM "t" AS "a" '
196
+ 'GROUP BY GROUPING SETS (("a"."a", "a"."b"), (%s))')
197
+ self.assertEqual(tuple(query.params), ('*', 'foo',))
198
+
199
+ query = self.table.select(
200
+ Literal('*'),
201
+ group_by=[
202
+ self.table.a, Grouping((self.table.b,), (self.table.c,))])
203
+ self.assertEqual(str(query),
204
+ 'SELECT %s FROM "t" AS "a" '
205
+ 'GROUP BY "a"."a", GROUPING SETS (("a"."b"), ("a"."c"))')
206
+ self.assertEqual(tuple(query.params), ('*',))
207
+
208
+ def test_select_group_by_rollup(self):
209
+ query = self.table.select(
210
+ Literal('*'),
211
+ group_by=Rollup(self.table.a, self.table.b, Literal('foo')))
212
+ self.assertEqual(str(query),
213
+ 'SELECT %s FROM "t" AS "a" '
214
+ 'GROUP BY ROLLUP ("a"."a", "a"."b", %s)')
215
+ self.assertEqual(tuple(query.params), ('*', 'foo'))
216
+
217
+ query = self.table.select(
218
+ Literal('*'),
219
+ group_by=Rollup((self.table.a, self.table.b), self.table.c))
220
+ self.assertEqual(str(query),
221
+ 'SELECT %s FROM "t" AS "a" '
222
+ 'GROUP BY ROLLUP (("a"."a", "a"."b"), "a"."c")')
223
+ self.assertEqual(tuple(query.params), ('*',))
224
+
225
+ def test_select_group_by_cube(self):
226
+ query = self.table.select(
227
+ Literal('*'),
228
+ group_by=Cube(self.table.a, self.table.b))
229
+ self.assertEqual(str(query),
230
+ 'SELECT %s FROM "t" AS "a" '
231
+ 'GROUP BY CUBE ("a"."a", "a"."b")')
232
+ self.assertEqual(tuple(query.params), ('*',))
233
+
188
234
  def test_select_having(self):
189
235
  col1 = self.table.col1
190
236
  col2 = self.table.col2
Binary file
Binary file
Binary file
Binary file
Binary file