deriva 1.7.3__py3-none-any.whl → 1.7.5__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.
- deriva/config/rollback_annotation.py +1 -1
- deriva/core/__init__.py +1 -1
- deriva/core/datapath.py +55 -55
- deriva/core/ermrest_model.py +702 -186
- deriva/core/hatrac_cli.py +3 -5
- deriva/core/hatrac_store.py +9 -20
- deriva/core/mmo.py +379 -0
- deriva/transfer/download/processors/postprocess/transfer_post_processor.py +2 -2
- deriva/transfer/download/processors/query/base_query_processor.py +2 -1
- deriva/transfer/upload/deriva_upload.py +4 -0
- {deriva-1.7.3.dist-info → deriva-1.7.5.dist-info}/METADATA +1 -1
- {deriva-1.7.3.dist-info → deriva-1.7.5.dist-info}/RECORD +25 -16
- tests/deriva/core/mmo/__init__.py +0 -0
- tests/deriva/core/mmo/base.py +300 -0
- tests/deriva/core/mmo/test_mmo_drop.py +252 -0
- tests/deriva/core/mmo/test_mmo_find.py +90 -0
- tests/deriva/core/mmo/test_mmo_prune.py +196 -0
- tests/deriva/core/mmo/test_mmo_rename.py +222 -0
- tests/deriva/core/mmo/test_mmo_replace.py +180 -0
- tests/deriva/core/test_datapath.py +52 -26
- tests/deriva/core/test_ermrest_model.py +782 -0
- {deriva-1.7.3.dist-info → deriva-1.7.5.dist-info}/LICENSE +0 -0
- {deriva-1.7.3.dist-info → deriva-1.7.5.dist-info}/WHEEL +0 -0
- {deriva-1.7.3.dist-info → deriva-1.7.5.dist-info}/entry_points.txt +0 -0
- {deriva-1.7.3.dist-info → deriva-1.7.5.dist-info}/top_level.txt +0 -0
deriva/core/ermrest_model.py
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import base64
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
4
8
|
from collections import OrderedDict
|
|
5
9
|
from collections.abc import Iterable
|
|
6
10
|
from enum import Enum
|
|
7
|
-
import json
|
|
8
|
-
import re
|
|
9
|
-
import base64
|
|
10
|
-
import hashlib
|
|
11
11
|
|
|
12
|
-
from . import AttrDict, tag, urlquote, stob
|
|
12
|
+
from . import AttrDict, tag, urlquote, stob, mmo
|
|
13
|
+
|
|
13
14
|
|
|
14
15
|
class NoChange (object):
|
|
15
16
|
"""Special class used to distinguish no-change default arguments to methods.
|
|
@@ -23,6 +24,12 @@ class NoChange (object):
|
|
|
23
24
|
# singletone to use in APIs below
|
|
24
25
|
nochange = NoChange()
|
|
25
26
|
|
|
27
|
+
class UpdateMappings (str, Enum):
|
|
28
|
+
"""Update Mappings flag enum"""
|
|
29
|
+
no_update = ''
|
|
30
|
+
deferred = 'deferred'
|
|
31
|
+
immediate = 'immediate'
|
|
32
|
+
|
|
26
33
|
def make_id(*components):
|
|
27
34
|
"""Build an identifier that will be OK for ERMrest and Postgres.
|
|
28
35
|
|
|
@@ -91,6 +98,19 @@ def make_id(*components):
|
|
|
91
98
|
# last-ditch (e.g. multibyte unicode suffix worst case)
|
|
92
99
|
return truncate(naive_result, 55) + naive_hash
|
|
93
100
|
|
|
101
|
+
def sql_identifier(s):
|
|
102
|
+
# double " to protect from SQL
|
|
103
|
+
return '"%s"' % (s.replace('"', '""'))
|
|
104
|
+
|
|
105
|
+
def sql_literal(v):
|
|
106
|
+
if v is None:
|
|
107
|
+
return 'NULL'
|
|
108
|
+
if type(v) is list:
|
|
109
|
+
s = json.dumps(v)
|
|
110
|
+
# double ' to protect from SQL
|
|
111
|
+
s = '%s' % v
|
|
112
|
+
return "'%s'" % (s.replace("'", "''"))
|
|
113
|
+
|
|
94
114
|
def presence_annotation(tag_uri):
|
|
95
115
|
"""Decorator to establish property getter/setter/deleter for presence annotations.
|
|
96
116
|
|
|
@@ -521,6 +541,15 @@ class Schema (object):
|
|
|
521
541
|
for tname, tdoc in schema_doc.get('tables', {}).items()
|
|
522
542
|
}
|
|
523
543
|
|
|
544
|
+
def __repr__(self):
|
|
545
|
+
cls = type(self)
|
|
546
|
+
return "<%s.%s object %r at 0x%x>" % (
|
|
547
|
+
cls.__module__,
|
|
548
|
+
cls.__name__,
|
|
549
|
+
self.name,
|
|
550
|
+
id(self),
|
|
551
|
+
)
|
|
552
|
+
|
|
524
553
|
@property
|
|
525
554
|
def catalog(self):
|
|
526
555
|
return self.model.catalog
|
|
@@ -625,16 +654,16 @@ class Schema (object):
|
|
|
625
654
|
for tname, table in self.tables.items():
|
|
626
655
|
table.apply(existing.tables[tname] if existing else None)
|
|
627
656
|
|
|
628
|
-
def alter(self, schema_name=nochange, comment=nochange, acls=nochange, annotations=nochange):
|
|
657
|
+
def alter(self, schema_name=nochange, comment=nochange, acls=nochange, annotations=nochange, update_mappings=UpdateMappings.no_update):
|
|
629
658
|
"""Alter existing schema definition.
|
|
630
659
|
|
|
631
660
|
:param schema_name: Replacement schema name (default nochange)
|
|
632
661
|
:param comment: Replacement comment (default nochange)
|
|
633
662
|
:param acls: Replacement ACL configuration (default nochange)
|
|
634
663
|
:param annotations: Replacement annotations (default nochange)
|
|
664
|
+
:param update_mappings: Update annotations to reflect changes (default UpdateMappings.no_updates)
|
|
635
665
|
|
|
636
666
|
Returns self (to allow for optional chained access).
|
|
637
|
-
|
|
638
667
|
"""
|
|
639
668
|
changes = strip_nochange({
|
|
640
669
|
'schema_name': schema_name,
|
|
@@ -648,9 +677,14 @@ class Schema (object):
|
|
|
648
677
|
changed = r.json() # use changed vs changes to get server-digested values
|
|
649
678
|
|
|
650
679
|
if 'schema_name' in changes:
|
|
680
|
+
old_schema_name = self.name
|
|
651
681
|
del self.model.schemas[self.name]
|
|
652
682
|
self.name = changed['schema_name']
|
|
653
683
|
self.model.schemas[self.name] = self
|
|
684
|
+
if update_mappings:
|
|
685
|
+
mmo.replace(self.model, [old_schema_name, None], [self.name, None])
|
|
686
|
+
if update_mappings == UpdateMappings.immediate:
|
|
687
|
+
self.model.apply()
|
|
654
688
|
|
|
655
689
|
if 'comment' in changes:
|
|
656
690
|
self.comment = changed['comment']
|
|
@@ -686,11 +720,19 @@ class Schema (object):
|
|
|
686
720
|
self.model.digest_fkeys()
|
|
687
721
|
return newtable
|
|
688
722
|
|
|
689
|
-
def drop(self):
|
|
723
|
+
def drop(self, cascade=False, update_mappings=UpdateMappings.no_update):
|
|
690
724
|
"""Remove this schema from the remote database.
|
|
725
|
+
|
|
726
|
+
:param cascade: drop dependent objects.
|
|
727
|
+
:param update_mappings: Update annotations to reflect changes (default UpdateMappings.no_updates)
|
|
691
728
|
"""
|
|
692
729
|
if self.name not in self.model.schemas:
|
|
693
730
|
raise ValueError('Schema %s does not appear to belong to model.' % (self,))
|
|
731
|
+
|
|
732
|
+
if cascade:
|
|
733
|
+
for table in list(self.tables.values()):
|
|
734
|
+
table.drop(cascade=True, update_mappings=update_mappings)
|
|
735
|
+
|
|
694
736
|
self.catalog.delete(self.uri_path).raise_for_status()
|
|
695
737
|
del self.model.schemas[self.name]
|
|
696
738
|
for table in self.tables.values():
|
|
@@ -770,6 +812,8 @@ class FindAssociationResult (object):
|
|
|
770
812
|
class Table (object):
|
|
771
813
|
"""Named table.
|
|
772
814
|
"""
|
|
815
|
+
default_key_column_search_order = ["Name", "name", "ID", "id"]
|
|
816
|
+
|
|
773
817
|
def __init__(self, schema, tname, table_doc):
|
|
774
818
|
self.schema = schema
|
|
775
819
|
self.name = tname
|
|
@@ -792,6 +836,16 @@ class Table (object):
|
|
|
792
836
|
])
|
|
793
837
|
self.referenced_by = KeyedList([])
|
|
794
838
|
|
|
839
|
+
def __repr__(self):
|
|
840
|
+
cls = type(self)
|
|
841
|
+
return "<%s.%s object %r.%r at 0x%x>" % (
|
|
842
|
+
cls.__module__,
|
|
843
|
+
cls.__name__,
|
|
844
|
+
self.schema.name if self.schema is not None else None,
|
|
845
|
+
self.name,
|
|
846
|
+
id(self),
|
|
847
|
+
)
|
|
848
|
+
|
|
795
849
|
@property
|
|
796
850
|
def columns(self):
|
|
797
851
|
"""Sugared access to self.column_definitions"""
|
|
@@ -825,20 +879,211 @@ class Table (object):
|
|
|
825
879
|
def system_key_defs(cls, custom=[]):
|
|
826
880
|
"""Build standard system key definitions, merging optional custom definitions."""
|
|
827
881
|
def ktup(k):
|
|
828
|
-
return
|
|
882
|
+
return frozenset(k['unique_columns'])
|
|
883
|
+
customized = { ktup(kdef): kdef for kdef in custom }
|
|
829
884
|
return [
|
|
830
885
|
kdef for kdef in [
|
|
831
886
|
Key.define(['RID'])
|
|
832
887
|
]
|
|
833
|
-
if ktup(kdef) not in
|
|
888
|
+
if ktup(kdef) not in customized
|
|
834
889
|
] + custom
|
|
835
890
|
|
|
836
891
|
@classmethod
|
|
837
|
-
def
|
|
892
|
+
def system_fkey_defs(cls, tname, custom=[]):
|
|
893
|
+
"""Build standard system fkey definitions, merging optional custom definitions."""
|
|
894
|
+
def fktup(fk):
|
|
895
|
+
return (
|
|
896
|
+
fk["referenced_columns"][0]["schema_name"],
|
|
897
|
+
fk["referenced_columns"][0]["table_name"],
|
|
898
|
+
frozenset(zip(
|
|
899
|
+
tuple( c["column_name"] for c in fk["foreign_key_columns"] ),
|
|
900
|
+
tuple( c["column_name"] for c in fk["referenced_columns"] ),
|
|
901
|
+
))
|
|
902
|
+
)
|
|
903
|
+
customized = { fktup(fkdef) for fkdef in custom }
|
|
904
|
+
return [
|
|
905
|
+
fkdef for fkdef in [
|
|
906
|
+
ForeignKey.define(
|
|
907
|
+
["RCB"], "public", "ERMrest_Client", ["ID"],
|
|
908
|
+
constraint_name=make_id(tname, "RCB", "fkey"),
|
|
909
|
+
),
|
|
910
|
+
ForeignKey.define(
|
|
911
|
+
["RMB"], "public", "ERMrest_Client", ["ID"],
|
|
912
|
+
constraint_name=make_id(tname, "RMB", "fkey"),
|
|
913
|
+
),
|
|
914
|
+
]
|
|
915
|
+
if fktup(fkdef) not in customized
|
|
916
|
+
] + custom
|
|
917
|
+
|
|
918
|
+
@classmethod
|
|
919
|
+
def _expand_references(
|
|
920
|
+
cls,
|
|
921
|
+
table_name: str,
|
|
922
|
+
column_defs: list[Key | Table | dict | tuple[str, bool, Key | Table] | tuple[str, Key | Table]],
|
|
923
|
+
fkey_defs: Iterable[dict],
|
|
924
|
+
used_names: set[str] =set(),
|
|
925
|
+
key_column_search_order: Iterable[str] | None = None,
|
|
926
|
+
):
|
|
927
|
+
"""Expand implicit references in column_defs into actual column and fkey definitions.
|
|
928
|
+
|
|
929
|
+
:param table_name: Name of table, needed to build fkey constraint names
|
|
930
|
+
:param column_defs: List of column definitions and/or reference targets (see below)
|
|
931
|
+
:param fkey_defs: List of foreign key definitions
|
|
932
|
+
:param used_names: Set of reference base names to consider already in use
|
|
933
|
+
|
|
934
|
+
Each reference target may be one of:
|
|
935
|
+
- Key
|
|
936
|
+
- Table
|
|
937
|
+
- tuple (str, Key|Table)
|
|
938
|
+
- tuple (str, bool, Key|Table)
|
|
939
|
+
|
|
940
|
+
A target Key specifies the columns of the remote table to
|
|
941
|
+
reference. A target Table instance specifies the remote table
|
|
942
|
+
and relies on heuristics to choose the target Key. A target
|
|
943
|
+
tuple specifies a string "base name" and optionally a boolean
|
|
944
|
+
"nullok" for the implied referencing columns. When omitted, a
|
|
945
|
+
default base name is constructed from target table
|
|
946
|
+
information, and a default nullok=False is specified for
|
|
947
|
+
referencing columns. The key_column_search_order parameter
|
|
948
|
+
influences the heuristic for selecting a target Key for an
|
|
949
|
+
input target Table.
|
|
950
|
+
|
|
951
|
+
This method mutates the input column_defs and used_names
|
|
952
|
+
containers. This can be abused to chain state through a
|
|
953
|
+
sequence of calls.
|
|
954
|
+
|
|
955
|
+
Returns the column_defs and fkey_defs which includes input
|
|
956
|
+
definitions and implied definitions while removing the corresponding input
|
|
957
|
+
reference targets.
|
|
958
|
+
|
|
959
|
+
"""
|
|
960
|
+
out_column_defs = []
|
|
961
|
+
out_fkey_defs = list(fkey_defs)
|
|
962
|
+
|
|
963
|
+
# materialize for mutation and replay
|
|
964
|
+
column_defs = list(column_defs)
|
|
965
|
+
|
|
966
|
+
if key_column_search_order is not None:
|
|
967
|
+
# materialize iterable for reuse
|
|
968
|
+
key_column_search_order = list(key_column_search_order)
|
|
969
|
+
else:
|
|
970
|
+
key_column_search_order = cls.default_key_column_search_order
|
|
971
|
+
|
|
972
|
+
def check_basename(basename):
|
|
973
|
+
if not isinstance(base_name, str):
|
|
974
|
+
raise TypeError('Base name %r is not of required type str' % (base_name,))
|
|
975
|
+
if base_name in used_names:
|
|
976
|
+
raise ValueError('Base name %r is not unique among inputs' % (base_name,))
|
|
977
|
+
used_names.add(base_name)
|
|
978
|
+
|
|
979
|
+
def choose_basename(key):
|
|
980
|
+
base_name = key.table.name
|
|
981
|
+
n = 2
|
|
982
|
+
while base_name in used_names or any(used.startswith(base_name) for used in used_names):
|
|
983
|
+
base_name = '%s%d' % (key.table.name, n)
|
|
984
|
+
n += 1
|
|
985
|
+
used_names.add(base_name)
|
|
986
|
+
return base_name
|
|
987
|
+
|
|
988
|
+
def check_key(key):
|
|
989
|
+
if isinstance(key, Table):
|
|
990
|
+
# opportunistic case: prefer (non-nullable) "name" or "id" keys, if found
|
|
991
|
+
for cname in key_column_search_order:
|
|
992
|
+
try:
|
|
993
|
+
candidate = key.key_by_columns([cname])
|
|
994
|
+
if not candidate.unique_columns[0].nullok:
|
|
995
|
+
return candidate
|
|
996
|
+
except (KeyError, ValueError) as e:
|
|
997
|
+
continue
|
|
998
|
+
|
|
999
|
+
# general case: try to use RID key
|
|
1000
|
+
try:
|
|
1001
|
+
return key.key_by_columns(["RID"])
|
|
1002
|
+
except (KeyError, ValueError) as e:
|
|
1003
|
+
raise ValueError('Could not determine default key for table %s' % (key,))
|
|
1004
|
+
elif isinstance(key, Key):
|
|
1005
|
+
return key
|
|
1006
|
+
raise TypeError('Expected Key or Table instance as target reference, not %s' % (key,))
|
|
1007
|
+
|
|
1008
|
+
# check and normalize cdefs into list[(str, Key)] with distinct base names
|
|
1009
|
+
for i in range(len(column_defs)):
|
|
1010
|
+
if isinstance(column_defs[i], tuple):
|
|
1011
|
+
if len(column_defs[i]) == 2:
|
|
1012
|
+
base_name, key = column_defs[i]
|
|
1013
|
+
nullok = False
|
|
1014
|
+
elif len(column_defs[i]) == 3:
|
|
1015
|
+
base_name, nullok, key = column_defs[i]
|
|
1016
|
+
else:
|
|
1017
|
+
raise ValueError('Expected column definition tuple (str, Key|Table) or (str, bool, Key|Table), not %s' % (len(column_defs[i]),))
|
|
1018
|
+
check_basename(base_name)
|
|
1019
|
+
key = check_key(key)
|
|
1020
|
+
column_defs[i] = (base_name, nullok, key)
|
|
1021
|
+
elif isinstance(column_defs[i], (Key, Table)):
|
|
1022
|
+
key = check_key(column_defs[i])
|
|
1023
|
+
base_name = choose_basename(key)
|
|
1024
|
+
column_defs[i] = (base_name, False, key)
|
|
1025
|
+
elif isinstance(column_defs[i], dict):
|
|
1026
|
+
pass
|
|
1027
|
+
else:
|
|
1028
|
+
raise TypeError('Expected column definition dict, Key, or Table input, not %s' % (column_defs[i],))
|
|
1029
|
+
|
|
1030
|
+
def simplify_type(ctype):
|
|
1031
|
+
if ctype.is_domain and ctype.typename.startswith('ermrest_'):
|
|
1032
|
+
return ctype.base_type
|
|
1033
|
+
return ctype
|
|
1034
|
+
|
|
1035
|
+
def cdefs_for_key(base_name, nullok, key):
|
|
1036
|
+
return [
|
|
1037
|
+
Column.define(
|
|
1038
|
+
'%s_%s' % (base_name, col.name) if len(key.unique_columns) > 1 else base_name,
|
|
1039
|
+
simplify_type(col.type),
|
|
1040
|
+
nullok=nullok,
|
|
1041
|
+
)
|
|
1042
|
+
for col in key.unique_columns
|
|
1043
|
+
]
|
|
1044
|
+
|
|
1045
|
+
def fkdef_for_key(base_name, nullok, key):
|
|
1046
|
+
return ForeignKey.define(
|
|
1047
|
+
[
|
|
1048
|
+
'%s_%s' % (base_name, col.name) if len(key.unique_columns) > 1 else base_name
|
|
1049
|
+
for col in key.unique_columns
|
|
1050
|
+
],
|
|
1051
|
+
key.table.schema.name,
|
|
1052
|
+
key.table.name,
|
|
1053
|
+
[ col.name for col in key.unique_columns ],
|
|
1054
|
+
on_update='CASCADE',
|
|
1055
|
+
on_delete='CASCADE',
|
|
1056
|
+
constraint_name=make_id(table_name, base_name, 'fkey'),
|
|
1057
|
+
)
|
|
1058
|
+
|
|
1059
|
+
for cdef in column_defs:
|
|
1060
|
+
if isinstance(cdef, tuple):
|
|
1061
|
+
out_column_defs.extend(cdefs_for_key(*cdef))
|
|
1062
|
+
out_fkey_defs.append(fkdef_for_key(*cdef))
|
|
1063
|
+
else:
|
|
1064
|
+
out_column_defs.append(cdef)
|
|
1065
|
+
|
|
1066
|
+
return out_column_defs, out_fkey_defs
|
|
1067
|
+
|
|
1068
|
+
@classmethod
|
|
1069
|
+
def define(
|
|
1070
|
+
cls,
|
|
1071
|
+
tname: str,
|
|
1072
|
+
column_defs: Iterable[dict | Key | Table | tuple[str, bool, Key | Table] | tuple[str, Key | Table]] = [],
|
|
1073
|
+
key_defs: Iterable[dict] = [],
|
|
1074
|
+
fkey_defs: Iterable[dict] = [],
|
|
1075
|
+
comment: str | None = None,
|
|
1076
|
+
acls: dict = {},
|
|
1077
|
+
acl_bindings: dict = {},
|
|
1078
|
+
annotations: dict = {},
|
|
1079
|
+
provide_system: bool = True,
|
|
1080
|
+
provide_system_fkeys: book = True,
|
|
1081
|
+
key_column_search_order: Iterable[str] | None = None,
|
|
1082
|
+
):
|
|
838
1083
|
"""Build a table definition.
|
|
839
1084
|
|
|
840
1085
|
:param tname: the name of the newly defined table
|
|
841
|
-
:param column_defs: a list of Column.define() results
|
|
1086
|
+
:param column_defs: a list of custom Column.define() results and/or reference targets (see below)
|
|
842
1087
|
:param key_defs: a list of Key.define() results for extra or overridden key constraint definitions
|
|
843
1088
|
:param fkey_defs: a list of ForeignKey.define() results for foreign key definitions
|
|
844
1089
|
:param comment: a comment string for the table
|
|
@@ -846,11 +1091,38 @@ class Table (object):
|
|
|
846
1091
|
:param acl_bindings: a dictionary of dynamic ACL bindings
|
|
847
1092
|
:param annotations: a dictionary of annotations
|
|
848
1093
|
:param provide_system: whether to inject standard system column definitions when missing from column_defs
|
|
1094
|
+
:param provide_system_fkeys: whether to also inject foreign key definitions for RCB/RMB
|
|
1095
|
+
:param key_column_search_order: override heuristic for choosing a Key from a Table input
|
|
1096
|
+
|
|
1097
|
+
Each reference target may be one of:
|
|
1098
|
+
- Key
|
|
1099
|
+
- Table
|
|
1100
|
+
- tuple (str, Key|Table)
|
|
1101
|
+
- tuple (str, bool, Key|Table)
|
|
1102
|
+
|
|
1103
|
+
A target Key specifies the columns of the remote table to
|
|
1104
|
+
reference. A target Table instance specifies the remote table
|
|
1105
|
+
and relies on heuristics to choose the target Key. A target
|
|
1106
|
+
tuple specifies a string "base name" and optionally a boolean
|
|
1107
|
+
"nullok" for the implied referencing columns. When omitted, a
|
|
1108
|
+
default base name is constructed from target table
|
|
1109
|
+
information, and a default nullok=False is specified for
|
|
1110
|
+
referencing columns. The key_column_search_order parameter
|
|
1111
|
+
influences the heuristic for selecting a target Key for an
|
|
1112
|
+
input target Table.
|
|
849
1113
|
|
|
850
1114
|
"""
|
|
1115
|
+
column_defs = list(column_defs) # materialize to allow replay
|
|
1116
|
+
used_names = { cdef["name"] for cdef in column_defs if isinstance(cdef, dict) and 'name' in cdef }
|
|
1117
|
+
column_defs, fkey_defs = cls._expand_references(tname, column_defs, fkey_defs, used_names, key_column_search_order)
|
|
1118
|
+
|
|
851
1119
|
if provide_system:
|
|
852
1120
|
column_defs = cls.system_column_defs(column_defs)
|
|
853
1121
|
key_defs = cls.system_key_defs(key_defs)
|
|
1122
|
+
if provide_system_fkeys:
|
|
1123
|
+
fkey_defs = cls.system_fkey_defs(tname, fkey_defs)
|
|
1124
|
+
else:
|
|
1125
|
+
key_defs = list(key_defs)
|
|
854
1126
|
|
|
855
1127
|
return {
|
|
856
1128
|
'table_name': tname,
|
|
@@ -864,13 +1136,29 @@ class Table (object):
|
|
|
864
1136
|
}
|
|
865
1137
|
|
|
866
1138
|
@classmethod
|
|
867
|
-
def define_vocabulary(
|
|
1139
|
+
def define_vocabulary(
|
|
1140
|
+
cls,
|
|
1141
|
+
tname: str,
|
|
1142
|
+
curie_template: str,
|
|
1143
|
+
uri_template: str = '/id/{RID}',
|
|
1144
|
+
column_defs: Iterable[dict | Key | Table | tuple[str, bool, Key | Table] | tuple[str, Key | Table]] = [],
|
|
1145
|
+
key_defs=[],
|
|
1146
|
+
fkey_defs=[],
|
|
1147
|
+
comment: str | None = None,
|
|
1148
|
+
acls: dict = {},
|
|
1149
|
+
acl_bindings: dict = {},
|
|
1150
|
+
annotations: dict = {},
|
|
1151
|
+
provide_system: bool = True,
|
|
1152
|
+
provide_system_fkeys: bool = True,
|
|
1153
|
+
provide_name_key: bool = True,
|
|
1154
|
+
key_column_search_order: Iterable[str] | None = None,
|
|
1155
|
+
):
|
|
868
1156
|
"""Build a vocabulary table definition.
|
|
869
1157
|
|
|
870
1158
|
:param tname: the name of the newly defined table
|
|
871
1159
|
:param curie_template: the RID-based template for the CURIE of locally-defined terms, e.g. 'MYPROJECT:{RID}'
|
|
872
1160
|
:param uri_template: the RID-based template for the URI of locally-defined terms, e.g. 'https://server.example.org/id/{RID}'
|
|
873
|
-
:param column_defs: a list of Column.define() results
|
|
1161
|
+
:param column_defs: a list of Column.define() results and/or reference targets (see below)
|
|
874
1162
|
:param key_defs: a list of Key.define() results for extra or overridden key constraint definitions
|
|
875
1163
|
:param fkey_defs: a list of ForeignKey.define() results for foreign key definitions
|
|
876
1164
|
:param comment: a comment string for the table
|
|
@@ -878,6 +1166,9 @@ class Table (object):
|
|
|
878
1166
|
:param acl_bindings: a dictionary of dynamic ACL bindings
|
|
879
1167
|
:param annotations: a dictionary of annotations
|
|
880
1168
|
:param provide_system: whether to inject standard system column definitions when missing from column_defs
|
|
1169
|
+
:param provide_system_fkeys: whether to also inject foreign key definitions for RCB/RMB
|
|
1170
|
+
:param provide_name_key: whether to inject a key definition for the Name column
|
|
1171
|
+
:param key_column_search_order: override heuristic for choosing a Key from a Table input
|
|
881
1172
|
|
|
882
1173
|
These core vocabulary columns are generated automatically if
|
|
883
1174
|
absent from the input column_defs.
|
|
@@ -888,7 +1179,10 @@ class Table (object):
|
|
|
888
1179
|
- Description: markdown, not null
|
|
889
1180
|
- Synonyms: text[]
|
|
890
1181
|
|
|
891
|
-
However, caller-supplied definitions override the default.
|
|
1182
|
+
However, caller-supplied definitions override the default. See
|
|
1183
|
+
Table.define() documentation for an explanation reference
|
|
1184
|
+
targets in the column_defs list and the related
|
|
1185
|
+
key_column_search_order parameter.
|
|
892
1186
|
|
|
893
1187
|
"""
|
|
894
1188
|
if not re.match("^[A-Za-z][-_A-Za-z0-9]*:[{]RID[}]$", curie_template):
|
|
@@ -938,41 +1232,52 @@ class Table (object):
|
|
|
938
1232
|
|
|
939
1233
|
def add_vocab_keys(custom):
|
|
940
1234
|
def ktup(k):
|
|
941
|
-
return
|
|
1235
|
+
return frozenset(k['unique_columns'])
|
|
942
1236
|
return [
|
|
943
1237
|
key_def
|
|
944
1238
|
for key_def in [
|
|
945
1239
|
Key.define(['ID']),
|
|
946
1240
|
Key.define(['URI']),
|
|
947
|
-
]
|
|
1241
|
+
] + ([ Key.define(['Name']) ] if provide_name_key else [])
|
|
948
1242
|
if ktup(key_def) not in { ktup(kdef): kdef for kdef in custom }
|
|
949
1243
|
] + custom
|
|
950
1244
|
|
|
1245
|
+
used_names = {'ID', 'URI', 'Name', 'Description', 'Synonyms'}
|
|
1246
|
+
column_defs, fkey_defs = cls._expand_references(tname, column_defs, fkey_defs, used_names, key_column_search_order)
|
|
1247
|
+
column_defs = add_vocab_columns(column_defs)
|
|
1248
|
+
key_defs = add_vocab_keys(key_defs)
|
|
1249
|
+
|
|
951
1250
|
return cls.define(
|
|
952
1251
|
tname,
|
|
953
|
-
|
|
954
|
-
|
|
1252
|
+
column_defs,
|
|
1253
|
+
key_defs,
|
|
955
1254
|
fkey_defs,
|
|
956
1255
|
comment,
|
|
957
1256
|
acls,
|
|
958
1257
|
acl_bindings,
|
|
959
1258
|
annotations,
|
|
960
|
-
provide_system
|
|
1259
|
+
provide_system=provide_system,
|
|
1260
|
+
provide_system_fkeys=provide_system_fkeys,
|
|
1261
|
+
key_column_search_order=key_column_search_order,
|
|
961
1262
|
)
|
|
962
1263
|
|
|
963
1264
|
@classmethod
|
|
964
|
-
def define_asset(
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1265
|
+
def define_asset(
|
|
1266
|
+
cls,
|
|
1267
|
+
sname: str,
|
|
1268
|
+
tname: str,
|
|
1269
|
+
hatrac_template=None,
|
|
1270
|
+
column_defs: Iterable[dict | Key | Table | tuple[str, bool, Key | Table] | tuple[str, Key | Table]] = [],
|
|
1271
|
+
key_defs=[],
|
|
1272
|
+
fkey_defs=[],
|
|
1273
|
+
comment: str | None = None,
|
|
1274
|
+
acls: dict = {},
|
|
1275
|
+
acl_bindings: dict = {},
|
|
1276
|
+
annotations: dict = {},
|
|
1277
|
+
provide_system: bool = True,
|
|
1278
|
+
provide_system_fkeys: bool = True,
|
|
1279
|
+
key_column_search_order: Iterable[str] | None = None,
|
|
1280
|
+
):
|
|
976
1281
|
"""Build an asset table definition.
|
|
977
1282
|
|
|
978
1283
|
:param sname: the name of the schema for the asset table
|
|
@@ -982,7 +1287,7 @@ class Table (object):
|
|
|
982
1287
|
/hatrac/schema_name/table_name/md5.filename
|
|
983
1288
|
where the filename and md5 value is computed on upload and the schema_name and table_name are the
|
|
984
1289
|
values of the provided arguments. If value is set to False, no hatrac_template is used.
|
|
985
|
-
:param column_defs: a list of Column.define() results
|
|
1290
|
+
:param column_defs: a list of Column.define() results and/or reference targets (see below)
|
|
986
1291
|
:param key_defs: a list of Key.define() results for extra or overridden key constraint definitions
|
|
987
1292
|
:param fkey_defs: a list of ForeignKey.define() results for foreign key definitions
|
|
988
1293
|
:param comment: a comment string for the table
|
|
@@ -990,22 +1295,28 @@ class Table (object):
|
|
|
990
1295
|
:param acl_bindings: a dictionary of dynamic ACL bindings
|
|
991
1296
|
:param annotations: a dictionary of annotations
|
|
992
1297
|
:param provide_system: whether to inject standard system column definitions when missing from column_defs
|
|
1298
|
+
:param provide_system_fkeys: whether to also inject foreign key definitions for RCB/RMB
|
|
1299
|
+
:param key_column_search_order: override heuristic for choosing a Key from a Table input
|
|
993
1300
|
|
|
994
1301
|
These core asset table columns are generated automatically if
|
|
995
1302
|
absent from the input column_defs.
|
|
996
1303
|
|
|
997
1304
|
- Filename: ermrest_curie, unique not null, default curie template "%s:{RID}" % curie_prefix
|
|
998
1305
|
- URL: Location of the asset, unique not null. Default template is:
|
|
999
|
-
/hatrac/sname/tname/{{{MD5}}}.{{{Filename}}} where tname is the name of the asset table.
|
|
1306
|
+
/hatrac/cat_id/sname/tname/{{{MD5}}}.{{{Filename}}} where tname is the name of the asset table.
|
|
1000
1307
|
- Length: Length of the asset.
|
|
1001
1308
|
- MD5: text
|
|
1002
1309
|
- Description: markdown, not null
|
|
1003
1310
|
|
|
1004
|
-
However, caller-supplied definitions override the
|
|
1311
|
+
However, caller-supplied definitions override the
|
|
1312
|
+
default. See Table.define() documentation for an explanation
|
|
1313
|
+
reference targets in the column_defs list and the related
|
|
1314
|
+
key_column_search_order parameter.
|
|
1005
1315
|
|
|
1006
1316
|
In addition to creating the columns, this function also creates an asset annotation on the URL column to
|
|
1007
1317
|
facilitate use of the table by Chaise.
|
|
1008
|
-
|
|
1318
|
+
|
|
1319
|
+
"""
|
|
1009
1320
|
|
|
1010
1321
|
if hatrac_template is None:
|
|
1011
1322
|
hatrac_template = '/hatrac/{{$catalog.id}}/%s/%s/{{{MD5}}}.{{#encode}}{{{Filename}}}{{/encode}}' % (sname, tname)
|
|
@@ -1039,7 +1350,7 @@ class Table (object):
|
|
|
1039
1350
|
|
|
1040
1351
|
def add_asset_keys(custom):
|
|
1041
1352
|
def ktup(k):
|
|
1042
|
-
return
|
|
1353
|
+
return frozenset(k['unique_columns'])
|
|
1043
1354
|
|
|
1044
1355
|
return [
|
|
1045
1356
|
key_def
|
|
@@ -1060,6 +1371,9 @@ class Table (object):
|
|
|
1060
1371
|
asset_annotations.update(custom)
|
|
1061
1372
|
return asset_annotations
|
|
1062
1373
|
|
|
1374
|
+
used_names = {'URL', 'Filename', 'Description', 'Length', 'MD5'}
|
|
1375
|
+
column_defs, fkey_defs = cls._expand_references(tname, column_defs, fkey_defs, used_names, key_column_search_order)
|
|
1376
|
+
|
|
1063
1377
|
return cls.define(
|
|
1064
1378
|
tname,
|
|
1065
1379
|
add_asset_columns(column_defs),
|
|
@@ -1069,29 +1383,49 @@ class Table (object):
|
|
|
1069
1383
|
acls,
|
|
1070
1384
|
acl_bindings,
|
|
1071
1385
|
add_asset_annotations(annotations),
|
|
1072
|
-
provide_system
|
|
1386
|
+
provide_system=provide_system,
|
|
1387
|
+
provide_system_fkeys=provide_system_fkeys,
|
|
1388
|
+
key_column_search_order=key_column_search_order,
|
|
1073
1389
|
)
|
|
1074
1390
|
|
|
1075
1391
|
@classmethod
|
|
1076
|
-
def define_page(
|
|
1392
|
+
def define_page(
|
|
1393
|
+
cls,
|
|
1394
|
+
tname,
|
|
1395
|
+
column_defs: Iterable[dict | Key | Table | tuple[str, bool, Key | Table] | tuple[str, Key | Table]] = [],
|
|
1396
|
+
key_defs=[],
|
|
1397
|
+
fkey_defs=[],
|
|
1398
|
+
comment: str | None = None,
|
|
1399
|
+
acls: dict = {},
|
|
1400
|
+
acl_bindings: dict = {},
|
|
1401
|
+
annotations: dict = {},
|
|
1402
|
+
provide_system: bool = True,
|
|
1403
|
+
provide_system_fkeys: bool = True,
|
|
1404
|
+
key_column_search_order: Iterable[str] | None = None,
|
|
1405
|
+
):
|
|
1077
1406
|
"""Build a wiki-like "page" table definition.
|
|
1078
1407
|
|
|
1079
1408
|
:param tname: the name of the newly defined table
|
|
1080
1409
|
:param column_defs: a list of Column.define() results for extra or overridden column definitions
|
|
1081
|
-
:param key_defs: a list of Key.define() results
|
|
1410
|
+
:param key_defs: a list of Key.define() results and/or reference targets (see below)
|
|
1082
1411
|
:param fkey_defs: a list of ForeignKey.define() results for foreign key definitions
|
|
1083
1412
|
:param comment: a comment string for the table
|
|
1084
1413
|
:param acls: a dictionary of ACLs for specific access modes
|
|
1085
1414
|
:param acl_bindings: a dictionary of dynamic ACL bindings
|
|
1086
1415
|
:param annotations: a dictionary of annotations
|
|
1087
1416
|
:param provide_system: whether to inject standard system column definitions when missing from column_defs
|
|
1417
|
+
:param provide_system_fkeys: whether to also inject foreign key definitions for RCB/RMB
|
|
1418
|
+
:param key_column_search_order: override heuristic for choosing a Key from a Table input
|
|
1088
1419
|
|
|
1089
1420
|
These core page columns are generated automatically if absent from the input column_defs.
|
|
1090
1421
|
|
|
1091
1422
|
- Title: text, unique not null
|
|
1092
1423
|
- Content: markdown
|
|
1093
1424
|
|
|
1094
|
-
However, caller-supplied definitions override the default.
|
|
1425
|
+
However, caller-supplied definitions override the default. See
|
|
1426
|
+
Table.define() documentation for an explanation reference
|
|
1427
|
+
targets in the column_defs list and the related
|
|
1428
|
+
key_column_search_order parameter.
|
|
1095
1429
|
"""
|
|
1096
1430
|
|
|
1097
1431
|
def add_page_columns(custom):
|
|
@@ -1116,7 +1450,7 @@ class Table (object):
|
|
|
1116
1450
|
|
|
1117
1451
|
def add_page_keys(custom):
|
|
1118
1452
|
def ktup(k):
|
|
1119
|
-
return
|
|
1453
|
+
return frozenset(k['unique_columns'])
|
|
1120
1454
|
return [
|
|
1121
1455
|
key_def
|
|
1122
1456
|
for key_def in [
|
|
@@ -1146,6 +1480,9 @@ class Table (object):
|
|
|
1146
1480
|
page_annotations.update(annotations)
|
|
1147
1481
|
return page_annotations
|
|
1148
1482
|
|
|
1483
|
+
used_names = {'Title', 'Content'}
|
|
1484
|
+
column_defs, fkey_defs = cls._expand_references(tname, column_defs, fkey_defs, used_names, key_column_search_order)
|
|
1485
|
+
|
|
1149
1486
|
return cls.define(
|
|
1150
1487
|
tname,
|
|
1151
1488
|
add_page_columns(column_defs),
|
|
@@ -1155,7 +1492,9 @@ class Table (object):
|
|
|
1155
1492
|
acls,
|
|
1156
1493
|
acl_bindings,
|
|
1157
1494
|
add_page_annotations(annotations),
|
|
1158
|
-
provide_system
|
|
1495
|
+
provide_system=provide_system,
|
|
1496
|
+
provide_system_fkeys=provide_system_fkeys,
|
|
1497
|
+
key_column_search_order=key_column_search_order,
|
|
1159
1498
|
)
|
|
1160
1499
|
|
|
1161
1500
|
@classmethod
|
|
@@ -1165,14 +1504,19 @@ class Table (object):
|
|
|
1165
1504
|
metadata: Iterable[Key | Table | dict | tuple[str, bool, Key | Table]] = [],
|
|
1166
1505
|
table_name: str | None = None,
|
|
1167
1506
|
comment: str | None = None,
|
|
1168
|
-
provide_system: bool = True
|
|
1507
|
+
provide_system: bool = True,
|
|
1508
|
+
provide_system_fkeys: bool = True,
|
|
1509
|
+
key_column_search_order: Iterable[str] | None = None,
|
|
1510
|
+
) -> dict:
|
|
1169
1511
|
"""Build an association table definition.
|
|
1170
1512
|
|
|
1171
|
-
:param associates:
|
|
1172
|
-
:param metadata: additional metadata fields for impure associations
|
|
1513
|
+
:param associates: reference targets being associated (see below)
|
|
1514
|
+
:param metadata: additional metadata fields and/or reference targets for impure associations
|
|
1173
1515
|
:param table_name: name for the association table or None for default naming
|
|
1174
1516
|
:param comment: comment for the association table or None for default comment
|
|
1175
1517
|
:param provide_system: add ERMrest system columns when True
|
|
1518
|
+
:param provide_system_fkeys: whether to also inject foreign key definitions for RCB/RMB
|
|
1519
|
+
:param key_column_search_order: override heuristic for choosing a Key from a Table input
|
|
1176
1520
|
|
|
1177
1521
|
This is a utility function to help build an association table
|
|
1178
1522
|
definition. It simplifies the task, but removes some
|
|
@@ -1188,154 +1532,91 @@ class Table (object):
|
|
|
1188
1532
|
An "impure" association table adds additional metadata
|
|
1189
1533
|
alongside the N foreign keys.
|
|
1190
1534
|
|
|
1191
|
-
The "associates" parameter takes an iterable of
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
the
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
will get a base name derived from the referened table name,
|
|
1209
|
-
and presumed as nullok=False.
|
|
1210
|
-
|
|
1211
|
-
If a Table instance is supplied instead of a Key instance for
|
|
1212
|
-
associates or metadata inputs, an attempt will be made to
|
|
1213
|
-
locate a key based on the RID system column. If this key
|
|
1214
|
-
cannot be found, a KeyError will be raised.
|
|
1535
|
+
The "associates" parameter takes an iterable of reference
|
|
1536
|
+
targets. The association will be comprised of foreign keys
|
|
1537
|
+
referencing these associates. This includes columns to store
|
|
1538
|
+
the associated foreign key values, foreign key constraints to
|
|
1539
|
+
the associated tables, and a composite key constraint
|
|
1540
|
+
covering all the associated foreign key values.
|
|
1541
|
+
|
|
1542
|
+
The "metadata" parameter takes an iterable Column.define()
|
|
1543
|
+
results and/or reference targets. The association table will
|
|
1544
|
+
be augmented with extra metadata columns and foreign keys as
|
|
1545
|
+
directed by these inputs.
|
|
1546
|
+
|
|
1547
|
+
See the Table.define() method documentation for more on
|
|
1548
|
+
reference targets. Association columns must be defined with
|
|
1549
|
+
nullok=False, so the associates parameter is restricted to a
|
|
1550
|
+
more limited form of reference target input without
|
|
1551
|
+
caller-controlled nullok boolean values.
|
|
1215
1552
|
|
|
1216
1553
|
"""
|
|
1217
1554
|
associates = list(associates)
|
|
1218
1555
|
metadata = list(metadata)
|
|
1219
1556
|
|
|
1557
|
+
if key_column_search_order is not None:
|
|
1558
|
+
# materialize iterable for reuse
|
|
1559
|
+
key_column_search_order = list(key_column_search_order)
|
|
1560
|
+
else:
|
|
1561
|
+
key_column_search_order = cls.default_key_column_search_order
|
|
1562
|
+
|
|
1220
1563
|
if len(associates) < 2:
|
|
1221
1564
|
raise ValueError('An association table requires at least 2 associates')
|
|
1222
1565
|
|
|
1223
|
-
cdefs = []
|
|
1224
|
-
kdefs = []
|
|
1225
|
-
fkdefs = []
|
|
1226
|
-
|
|
1227
1566
|
used_names = set()
|
|
1228
1567
|
|
|
1229
|
-
|
|
1230
|
-
if not isinstance(
|
|
1231
|
-
raise TypeError(
|
|
1232
|
-
if base_name in used_names:
|
|
1233
|
-
raise ValueError('Base name %r is not unique among associates and metadata' % (base_name,))
|
|
1234
|
-
used_names.add(base_name)
|
|
1235
|
-
|
|
1236
|
-
def choose_basename(key):
|
|
1237
|
-
base_name = key.table.name
|
|
1238
|
-
n = 2
|
|
1239
|
-
while base_name in used_names:
|
|
1240
|
-
base_name = '%s%d' % (key.table.name, n)
|
|
1241
|
-
n += 1
|
|
1242
|
-
used_names.add(base_name)
|
|
1243
|
-
return base_name
|
|
1244
|
-
|
|
1245
|
-
def check_key(key):
|
|
1246
|
-
if isinstance(key, Table):
|
|
1247
|
-
return key.key_by_columns(["RID"])
|
|
1248
|
-
return key
|
|
1568
|
+
for assoc in associates:
|
|
1569
|
+
if not isinstance(assoc, (tuple, Table, Key)):
|
|
1570
|
+
raise TypeError("Associates must be Table or Key instances, not %s" % (assoc,))
|
|
1249
1571
|
|
|
1250
|
-
#
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
check_basename(base_name)
|
|
1255
|
-
key = check_key(key)
|
|
1256
|
-
associates[i] = (base_name, key)
|
|
1257
|
-
else:
|
|
1258
|
-
key = check_key(associates[i])
|
|
1259
|
-
base_name = choose_basename(key)
|
|
1260
|
-
associates[i] = (base_name, key)
|
|
1572
|
+
# first pass: build "pure" association table parts
|
|
1573
|
+
# HACK: use dummy table name if we don't have one yet
|
|
1574
|
+
tname = table_name if table_name is not None else "dummy"
|
|
1575
|
+
cdefs, fkdefs = cls._expand_references(tname, associates, [], used_names)
|
|
1261
1576
|
|
|
1262
|
-
# build assoc table name if not provided
|
|
1263
1577
|
if table_name is None:
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
def fkdef_for_key(base_name, key):
|
|
1283
|
-
return ForeignKey.define(
|
|
1284
|
-
[
|
|
1285
|
-
'%s_%s' % (base_name, col.name) if len(key.unique_columns) > 1 else base_name
|
|
1286
|
-
for col in key.unique_columns
|
|
1287
|
-
],
|
|
1288
|
-
key.table.schema.name,
|
|
1289
|
-
key.table.name,
|
|
1290
|
-
[ col.name for col in key.unique_columns ],
|
|
1291
|
-
on_update='CASCADE',
|
|
1292
|
-
on_delete='CASCADE',
|
|
1293
|
-
constraint_name=make_id(table_name, base_name, 'fkey'),
|
|
1294
|
-
)
|
|
1295
|
-
|
|
1296
|
-
# build core association definition (i.e. the "pure" parts)
|
|
1578
|
+
# use first pass results to build table_name
|
|
1579
|
+
def get_assoc_name(assoc):
|
|
1580
|
+
if isinstance(assoc, tuple):
|
|
1581
|
+
table = assoc[1]
|
|
1582
|
+
elif isinstance(assoc, Key):
|
|
1583
|
+
table = key.table
|
|
1584
|
+
elif isinstance(assoc, Table):
|
|
1585
|
+
table = assoc
|
|
1586
|
+
else:
|
|
1587
|
+
raise ValueError("expected (str, Key|Table) | Key | Table, not %s" % (assoc,))
|
|
1588
|
+
return table.name
|
|
1589
|
+
table_name = make_id(*[ get_assoc_name(assoc) for assoc in associates ])
|
|
1590
|
+
# HACK: repeat first pass to make proper fkey def constraint names
|
|
1591
|
+
used_names = set()
|
|
1592
|
+
cdefs, fkdefs = cls._expand_references(table_name, associates, [], used_names)
|
|
1593
|
+
|
|
1594
|
+
# build assoc key from union of associates' foreign key columns
|
|
1297
1595
|
k_cnames = []
|
|
1298
|
-
for
|
|
1299
|
-
|
|
1300
|
-
fkdefs.append(fkdef_for_key(base_name, key))
|
|
1301
|
-
|
|
1302
|
-
k_cnames.extend([
|
|
1303
|
-
'%s_%s' % (base_name, col.name) if len(key.unique_columns) > 1 else base_name
|
|
1304
|
-
for col in key.unique_columns
|
|
1305
|
-
])
|
|
1596
|
+
for fkdef in fkdefs:
|
|
1597
|
+
k_cnames.extend([ colref["column_name"] for colref in fkdef["foreign_key_columns"] ])
|
|
1306
1598
|
|
|
1307
|
-
kdefs
|
|
1599
|
+
kdefs = [
|
|
1308
1600
|
Key.define(
|
|
1309
1601
|
k_cnames,
|
|
1310
1602
|
constraint_name=make_id(table_name, 'assoc', 'key'),
|
|
1311
1603
|
)
|
|
1604
|
+
]
|
|
1605
|
+
|
|
1606
|
+
# run second pass to expand out metadata targets
|
|
1607
|
+
cdefs, fkdefs = cls._expand_references(table_name, cdefs + metadata, fkdefs, used_names)
|
|
1608
|
+
|
|
1609
|
+
return Table.define(
|
|
1610
|
+
table_name,
|
|
1611
|
+
cdefs,
|
|
1612
|
+
kdefs,
|
|
1613
|
+
fkdefs,
|
|
1614
|
+
comment=comment,
|
|
1615
|
+
provide_system=provide_system,
|
|
1616
|
+
provide_system_fkeys=provide_system_fkeys,
|
|
1617
|
+
key_column_search_order=key_column_search_order,
|
|
1312
1618
|
)
|
|
1313
1619
|
|
|
1314
|
-
# check and normalize metadata into list[dict | (str, bool, Key)]
|
|
1315
|
-
for i in range(len(metadata)):
|
|
1316
|
-
if isinstance(metadata[i], tuple):
|
|
1317
|
-
base_name, nullok, key = metadata[i]
|
|
1318
|
-
check_basename(base_name)
|
|
1319
|
-
key = check_key(key)
|
|
1320
|
-
metadata[i] = (base_name, nullok, key)
|
|
1321
|
-
elif isinstance(metadata[i], dict):
|
|
1322
|
-
pass
|
|
1323
|
-
else:
|
|
1324
|
-
key = check_key(metadata[i])
|
|
1325
|
-
base_name = choose_basename(key)
|
|
1326
|
-
metadata[i] = (base_name, False, key)
|
|
1327
|
-
|
|
1328
|
-
# add metadata to definition
|
|
1329
|
-
for md in metadata:
|
|
1330
|
-
if isinstance(md, dict):
|
|
1331
|
-
cdefs.append(md)
|
|
1332
|
-
else:
|
|
1333
|
-
base_name, nullok, key = md
|
|
1334
|
-
cdefs.extend(cdefs_for_key(base_name, key, nullok))
|
|
1335
|
-
fkdefs.append(fkdef_for_key(base_name, key))
|
|
1336
|
-
|
|
1337
|
-
return Table.define(table_name, cdefs, kdefs, fkdefs, comment=comment, provide_system=provide_system)
|
|
1338
|
-
|
|
1339
1620
|
def prejson(self, prune=True):
|
|
1340
1621
|
return {
|
|
1341
1622
|
"schema_name": self.schema.name,
|
|
@@ -1416,7 +1697,8 @@ class Table (object):
|
|
|
1416
1697
|
comment=nochange,
|
|
1417
1698
|
acls=nochange,
|
|
1418
1699
|
acl_bindings=nochange,
|
|
1419
|
-
annotations=nochange
|
|
1700
|
+
annotations=nochange,
|
|
1701
|
+
update_mappings=UpdateMappings.no_update
|
|
1420
1702
|
):
|
|
1421
1703
|
"""Alter existing schema definition.
|
|
1422
1704
|
|
|
@@ -1426,6 +1708,7 @@ class Table (object):
|
|
|
1426
1708
|
:param acls: Replacement ACL configuration (default nochange)
|
|
1427
1709
|
:param acl_bindings: Replacement ACL bindings (default nochange)
|
|
1428
1710
|
:param annotations: Replacement annotations (default nochange)
|
|
1711
|
+
:param update_mappings: Update annotations to reflect changes (default UpdateMappings.no_updates)
|
|
1429
1712
|
|
|
1430
1713
|
A change of schema name is a transfer of the existing table to
|
|
1431
1714
|
an existing destination schema (not a rename of the current
|
|
@@ -1453,14 +1736,25 @@ class Table (object):
|
|
|
1453
1736
|
self.schema.tables[self.name] = self
|
|
1454
1737
|
|
|
1455
1738
|
if 'schema_name' in changes:
|
|
1739
|
+
old_schema_name = self.schema.name
|
|
1456
1740
|
del self.schema.tables[self.name]
|
|
1457
1741
|
self.schema = self.schema.model.schemas[changed['schema_name']]
|
|
1742
|
+
for key in self.keys:
|
|
1743
|
+
if key.constraint_schema:
|
|
1744
|
+
key.constraint_schema = self.schema
|
|
1458
1745
|
for fkey in self.foreign_keys:
|
|
1459
1746
|
if fkey.constraint_schema:
|
|
1460
1747
|
del fkey.constraint_schema._fkeys[fkey.constraint_name]
|
|
1461
1748
|
fkey.constraint_schema = self.schema
|
|
1462
1749
|
fkey.constraint_schema._fkeys[fkey.constraint_name] = fkey
|
|
1463
1750
|
self.schema.tables[self.name] = self
|
|
1751
|
+
if update_mappings:
|
|
1752
|
+
for key in self.keys:
|
|
1753
|
+
mmo.replace(self.schema.model, [old_schema_name] + [key.constraint_name], [self.schema.name] + [key.constraint_name])
|
|
1754
|
+
for fkey in self.foreign_keys:
|
|
1755
|
+
mmo.replace(self.schema.model, [old_schema_name] + [fkey.constraint_name], [self.schema.name] + [fkey.constraint_name])
|
|
1756
|
+
if update_mappings == UpdateMappings.immediate:
|
|
1757
|
+
self.schema.model.apply()
|
|
1464
1758
|
|
|
1465
1759
|
if 'comment' in changes:
|
|
1466
1760
|
self.comment = changed['comment']
|
|
@@ -1501,7 +1795,7 @@ class Table (object):
|
|
|
1501
1795
|
created = created[0]
|
|
1502
1796
|
return registerfunc(constructor(self, created))
|
|
1503
1797
|
|
|
1504
|
-
def create_column(self, column_def):
|
|
1798
|
+
def create_column(self, column_def: dict) -> Column:
|
|
1505
1799
|
"""Add a new column to this table in the remote database based on column_def.
|
|
1506
1800
|
|
|
1507
1801
|
Returns a new Column instance based on the server-supplied
|
|
@@ -1516,7 +1810,7 @@ class Table (object):
|
|
|
1516
1810
|
return col
|
|
1517
1811
|
return self._create_table_part('column', add_column, Column, column_def)
|
|
1518
1812
|
|
|
1519
|
-
def create_key(self, key_def):
|
|
1813
|
+
def create_key(self, key_def: dict) -> Key:
|
|
1520
1814
|
"""Add a new key to this table in the remote database based on key_def.
|
|
1521
1815
|
|
|
1522
1816
|
Returns a new Key instance based on the server-supplied
|
|
@@ -1529,12 +1823,12 @@ class Table (object):
|
|
|
1529
1823
|
return key
|
|
1530
1824
|
return self._create_table_part('key', add_key, Key, key_def)
|
|
1531
1825
|
|
|
1532
|
-
def create_fkey(self, fkey_def):
|
|
1826
|
+
def create_fkey(self, fkey_def: dict) -> ForeignKey:
|
|
1533
1827
|
"""Add a new foreign key to this table in the remote database based on fkey_def.
|
|
1534
1828
|
|
|
1535
1829
|
Returns a new ForeignKey instance based on the
|
|
1536
1830
|
server-supplied representation of the new foreign key, and
|
|
1537
|
-
adds it to self.
|
|
1831
|
+
adds it to self.foreign_keys too.
|
|
1538
1832
|
|
|
1539
1833
|
"""
|
|
1540
1834
|
def add_fkey(fkey):
|
|
@@ -1543,16 +1837,53 @@ class Table (object):
|
|
|
1543
1837
|
return fkey
|
|
1544
1838
|
return self._create_table_part('foreignkey', add_fkey, ForeignKey, fkey_def)
|
|
1545
1839
|
|
|
1546
|
-
def
|
|
1840
|
+
def create_reference(
|
|
1841
|
+
self,
|
|
1842
|
+
target: Key | Table | tuple[str, bool, Key | Table] | tuple[str, Key | Table],
|
|
1843
|
+
key_column_search_order: Iterable[str] | None = None,
|
|
1844
|
+
) -> tuple[ list[Column], ForeignKey ]:
|
|
1845
|
+
"""Add column(s) and a foreign key to this table in the remote database for a reference target.
|
|
1846
|
+
|
|
1847
|
+
See Table.define() documentation for more about reference
|
|
1848
|
+
targets.
|
|
1849
|
+
|
|
1850
|
+
Returns a list of new Column instances and a ForeignKey instance based on
|
|
1851
|
+
the server-supplied representation of the new model elements, and
|
|
1852
|
+
adds them to the self.columns and self.foreign_keys too.
|
|
1853
|
+
|
|
1854
|
+
"""
|
|
1855
|
+
used_names = { col.name for col in self.columns }
|
|
1856
|
+
cdefs, fkdefs = self._expand_references(self.name, [ target ], [], used_names, key_column_search_order)
|
|
1857
|
+
if not cdefs or len(fkdefs) != 1:
|
|
1858
|
+
raise NotImplementedError("BUG? got unexpected results from self._expand_reference()")
|
|
1859
|
+
cols = [ self.create_column(cdef) for cdef in cdefs ]
|
|
1860
|
+
fkeys = [ self.create_fkey(fkdef) for fkdef in fkdefs ]
|
|
1861
|
+
return cols, fkeys[0]
|
|
1862
|
+
|
|
1863
|
+
def drop(self, cascade=False, update_mappings=UpdateMappings.no_update):
|
|
1547
1864
|
"""Remove this table from the remote database.
|
|
1865
|
+
|
|
1866
|
+
:param cascade: drop dependent objects.
|
|
1867
|
+
:param update_mappings: update annotations to reflect changes (default False)
|
|
1548
1868
|
"""
|
|
1549
1869
|
if self.name not in self.schema.tables:
|
|
1550
1870
|
raise ValueError('Table %s does not appear to belong to schema %s.' % (self, self.schema))
|
|
1871
|
+
|
|
1872
|
+
if cascade:
|
|
1873
|
+
for fkey in list(self.referenced_by):
|
|
1874
|
+
fkey.drop(update_mappings=update_mappings)
|
|
1875
|
+
|
|
1551
1876
|
self.catalog.delete(self.uri_path).raise_for_status()
|
|
1552
1877
|
del self.schema.tables[self.name]
|
|
1553
1878
|
for fkey in self.foreign_keys:
|
|
1554
1879
|
fkey._cleanup()
|
|
1555
1880
|
|
|
1881
|
+
if update_mappings:
|
|
1882
|
+
for fkey in self.foreign_keys:
|
|
1883
|
+
mmo.prune(self.schema.model, [fkey.constraint_schema.name, fkey.constraint_name])
|
|
1884
|
+
if update_mappings == UpdateMappings.immediate:
|
|
1885
|
+
self.schema.model.apply()
|
|
1886
|
+
|
|
1556
1887
|
def key_by_columns(self, unique_columns, raise_nomatch=True):
|
|
1557
1888
|
"""Return key from self.keys with matching unique columns.
|
|
1558
1889
|
|
|
@@ -1742,7 +2073,39 @@ class Table (object):
|
|
|
1742
2073
|
# arbitrarily choose first fkey to self
|
|
1743
2074
|
# in case association is back to same table
|
|
1744
2075
|
break
|
|
1745
|
-
|
|
2076
|
+
|
|
2077
|
+
def sqlite3_table_name(self) -> str:
|
|
2078
|
+
"""Return SQLite3 mapped table name for this table"""
|
|
2079
|
+
return "%s:%s" % (
|
|
2080
|
+
self.schema.name,
|
|
2081
|
+
self.name,
|
|
2082
|
+
)
|
|
2083
|
+
|
|
2084
|
+
def sqlite3_ddl(self, keys: bool=True) -> str:
|
|
2085
|
+
"""Return SQLite3 table definition DDL statement for this table.
|
|
2086
|
+
|
|
2087
|
+
:param keys: If true, include unique constraints for each table key
|
|
2088
|
+
|
|
2089
|
+
Caveat: this utility does not produce:
|
|
2090
|
+
- column default expressions
|
|
2091
|
+
- foreign key constraint DDL
|
|
2092
|
+
|
|
2093
|
+
Both of these features are fragile in data export scenarios
|
|
2094
|
+
where we want to represent arbitrary ERMrest catalog dumps.
|
|
2095
|
+
|
|
2096
|
+
"""
|
|
2097
|
+
parts = [ col.sqlite3_ddl() for col in self.columns ]
|
|
2098
|
+
if keys:
|
|
2099
|
+
parts.extend([ key.sqlite3_ddl() for key in self.keys ])
|
|
2100
|
+
return ("""
|
|
2101
|
+
CREATE TABLE IF NOT EXISTS %(tname)s (
|
|
2102
|
+
%(body)s
|
|
2103
|
+
);
|
|
2104
|
+
""" % {
|
|
2105
|
+
'tname': sql_identifier(self.sqlite3_table_name()),
|
|
2106
|
+
'body': ',\n '.join(parts),
|
|
2107
|
+
})
|
|
2108
|
+
|
|
1746
2109
|
@presence_annotation(tag.immutable)
|
|
1747
2110
|
def immutable(self): pass
|
|
1748
2111
|
|
|
@@ -1840,6 +2203,17 @@ class Column (object):
|
|
|
1840
2203
|
self.default = column_doc.get('default')
|
|
1841
2204
|
self.comment = column_doc.get('comment')
|
|
1842
2205
|
|
|
2206
|
+
def __repr__(self):
|
|
2207
|
+
cls = type(self)
|
|
2208
|
+
return "<%s.%s object %r.%r.%r at 0x%x>" % (
|
|
2209
|
+
cls.__module__,
|
|
2210
|
+
cls.__name__,
|
|
2211
|
+
self.table.schema.name if self.table is not None and self.table.schema is not None else None,
|
|
2212
|
+
self.table.name if self.table is not None else None,
|
|
2213
|
+
self.name,
|
|
2214
|
+
id(self),
|
|
2215
|
+
)
|
|
2216
|
+
|
|
1843
2217
|
@property
|
|
1844
2218
|
def catalog(self):
|
|
1845
2219
|
return self.table.schema.model.catalog
|
|
@@ -1934,7 +2308,8 @@ class Column (object):
|
|
|
1934
2308
|
comment=nochange,
|
|
1935
2309
|
acls=nochange,
|
|
1936
2310
|
acl_bindings=nochange,
|
|
1937
|
-
annotations=nochange
|
|
2311
|
+
annotations=nochange,
|
|
2312
|
+
update_mappings=UpdateMappings.no_update
|
|
1938
2313
|
):
|
|
1939
2314
|
"""Alter existing schema definition.
|
|
1940
2315
|
|
|
@@ -1946,6 +2321,7 @@ class Column (object):
|
|
|
1946
2321
|
:param acls: Replacement ACL configuration (default nochange)
|
|
1947
2322
|
:param acl_bindings: Replacement ACL bindings (default nochange)
|
|
1948
2323
|
:param annotations: Replacement annotations (default nochange)
|
|
2324
|
+
:param update_mappings: Update annotations to reflect changes (default UpdateMappings.no_updates)
|
|
1949
2325
|
|
|
1950
2326
|
Returns self (to allow for optional chained access).
|
|
1951
2327
|
|
|
@@ -1972,8 +2348,14 @@ class Column (object):
|
|
|
1972
2348
|
|
|
1973
2349
|
if 'name' in changes:
|
|
1974
2350
|
del self.table.column_definitions.elements[self.name]
|
|
2351
|
+
oldname = self.name
|
|
1975
2352
|
self.name = changed['name']
|
|
1976
2353
|
self.table.column_definitions.elements[self.name] = self
|
|
2354
|
+
if update_mappings:
|
|
2355
|
+
basename = [self.table.schema.name, self.table.name]
|
|
2356
|
+
mmo.replace(self.table.schema.model, basename + [oldname], basename + [self.name])
|
|
2357
|
+
if update_mappings == UpdateMappings.immediate:
|
|
2358
|
+
self.table.schema.model.apply()
|
|
1977
2359
|
|
|
1978
2360
|
if 'type' in changes:
|
|
1979
2361
|
self.type = make_type(changed['type'])
|
|
@@ -2001,14 +2383,41 @@ class Column (object):
|
|
|
2001
2383
|
|
|
2002
2384
|
return self
|
|
2003
2385
|
|
|
2004
|
-
def drop(self):
|
|
2386
|
+
def drop(self, cascade=False, update_mappings=UpdateMappings.no_update):
|
|
2005
2387
|
"""Remove this column from the remote database.
|
|
2388
|
+
|
|
2389
|
+
:param cascade: drop dependent objects (default False)
|
|
2390
|
+
:param update_mappings: Update annotations to reflect changes (default UpdateMappings.no_updates)
|
|
2006
2391
|
"""
|
|
2007
2392
|
if self.name not in self.table.column_definitions.elements:
|
|
2008
2393
|
raise ValueError('Column %s does not appear to belong to table %s.' % (self, self.table))
|
|
2394
|
+
|
|
2395
|
+
if cascade:
|
|
2396
|
+
for fkey in list(self.table.foreign_keys):
|
|
2397
|
+
if self in fkey.foreign_key_columns:
|
|
2398
|
+
fkey.drop(update_mappings=update_mappings)
|
|
2399
|
+
for key in list(self.table.keys):
|
|
2400
|
+
if self in key.unique_columns:
|
|
2401
|
+
key.drop(cascade=True, update_mappings=update_mappings)
|
|
2402
|
+
|
|
2009
2403
|
self.catalog.delete(self.uri_path).raise_for_status()
|
|
2010
2404
|
del self.table.column_definitions[self.name]
|
|
2011
2405
|
|
|
2406
|
+
if update_mappings:
|
|
2407
|
+
mmo.prune(self.table.schema.model, [self.table.schema.name, self.table.name, self.name])
|
|
2408
|
+
if update_mappings == UpdateMappings.immediate:
|
|
2409
|
+
self.table.schema.model.apply()
|
|
2410
|
+
|
|
2411
|
+
def sqlite3_ddl(self) -> str:
|
|
2412
|
+
"""Return SQLite3 column definition DDL fragment for this column."""
|
|
2413
|
+
parts = [
|
|
2414
|
+
sql_identifier(self.name),
|
|
2415
|
+
self.type.sqlite3_ddl(),
|
|
2416
|
+
]
|
|
2417
|
+
if not self.nullok:
|
|
2418
|
+
parts.append('NOT NULL')
|
|
2419
|
+
return ' '.join(parts)
|
|
2420
|
+
|
|
2012
2421
|
@presence_annotation(tag.immutable)
|
|
2013
2422
|
def immutable(self): pass
|
|
2014
2423
|
|
|
@@ -2036,6 +2445,19 @@ def _constraint_name_parts(constraint, doc):
|
|
|
2036
2445
|
constraint_schema = None
|
|
2037
2446
|
elif names[0][0] == constraint.table.schema.name:
|
|
2038
2447
|
constraint_schema = constraint.table.schema
|
|
2448
|
+
elif names[0][0] == 'placeholder':
|
|
2449
|
+
# mitigate ermrest API response bug reflecting our 'placeholder' value
|
|
2450
|
+
if constraint.table.kind == 'table':
|
|
2451
|
+
if isinstance(constraint, Key):
|
|
2452
|
+
constraint_schema = constraint.table.schema
|
|
2453
|
+
elif isinstance(constraint, ForeignKey):
|
|
2454
|
+
# HACK: mostly correct for regular ermrest users
|
|
2455
|
+
# may be revised later during fkey digest for irregular cases with SQL views!
|
|
2456
|
+
constraint_schema = constraint.table.schema
|
|
2457
|
+
else:
|
|
2458
|
+
raise TypeError('_constraint_name_parts requires a Key or ForeignKey constraint argument, not %s' % (constraint,))
|
|
2459
|
+
else:
|
|
2460
|
+
constraint_schema = None
|
|
2039
2461
|
else:
|
|
2040
2462
|
raise ValueError('Unexpected schema name in constraint %s' % (names[0],))
|
|
2041
2463
|
constraint_name = names[0][1]
|
|
@@ -2057,6 +2479,16 @@ class Key (object):
|
|
|
2057
2479
|
for cname in key_doc['unique_columns']
|
|
2058
2480
|
])
|
|
2059
2481
|
|
|
2482
|
+
def __repr__(self):
|
|
2483
|
+
cls = type(self)
|
|
2484
|
+
return "<%s.%s object %r.%r at 0x%x>" % (
|
|
2485
|
+
cls.__module__,
|
|
2486
|
+
cls.__name__,
|
|
2487
|
+
self.constraint_schema.name if self.constraint_schema is not None else None,
|
|
2488
|
+
self.constraint_name,
|
|
2489
|
+
id(self),
|
|
2490
|
+
)
|
|
2491
|
+
|
|
2060
2492
|
@property
|
|
2061
2493
|
def columns(self):
|
|
2062
2494
|
"""Sugared access to self.unique_columns"""
|
|
@@ -2174,13 +2606,15 @@ class Key (object):
|
|
|
2174
2606
|
self,
|
|
2175
2607
|
constraint_name=nochange,
|
|
2176
2608
|
comment=nochange,
|
|
2177
|
-
annotations=nochange
|
|
2609
|
+
annotations=nochange,
|
|
2610
|
+
update_mappings=UpdateMappings.no_update
|
|
2178
2611
|
):
|
|
2179
2612
|
"""Alter existing schema definition.
|
|
2180
2613
|
|
|
2181
2614
|
:param constraint_name: Unqualified constraint name string
|
|
2182
2615
|
:param comment: Replacement comment (default nochange)
|
|
2183
2616
|
:param annotations: Replacement annotations (default nochange)
|
|
2617
|
+
:param update_mappings: Update annotations to reflect changes (default UpdateMappings.no_updates)
|
|
2184
2618
|
|
|
2185
2619
|
Returns self (to allow for optional chained access).
|
|
2186
2620
|
|
|
@@ -2200,7 +2634,13 @@ class Key (object):
|
|
|
2200
2634
|
changed = r.json() # use changed vs changes to get server-digested values
|
|
2201
2635
|
|
|
2202
2636
|
if 'names' in changes:
|
|
2637
|
+
oldname = self.constraint_name
|
|
2203
2638
|
self.constraint_name = changed['names'][0][1]
|
|
2639
|
+
if update_mappings:
|
|
2640
|
+
basename = [self.table.schema.name]
|
|
2641
|
+
mmo.replace(self.table.schema.model, basename + [oldname], basename + [self.constraint_name])
|
|
2642
|
+
if update_mappings == UpdateMappings.immediate:
|
|
2643
|
+
self.table.schema.model.apply()
|
|
2204
2644
|
|
|
2205
2645
|
if 'comment' in changes:
|
|
2206
2646
|
self.comment = changed['comment']
|
|
@@ -2211,14 +2651,34 @@ class Key (object):
|
|
|
2211
2651
|
|
|
2212
2652
|
return self
|
|
2213
2653
|
|
|
2214
|
-
def drop(self):
|
|
2654
|
+
def drop(self, cascade=False, update_mappings=UpdateMappings.no_update):
|
|
2215
2655
|
"""Remove this key from the remote database.
|
|
2656
|
+
|
|
2657
|
+
:param cascade: drop dependent objects (default False)
|
|
2658
|
+
:param update_mappings: Update annotations to reflect changes (default UpdateMappings.no_updates)
|
|
2216
2659
|
"""
|
|
2217
2660
|
if self.name not in self.table.keys.elements:
|
|
2218
2661
|
raise ValueError('Key %s does not appear to belong to table %s.' % (self, self.table))
|
|
2662
|
+
|
|
2663
|
+
if cascade:
|
|
2664
|
+
for fkey in list(self.table.referenced_by):
|
|
2665
|
+
assert self.table == fkey.pk_table, "Expected key.table and foreign_key.pk_table to match"
|
|
2666
|
+
if set(self.unique_columns) == set(fkey.referenced_columns):
|
|
2667
|
+
fkey.drop(update_mappings=update_mappings)
|
|
2668
|
+
|
|
2219
2669
|
self.catalog.delete(self.uri_path).raise_for_status()
|
|
2220
2670
|
del self.table.keys[self.name]
|
|
2221
2671
|
|
|
2672
|
+
if update_mappings:
|
|
2673
|
+
mmo.prune(self.table.schema.model, [self.constraint_schema.name, self.constraint_name])
|
|
2674
|
+
if update_mappings == UpdateMappings.immediate:
|
|
2675
|
+
self.table.schema.model.apply()
|
|
2676
|
+
|
|
2677
|
+
def sqlite3_ddl(self) -> str:
|
|
2678
|
+
"""Return SQLite3 unique constraint DDL fragment for this key."""
|
|
2679
|
+
parts = [ sql_identifier(col.name) for col in self.unique_columns ]
|
|
2680
|
+
return 'UNIQUE (%s)' % (', '.join(parts),)
|
|
2681
|
+
|
|
2222
2682
|
class ForeignKey (object):
|
|
2223
2683
|
"""Named foreign key.
|
|
2224
2684
|
"""
|
|
@@ -2246,6 +2706,16 @@ class ForeignKey (object):
|
|
|
2246
2706
|
self._referenced_columns_doc = fkey_doc['referenced_columns']
|
|
2247
2707
|
self.referenced_columns = None
|
|
2248
2708
|
|
|
2709
|
+
def __repr__(self):
|
|
2710
|
+
cls = type(self)
|
|
2711
|
+
return "<%s.%s object %r.%r at 0x%x>" % (
|
|
2712
|
+
cls.__module__,
|
|
2713
|
+
cls.__name__,
|
|
2714
|
+
self.constraint_schema.name if self.constraint_schema is not None else None,
|
|
2715
|
+
self.constraint_name,
|
|
2716
|
+
id(self),
|
|
2717
|
+
)
|
|
2718
|
+
|
|
2249
2719
|
def digest_referenced_columns(self, model):
|
|
2250
2720
|
"""Finish construction deferred until model is known with all tables."""
|
|
2251
2721
|
if self.referenced_columns is None:
|
|
@@ -2258,6 +2728,13 @@ class ForeignKey (object):
|
|
|
2258
2728
|
])
|
|
2259
2729
|
self._referenced_columns_doc = None
|
|
2260
2730
|
self.pk_table.referenced_by.append(self)
|
|
2731
|
+
# HACK: clean up schema qualification for psuedo constraint
|
|
2732
|
+
# this may happen only with SQL views in the ermrest catalog
|
|
2733
|
+
if self.pk_table.kind != 'table' and self.constraint_name in self.table.schema._fkeys:
|
|
2734
|
+
del self.table.schema._fkeys[self.constraint_name]
|
|
2735
|
+
self.table.schema.model._fkeys[self.constraint_name] = self
|
|
2736
|
+
del self.table.foreign_keys.elements[(self.table.schema, self.constraint_name)]
|
|
2737
|
+
self.table.foreign_keys.elements[(None, self.constraint_name)] = self
|
|
2261
2738
|
|
|
2262
2739
|
@property
|
|
2263
2740
|
def column_map(self):
|
|
@@ -2437,7 +2914,8 @@ class ForeignKey (object):
|
|
|
2437
2914
|
comment=nochange,
|
|
2438
2915
|
acls=nochange,
|
|
2439
2916
|
acl_bindings=nochange,
|
|
2440
|
-
annotations=nochange
|
|
2917
|
+
annotations=nochange,
|
|
2918
|
+
update_mappings=UpdateMappings.no_update
|
|
2441
2919
|
):
|
|
2442
2920
|
"""Alter existing schema definition.
|
|
2443
2921
|
|
|
@@ -2448,6 +2926,7 @@ class ForeignKey (object):
|
|
|
2448
2926
|
:param acls: Replacement ACL configuration (default nochange)
|
|
2449
2927
|
:param acl_bindings: Replacement ACL bindings (default nochange)
|
|
2450
2928
|
:param annotations: Replacement annotations (default nochange)
|
|
2929
|
+
:param update_mappings: Update annotations to reflect changes (default UpdateMappings.no_updates)
|
|
2451
2930
|
|
|
2452
2931
|
Returns self (to allow for optional chained access).
|
|
2453
2932
|
|
|
@@ -2475,11 +2954,17 @@ class ForeignKey (object):
|
|
|
2475
2954
|
del self.constraint_schema._fkeys[self.constraint_name]
|
|
2476
2955
|
else:
|
|
2477
2956
|
del self.table.schema.model._pseudo_fkeys[self.constraint_name]
|
|
2957
|
+
oldname = self.constraint_name
|
|
2478
2958
|
self.constraint_name = changed['names'][0][1]
|
|
2479
2959
|
if self.constraint_schema:
|
|
2480
2960
|
self.constraint_schema._fkeys[self.constraint_name] = self
|
|
2481
2961
|
else:
|
|
2482
2962
|
self.table.schema.model._pseudo_fkeys[self.constraint_name] = self
|
|
2963
|
+
if update_mappings:
|
|
2964
|
+
basename = [self.table.schema.name]
|
|
2965
|
+
mmo.replace(self.table.schema.model, basename + [oldname], basename + [self.constraint_name])
|
|
2966
|
+
if update_mappings == UpdateMappings.immediate:
|
|
2967
|
+
self.table.schema.model.apply()
|
|
2483
2968
|
|
|
2484
2969
|
if 'on_update' in changes:
|
|
2485
2970
|
self.on_update = changed['on_update']
|
|
@@ -2504,8 +2989,10 @@ class ForeignKey (object):
|
|
|
2504
2989
|
|
|
2505
2990
|
return self
|
|
2506
2991
|
|
|
2507
|
-
def drop(self):
|
|
2992
|
+
def drop(self, update_mappings=UpdateMappings.no_update):
|
|
2508
2993
|
"""Remove this foreign key from the remote database.
|
|
2994
|
+
|
|
2995
|
+
:param update_mappings: Update annotations to reflect changes (default UpdateMappings.no_updates)
|
|
2509
2996
|
"""
|
|
2510
2997
|
if self.name not in self.table.foreign_keys.elements:
|
|
2511
2998
|
raise ValueError('Foreign key %s does not appear to belong to table %s.' % (self, self.table))
|
|
@@ -2513,6 +3000,11 @@ class ForeignKey (object):
|
|
|
2513
3000
|
del self.table.foreign_keys[self.name]
|
|
2514
3001
|
self._cleanup()
|
|
2515
3002
|
|
|
3003
|
+
if update_mappings:
|
|
3004
|
+
mmo.prune(self.table.schema.model, [self.constraint_schema.name, self.constraint_name])
|
|
3005
|
+
if update_mappings == UpdateMappings.immediate:
|
|
3006
|
+
self.table.schema.model.apply()
|
|
3007
|
+
|
|
2516
3008
|
def _cleanup(self):
|
|
2517
3009
|
"""Cleanup references in the local model following drop from remote database.
|
|
2518
3010
|
"""
|
|
@@ -2548,6 +3040,22 @@ class Type (object):
|
|
|
2548
3040
|
}
|
|
2549
3041
|
return d
|
|
2550
3042
|
|
|
3043
|
+
def sqlite3_ddl(self) -> str:
|
|
3044
|
+
"""Return a SQLite3 column type DDL fragment for this type"""
|
|
3045
|
+
return {
|
|
3046
|
+
'boolean': 'boolean',
|
|
3047
|
+
'date': 'date',
|
|
3048
|
+
'float4': 'real',
|
|
3049
|
+
'float8': 'real',
|
|
3050
|
+
'int2': 'integer',
|
|
3051
|
+
'int4': 'integer',
|
|
3052
|
+
'int8': 'integer',
|
|
3053
|
+
'json': 'json',
|
|
3054
|
+
'jsonb': 'json',
|
|
3055
|
+
'timestamptz': 'datetime',
|
|
3056
|
+
'timestamp': 'datetime',
|
|
3057
|
+
}.get(self.typename, 'text')
|
|
3058
|
+
|
|
2551
3059
|
class DomainType (Type):
|
|
2552
3060
|
"""Named domain type.
|
|
2553
3061
|
"""
|
|
@@ -2564,6 +3072,10 @@ class DomainType (Type):
|
|
|
2564
3072
|
})
|
|
2565
3073
|
return d
|
|
2566
3074
|
|
|
3075
|
+
def sqlite3_ddl(self) -> str:
|
|
3076
|
+
"""Return a SQLite3 column type DDL fragment for this type"""
|
|
3077
|
+
return self.base_type.sqlite3_ddl()
|
|
3078
|
+
|
|
2567
3079
|
class ArrayType (Type):
|
|
2568
3080
|
"""Named domain type.
|
|
2569
3081
|
"""
|
|
@@ -2580,6 +3092,10 @@ class ArrayType (Type):
|
|
|
2580
3092
|
})
|
|
2581
3093
|
return d
|
|
2582
3094
|
|
|
3095
|
+
def sqlite3_ddl(self) -> str:
|
|
3096
|
+
"""Return a SQLite3 column type DDL fragment for this type"""
|
|
3097
|
+
return 'json'
|
|
3098
|
+
|
|
2583
3099
|
builtin_types = AttrDict(
|
|
2584
3100
|
# first define standard scalar types
|
|
2585
3101
|
{
|