Skip to content

Commit caa4979

Browse files
author
James Robinson
authored
Drive full introspsection and reporting back to gate via SQL cell \introspect (#72)
1 parent 36bdc26 commit caa4979

File tree

5 files changed

+794
-33
lines changed

5 files changed

+794
-33
lines changed
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# Declare the message types
2+
3+
import enum
4+
from typing import List, Optional
5+
6+
from pydantic import BaseModel, Field, root_validator
7+
8+
r"""
9+
Messaging types used for SQL cell meta command \introspect when describing
10+
the structure of a discovered table or view up to Gate for permanent storage for
11+
front-end GUI schema navigation uses.
12+
13+
These types culminating in RelationStructureDescription are inputs to the Gate
14+
POST route(s).
15+
"""
16+
17+
18+
@enum.unique
19+
class RelationKind(str, enum.Enum):
20+
"""Enumeration differentating between tables and views"""
21+
22+
table = "table"
23+
view = "view"
24+
25+
class Config:
26+
extra = "forbid"
27+
28+
29+
class ColumnModel(BaseModel):
30+
"""Pydantic model defining a column of an introspected table or view.
31+
32+
Used in two contexts:
33+
* SQLAlchemy validation prior to assigning new value into DatasourceRelationDAO.columns JSONB column
34+
* Subcomponent of upcoming pydantic model(s) used for route I/O (ENG-5356, ENG-5359).
35+
"""
36+
37+
name: str
38+
is_nullable: bool
39+
data_type: str
40+
default_expression: Optional[str] = None
41+
comment: Optional[str] = None
42+
43+
class Config:
44+
extra = "forbid"
45+
46+
47+
class IndexModel(BaseModel):
48+
"""Pydantic model defining an introspected index.
49+
50+
Used in two contexts:
51+
* SQLAlchemy validation prior to assigning new value into DatasourceRelationDAO.indexes JSONB column
52+
* Subcomponent of upcoming pydantic models used for route I/O (ENG-5356, ENG-5359).
53+
"""
54+
55+
name: str
56+
is_unique: bool
57+
columns: List[str]
58+
59+
class Config:
60+
extra = "forbid"
61+
62+
63+
class UniqueConstraintModel(BaseModel):
64+
"""Pydantic model defining an introspected unique constraint.
65+
66+
Used in two contexts:
67+
* SQLAlchemy validation prior to assigning new value into DatasourceRelationDAO.unique_constraints JSONB column
68+
* Subcomponent of upcoming pydantic models used for route I/O (ENG-5356, ENG-5359).
69+
"""
70+
71+
name: str
72+
columns: List[str]
73+
74+
class Config:
75+
extra = "forbid"
76+
77+
78+
class CheckConstraintModel(BaseModel):
79+
"""Pydantic model defining an introspected check constraint.
80+
81+
Used in two contexts:
82+
* SQLAlchemy validation prior to assigning new value into DatasourceRelationDAO.check_constraints JSONB column
83+
* Subcomponent of upcoming pydantic models used for route I/O (ENG-5356, ENG-5359).
84+
"""
85+
86+
name: str
87+
expression: str
88+
89+
class Config:
90+
extra = "forbid"
91+
92+
93+
class ForeignKeysModel(BaseModel):
94+
"""Pydantic model defining an introspected foreign key constraint.
95+
96+
Used in two contexts:
97+
* SQLAlchemy validation prior to assigning new value into DatasourceRelationDAO.foreign_keys JSONB column
98+
* Subcomponent of upcoming pydantic models used for route I/O (ENG-5356, ENG-5359).
99+
"""
100+
101+
name: str
102+
referenced_schema: str
103+
referenced_relation: str
104+
columns: List[str]
105+
referenced_columns: List[str]
106+
107+
@root_validator
108+
def check_lists_same_length(cls, values):
109+
if not ("columns" in values and "referenced_columns" in values):
110+
raise ValueError("columns and referenced_columns required")
111+
112+
if len(values["columns"]) != len(values["referenced_columns"]):
113+
raise ValueError("columns and referenced_columns must be same length")
114+
115+
return values
116+
117+
class Config:
118+
extra = "forbid"
119+
120+
121+
class RelationStructureDescription(BaseModel):
122+
"""Pydantic model describing the POST structure kernel-space will use to describe a
123+
schema-discovered table or view within a datasource.
124+
"""
125+
126+
# First, the singular fields.
127+
schema_name: str = Field(
128+
description="Name of schema containing the relation. Empty string for degenerate value."
129+
)
130+
relation_name: str = Field(description="Name of the table or view")
131+
kind: RelationKind = Field(description="Relation type: table or a view")
132+
relation_comment: Optional[str] = Field(description="Optional comment describing the relation.")
133+
view_definition: Optional[str] = Field(description="Definition of the view if kind=view")
134+
primary_key_name: Optional[str] = Field(
135+
description="Name of the primary key constraint, if any"
136+
)
137+
138+
# Now the plural fields.
139+
primary_key_columns: List[str] = Field(
140+
description="List of column names comprising the primary key, if any."
141+
)
142+
columns: List[ColumnModel] = Field(description="List of column definitions")
143+
indexes: List[IndexModel] = Field(description="List of index definitions")
144+
unique_constraints: List[UniqueConstraintModel] = Field(
145+
description="List of unique constraint definitions"
146+
)
147+
check_constraints: List[CheckConstraintModel] = Field(
148+
description="List of check constraint definitions"
149+
)
150+
foreign_keys: List[ForeignKeysModel] = Field(description="List of foreign key definitions")
151+
152+
@root_validator
153+
def view_definition_vs_kind(cls, values):
154+
"""Fail if a tring to describe a view with None for the view definition. At worst
155+
empty string is allowed.
156+
157+
Likewise, if describing a table, then view definition _must_ be None.
158+
"""
159+
if not (values.get("view_definition") is None) == (
160+
values.get("kind") == RelationKind.table
161+
):
162+
raise ValueError("Views require definitions, tables must not have view definition")
163+
164+
return values
165+
166+
@root_validator
167+
def pkey_name_only_if_has_pkey_columns(cls, values):
168+
if len(values.get("primary_key_columns")) > 0 and not values.get('primary_key_name'):
169+
raise ValueError("primary_key_columns requires nonempty primary_key_name")
170+
elif (
171+
len(values.get("primary_key_columns")) == 0
172+
and values.get('primary_key_name') is not None
173+
):
174+
raise ValueError("No primary_key_columns requires primary_key_name = None")
175+
176+
return values
177+
178+
class Config:
179+
extra = "forbid"

0 commit comments

Comments
 (0)