dbgraph 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dbgraph/__init__.py +21 -0
- dbgraph/application.py +70 -0
- dbgraph/builder/__init__.py +0 -0
- dbgraph/builder/graph_builder.py +22 -0
- dbgraph/builder/sql_graph_builder.py +245 -0
- dbgraph/descriptor/__init__.py +0 -0
- dbgraph/descriptor/graph_descriptor.py +68 -0
- dbgraph/descriptor/langchain_graph_descriptor.py +74 -0
- dbgraph/descriptor/prompt_templates.py +80 -0
- dbgraph/entity/__init__.py +0 -0
- dbgraph/entity/aspect.py +153 -0
- dbgraph/entity/asset.py +26 -0
- dbgraph/entity/asset_type.py +13 -0
- dbgraph/entity/dbgraph.py +152 -0
- dbgraph/entity/link.py +37 -0
- dbgraph/entity/link_type.py +13 -0
- dbgraph/entity/rdbgraph.py +76 -0
- dbgraph/event/__init__.py +0 -0
- dbgraph/event/event.py +20 -0
- dbgraph/event/event_bus.py +61 -0
- dbgraph/event/event_handler.py +11 -0
- dbgraph/io/__init__.py +0 -0
- dbgraph/io/graph_loader.py +11 -0
- dbgraph/io/graph_writer.py +11 -0
- dbgraph/io/json_graph_loader.py +160 -0
- dbgraph/io/json_graph_writer.py +17 -0
- dbgraph/persistent/__init__.py +0 -0
- dbgraph/persistent/graph_persistent.py +98 -0
- dbgraph/persistent/models.py +222 -0
- dbgraph/persistent/sql_graph_persistent.py +388 -0
- dbgraph/render/__init__.py +0 -0
- dbgraph/render/graph_renderer.py +11 -0
- dbgraph/render/markdown_renderer.py +111 -0
- dbgraph/render/text_renderer.py +11 -0
- dbgraph/search/__init__.py +0 -0
- dbgraph/search/bm25_search_engine.py +33 -0
- dbgraph/search/search_engine.py +15 -0
- dbgraph/utils/__init__.py +0 -0
- dbgraph/utils/singleton.py +37 -0
- dbgraph-0.1.0.dist-info/METADATA +165 -0
- dbgraph-0.1.0.dist-info/RECORD +42 -0
- dbgraph-0.1.0.dist-info/WHEEL +4 -0
dbgraph/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from dbgraph.entity.asset import Asset
|
|
2
|
+
from dbgraph.entity.asset_type import AssetType
|
|
3
|
+
from dbgraph.entity.dbgraph import DatabaseGraph
|
|
4
|
+
from dbgraph.entity.link import Link
|
|
5
|
+
from dbgraph.entity.link_type import LinkType
|
|
6
|
+
from dbgraph.builder.sql_graph_builder import SQLGraphBuilder
|
|
7
|
+
from dbgraph.persistent.sql_graph_persistent import SQLGraphPersistent
|
|
8
|
+
from dbgraph.descriptor.langchain_graph_descriptor import LangchainGraphDescriptor
|
|
9
|
+
from dbgraph.render.markdown_renderer import MarkdownRenderer
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Asset",
|
|
13
|
+
"AssetType",
|
|
14
|
+
"DatabaseGraph",
|
|
15
|
+
"Link",
|
|
16
|
+
"LinkType",
|
|
17
|
+
"SQLGraphBuilder",
|
|
18
|
+
"SQLGraphPersistent",
|
|
19
|
+
"LangchainGraphDescriptor",
|
|
20
|
+
"MarkdownRenderer"
|
|
21
|
+
]
|
dbgraph/application.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from uuid import uuid4
|
|
3
|
+
|
|
4
|
+
from dbgraph.builder.graph_builder import GraphBuilder
|
|
5
|
+
from dbgraph.builder.sql_graph_builder import SQLGraphBuilder
|
|
6
|
+
from dbgraph.descriptor.graph_descriptor import GraphDescriptor
|
|
7
|
+
from dbgraph.entity.asset import Asset
|
|
8
|
+
from dbgraph.entity.dbgraph import DatabaseGraph
|
|
9
|
+
from dbgraph.entity.link import Link
|
|
10
|
+
from dbgraph.entity.rdbgraph import RDatabaseGraph
|
|
11
|
+
from dbgraph.persistent.graph_persistent import GraphPersistent
|
|
12
|
+
from dbgraph.persistent.sql_graph_persistent import SQLGraphPersistent
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Application:
|
|
17
|
+
"""God class to control the logical flow within dbgraph"""
|
|
18
|
+
|
|
19
|
+
source_database_type: str
|
|
20
|
+
graph_persistent_uri: str
|
|
21
|
+
graph_persistent_type: str
|
|
22
|
+
descriptor: GraphDescriptor | None = None
|
|
23
|
+
|
|
24
|
+
def __post_init__(self):
|
|
25
|
+
self.graph_persistent = self.create_graph_persistent()
|
|
26
|
+
|
|
27
|
+
def create_graph_builder(self, database_uri: str) -> GraphBuilder:
|
|
28
|
+
match self.source_database_type:
|
|
29
|
+
case "sql":
|
|
30
|
+
builder = SQLGraphBuilder(database_uri)
|
|
31
|
+
case _:
|
|
32
|
+
raise NotImplementedError()
|
|
33
|
+
return builder
|
|
34
|
+
|
|
35
|
+
def create_graph_persistent(self) -> GraphPersistent:
|
|
36
|
+
match self.graph_persistent_type:
|
|
37
|
+
case "sql":
|
|
38
|
+
persistent = SQLGraphPersistent(self.graph_persistent_uri)
|
|
39
|
+
case _:
|
|
40
|
+
raise NotImplementedError()
|
|
41
|
+
return persistent
|
|
42
|
+
|
|
43
|
+
def build_graph(self, name: str, database_uri: str, fill_semantic: bool) -> str:
|
|
44
|
+
"""Build the graph, return its ID in the database"""
|
|
45
|
+
builder = self.create_graph_builder(database_uri)
|
|
46
|
+
graph = builder.build_graph()
|
|
47
|
+
if fill_semantic and self.descriptor is not None:
|
|
48
|
+
graph = self.descriptor.rfill_semantic_aspects(
|
|
49
|
+
RDatabaseGraph.from_graph(graph)
|
|
50
|
+
)
|
|
51
|
+
graph_id = str(uuid4())
|
|
52
|
+
self.graph_persistent.create_graph(graph_id, name)
|
|
53
|
+
self.graph_persistent.save_graph(graph, name)
|
|
54
|
+
return graph_id
|
|
55
|
+
|
|
56
|
+
def load_graph(self, graph_id: str) -> DatabaseGraph:
|
|
57
|
+
"""Load the graph using its ID"""
|
|
58
|
+
return self.graph_persistent.load_graph(graph_id)
|
|
59
|
+
|
|
60
|
+
def delete_graph(self, graph_id: str):
|
|
61
|
+
"""Delete the graph using its ID"""
|
|
62
|
+
self.graph_persistent.delete_graph(graph_id)
|
|
63
|
+
|
|
64
|
+
def get_asset(self, asset_id: str, graph_id: str) -> Asset:
|
|
65
|
+
"""Find an asset using its ID"""
|
|
66
|
+
return self.graph_persistent.get_asset(asset_id, graph_id)
|
|
67
|
+
|
|
68
|
+
def get_link(self, link_id: str, graph_id: str) -> Link:
|
|
69
|
+
"""Find a link using its ID"""
|
|
70
|
+
return self.graph_persistent.get_link(link_id, graph_id)
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from dbgraph.entity.asset import Asset
|
|
4
|
+
from dbgraph.entity.dbgraph import DatabaseGraph
|
|
5
|
+
from dbgraph.entity.link import Link
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GraphBuilder(ABC):
|
|
9
|
+
"""Interface for relational database graph builder"""
|
|
10
|
+
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def _build_assets(self) -> list[Asset]:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def _build_links(self) -> list[Link]:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
def build_graph(self) -> DatabaseGraph:
|
|
20
|
+
assets = self._build_assets()
|
|
21
|
+
links = self._build_links()
|
|
22
|
+
return DatabaseGraph(assets, links)
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import (
|
|
6
|
+
Column,
|
|
7
|
+
MetaData,
|
|
8
|
+
Numeric,
|
|
9
|
+
PrimaryKeyConstraint,
|
|
10
|
+
String,
|
|
11
|
+
Table,
|
|
12
|
+
create_engine,
|
|
13
|
+
func,
|
|
14
|
+
inspect,
|
|
15
|
+
select,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from dbgraph.builder.graph_builder import GraphBuilder
|
|
19
|
+
from dbgraph.entity.aspect import (
|
|
20
|
+
RCategoricalStatistics,
|
|
21
|
+
RColumnSchemaAspect,
|
|
22
|
+
RColumnStatisticsAspect,
|
|
23
|
+
RForeignKeyAspect,
|
|
24
|
+
RNumericalStatistics,
|
|
25
|
+
RTableSchemaAspect,
|
|
26
|
+
RTableStatisticsAspect,
|
|
27
|
+
)
|
|
28
|
+
from dbgraph.entity.asset import Asset
|
|
29
|
+
from dbgraph.entity.asset_type import AssetType
|
|
30
|
+
from dbgraph.entity.link import Link
|
|
31
|
+
from dbgraph.entity.link_type import LinkType
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class SQLGraphBuilder(GraphBuilder):
|
|
36
|
+
"""Build the database graph using SQL Alchemy"""
|
|
37
|
+
|
|
38
|
+
db_uri: str
|
|
39
|
+
max_worker: int = 4
|
|
40
|
+
|
|
41
|
+
def __post_init__(self):
|
|
42
|
+
self.engine = create_engine(self.db_uri)
|
|
43
|
+
self.columns_assets: dict[str, list[Asset]] = {}
|
|
44
|
+
self.tables_assets: dict[str, Asset] = {}
|
|
45
|
+
self._tables_orm: dict[str, Table] = {
|
|
46
|
+
table.name: table for table in self._orm_tables
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def _orm_tables(self) -> list[Table]:
|
|
51
|
+
orm_tables = []
|
|
52
|
+
for table_name in self.table_names:
|
|
53
|
+
metadata = MetaData()
|
|
54
|
+
table = Table(table_name, metadata, autoload_with=self.engine)
|
|
55
|
+
orm_tables.append(table)
|
|
56
|
+
return orm_tables
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def table_names(self) -> list[str]:
|
|
60
|
+
inspector = inspect(self.engine)
|
|
61
|
+
return inspector.get_table_names()
|
|
62
|
+
|
|
63
|
+
def _get_numerical_stats_aspect(self, col: Column) -> RNumericalStatistics:
|
|
64
|
+
stmt = select(
|
|
65
|
+
func.min(col).label("min"),
|
|
66
|
+
func.max(col).label("max"),
|
|
67
|
+
func.avg(col).label("mean"),
|
|
68
|
+
)
|
|
69
|
+
with self.engine.connect() as conn:
|
|
70
|
+
result = conn.execute(stmt).one()
|
|
71
|
+
return RNumericalStatistics(
|
|
72
|
+
min=float(result.min), max=float(result.max), mean=float(result.mean)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def _get_null_info(self, col: Column) -> tuple[int, int]:
|
|
76
|
+
stmt = select(
|
|
77
|
+
func.count(col).label("non_null_count"),
|
|
78
|
+
(func.count() - func.count(col)).label("null_count"),
|
|
79
|
+
)
|
|
80
|
+
with self.engine.connect() as conn:
|
|
81
|
+
result = conn.execute(stmt).one()
|
|
82
|
+
return int(result.non_null_count), int(result.null_count)
|
|
83
|
+
|
|
84
|
+
def _get_cat_stats_aspect(self, col: Column) -> RCategoricalStatistics:
|
|
85
|
+
stmt = (
|
|
86
|
+
select(
|
|
87
|
+
col.label("value"),
|
|
88
|
+
func.count().label("value_count"),
|
|
89
|
+
)
|
|
90
|
+
.where(col.is_not(None))
|
|
91
|
+
.group_by(col)
|
|
92
|
+
.order_by(func.count().desc())
|
|
93
|
+
.limit(10)
|
|
94
|
+
)
|
|
95
|
+
with self.engine.connect() as conn:
|
|
96
|
+
rows = conn.execute(stmt).all()
|
|
97
|
+
value_counts = {str(row[0]): int(row[1]) for row in rows}
|
|
98
|
+
return RCategoricalStatistics(value_counts)
|
|
99
|
+
|
|
100
|
+
def _get_stats_aspect(self, col: Column) -> RColumnStatisticsAspect:
|
|
101
|
+
non_null_count, null_count = self._get_null_info(col)
|
|
102
|
+
if isinstance(col.type, Numeric):
|
|
103
|
+
num_aspect = self._get_numerical_stats_aspect(col)
|
|
104
|
+
cat_aspect = None
|
|
105
|
+
elif isinstance(col.type, String):
|
|
106
|
+
num_aspect = None
|
|
107
|
+
cat_aspect = self._get_cat_stats_aspect(col)
|
|
108
|
+
else:
|
|
109
|
+
num_aspect = None
|
|
110
|
+
cat_aspect = None
|
|
111
|
+
return RColumnStatisticsAspect(
|
|
112
|
+
name=f"{col.name}_column_stats",
|
|
113
|
+
numerical_stats=num_aspect,
|
|
114
|
+
categorical_stats=cat_aspect,
|
|
115
|
+
non_null_count=non_null_count,
|
|
116
|
+
null_count=null_count,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def _make_column_asset(self, col: Column, pks: PrimaryKeyConstraint) -> Asset:
|
|
120
|
+
schema_aspect = RColumnSchemaAspect(
|
|
121
|
+
name=f"{col.name}_column_schema",
|
|
122
|
+
dtype=str(col.type),
|
|
123
|
+
is_nullable=col.nullable or False,
|
|
124
|
+
is_pk=col.name in [c.name for c in pks.columns],
|
|
125
|
+
)
|
|
126
|
+
stats_aspect = self._get_stats_aspect(col)
|
|
127
|
+
return Asset(
|
|
128
|
+
asset_id=str(uuid4()),
|
|
129
|
+
name=col.name,
|
|
130
|
+
type=AssetType.RCOLUMN,
|
|
131
|
+
aspects={
|
|
132
|
+
"schema_properties": schema_aspect,
|
|
133
|
+
"statistical_properties": stats_aspect,
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def _get_columns_assets(self, table_name: str) -> list[Asset]:
|
|
138
|
+
table = self._tables_orm[table_name]
|
|
139
|
+
assets = []
|
|
140
|
+
pks = table.primary_key
|
|
141
|
+
with ThreadPoolExecutor(max_workers=self.max_worker) as executor:
|
|
142
|
+
assets = list(
|
|
143
|
+
executor.map(
|
|
144
|
+
self._make_column_asset, table.columns, [pks] * len(table.columns)
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
return assets
|
|
148
|
+
|
|
149
|
+
def _get_table_stats_aspect(self, table_name: str) -> RTableStatisticsAspect:
|
|
150
|
+
table = self._tables_orm[table_name]
|
|
151
|
+
nrow = self._get_table_nrow(table_name)
|
|
152
|
+
ncol = len(table.columns)
|
|
153
|
+
return RTableStatisticsAspect(
|
|
154
|
+
name=f"{table.name}_table_stats", num_rows=nrow, num_columns=ncol
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def _get_table_nrow(self, table_name: str) -> int:
|
|
158
|
+
table = self._tables_orm[table_name]
|
|
159
|
+
|
|
160
|
+
stmt = select(func.count()).select_from(table)
|
|
161
|
+
|
|
162
|
+
with self.engine.connect() as conn:
|
|
163
|
+
nrow = conn.execute(stmt).scalar()
|
|
164
|
+
if nrow is None:
|
|
165
|
+
raise RuntimeError("Can't get number of row")
|
|
166
|
+
return int(nrow)
|
|
167
|
+
|
|
168
|
+
def _get_table_schema_aspect(self, table_name: str) -> RTableSchemaAspect:
|
|
169
|
+
table = self._tables_orm[table_name]
|
|
170
|
+
pks = [c.name for c in table.primary_key.columns]
|
|
171
|
+
indices = {str(i.name): [c.name for c in i.columns] for i in table.indexes}
|
|
172
|
+
|
|
173
|
+
return RTableSchemaAspect(
|
|
174
|
+
name=f"{table_name}_table_schema", pks=pks, indices=indices
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def _make_table_asset(self, table_name: str) -> Asset:
|
|
178
|
+
stats_aspect = self._get_table_stats_aspect(table_name)
|
|
179
|
+
schema_aspect = self._get_table_schema_aspect(table_name)
|
|
180
|
+
table_asset = Asset(
|
|
181
|
+
asset_id=str(uuid4()),
|
|
182
|
+
name=table_name,
|
|
183
|
+
type=AssetType.RTABLE,
|
|
184
|
+
aspects={
|
|
185
|
+
"schema_properties": schema_aspect,
|
|
186
|
+
"statistical_properties": stats_aspect,
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
self.columns_assets[table_asset.name] = self._get_columns_assets(table_name)
|
|
190
|
+
self.tables_assets[table_asset.name] = table_asset
|
|
191
|
+
return table_asset
|
|
192
|
+
|
|
193
|
+
def _build_assets(self) -> list[Asset]:
|
|
194
|
+
with ThreadPoolExecutor(max_workers=self.max_worker) as executor:
|
|
195
|
+
assets = list(executor.map(self._make_table_asset, self.table_names))
|
|
196
|
+
for assets_list in self.columns_assets.values():
|
|
197
|
+
assets.extend(assets_list)
|
|
198
|
+
return assets
|
|
199
|
+
|
|
200
|
+
def _build_contain_links(self) -> list[Link]:
|
|
201
|
+
contain_links = []
|
|
202
|
+
for table_name, columns_assets in self.columns_assets.items():
|
|
203
|
+
table_asset = self.tables_assets[table_name]
|
|
204
|
+
for column_asset in columns_assets:
|
|
205
|
+
link = Link(
|
|
206
|
+
link_id=str(uuid4()),
|
|
207
|
+
name=f"{table_asset.name}_{column_asset.name}",
|
|
208
|
+
source_id=table_asset.asset_id,
|
|
209
|
+
destination_id=column_asset.asset_id,
|
|
210
|
+
type=LinkType.CONTAIN,
|
|
211
|
+
)
|
|
212
|
+
contain_links.append(link)
|
|
213
|
+
return contain_links
|
|
214
|
+
|
|
215
|
+
def _build_fk_links(self) -> list[Link]:
|
|
216
|
+
links = []
|
|
217
|
+
for table_name in self.tables_assets:
|
|
218
|
+
table = self._tables_orm[table_name]
|
|
219
|
+
for fk in table.foreign_keys:
|
|
220
|
+
to_table = fk.column.table.name
|
|
221
|
+
from_table = table.name
|
|
222
|
+
name = f"{from_table}_{to_table}_fk"
|
|
223
|
+
link = Link(
|
|
224
|
+
link_id=str(uuid4()),
|
|
225
|
+
name=name,
|
|
226
|
+
source_id=self.tables_assets[from_table].asset_id,
|
|
227
|
+
destination_id=self.tables_assets[to_table].asset_id,
|
|
228
|
+
type=LinkType.FOREIGN_KEY,
|
|
229
|
+
aspects={
|
|
230
|
+
"foreign_key_properties": RForeignKeyAspect(
|
|
231
|
+
name=name,
|
|
232
|
+
from_column=fk.parent.name,
|
|
233
|
+
to_column=fk.column.name,
|
|
234
|
+
on_delete=fk.ondelete or "",
|
|
235
|
+
on_update=fk.onupdate or "",
|
|
236
|
+
)
|
|
237
|
+
},
|
|
238
|
+
)
|
|
239
|
+
links.append(link)
|
|
240
|
+
return links
|
|
241
|
+
|
|
242
|
+
def _build_links(self) -> list[Link]:
|
|
243
|
+
contain_links = self._build_contain_links()
|
|
244
|
+
fk_links = self._build_fk_links()
|
|
245
|
+
return contain_links + fk_links
|
|
File without changes
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from dbgraph.entity.aspect import SemanticAspect
|
|
6
|
+
from dbgraph.entity.asset import Asset
|
|
7
|
+
from dbgraph.entity.rdbgraph import RDatabaseGraph
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class GraphDescriptor(ABC):
|
|
12
|
+
"""Base class that generate semantic aspects for a database graph or a sub-graph"""
|
|
13
|
+
|
|
14
|
+
max_workers: int
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def get_semantic_aspect(
|
|
18
|
+
self, asset: Asset, context: RDatabaseGraph
|
|
19
|
+
) -> SemanticAspect:
|
|
20
|
+
"""Get the description of this asset.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
asset: Target asset
|
|
24
|
+
context: an instance of `RDatabaseGraph`, could be the whole database graph or a sub-graph
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
semantic aspect for this asset
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def rfill_semantic_aspects(self, graph: RDatabaseGraph) -> RDatabaseGraph:
|
|
31
|
+
tables = graph.get_tables()
|
|
32
|
+
assets = tables
|
|
33
|
+
contexts = [graph.select_connected_tables(table.asset_id) for table in tables]
|
|
34
|
+
for table in tables:
|
|
35
|
+
context = RDatabaseGraph(assets=[table], links=[])
|
|
36
|
+
columns = graph.get_columns(table.asset_id)
|
|
37
|
+
for column in columns:
|
|
38
|
+
assets.append(column)
|
|
39
|
+
contexts.append(context)
|
|
40
|
+
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
|
41
|
+
results = {
|
|
42
|
+
asset.asset_id: future
|
|
43
|
+
for asset, future in zip(
|
|
44
|
+
assets, executor.map(self.get_semantic_aspect, assets, contexts)
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
for key, aspect in results.items():
|
|
48
|
+
asset = graph.get_asset(key)
|
|
49
|
+
asset.aspects["semantic_properties"] = aspect
|
|
50
|
+
return graph
|
|
51
|
+
|
|
52
|
+
def rfill_semantic_aspects_seq(self, graph: RDatabaseGraph) -> RDatabaseGraph:
|
|
53
|
+
tables = graph.get_tables()
|
|
54
|
+
assets = tables
|
|
55
|
+
contexts = [graph.select_connected_tables(table.asset_id) for table in tables]
|
|
56
|
+
for table in tables:
|
|
57
|
+
context = RDatabaseGraph(assets=[table], links=[])
|
|
58
|
+
columns = graph.get_columns(table.asset_id)
|
|
59
|
+
for column in columns:
|
|
60
|
+
assets.append(column)
|
|
61
|
+
contexts.append(context)
|
|
62
|
+
results = {}
|
|
63
|
+
for asset, context in zip(assets, contexts):
|
|
64
|
+
results[asset.asset_id] = self.get_semantic_aspect(asset, context)
|
|
65
|
+
for key, aspect in results.items():
|
|
66
|
+
asset = graph.get_asset(key)
|
|
67
|
+
asset.aspects["semantic_properties"] = aspect
|
|
68
|
+
return graph
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from langchain.chat_models import BaseChatModel
|
|
4
|
+
from langchain_core.messages import SystemMessage
|
|
5
|
+
from langchain_core.messages.human import HumanMessage
|
|
6
|
+
|
|
7
|
+
from dbgraph import AssetType
|
|
8
|
+
from dbgraph.descriptor.graph_descriptor import GraphDescriptor
|
|
9
|
+
from dbgraph.entity.aspect import SemanticAspect
|
|
10
|
+
from dbgraph.entity.asset import Asset
|
|
11
|
+
from dbgraph.entity.rdbgraph import RDatabaseGraph
|
|
12
|
+
from dbgraph.render.markdown_renderer import MarkdownRenderer
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class LangchainGraphDescriptor(GraphDescriptor):
|
|
17
|
+
"""Implementation of `GraphDescriptor` using Langchain"""
|
|
18
|
+
|
|
19
|
+
model: BaseChatModel
|
|
20
|
+
table_system_prompt: str
|
|
21
|
+
table_question_prompt: str
|
|
22
|
+
column_system_prompt: str
|
|
23
|
+
column_question_prompt: str
|
|
24
|
+
markdown_renderer: MarkdownRenderer
|
|
25
|
+
|
|
26
|
+
def _generate(self, system_prompt: str, question: str) -> str:
|
|
27
|
+
response = self.model.invoke(
|
|
28
|
+
[SystemMessage(content=system_prompt), HumanMessage(content=question)]
|
|
29
|
+
)
|
|
30
|
+
if response.content is None:
|
|
31
|
+
raise ValueError("Couldn't generate description")
|
|
32
|
+
return str(response.content)
|
|
33
|
+
|
|
34
|
+
def _get_semantic_aspect_table(
|
|
35
|
+
self, asset: Asset, context: RDatabaseGraph
|
|
36
|
+
) -> SemanticAspect:
|
|
37
|
+
self.markdown_renderer.render(context)
|
|
38
|
+
system_prompt = (
|
|
39
|
+
self.table_system_prompt + "\n" + self.markdown_renderer.get_content()
|
|
40
|
+
)
|
|
41
|
+
question = self.table_question_prompt + " " + asset.name
|
|
42
|
+
description = self._generate(system_prompt, question)
|
|
43
|
+
return SemanticAspect(
|
|
44
|
+
name=f"{asset.name}_semantic_properties",
|
|
45
|
+
description=description,
|
|
46
|
+
keywords=[],
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def _get_semantic_aspect_column(
|
|
50
|
+
self, asset: Asset, context: RDatabaseGraph
|
|
51
|
+
) -> SemanticAspect:
|
|
52
|
+
self.markdown_renderer.render(context)
|
|
53
|
+
system_prompt = (
|
|
54
|
+
self.column_system_prompt + "\n" + self.markdown_renderer.get_content()
|
|
55
|
+
)
|
|
56
|
+
question = self.column_question_prompt + " " + asset.name
|
|
57
|
+
description = self._generate(system_prompt, question)
|
|
58
|
+
return SemanticAspect(
|
|
59
|
+
name=f"{asset.name}_semantic_properties",
|
|
60
|
+
description=description,
|
|
61
|
+
keywords=[],
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def get_semantic_aspect(
|
|
65
|
+
self, asset: Asset, context: RDatabaseGraph
|
|
66
|
+
) -> SemanticAspect:
|
|
67
|
+
if asset.type == AssetType.RCOLUMN:
|
|
68
|
+
return self._get_semantic_aspect_column(asset, context)
|
|
69
|
+
elif asset.type == AssetType.RTABLE:
|
|
70
|
+
return self._get_semantic_aspect_table(asset, context)
|
|
71
|
+
else:
|
|
72
|
+
raise NotImplementedError(
|
|
73
|
+
"Only support for table and column in relational databases"
|
|
74
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
TABLE_SYSTEM_PROMPT = """
|
|
2
|
+
You are an expert in SQL database schema understanding.
|
|
3
|
+
|
|
4
|
+
Generate a concise semantic description of the database table from the provided schema.
|
|
5
|
+
|
|
6
|
+
Your description should capture:
|
|
7
|
+
1. WHAT the table represents.
|
|
8
|
+
2. WHAT a single row represents.
|
|
9
|
+
3. The table's primary business or technical purpose.
|
|
10
|
+
4. Important relationships to other entities when they can be inferred from foreign keys.
|
|
11
|
+
5. Whether the table represents an entity, relationship/mapping, transaction, event, log, audit record, or reference data when this is clear from the schema.
|
|
12
|
+
|
|
13
|
+
Inference rules:
|
|
14
|
+
- Use the table name, column names, data types, primary keys, foreign keys, unique constraints, and relationships.
|
|
15
|
+
- Prefer semantic meaning over simply repeating column names.
|
|
16
|
+
- Do not infer information that is not reasonably supported by the schema.
|
|
17
|
+
- When uncertain, use conservative wording rather than guessing.
|
|
18
|
+
- For junction tables, describe the relationship between the referenced entities.
|
|
19
|
+
- For tables with timestamps/status fields, mention their temporal or state-tracking role only when supported by the schema.
|
|
20
|
+
|
|
21
|
+
Output rules:
|
|
22
|
+
- Return ONLY the table description.
|
|
23
|
+
- Use plain text, with no Markdown.
|
|
24
|
+
- Use 1 concise sentence, or 2 sentences if necessary.
|
|
25
|
+
- Do not start with "Description:".
|
|
26
|
+
- Do not list columns unless necessary to explain the table.
|
|
27
|
+
- Do not include example values.
|
|
28
|
+
- Do not include SQL, comments, explanations, or reasoning.
|
|
29
|
+
- Do not ask for additional information.
|
|
30
|
+
|
|
31
|
+
Relevant schema:
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
TABLE_QUESTION_PROMPT = """
|
|
35
|
+
Generate a short description for the following table:
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
COLUMN_SYSTEM_PROMPT = """
|
|
39
|
+
You are an expert in SQL database schema understanding.
|
|
40
|
+
|
|
41
|
+
Your task is to generate a concise semantic description of the given column based only on the provided schema.
|
|
42
|
+
|
|
43
|
+
The description should explain the semantic meaning and role of the column within its table, rather than merely restating its name or data type.
|
|
44
|
+
|
|
45
|
+
Focus on:
|
|
46
|
+
- WHAT the column represents.
|
|
47
|
+
- WHAT the value means in the context of the table.
|
|
48
|
+
- The role of the column when it can be inferred, such as identifier, attribute, status, flag, timestamp, date, amount, quantity, metric, category, or foreign key.
|
|
49
|
+
- The entity or concept that the column refers to.
|
|
50
|
+
- For foreign keys, describe what entity the column identifies or references.
|
|
51
|
+
- For status/type/category columns, describe what concept or state they represent.
|
|
52
|
+
- For boolean/flag columns, describe the condition or property represented by the flag.
|
|
53
|
+
- For date/time columns, describe the business or technical event represented by the timestamp when it can be inferred.
|
|
54
|
+
- For numeric columns, describe the semantic meaning of the number rather than only saying it is a numeric value.
|
|
55
|
+
- Use surrounding columns, primary keys, foreign keys, constraints, and the table's purpose to disambiguate the column's meaning.
|
|
56
|
+
|
|
57
|
+
Inference rules:
|
|
58
|
+
- Interpret the column in the context of the table, not in isolation.
|
|
59
|
+
- Prefer semantic meaning over literal column-name expansion.
|
|
60
|
+
- Use relationships and constraints to infer meaning when possible.
|
|
61
|
+
- Do not invent business rules, meanings, units, or semantics that are not reasonably supported by the schema.
|
|
62
|
+
- If the meaning is ambiguous, provide the most conservative description supported by the available schema.
|
|
63
|
+
- Do not assume that a column name has a standard meaning if the schema provides evidence otherwise.
|
|
64
|
+
|
|
65
|
+
Output rules:
|
|
66
|
+
- Return ONLY the column description.
|
|
67
|
+
- Use plain text with no Markdown.
|
|
68
|
+
- Use 1 concise sentence, preferably under 25 words.
|
|
69
|
+
- Do not start with "Description:".
|
|
70
|
+
- Do not include example values.
|
|
71
|
+
- Do not list the column's data type unless it is essential to its semantic meaning.
|
|
72
|
+
- Do not include explanations, reasoning, comments, or SQL.
|
|
73
|
+
- Do not ask questions or request additional information.
|
|
74
|
+
|
|
75
|
+
Relevant schema:
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
COLUMN_QUESTION_PROMPT = """
|
|
79
|
+
Generate a short description for the following column:
|
|
80
|
+
"""
|
|
File without changes
|