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.
Files changed (42) hide show
  1. dbgraph/__init__.py +21 -0
  2. dbgraph/application.py +70 -0
  3. dbgraph/builder/__init__.py +0 -0
  4. dbgraph/builder/graph_builder.py +22 -0
  5. dbgraph/builder/sql_graph_builder.py +245 -0
  6. dbgraph/descriptor/__init__.py +0 -0
  7. dbgraph/descriptor/graph_descriptor.py +68 -0
  8. dbgraph/descriptor/langchain_graph_descriptor.py +74 -0
  9. dbgraph/descriptor/prompt_templates.py +80 -0
  10. dbgraph/entity/__init__.py +0 -0
  11. dbgraph/entity/aspect.py +153 -0
  12. dbgraph/entity/asset.py +26 -0
  13. dbgraph/entity/asset_type.py +13 -0
  14. dbgraph/entity/dbgraph.py +152 -0
  15. dbgraph/entity/link.py +37 -0
  16. dbgraph/entity/link_type.py +13 -0
  17. dbgraph/entity/rdbgraph.py +76 -0
  18. dbgraph/event/__init__.py +0 -0
  19. dbgraph/event/event.py +20 -0
  20. dbgraph/event/event_bus.py +61 -0
  21. dbgraph/event/event_handler.py +11 -0
  22. dbgraph/io/__init__.py +0 -0
  23. dbgraph/io/graph_loader.py +11 -0
  24. dbgraph/io/graph_writer.py +11 -0
  25. dbgraph/io/json_graph_loader.py +160 -0
  26. dbgraph/io/json_graph_writer.py +17 -0
  27. dbgraph/persistent/__init__.py +0 -0
  28. dbgraph/persistent/graph_persistent.py +98 -0
  29. dbgraph/persistent/models.py +222 -0
  30. dbgraph/persistent/sql_graph_persistent.py +388 -0
  31. dbgraph/render/__init__.py +0 -0
  32. dbgraph/render/graph_renderer.py +11 -0
  33. dbgraph/render/markdown_renderer.py +111 -0
  34. dbgraph/render/text_renderer.py +11 -0
  35. dbgraph/search/__init__.py +0 -0
  36. dbgraph/search/bm25_search_engine.py +33 -0
  37. dbgraph/search/search_engine.py +15 -0
  38. dbgraph/utils/__init__.py +0 -0
  39. dbgraph/utils/singleton.py +37 -0
  40. dbgraph-0.1.0.dist-info/METADATA +165 -0
  41. dbgraph-0.1.0.dist-info/RECORD +42 -0
  42. dbgraph-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,153 @@
1
+ from dataclasses import dataclass
2
+ from enum import StrEnum
3
+
4
+
5
+ @dataclass
6
+ class Aspect:
7
+ """Represent a set of properties that are related to an aspect
8
+
9
+ Attributes:
10
+ name: The name of this aspect
11
+ """
12
+
13
+ name: str
14
+
15
+
16
+ @dataclass
17
+ class SemanticAspect(Aspect):
18
+ """Represent properties related to semantic meaning of the asset
19
+
20
+ Attributes:
21
+ description: The natural language description for this asset
22
+ keywords: list of relevant keywords
23
+ """
24
+
25
+ description: str
26
+ keywords: list[str]
27
+
28
+
29
+ @dataclass
30
+ class RTableSchemaAspect(Aspect):
31
+ """Group of properties relevant to relational databases' table schema
32
+
33
+ Attributes:
34
+ pks: list of column names belong to the primary key
35
+ indices: a dictionary which map the table's indices with the columns belong to corresponding indices
36
+ """
37
+
38
+ pks: list[str]
39
+ indices: dict[str, list[str]]
40
+
41
+
42
+ @dataclass
43
+ class RTableStatisticsAspect(Aspect):
44
+ """Group of properties relevant to relational databases' table statistics
45
+
46
+ Attributes:
47
+ num_columns: number of columns
48
+ num_rows: number of rows
49
+ """
50
+
51
+ num_columns: int
52
+ num_rows: int
53
+
54
+
55
+ @dataclass
56
+ class RColumnSchemaAspect(Aspect):
57
+ """Group of properties relevant to RDB's column schema
58
+
59
+ Attributes:
60
+ dtype: data type
61
+ is_pk: whether or not this column belongs to primary key
62
+ is_nullable: whether or not this column is nullable
63
+ """
64
+
65
+ dtype: str
66
+ is_pk: bool
67
+ is_nullable: bool
68
+
69
+
70
+ @dataclass
71
+ class RNumericalStatistics:
72
+ """Statistics for numerical columns
73
+
74
+ Attributes:
75
+ mean: Mean value
76
+ std: Standard deviation
77
+ min: Minimum value
78
+ q2: 2nd quantile
79
+ median: Median value
80
+ q3: 3rd quantile
81
+ max: Maximum value
82
+
83
+ """
84
+
85
+ mean: float | None = None
86
+ std: float | None = None
87
+ min: float | None = None
88
+ q2: float | None = None
89
+ median: float | None = None
90
+ q3: float | None = None
91
+ max: float | None = None
92
+
93
+
94
+ @dataclass
95
+ class RCategoricalStatistics:
96
+ """Statistics for categorical columns
97
+
98
+ Attributes:
99
+ value_counts: top 10 most frequent values in a column and
100
+ its corresponding frequency
101
+ """
102
+
103
+ value_counts: dict[str, int]
104
+
105
+
106
+ @dataclass
107
+ class RColumnStatisticsAspect(Aspect):
108
+ """Group of properties relevant to RDB's column statistics
109
+
110
+ Attributes:
111
+ non_null_count: Number of non null records
112
+ null_count: Number of null records
113
+ numerical_stats: numerical statistics of this column if its data type is numeric
114
+ categorical_stats: categorical statistics of this column if its data type is string-like
115
+ """
116
+
117
+ non_null_count: int
118
+ null_count: int
119
+ numerical_stats: RNumericalStatistics | None = None
120
+ categorical_stats: RCategoricalStatistics | None = None
121
+
122
+
123
+ class FKBehavior(StrEnum):
124
+ """Defines the behavior of a foreign key when its referenced row changes.
125
+
126
+ Attributes:
127
+ CASCADE: Automatically propagates the change to referencing rows.
128
+ SET_NULL: Sets the foreign key value to ``NULL``.
129
+ RESTRICT: Prevents the operation if referencing rows exist.
130
+ NO_ACTION: Does not perform any action on referencing rows.
131
+ """
132
+
133
+ CASCADE = "CASCADE"
134
+ SET_NULL = "SET NULL"
135
+ RESTRICT = "RESTRICT"
136
+ NO_ACTION = "NO ACTION"
137
+
138
+
139
+ @dataclass
140
+ class RForeignKeyAspect(Aspect):
141
+ """Group of properties relevant to foreign keys
142
+
143
+ Attributes:
144
+ from_column: the source column
145
+ to_column: referred column
146
+ on_delete: foreign key behavior on delete event
147
+ on_update: foreign key behavior on update event
148
+ """
149
+
150
+ from_column: str
151
+ to_column: str
152
+ on_delete: FKBehavior | str
153
+ on_update: FKBehavior | str
@@ -0,0 +1,26 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ from dbgraph.entity.aspect import (
4
+ Aspect,
5
+ )
6
+ from dbgraph.entity.asset_type import AssetType
7
+
8
+
9
+ @dataclass
10
+ class Asset:
11
+ """
12
+ Represent a data asset, such as a table in SQL databases or a document in MongoDB.
13
+
14
+ It acts as a vertex in the graph.
15
+
16
+ Attributes:
17
+ asset_id: ID of this asset
18
+ name: original name of this asset
19
+ type: type of asset
20
+ aspects: properties aspects belong to this asset
21
+ """
22
+
23
+ asset_id: str
24
+ name: str
25
+ type: AssetType
26
+ aspects: dict[str, Aspect] = field(default_factory=dict)
@@ -0,0 +1,13 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class AssetType(StrEnum):
5
+ """Type of `Asset`.
6
+
7
+ Attributes:
8
+ table: table in SQL databases
9
+ column: column in SQL databases
10
+ """
11
+
12
+ RTABLE = "table"
13
+ RCOLUMN = "column"
@@ -0,0 +1,152 @@
1
+ from __future__ import annotations
2
+
3
+ import itertools
4
+ from dataclasses import dataclass
5
+
6
+ import rustworkx as rx
7
+
8
+ from dbgraph.entity.asset import Asset
9
+ from dbgraph.entity.link import Link
10
+
11
+
12
+ @dataclass
13
+ class DatabaseGraph:
14
+ """Represents a database as a graph of assets and relationships.
15
+
16
+ A `DatabaseGraph` models the structure of a database using
17
+ assets as nodes and links as relationships between those assets.
18
+ It provides methods for creating, querying, and traversing the
19
+ database structure in a graph-oriented way.
20
+
21
+ Attributes:
22
+ assets: The collection of `Asset` objects in the graph.
23
+ links: The collection of `Link` objects representing
24
+ relationships between assets.
25
+ """
26
+
27
+ assets: list[Asset]
28
+ links: list[Link]
29
+
30
+ def __post_init__(self):
31
+ self._asset_id_to_node_idx = {a.asset_id: i for i, a in enumerate(self.assets)}
32
+ self._nodes_data = {i: asset for i, asset in enumerate(self.assets)}
33
+ self._edges_data = {i: link for i, link in enumerate(self.links)}
34
+ links_tuples = [
35
+ (
36
+ self._asset_id_to_node_idx[link.source_id],
37
+ self._asset_id_to_node_idx[link.destination_id],
38
+ link,
39
+ )
40
+ for link in self.links
41
+ ]
42
+ self._graph = rx.PyGraph()
43
+ self._graph.add_nodes_from(self.assets)
44
+ self._graph.add_edges_from(links_tuples)
45
+
46
+ def _node_idx(self, asset_id: str) -> int:
47
+ try:
48
+ return self._asset_id_to_node_idx[asset_id]
49
+ except KeyError:
50
+ raise KeyError(f"Asset with id={asset_id} not found!")
51
+
52
+ def _neighbors(self, asset_id: str) -> tuple[set[int], list[Link]]:
53
+ """Return list of asset's indices in the graph and those links"""
54
+ node_idx = self._node_idx(asset_id)
55
+
56
+ # Find links
57
+ links: dict[int, Link] = self._graph.adj(node_idx)
58
+
59
+ # Find assets indices
60
+ asset_indices = {self._node_idx(link.destination_id) for link in links.values()}
61
+ asset_indices = asset_indices.union(
62
+ {self._node_idx(link.source_id) for link in links.values()}
63
+ )
64
+
65
+ return asset_indices, list(links.values())
66
+
67
+ def get_asset(self, asset_id: str) -> Asset:
68
+ """Return the asset using asset_id"""
69
+ return self._graph.get_node_data(self._node_idx(asset_id))
70
+
71
+ def get_link(self, src_id: str, dst_id: str) -> Link:
72
+ """Return the link connect these two Assets"""
73
+ return self._graph.get_edge_data(self._node_idx(src_id), self._node_idx(dst_id))
74
+
75
+ def select_neighbors(self, asset_id: str) -> DatabaseGraph:
76
+ """Get this node and its neighbors.
77
+
78
+ Args:
79
+ asset_id: ID of the asset to be queried
80
+
81
+ Return:
82
+ subgraph: an instance of `DatabaseGraph` as subgraph
83
+ """
84
+
85
+ asset_indices, links = self._neighbors(asset_id)
86
+ assets = [self._graph.get_node_data(i) for i in asset_indices]
87
+ return DatabaseGraph(assets, links)
88
+
89
+ def select_shortest_paths(self, src_id: str, dst_id: str) -> list[DatabaseGraph]:
90
+ """
91
+ Find shortest paths between two Assets.
92
+
93
+ Args:
94
+ src_id: ID of the source asset
95
+ dst_id: ID of the destination asset
96
+
97
+ Returns:
98
+ subgraphs: list of `DatabaseGraph` which are the shortest paths
99
+ between source and destination Assets
100
+ """
101
+ src_idx = self._node_idx(src_id)
102
+ dst_idx = self._node_idx(dst_id)
103
+
104
+ paths = rx.all_shortest_paths(self._graph, src_idx, dst_idx)
105
+
106
+ # Get a list of lists of assets
107
+ assets_lists = [[self._graph.get_node_data(i) for i in path] for path in paths]
108
+
109
+ # Get a list of lists of links
110
+ pairs_lists = [itertools.pairwise(path) for path in paths]
111
+ links_lists = [
112
+ [self._graph.get_edge_data(pair[0], pair[1]) for pair in pairs]
113
+ for pairs in pairs_lists
114
+ ]
115
+ return [
116
+ DatabaseGraph(assets, links)
117
+ for assets, links in zip(assets_lists, links_lists)
118
+ ]
119
+
120
+ def get_neighbors(self, asset_id: str) -> list[Asset]:
121
+ """Get neighbors assets of an asset
122
+
123
+ Args:
124
+ asset_id: target asset
125
+
126
+ Return:
127
+ list of neighbor `Asset`s
128
+ """
129
+ asset_indices, _ = self._neighbors(asset_id)
130
+ # Remove itself
131
+ try:
132
+ asset_indices.remove(self._node_idx(asset_id))
133
+ except KeyError:
134
+ pass
135
+ return [self._graph.get_node_data(i) for i in asset_indices]
136
+
137
+ def find_shortest_paths(self, src_id: str, dst_id: str) -> list[list[Asset]]:
138
+ """Find shortest paths between two Assets
139
+
140
+ Args:
141
+ src_id: ID of the source asset
142
+ dst_id: ID of the destination asset
143
+
144
+ Returns:
145
+ assets: list of lists of assets. Each list is a shortest path
146
+ from source Asset to destination Asset
147
+ """
148
+ src_idx = self._node_idx(src_id)
149
+ dst_idx = self._node_idx(dst_id)
150
+ paths = rx.all_shortest_paths(self._graph, src_idx, dst_idx)
151
+
152
+ return [[self._graph.get_node_data(i) for i in path] for path in paths]
dbgraph/entity/link.py ADDED
@@ -0,0 +1,37 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import cast
3
+
4
+ from dbgraph.entity.aspect import Aspect, RForeignKeyAspect
5
+ from dbgraph.entity.link_type import LinkType
6
+
7
+
8
+ @dataclass
9
+ class Link:
10
+ """
11
+ Represent relationship between two assets.
12
+
13
+ Attributes:
14
+ link_id: ID of this link
15
+ name: name
16
+ type: type of link
17
+ source_id: ID of the source `Asset`
18
+ destination_id: ID of the destination `Asset`
19
+ aspects: properties aspects belong to this link
20
+ """
21
+
22
+ link_id: str
23
+ name: str
24
+ type: LinkType
25
+ source_id: str
26
+ destination_id: str
27
+ aspects: dict[str, Aspect] = field(default_factory=dict)
28
+
29
+ def to_markdown(self, src_name: str, dst_name: str) -> str:
30
+ match self.type:
31
+ case LinkType.CONTAIN:
32
+ return f"- Column `{dst_name}` belongs to table `{src_name}`\n"
33
+ case LinkType.FOREIGN_KEY:
34
+ fk_aspect = cast(
35
+ RForeignKeyAspect, self.aspects["foreign_key_properties"]
36
+ )
37
+ return f"- `{src_name}`.`{fk_aspect.from_column}` -> `{dst_name}`.`{fk_aspect.to_column}`\n"
@@ -0,0 +1,13 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class LinkType(StrEnum):
5
+ """Type of link.
6
+
7
+ Attributes:
8
+ foreign-key: Foreign key constraint
9
+ contain: containment relationship (table - columns)
10
+ """
11
+
12
+ FOREIGN_KEY = "foreign-key"
13
+ CONTAIN = "contain"
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ from dbgraph import LinkType
4
+ from dbgraph.entity.asset import Asset
5
+ from dbgraph.entity.asset_type import AssetType
6
+ from dbgraph.entity.dbgraph import DatabaseGraph
7
+ from dbgraph.entity.link import Link
8
+
9
+
10
+ class RDatabaseGraph(DatabaseGraph):
11
+ """Graph for Relational Database"""
12
+
13
+ @staticmethod
14
+ def from_graph(graph: DatabaseGraph) -> RDatabaseGraph:
15
+ return RDatabaseGraph(graph.assets, graph.links)
16
+
17
+ def get_columns(self, table_id: str) -> list[Asset]:
18
+ """Get list of assets which are columns belong to this table
19
+
20
+ Args:
21
+ table_id: ID of the target table
22
+
23
+ Returns:
24
+ assets: columns belong to this table
25
+ """
26
+ neighbors = self.get_neighbors(table_id)
27
+ return [n for n in neighbors if n.type == AssetType.RCOLUMN]
28
+
29
+ def get_tables(self) -> list[Asset]:
30
+ """Get all the tables in the graph"""
31
+ return [a for a in self.assets if a.type == AssetType.RTABLE]
32
+
33
+ def get_foreign_keys(self) -> list[Link]:
34
+ """Get all foreign keys in the graph"""
35
+ return [link for link in self.links if link.type == LinkType.FOREIGN_KEY]
36
+
37
+ def get_connected_tables(self, table_id: str) -> list[Asset]:
38
+ """Get all connected table
39
+
40
+ Args:
41
+ table_id: target table
42
+
43
+ Returns:
44
+ assets: list of references table
45
+ """
46
+ neighbors = self.get_neighbors(table_id)
47
+ return [n for n in neighbors if n.type == AssetType.RTABLE]
48
+
49
+ def select_connected_tables(self, table_id: str) -> RDatabaseGraph:
50
+ """Get references tables with their columns
51
+
52
+ Args:
53
+ table_id: target table
54
+
55
+ Returns:
56
+ subgraph: the subgraph of which include connected tables and their columns
57
+ """
58
+ refs_tables = self.get_connected_tables(table_id)
59
+ assets: list[Asset] = [table for table in refs_tables]
60
+ target_table: Asset = self.get_asset(table_id)
61
+ target_table_columns = self.get_columns(table_id)
62
+ assets.append(target_table)
63
+ assets.extend(target_table_columns)
64
+ links: list[Link] = [
65
+ self.get_link(target_table.asset_id, column.asset_id)
66
+ for column in target_table_columns
67
+ ]
68
+ for table in refs_tables:
69
+ fk_link = self.get_link(table_id, table.asset_id)
70
+ links.append(fk_link)
71
+ columns = self.get_columns(table.asset_id)
72
+ for column in columns:
73
+ assets.append(column)
74
+ link = self.get_link(table.asset_id, column.asset_id)
75
+ links.append(link)
76
+ return RDatabaseGraph(assets, links)
File without changes
dbgraph/event/event.py ADDED
@@ -0,0 +1,20 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class Event:
6
+ """Base event used in DBGraph"""
7
+
8
+ message: str
9
+
10
+
11
+ @dataclass
12
+ class ProgressEvent(Event):
13
+ """Event with progress"""
14
+
15
+ target: float
16
+ current: float
17
+
18
+ def get_percentage(self) -> float:
19
+ """Return the progress of the task in percentage"""
20
+ return self.current / self.target
@@ -0,0 +1,61 @@
1
+ import queue
2
+ import threading
3
+
4
+ from dbgraph.event.event import Event
5
+ from dbgraph.event.event_handler import EventHandler
6
+
7
+
8
+ class EventBus:
9
+ """Store events in a queue and manage a background thread to distribute events"""
10
+
11
+ def __init__(self) -> None:
12
+ self.queue = queue.Queue()
13
+ self.handlers: dict[type[Event], set[EventHandler]] = {}
14
+ self._thread: threading.Thread | None = None
15
+ self._stop_event = threading.Event()
16
+
17
+ def subscribe(self, event_type: type[Event], handler: EventHandler):
18
+ """Subscribe a handler to specific type of event"""
19
+ if event_type in self.handlers:
20
+ self.handlers[event_type].add(handler)
21
+ else:
22
+ self.handlers[event_type] = {handler}
23
+
24
+ def unsubscribe(self, event_type: type[Event], handler: EventHandler):
25
+ """Unsubscribe a handler from listening to specific event type"""
26
+ self.handlers[event_type].remove(handler)
27
+
28
+ def publish(self, event: Event):
29
+ """Push an event into the event queue"""
30
+ self.queue.put(event)
31
+
32
+ def _dispatch(self):
33
+ """Get the latest event and let the appropriate handler process it"""
34
+ event = self.queue.get()
35
+ for handler in self.handlers[type(event)]:
36
+ handler.handle(event)
37
+
38
+ def _run(self):
39
+ """Run the event loop"""
40
+ while not self._stop_event.is_set():
41
+ try:
42
+ self._dispatch()
43
+ except queue.Empty:
44
+ continue
45
+
46
+ def start(self):
47
+ """Start the event background thread"""
48
+ if self._thread is not None and self._thread.is_alive():
49
+ return
50
+ self._stop_event.clear()
51
+ self._thread = threading.Thread(
52
+ target=self._run, name="dbgraph-event-bus", daemon=True
53
+ )
54
+ self._thread.start()
55
+
56
+ def stop(self):
57
+ """Stop the event background thread"""
58
+ self._stop_event.set()
59
+ if self._thread is not None:
60
+ self._thread.join()
61
+ self._thread = None
@@ -0,0 +1,11 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from dbgraph.event.event import Event
4
+
5
+
6
+ class EventHandler(ABC):
7
+ """Interface for event handler"""
8
+
9
+ @abstractmethod
10
+ def handle(self, event: Event):
11
+ """Handle the event"""
dbgraph/io/__init__.py ADDED
File without changes
@@ -0,0 +1,11 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from dbgraph.entity.dbgraph import DatabaseGraph
4
+
5
+
6
+ class GraphLoader(ABC):
7
+ """Interface for classes that read graphs from disk"""
8
+
9
+ @abstractmethod
10
+ def load(self) -> DatabaseGraph:
11
+ pass
@@ -0,0 +1,11 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from dbgraph.entity.dbgraph import DatabaseGraph
4
+
5
+
6
+ class GraphWriter(ABC):
7
+ """Interface for classes that write graph to disk"""
8
+
9
+ @abstractmethod
10
+ def write(self, graph: DatabaseGraph):
11
+ pass