python-sql 1.4.3__py3-none-any.whl → 1.5.1__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.3.dist-info → python_sql-1.5.1.dist-info}/METADATA +1 -1
- {python_sql-1.4.3.dist-info → python_sql-1.5.1.dist-info}/RECORD +12 -16
- {python_sql-1.4.3.dist-info → python_sql-1.5.1.dist-info}/WHEEL +1 -1
- sql/__init__.py +502 -18
- sql/functions.py +5 -2
- sql/operators.py +2 -2
- 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 +57 -11
- sql/tests/test_window.py +6 -6
- 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.3.dist-info → python_sql-1.5.1.dist-info}/top_level.txt +0 -0
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
sql/__init__.py,sha256=
|
|
1
|
+
sql/__init__.py,sha256=gmmbZtl-Xz2V_wc5HDJMcTdjoXdfgYueu42AaAoha_Y,60610
|
|
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
|
|
@@ -20,19 +15,20 @@ sql/tests/test_conditionals.py,sha256=kNkgsjOatXpJnF9AED03ANNd0zWIwWTrnWdi4PWfl3
|
|
|
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=MSwnN-JF5sxia_rxfWvVgtxApXSNJi7gEQaQ6eg7QTc,19903
|
|
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
|
-
sql/tests/test_window.py,sha256=
|
|
29
|
+
sql/tests/test_window.py,sha256=4WJCrdk0YeCujhZqu87UnXNhdJelOuXVJ-ttwSOR_S0,2392
|
|
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.1.dist-info/METADATA,sha256=XNgCKTalL5eVIhPQZzBfiCQsCyeL1uAsb0w67hWo9So,8514
|
|
32
|
+
python_sql-1.5.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
33
|
+
python_sql-1.5.1.dist-info/top_level.txt,sha256=hwJHmWfbRP5a_XNfqgHwi2zrjAAzAJk24QZoTocslow,4
|
|
34
|
+
python_sql-1.5.1.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.1'
|
|
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):
|
|
@@ -376,13 +380,14 @@ class SelectQuery(WithQuery):
|
|
|
376
380
|
|
|
377
381
|
@property
|
|
378
382
|
def _limit_offset_str(self):
|
|
383
|
+
param = Flavor.get().param
|
|
379
384
|
if Flavor.get().limitstyle == 'limit':
|
|
380
385
|
offset = ''
|
|
381
386
|
if self.offset:
|
|
382
|
-
offset = ' OFFSET %s' %
|
|
387
|
+
offset = ' OFFSET %s' % param
|
|
383
388
|
limit = ''
|
|
384
389
|
if self.limit is not None:
|
|
385
|
-
limit = ' LIMIT %s' %
|
|
390
|
+
limit = ' LIMIT %s' % param
|
|
386
391
|
elif self.offset:
|
|
387
392
|
max_limit = Flavor.get().max_limit
|
|
388
393
|
if max_limit:
|
|
@@ -391,12 +396,27 @@ class SelectQuery(WithQuery):
|
|
|
391
396
|
else:
|
|
392
397
|
offset = ''
|
|
393
398
|
if self.offset:
|
|
394
|
-
offset = ' OFFSET (%s) ROWS' %
|
|
399
|
+
offset = ' OFFSET (%s) ROWS' % param
|
|
395
400
|
fetch = ''
|
|
396
401
|
if self.limit is not None:
|
|
397
|
-
fetch = ' FETCH FIRST (%s) ROWS ONLY' %
|
|
402
|
+
fetch = ' FETCH FIRST (%s) ROWS ONLY' % param
|
|
398
403
|
return offset + fetch
|
|
399
404
|
|
|
405
|
+
@property
|
|
406
|
+
def _limit_offset_params(self):
|
|
407
|
+
p = []
|
|
408
|
+
if Flavor.get().limitstyle == 'limit':
|
|
409
|
+
if self.limit is not None:
|
|
410
|
+
p.append(self.limit)
|
|
411
|
+
if self.offset:
|
|
412
|
+
p.append(self.offset)
|
|
413
|
+
else:
|
|
414
|
+
if self.offset:
|
|
415
|
+
p.append(self.offset)
|
|
416
|
+
if self.limit is not None:
|
|
417
|
+
p.append(self.limit)
|
|
418
|
+
return tuple(p)
|
|
419
|
+
|
|
400
420
|
def as_(self, output_name):
|
|
401
421
|
return As(self, output_name)
|
|
402
422
|
|
|
@@ -659,21 +679,25 @@ class Select(FromItem, SelectQuery):
|
|
|
659
679
|
p.extend(self.having.params)
|
|
660
680
|
for window in self.windows:
|
|
661
681
|
p.extend(window.params)
|
|
682
|
+
p.extend(self._limit_offset_params)
|
|
662
683
|
return tuple(p)
|
|
663
684
|
|
|
664
685
|
|
|
665
686
|
class Insert(WithQuery):
|
|
666
|
-
__slots__ = ('_table', '_columns', '_values', '_returning')
|
|
687
|
+
__slots__ = ('_table', '_columns', '_values', '_on_conflict', '_returning')
|
|
667
688
|
|
|
668
|
-
def __init__(
|
|
669
|
-
|
|
689
|
+
def __init__(
|
|
690
|
+
self, table, columns=None, values=None, returning=None,
|
|
691
|
+
on_conflict=None, **kwargs):
|
|
670
692
|
self._table = None
|
|
671
693
|
self._columns = None
|
|
672
694
|
self._values = None
|
|
695
|
+
self._on_conflict = None
|
|
673
696
|
self._returning = None
|
|
674
697
|
self.table = table
|
|
675
698
|
self.columns = columns
|
|
676
699
|
self.values = values
|
|
700
|
+
self.on_conflict = on_conflict
|
|
677
701
|
self.returning = returning
|
|
678
702
|
super(Insert, self).__init__(**kwargs)
|
|
679
703
|
|
|
@@ -709,6 +733,17 @@ class Insert(WithQuery):
|
|
|
709
733
|
value = Values(value)
|
|
710
734
|
self._values = value
|
|
711
735
|
|
|
736
|
+
@property
|
|
737
|
+
def on_conflict(self):
|
|
738
|
+
return self._on_conflict
|
|
739
|
+
|
|
740
|
+
@on_conflict.setter
|
|
741
|
+
def on_conflict(self, value):
|
|
742
|
+
if value is not None:
|
|
743
|
+
assert isinstance(value, Conflict)
|
|
744
|
+
assert value.table == self.table
|
|
745
|
+
self._on_conflict = value
|
|
746
|
+
|
|
712
747
|
@property
|
|
713
748
|
def returning(self):
|
|
714
749
|
return self._returning
|
|
@@ -743,13 +778,20 @@ class Insert(WithQuery):
|
|
|
743
778
|
# TODO manage DEFAULT
|
|
744
779
|
elif self.values is None:
|
|
745
780
|
values = ' DEFAULT VALUES'
|
|
781
|
+
on_conflict = ''
|
|
782
|
+
if self.on_conflict:
|
|
783
|
+
on_conflict = ' %s' % self.on_conflict
|
|
746
784
|
returning = ''
|
|
747
785
|
if self.returning:
|
|
748
786
|
returning = ' RETURNING ' + ', '.join(
|
|
749
787
|
map(self._format, self.returning))
|
|
788
|
+
if on_conflict or returning:
|
|
789
|
+
table = '%s AS "%s"' % (self.table, self.table.alias)
|
|
790
|
+
else:
|
|
791
|
+
table = str(self.table)
|
|
750
792
|
return (self._with_str()
|
|
751
|
-
+ 'INSERT INTO %s
|
|
752
|
-
+ columns + values + returning)
|
|
793
|
+
+ 'INSERT INTO %s' % table
|
|
794
|
+
+ columns + values + on_conflict + returning)
|
|
753
795
|
|
|
754
796
|
@property
|
|
755
797
|
def params(self):
|
|
@@ -757,12 +799,149 @@ class Insert(WithQuery):
|
|
|
757
799
|
p.extend(self._with_params())
|
|
758
800
|
if isinstance(self.values, Query):
|
|
759
801
|
p.extend(self.values.params)
|
|
802
|
+
if self.on_conflict:
|
|
803
|
+
p.extend(self.on_conflict.params)
|
|
760
804
|
if self.returning:
|
|
761
805
|
for exp in self.returning:
|
|
762
806
|
p.extend(exp.params)
|
|
763
807
|
return tuple(p)
|
|
764
808
|
|
|
765
809
|
|
|
810
|
+
class Conflict(object):
|
|
811
|
+
__slots__ = (
|
|
812
|
+
'_table', '_indexed_columns', '_index_where', '_columns', '_values',
|
|
813
|
+
'_where')
|
|
814
|
+
|
|
815
|
+
def __init__(
|
|
816
|
+
self, table, indexed_columns=None, index_where=None,
|
|
817
|
+
columns=None, values=None, where=None):
|
|
818
|
+
self._table = None
|
|
819
|
+
self._indexed_columns = None
|
|
820
|
+
self._index_where = None
|
|
821
|
+
self._columns = None
|
|
822
|
+
self._values = None
|
|
823
|
+
self._where = None
|
|
824
|
+
self.table = table
|
|
825
|
+
self.indexed_columns = indexed_columns
|
|
826
|
+
self.index_where = index_where
|
|
827
|
+
self.columns = columns
|
|
828
|
+
self.values = values
|
|
829
|
+
self.where = where
|
|
830
|
+
|
|
831
|
+
@property
|
|
832
|
+
def table(self):
|
|
833
|
+
return self._table
|
|
834
|
+
|
|
835
|
+
@table.setter
|
|
836
|
+
def table(self, value):
|
|
837
|
+
assert isinstance(value, Table)
|
|
838
|
+
self._table = value
|
|
839
|
+
|
|
840
|
+
@property
|
|
841
|
+
def indexed_columns(self):
|
|
842
|
+
return self._indexed_columns
|
|
843
|
+
|
|
844
|
+
@indexed_columns.setter
|
|
845
|
+
def indexed_columns(self, value):
|
|
846
|
+
if value is not None:
|
|
847
|
+
assert all(isinstance(col, Column) for col in value)
|
|
848
|
+
assert all(col.table == self.table for col in value)
|
|
849
|
+
self._indexed_columns = value
|
|
850
|
+
|
|
851
|
+
@property
|
|
852
|
+
def index_where(self):
|
|
853
|
+
return self._index_where
|
|
854
|
+
|
|
855
|
+
@index_where.setter
|
|
856
|
+
def index_where(self, value):
|
|
857
|
+
from sql.operators import And, Or
|
|
858
|
+
if value is not None:
|
|
859
|
+
assert isinstance(value, (Expression, And, Or))
|
|
860
|
+
self._index_where = value
|
|
861
|
+
|
|
862
|
+
@property
|
|
863
|
+
def columns(self):
|
|
864
|
+
return self._columns
|
|
865
|
+
|
|
866
|
+
@columns.setter
|
|
867
|
+
def columns(self, value):
|
|
868
|
+
if value is not None:
|
|
869
|
+
assert all(isinstance(col, Column) for col in value)
|
|
870
|
+
assert all(col.table == self.table for col in value)
|
|
871
|
+
self._columns = value
|
|
872
|
+
|
|
873
|
+
@property
|
|
874
|
+
def values(self):
|
|
875
|
+
return self._values
|
|
876
|
+
|
|
877
|
+
@values.setter
|
|
878
|
+
def values(self, value):
|
|
879
|
+
if value is not None:
|
|
880
|
+
assert isinstance(value, (list, Select))
|
|
881
|
+
if isinstance(value, list):
|
|
882
|
+
value = Values([value])
|
|
883
|
+
self._values = value
|
|
884
|
+
|
|
885
|
+
@property
|
|
886
|
+
def where(self):
|
|
887
|
+
return self._where
|
|
888
|
+
|
|
889
|
+
@where.setter
|
|
890
|
+
def where(self, value):
|
|
891
|
+
from sql.operators import And, Or
|
|
892
|
+
if value is not None:
|
|
893
|
+
assert isinstance(value, (Expression, And, Or))
|
|
894
|
+
self._where = value
|
|
895
|
+
|
|
896
|
+
def __str__(self):
|
|
897
|
+
indexed_columns = ''
|
|
898
|
+
if self.indexed_columns:
|
|
899
|
+
assert all(c.table == self.table for c in self.indexed_columns)
|
|
900
|
+
# Get columns without alias
|
|
901
|
+
indexed_columns = ', '.join(
|
|
902
|
+
c.column_name for c in self.indexed_columns)
|
|
903
|
+
indexed_columns = ' (' + indexed_columns + ')'
|
|
904
|
+
if self.index_where:
|
|
905
|
+
indexed_columns += ' WHERE ' + str(self.index_where)
|
|
906
|
+
else:
|
|
907
|
+
assert not self.index_where
|
|
908
|
+
do = ''
|
|
909
|
+
if not self.columns:
|
|
910
|
+
assert not self.values
|
|
911
|
+
assert not self.where
|
|
912
|
+
do = 'NOTHING'
|
|
913
|
+
else:
|
|
914
|
+
assert all(c.table == self.table for c in self.columns)
|
|
915
|
+
# Get columns without alias
|
|
916
|
+
do = ', '.join(c.column_name for c in self.columns)
|
|
917
|
+
# TODO manage DEFAULT
|
|
918
|
+
values = str(self.values)
|
|
919
|
+
if values.startswith('VALUES'):
|
|
920
|
+
values = values[len('VALUES'):]
|
|
921
|
+
else:
|
|
922
|
+
values = ' (' + values + ')'
|
|
923
|
+
if len(self.columns) == 1:
|
|
924
|
+
# PostgreSQL would require ROW expression
|
|
925
|
+
# with single column with parenthesis
|
|
926
|
+
do = 'UPDATE SET ' + do + ' =' + values
|
|
927
|
+
else:
|
|
928
|
+
do = 'UPDATE SET (' + do + ') =' + values
|
|
929
|
+
if self.where:
|
|
930
|
+
do += ' WHERE %s' % self.where
|
|
931
|
+
return 'ON CONFLICT' + indexed_columns + ' DO ' + do
|
|
932
|
+
|
|
933
|
+
@property
|
|
934
|
+
def params(self):
|
|
935
|
+
p = []
|
|
936
|
+
if self.index_where:
|
|
937
|
+
p.extend(self.index_where.params)
|
|
938
|
+
if self.values:
|
|
939
|
+
p.extend(self.values.params)
|
|
940
|
+
if self.where:
|
|
941
|
+
p.extend(self.where.params)
|
|
942
|
+
return p
|
|
943
|
+
|
|
944
|
+
|
|
766
945
|
class Update(Insert):
|
|
767
946
|
__slots__ = ('_where', '_values', 'from_')
|
|
768
947
|
|
|
@@ -919,6 +1098,207 @@ class Delete(WithQuery):
|
|
|
919
1098
|
return tuple(p)
|
|
920
1099
|
|
|
921
1100
|
|
|
1101
|
+
class Merge(WithQuery):
|
|
1102
|
+
__slots__ = ('_target', '_source', '_condition', '_whens')
|
|
1103
|
+
|
|
1104
|
+
def __init__(self, target, source, condition, *whens, **kwargs):
|
|
1105
|
+
self._target = None
|
|
1106
|
+
self._source = None
|
|
1107
|
+
self._condition = None
|
|
1108
|
+
self._whens = None
|
|
1109
|
+
self.target = target
|
|
1110
|
+
self.source = source
|
|
1111
|
+
self.condition = condition
|
|
1112
|
+
self.whens = whens
|
|
1113
|
+
super().__init__(**kwargs)
|
|
1114
|
+
|
|
1115
|
+
@property
|
|
1116
|
+
def target(self):
|
|
1117
|
+
return self._target
|
|
1118
|
+
|
|
1119
|
+
@target.setter
|
|
1120
|
+
def target(self, value):
|
|
1121
|
+
assert isinstance(value, Table)
|
|
1122
|
+
self._target = value
|
|
1123
|
+
|
|
1124
|
+
@property
|
|
1125
|
+
def source(self):
|
|
1126
|
+
return self._source
|
|
1127
|
+
|
|
1128
|
+
@source.setter
|
|
1129
|
+
def source(self, value):
|
|
1130
|
+
assert isinstance(value, (Table, SelectQuery, Values))
|
|
1131
|
+
self._source = value
|
|
1132
|
+
|
|
1133
|
+
@property
|
|
1134
|
+
def condition(self):
|
|
1135
|
+
return self._condition
|
|
1136
|
+
|
|
1137
|
+
@condition.setter
|
|
1138
|
+
def condition(self, value):
|
|
1139
|
+
assert isinstance(value, Expression)
|
|
1140
|
+
self._condition = value
|
|
1141
|
+
|
|
1142
|
+
@property
|
|
1143
|
+
def whens(self):
|
|
1144
|
+
return self._whens
|
|
1145
|
+
|
|
1146
|
+
@whens.setter
|
|
1147
|
+
def whens(self, value):
|
|
1148
|
+
assert all(isinstance(w, Matched) for w in value)
|
|
1149
|
+
self._whens = tuple(value)
|
|
1150
|
+
|
|
1151
|
+
def __str__(self):
|
|
1152
|
+
with AliasManager():
|
|
1153
|
+
if isinstance(self.source, (Select, Values)):
|
|
1154
|
+
source = '(%s)' % self.source
|
|
1155
|
+
else:
|
|
1156
|
+
source = self.source
|
|
1157
|
+
if self.condition:
|
|
1158
|
+
condition = 'ON %s' % self.condition
|
|
1159
|
+
else:
|
|
1160
|
+
condition = ''
|
|
1161
|
+
return (self._with_str()
|
|
1162
|
+
+ 'MERGE INTO %s AS "%s" ' % (self.target, self.target.alias)
|
|
1163
|
+
+ 'USING %s AS "%s" ' % (source, self.source.alias)
|
|
1164
|
+
+ condition + ' ' + ' '.join(map(str, self.whens)))
|
|
1165
|
+
|
|
1166
|
+
@property
|
|
1167
|
+
def params(self):
|
|
1168
|
+
p = []
|
|
1169
|
+
p.extend(self._with_params())
|
|
1170
|
+
if isinstance(self.source, (SelectQuery, Values)):
|
|
1171
|
+
p.extend(self.source.params)
|
|
1172
|
+
if self.condition:
|
|
1173
|
+
p.extend(self.condition.params)
|
|
1174
|
+
for match in self.whens:
|
|
1175
|
+
p.extend(match.params)
|
|
1176
|
+
return tuple(p)
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
class Matched(object):
|
|
1180
|
+
__slots__ = ('_condition',)
|
|
1181
|
+
_when = 'MATCHED'
|
|
1182
|
+
|
|
1183
|
+
def __init__(self, condition=None):
|
|
1184
|
+
self._condition = None
|
|
1185
|
+
self.condition = condition
|
|
1186
|
+
|
|
1187
|
+
@property
|
|
1188
|
+
def condition(self):
|
|
1189
|
+
return self._condition
|
|
1190
|
+
|
|
1191
|
+
@condition.setter
|
|
1192
|
+
def condition(self, value):
|
|
1193
|
+
if value is not None:
|
|
1194
|
+
assert isinstance(value, Expression)
|
|
1195
|
+
self._condition = value
|
|
1196
|
+
|
|
1197
|
+
def _then_str(self):
|
|
1198
|
+
return 'DO NOTHING'
|
|
1199
|
+
|
|
1200
|
+
def __str__(self):
|
|
1201
|
+
if self.condition is not None:
|
|
1202
|
+
condition = ' AND ' + str(self.condition)
|
|
1203
|
+
else:
|
|
1204
|
+
condition = ''
|
|
1205
|
+
return 'WHEN ' + self._when + condition + ' THEN ' + self._then_str()
|
|
1206
|
+
|
|
1207
|
+
@property
|
|
1208
|
+
def params(self):
|
|
1209
|
+
p = []
|
|
1210
|
+
if self.condition:
|
|
1211
|
+
p.extend(self.condition.params)
|
|
1212
|
+
return tuple(p)
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
class _MatchedValues(Matched):
|
|
1216
|
+
__slots__ = ('_columns', '_values')
|
|
1217
|
+
|
|
1218
|
+
def __init__(self, columns, values, **kwargs):
|
|
1219
|
+
self._columns = columns
|
|
1220
|
+
self._values = values
|
|
1221
|
+
self.columns = columns
|
|
1222
|
+
self.values = values
|
|
1223
|
+
super().__init__(**kwargs)
|
|
1224
|
+
|
|
1225
|
+
@property
|
|
1226
|
+
def columns(self):
|
|
1227
|
+
return self._columns
|
|
1228
|
+
|
|
1229
|
+
@columns.setter
|
|
1230
|
+
def columns(self, value):
|
|
1231
|
+
assert all(isinstance(col, Column) for col in value)
|
|
1232
|
+
self._columns = value
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
class MatchedUpdate(_MatchedValues, Matched):
|
|
1236
|
+
__slots__ = ()
|
|
1237
|
+
|
|
1238
|
+
@property
|
|
1239
|
+
def values(self):
|
|
1240
|
+
return self._values
|
|
1241
|
+
|
|
1242
|
+
@values.setter
|
|
1243
|
+
def values(self, value):
|
|
1244
|
+
self._values = value
|
|
1245
|
+
|
|
1246
|
+
def _then_str(self):
|
|
1247
|
+
columns = [c.column_name for c in self.columns]
|
|
1248
|
+
return 'UPDATE SET ' + ', '.join(
|
|
1249
|
+
'%s = %s' % (c, Update._format(v))
|
|
1250
|
+
for c, v in zip(columns, self.values))
|
|
1251
|
+
|
|
1252
|
+
@property
|
|
1253
|
+
def params(self):
|
|
1254
|
+
p = list(super().params)
|
|
1255
|
+
for value in self.values:
|
|
1256
|
+
if isinstance(value, (Expression, Select)):
|
|
1257
|
+
p.extend(value.params)
|
|
1258
|
+
else:
|
|
1259
|
+
p.append(value)
|
|
1260
|
+
return tuple(p)
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
class MatchedDelete(Matched):
|
|
1264
|
+
__slots__ = ()
|
|
1265
|
+
|
|
1266
|
+
def _then_str(self):
|
|
1267
|
+
return 'DELETE'
|
|
1268
|
+
|
|
1269
|
+
|
|
1270
|
+
class NotMatched(Matched):
|
|
1271
|
+
__slots__ = ()
|
|
1272
|
+
_when = 'NOT MATCHED'
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
class NotMatchedInsert(_MatchedValues, NotMatched):
|
|
1276
|
+
__slots__ = ()
|
|
1277
|
+
|
|
1278
|
+
@property
|
|
1279
|
+
def values(self):
|
|
1280
|
+
return self._values
|
|
1281
|
+
|
|
1282
|
+
@values.setter
|
|
1283
|
+
def values(self, value):
|
|
1284
|
+
self._values = Values([value])
|
|
1285
|
+
|
|
1286
|
+
def _then_str(self):
|
|
1287
|
+
columns = ', '.join(c.column_name for c in self.columns)
|
|
1288
|
+
columns = '(' + columns + ')'
|
|
1289
|
+
if self.values is None:
|
|
1290
|
+
values = ' DEFAULT VALUES '
|
|
1291
|
+
else:
|
|
1292
|
+
values = ' ' + str(self.values)
|
|
1293
|
+
return 'INSERT ' + columns + values
|
|
1294
|
+
|
|
1295
|
+
@property
|
|
1296
|
+
def params(self):
|
|
1297
|
+
p = list(super().params)
|
|
1298
|
+
p.extend(self.values.params)
|
|
1299
|
+
return tuple(p)
|
|
1300
|
+
|
|
1301
|
+
|
|
922
1302
|
class CombiningQuery(FromItem, SelectQuery):
|
|
923
1303
|
__slots__ = ('queries', 'all_')
|
|
924
1304
|
_operator = ''
|
|
@@ -989,9 +1369,11 @@ class Table(FromItem):
|
|
|
989
1369
|
def params(self):
|
|
990
1370
|
return ()
|
|
991
1371
|
|
|
992
|
-
def insert(
|
|
1372
|
+
def insert(
|
|
1373
|
+
self, columns=None, values=None, returning=None, with_=None,
|
|
1374
|
+
on_conflict=None):
|
|
993
1375
|
return Insert(self, columns=columns, values=values,
|
|
994
|
-
returning=returning, with_=with_)
|
|
1376
|
+
on_conflict=on_conflict, returning=returning, with_=with_)
|
|
995
1377
|
|
|
996
1378
|
def update(self, columns, values, from_=None, where=None, returning=None,
|
|
997
1379
|
with_=None):
|
|
@@ -1003,6 +1385,25 @@ class Table(FromItem):
|
|
|
1003
1385
|
return Delete(self, only=only, using=using, where=where,
|
|
1004
1386
|
returning=returning, with_=with_)
|
|
1005
1387
|
|
|
1388
|
+
def merge(self, source, condition, *whens, with_=None):
|
|
1389
|
+
return Merge(self, source, condition, *whens, with_=with_)
|
|
1390
|
+
|
|
1391
|
+
|
|
1392
|
+
class _Excluded(Table):
|
|
1393
|
+
def __init__(self):
|
|
1394
|
+
super().__init__('EXCLUDED')
|
|
1395
|
+
|
|
1396
|
+
@property
|
|
1397
|
+
def alias(self):
|
|
1398
|
+
return 'EXCLUDED'
|
|
1399
|
+
|
|
1400
|
+
@property
|
|
1401
|
+
def has_alias(self):
|
|
1402
|
+
return False
|
|
1403
|
+
|
|
1404
|
+
|
|
1405
|
+
Excluded = _Excluded()
|
|
1406
|
+
|
|
1006
1407
|
|
|
1007
1408
|
class Join(FromItem):
|
|
1008
1409
|
__slots__ = ('_left', '_right', '_condition', '_type_')
|
|
@@ -1070,10 +1471,14 @@ class Join(FromItem):
|
|
|
1070
1471
|
def params(self):
|
|
1071
1472
|
p = []
|
|
1072
1473
|
for item in (self.left, self.right):
|
|
1073
|
-
|
|
1474
|
+
try:
|
|
1074
1475
|
p.extend(item.params)
|
|
1075
|
-
|
|
1476
|
+
except AttributeError:
|
|
1477
|
+
pass
|
|
1478
|
+
try:
|
|
1076
1479
|
p.extend(self.condition.params)
|
|
1480
|
+
except AttributeError:
|
|
1481
|
+
pass
|
|
1077
1482
|
return tuple(p)
|
|
1078
1483
|
|
|
1079
1484
|
@property
|
|
@@ -1442,6 +1847,79 @@ class Collate(Expression):
|
|
|
1442
1847
|
return (self.expression,)
|
|
1443
1848
|
|
|
1444
1849
|
|
|
1850
|
+
class Grouping(Expression):
|
|
1851
|
+
__slots__ = ('_sets',)
|
|
1852
|
+
|
|
1853
|
+
def __init__(self, *sets):
|
|
1854
|
+
super().__init__()
|
|
1855
|
+
self.sets = sets
|
|
1856
|
+
|
|
1857
|
+
@property
|
|
1858
|
+
def sets(self):
|
|
1859
|
+
return self._sets
|
|
1860
|
+
|
|
1861
|
+
@sets.setter
|
|
1862
|
+
def sets(self, value):
|
|
1863
|
+
assert all(
|
|
1864
|
+
isinstance(col, Expression) for cols in value for col in cols)
|
|
1865
|
+
self._sets = tuple(tuple(cols) for cols in value)
|
|
1866
|
+
|
|
1867
|
+
def __str__(self):
|
|
1868
|
+
return 'GROUPING SETS (%s)' % (
|
|
1869
|
+
', '.join(
|
|
1870
|
+
'(%s)' % ', '.join(str(col) for col in cols)
|
|
1871
|
+
for cols in self.sets))
|
|
1872
|
+
|
|
1873
|
+
@property
|
|
1874
|
+
def params(self):
|
|
1875
|
+
return sum((col.params for cols in self.sets for col in cols), ())
|
|
1876
|
+
|
|
1877
|
+
|
|
1878
|
+
class Rollup(Expression):
|
|
1879
|
+
__slots__ = ('_expressions',)
|
|
1880
|
+
|
|
1881
|
+
def __init__(self, *expressions):
|
|
1882
|
+
super().__init__()
|
|
1883
|
+
self.expressions = expressions
|
|
1884
|
+
|
|
1885
|
+
@property
|
|
1886
|
+
def expressions(self):
|
|
1887
|
+
return self._expressions
|
|
1888
|
+
|
|
1889
|
+
@expressions.setter
|
|
1890
|
+
def expressions(self, value):
|
|
1891
|
+
assert all(
|
|
1892
|
+
isinstance(col, Expression)
|
|
1893
|
+
or all(isinstance(c, Expression) for c in col)
|
|
1894
|
+
for col in value)
|
|
1895
|
+
self._expressions = tuple(value)
|
|
1896
|
+
|
|
1897
|
+
def __str__(self):
|
|
1898
|
+
def format(col):
|
|
1899
|
+
if isinstance(col, Expression):
|
|
1900
|
+
return str(col)
|
|
1901
|
+
else:
|
|
1902
|
+
return '(%s)' % ', '.join(str(c) for c in col)
|
|
1903
|
+
return '%s (%s)' % (
|
|
1904
|
+
self.__class__.__name__.upper(),
|
|
1905
|
+
', '.join(format(col) for col in self.expressions))
|
|
1906
|
+
|
|
1907
|
+
@property
|
|
1908
|
+
def params(self):
|
|
1909
|
+
p = []
|
|
1910
|
+
for col in self.expressions:
|
|
1911
|
+
if isinstance(col, Expression):
|
|
1912
|
+
p.extend(col.params)
|
|
1913
|
+
else:
|
|
1914
|
+
for c in col:
|
|
1915
|
+
p.extend(c.params)
|
|
1916
|
+
return tuple(p)
|
|
1917
|
+
|
|
1918
|
+
|
|
1919
|
+
class Cube(Rollup):
|
|
1920
|
+
pass
|
|
1921
|
+
|
|
1922
|
+
|
|
1445
1923
|
class Window(object):
|
|
1446
1924
|
__slots__ = (
|
|
1447
1925
|
'_partition', '_order_by', '_frame', '_start', '_end', '_exclude')
|
|
@@ -1531,6 +2009,7 @@ class Window(object):
|
|
|
1531
2009
|
return AliasManager.contains(self)
|
|
1532
2010
|
|
|
1533
2011
|
def __str__(self):
|
|
2012
|
+
param = Flavor.get().param
|
|
1534
2013
|
partition = ''
|
|
1535
2014
|
if self.partition:
|
|
1536
2015
|
partition = 'PARTITION BY ' + ', '.join(map(str, self.partition))
|
|
@@ -1544,9 +2023,9 @@ class Window(object):
|
|
|
1544
2023
|
elif not frame:
|
|
1545
2024
|
return 'CURRENT ROW'
|
|
1546
2025
|
elif frame < 0:
|
|
1547
|
-
return '%s PRECEDING' %
|
|
2026
|
+
return '%s PRECEDING' % param
|
|
1548
2027
|
elif frame > 0:
|
|
1549
|
-
return '%s FOLLOWING' %
|
|
2028
|
+
return '%s FOLLOWING' % param
|
|
1550
2029
|
|
|
1551
2030
|
frame = ''
|
|
1552
2031
|
if self.frame:
|
|
@@ -1567,6 +2046,11 @@ class Window(object):
|
|
|
1567
2046
|
if self.order_by:
|
|
1568
2047
|
for expression in self.order_by:
|
|
1569
2048
|
p.extend(expression.params)
|
|
2049
|
+
if self.frame:
|
|
2050
|
+
if self.start:
|
|
2051
|
+
p.append(abs(self.start))
|
|
2052
|
+
if self.end:
|
|
2053
|
+
p.append(abs(self.end))
|
|
1570
2054
|
return tuple(p)
|
|
1571
2055
|
|
|
1572
2056
|
|
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):
|
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
|
|
@@ -207,13 +253,13 @@ class TestSelect(unittest.TestCase):
|
|
|
207
253
|
Flavor.set(Flavor(limitstyle='limit'))
|
|
208
254
|
query = self.table.select(limit=50, offset=10)
|
|
209
255
|
self.assertEqual(str(query),
|
|
210
|
-
'SELECT * FROM "t" AS "a" LIMIT
|
|
211
|
-
self.assertEqual(tuple(query.params), ())
|
|
256
|
+
'SELECT * FROM "t" AS "a" LIMIT %s OFFSET %s')
|
|
257
|
+
self.assertEqual(tuple(query.params), (50, 10))
|
|
212
258
|
|
|
213
259
|
query.limit = None
|
|
214
260
|
self.assertEqual(str(query),
|
|
215
|
-
'SELECT * FROM "t" AS "a" OFFSET
|
|
216
|
-
self.assertEqual(tuple(query.params), ())
|
|
261
|
+
'SELECT * FROM "t" AS "a" OFFSET %s')
|
|
262
|
+
self.assertEqual(tuple(query.params), (10,))
|
|
217
263
|
|
|
218
264
|
query.offset = 0
|
|
219
265
|
self.assertEqual(str(query),
|
|
@@ -234,8 +280,8 @@ class TestSelect(unittest.TestCase):
|
|
|
234
280
|
|
|
235
281
|
query.offset = 10
|
|
236
282
|
self.assertEqual(str(query),
|
|
237
|
-
'SELECT * FROM "t" AS "a" LIMIT -1 OFFSET
|
|
238
|
-
self.assertEqual(tuple(query.params), ())
|
|
283
|
+
'SELECT * FROM "t" AS "a" LIMIT -1 OFFSET %s')
|
|
284
|
+
self.assertEqual(tuple(query.params), (10,))
|
|
239
285
|
finally:
|
|
240
286
|
Flavor.set(Flavor())
|
|
241
287
|
|
|
@@ -245,13 +291,13 @@ class TestSelect(unittest.TestCase):
|
|
|
245
291
|
query = self.table.select(limit=50, offset=10)
|
|
246
292
|
self.assertEqual(str(query),
|
|
247
293
|
'SELECT * FROM "t" AS "a" '
|
|
248
|
-
'OFFSET (
|
|
249
|
-
self.assertEqual(tuple(query.params), ())
|
|
294
|
+
'OFFSET (%s) ROWS FETCH FIRST (%s) ROWS ONLY')
|
|
295
|
+
self.assertEqual(tuple(query.params), (10, 50))
|
|
250
296
|
|
|
251
297
|
query.limit = None
|
|
252
298
|
self.assertEqual(str(query),
|
|
253
|
-
'SELECT * FROM "t" AS "a" OFFSET (
|
|
254
|
-
self.assertEqual(tuple(query.params), ())
|
|
299
|
+
'SELECT * FROM "t" AS "a" OFFSET (%s) ROWS')
|
|
300
|
+
self.assertEqual(tuple(query.params), (10,))
|
|
255
301
|
|
|
256
302
|
query.offset = 0
|
|
257
303
|
self.assertEqual(str(query),
|
sql/tests/test_window.py
CHANGED
|
@@ -33,22 +33,22 @@ class TestWindow(unittest.TestCase):
|
|
|
33
33
|
window.start = -1
|
|
34
34
|
self.assertEqual(str(window),
|
|
35
35
|
'PARTITION BY "c" RANGE '
|
|
36
|
-
'BETWEEN
|
|
37
|
-
self.assertEqual(window.params, ())
|
|
36
|
+
'BETWEEN %s PRECEDING AND CURRENT ROW')
|
|
37
|
+
self.assertEqual(window.params, (1,))
|
|
38
38
|
|
|
39
39
|
window.start = 0
|
|
40
40
|
window.end = 1
|
|
41
41
|
self.assertEqual(str(window),
|
|
42
42
|
'PARTITION BY "c" RANGE '
|
|
43
|
-
'BETWEEN CURRENT ROW AND
|
|
44
|
-
self.assertEqual(window.params, ())
|
|
43
|
+
'BETWEEN CURRENT ROW AND %s FOLLOWING')
|
|
44
|
+
self.assertEqual(window.params, (1,))
|
|
45
45
|
|
|
46
46
|
window.start = 1
|
|
47
47
|
window.end = None
|
|
48
48
|
self.assertEqual(str(window),
|
|
49
49
|
'PARTITION BY "c" RANGE '
|
|
50
|
-
'BETWEEN
|
|
51
|
-
self.assertEqual(window.params, ())
|
|
50
|
+
'BETWEEN %s FOLLOWING AND UNBOUNDED FOLLOWING')
|
|
51
|
+
self.assertEqual(window.params, (1,))
|
|
52
52
|
|
|
53
53
|
def test_window_exclude(self):
|
|
54
54
|
t = Table('t')
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|