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.
- {python_sql-1.4.2.dist-info → python_sql-1.5.0.dist-info}/METADATA +2 -1
- {python_sql-1.4.2.dist-info → python_sql-1.5.0.dist-info}/RECORD +12 -16
- {python_sql-1.4.2.dist-info → python_sql-1.5.0.dist-info}/WHEEL +1 -1
- sql/__init__.py +483 -18
- sql/functions.py +5 -2
- sql/operators.py +2 -2
- sql/tests/test_combining_query.py +13 -1
- sql/tests/test_insert.py +101 -8
- sql/tests/test_merge.py +111 -0
- sql/tests/test_operators.py +12 -12
- sql/tests/test_select.py +47 -1
- sql/__pycache__/__init__.cpython-39.pyc +0 -0
- sql/__pycache__/aggregate.cpython-39.pyc +0 -0
- sql/__pycache__/conditionals.cpython-39.pyc +0 -0
- sql/__pycache__/functions.cpython-39.pyc +0 -0
- sql/__pycache__/operators.cpython-39.pyc +0 -0
- {python_sql-1.4.2.dist-info → python_sql-1.5.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-sql
|
|
3
|
-
Version: 1.
|
|
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=
|
|
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=
|
|
5
|
-
sql/operators.py,sha256=
|
|
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=
|
|
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=
|
|
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/
|
|
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=
|
|
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.
|
|
36
|
-
python_sql-1.
|
|
37
|
-
python_sql-1.
|
|
38
|
-
python_sql-1.
|
|
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,,
|
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.
|
|
11
|
-
__all__ = [
|
|
12
|
-
'
|
|
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__(
|
|
669
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
942
|
-
p.extend(
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
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(
|
|
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
|
-
|
|
1457
|
+
try:
|
|
1070
1458
|
p.extend(item.params)
|
|
1071
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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))
|
sql/tests/test_merge.py
ADDED
|
@@ -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,))
|
sql/tests/test_operators.py
CHANGED
|
@@ -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
|
|
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'
|
|
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'
|
|
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
|
|
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)
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
File without changes
|