post-graph 0.1.0__tar.gz
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.
- post_graph-0.1.0/.gitignore +222 -0
- post_graph-0.1.0/PKG-INFO +290 -0
- post_graph-0.1.0/README.md +264 -0
- post_graph-0.1.0/demo.py +567 -0
- post_graph-0.1.0/demo_schema_per_realm.py +281 -0
- post_graph-0.1.0/demo_transaction.py +202 -0
- post_graph-0.1.0/greek_gods.py +210 -0
- post_graph-0.1.0/post_graph/__init__.py +25 -0
- post_graph-0.1.0/post_graph/client_asyncpg.py +1265 -0
- post_graph-0.1.0/post_graph/client_sqlalchemy.py +1307 -0
- post_graph-0.1.0/post_graph/errors.py +28 -0
- post_graph-0.1.0/post_graph/models.py +279 -0
- post_graph-0.1.0/pyproject.toml +41 -0
- post_graph-0.1.0/requirements.txt +2 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Secrets and keys
|
|
2
|
+
**/*secret*
|
|
3
|
+
**/*key*
|
|
4
|
+
|
|
5
|
+
# Byte-compiled / optimized / DLL files
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[codz]
|
|
8
|
+
*$py.class
|
|
9
|
+
|
|
10
|
+
# C extensions
|
|
11
|
+
*.so
|
|
12
|
+
|
|
13
|
+
# Distribution / packaging
|
|
14
|
+
.Python
|
|
15
|
+
build/
|
|
16
|
+
develop-eggs/
|
|
17
|
+
dist/
|
|
18
|
+
downloads/
|
|
19
|
+
eggs/
|
|
20
|
+
.eggs/
|
|
21
|
+
lib/
|
|
22
|
+
lib64/
|
|
23
|
+
parts/
|
|
24
|
+
sdist/
|
|
25
|
+
var/
|
|
26
|
+
wheels/
|
|
27
|
+
share/python-wheels/
|
|
28
|
+
*.egg-info/
|
|
29
|
+
.installed.cfg
|
|
30
|
+
*.egg
|
|
31
|
+
MANIFEST
|
|
32
|
+
|
|
33
|
+
# PyInstaller
|
|
34
|
+
# Usually these files are written by a python script from a template
|
|
35
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
36
|
+
*.manifest
|
|
37
|
+
*.spec
|
|
38
|
+
|
|
39
|
+
# Installer logs
|
|
40
|
+
pip-log.txt
|
|
41
|
+
pip-delete-this-directory.txt
|
|
42
|
+
|
|
43
|
+
# Unit test / coverage reports
|
|
44
|
+
htmlcov/
|
|
45
|
+
.tox/
|
|
46
|
+
.nox/
|
|
47
|
+
.coverage
|
|
48
|
+
.coverage.*
|
|
49
|
+
.cache
|
|
50
|
+
nosetests.xml
|
|
51
|
+
coverage.xml
|
|
52
|
+
*.cover
|
|
53
|
+
*.py.cover
|
|
54
|
+
.hypothesis/
|
|
55
|
+
.pytest_cache/
|
|
56
|
+
cover/
|
|
57
|
+
|
|
58
|
+
# Translations
|
|
59
|
+
*.mo
|
|
60
|
+
*.pot
|
|
61
|
+
|
|
62
|
+
# Django stuff:
|
|
63
|
+
*.log
|
|
64
|
+
local_settings.py
|
|
65
|
+
db.sqlite3
|
|
66
|
+
db.sqlite3-journal
|
|
67
|
+
|
|
68
|
+
# Flask stuff:
|
|
69
|
+
instance/
|
|
70
|
+
.webassets-cache
|
|
71
|
+
|
|
72
|
+
# Scrapy stuff:
|
|
73
|
+
.scrapy
|
|
74
|
+
|
|
75
|
+
# Sphinx documentation
|
|
76
|
+
docs/_build/
|
|
77
|
+
|
|
78
|
+
# PyBuilder
|
|
79
|
+
.pybuilder/
|
|
80
|
+
target/
|
|
81
|
+
|
|
82
|
+
# Jupyter Notebook
|
|
83
|
+
.ipynb_checkpoints
|
|
84
|
+
|
|
85
|
+
# IPython
|
|
86
|
+
profile_default/
|
|
87
|
+
ipython_config.py
|
|
88
|
+
|
|
89
|
+
# pyenv
|
|
90
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
91
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
92
|
+
# .python-version
|
|
93
|
+
|
|
94
|
+
# pipenv
|
|
95
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
96
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
97
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
98
|
+
# install all needed dependencies.
|
|
99
|
+
# Pipfile.lock
|
|
100
|
+
|
|
101
|
+
# UV
|
|
102
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
103
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
104
|
+
# commonly ignored for libraries.
|
|
105
|
+
# uv.lock
|
|
106
|
+
|
|
107
|
+
# poetry
|
|
108
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
109
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
110
|
+
# commonly ignored for libraries.
|
|
111
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
112
|
+
# poetry.lock
|
|
113
|
+
# poetry.toml
|
|
114
|
+
|
|
115
|
+
# pdm
|
|
116
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
117
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
118
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
119
|
+
# pdm.lock
|
|
120
|
+
# pdm.toml
|
|
121
|
+
.pdm-python
|
|
122
|
+
.pdm-build/
|
|
123
|
+
|
|
124
|
+
# pixi
|
|
125
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
126
|
+
# pixi.lock
|
|
127
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
128
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
129
|
+
.pixi
|
|
130
|
+
|
|
131
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
132
|
+
__pypackages__/
|
|
133
|
+
|
|
134
|
+
# Celery stuff
|
|
135
|
+
celerybeat-schedule
|
|
136
|
+
celerybeat.pid
|
|
137
|
+
|
|
138
|
+
# Redis
|
|
139
|
+
*.rdb
|
|
140
|
+
*.aof
|
|
141
|
+
*.pid
|
|
142
|
+
|
|
143
|
+
# RabbitMQ
|
|
144
|
+
mnesia/
|
|
145
|
+
rabbitmq/
|
|
146
|
+
rabbitmq-data/
|
|
147
|
+
|
|
148
|
+
# ActiveMQ
|
|
149
|
+
activemq-data/
|
|
150
|
+
|
|
151
|
+
# SageMath parsed files
|
|
152
|
+
*.sage.py
|
|
153
|
+
|
|
154
|
+
# Environments
|
|
155
|
+
.env
|
|
156
|
+
.envrc
|
|
157
|
+
.venv
|
|
158
|
+
env/
|
|
159
|
+
venv/
|
|
160
|
+
ENV/
|
|
161
|
+
env.bak/
|
|
162
|
+
venv.bak/
|
|
163
|
+
|
|
164
|
+
# Spyder project settings
|
|
165
|
+
.spyderproject
|
|
166
|
+
.spyproject
|
|
167
|
+
|
|
168
|
+
# Rope project settings
|
|
169
|
+
.ropeproject
|
|
170
|
+
|
|
171
|
+
# mkdocs documentation
|
|
172
|
+
/site
|
|
173
|
+
|
|
174
|
+
# mypy
|
|
175
|
+
.mypy_cache/
|
|
176
|
+
.dmypy.json
|
|
177
|
+
dmypy.json
|
|
178
|
+
|
|
179
|
+
# Pyre type checker
|
|
180
|
+
.pyre/
|
|
181
|
+
|
|
182
|
+
# pytype static type analyzer
|
|
183
|
+
.pytype/
|
|
184
|
+
|
|
185
|
+
# Cython debug symbols
|
|
186
|
+
cython_debug/
|
|
187
|
+
|
|
188
|
+
# PyCharm
|
|
189
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
190
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
191
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
192
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
193
|
+
# .idea/
|
|
194
|
+
|
|
195
|
+
# Abstra
|
|
196
|
+
# Abstra is an AI-powered process automation framework.
|
|
197
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
198
|
+
# Learn more at https://abstra.io/docs
|
|
199
|
+
.abstra/
|
|
200
|
+
|
|
201
|
+
# Visual Studio Code
|
|
202
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
203
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
204
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
205
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
206
|
+
# .vscode/
|
|
207
|
+
# Temporary file for partial code execution
|
|
208
|
+
tempCodeRunnerFile.py
|
|
209
|
+
|
|
210
|
+
# Ruff stuff:
|
|
211
|
+
.ruff_cache/
|
|
212
|
+
|
|
213
|
+
# PyPI configuration file
|
|
214
|
+
.pypirc
|
|
215
|
+
|
|
216
|
+
# Marimo
|
|
217
|
+
marimo/_static/
|
|
218
|
+
marimo/_lsp/
|
|
219
|
+
__marimo__/
|
|
220
|
+
|
|
221
|
+
# Streamlit
|
|
222
|
+
.streamlit/secrets.toml
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: post-graph
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: High-performance PostgreSQL-backed graph database library supporting multi-tenant realms, schema-per-realm, shadow auditing, and append-only data history.
|
|
5
|
+
Project-URL: Homepage, https://github.com/crajah/post-graph
|
|
6
|
+
Project-URL: Repository, https://github.com/crajah/post-graph
|
|
7
|
+
Author-email: Chandan Rajah <chandan.rajah@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: asyncpg,database,graph,postgres,postgresql,sqlalchemy
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Database
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Requires-Dist: asyncpg>=0.29.0
|
|
21
|
+
Provides-Extra: all
|
|
22
|
+
Requires-Dist: sqlalchemy>=2.0.0; extra == 'all'
|
|
23
|
+
Provides-Extra: sqlalchemy
|
|
24
|
+
Requires-Dist: sqlalchemy>=2.0.0; extra == 'sqlalchemy'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# post-graph: Generic PostgreSQL Graph Database Library
|
|
28
|
+
|
|
29
|
+
A generic, high-performance mechanism to use PostgreSQL as a graph database in Python. It supports **multi-tenancy (realms)**, automatic **shadow audit tables**, and advanced **graph traversals (recursive CTEs)**.
|
|
30
|
+
|
|
31
|
+
Features:
|
|
32
|
+
* **Table-Per-Vertex & Table-Per-Edge Architecture**: Graph elements are mapped to individual tables, utilizing PostgreSQL's native schema constraints and relations.
|
|
33
|
+
* **Multi-Tenancy**: Built-in logical isolation using a `realm` field, allowing multiple tenants to share the same database while enforcing partition boundaries.
|
|
34
|
+
* **Shadow Auditing**: Automatically creates a matching `{table_name}_audit` table for every vertex and edge table. Operates at the database trigger level to log operations (`INSERT`, `UPDATE`, `DELETE`), capturing the complete row history and the initiating `user_id` passed securely via session parameters.
|
|
35
|
+
* **Dynamic Metadata Discovery**: Queries the PostgreSQL catalog tables to dynamically determine the structure and connections of edge tables during traversals.
|
|
36
|
+
* **Advanced Graph CTEs**: High-performance recursive CTE queries for single-hop neighbor queries, multi-hop path traversals, and shortest-path calculation (with early-termination and cycle prevention).
|
|
37
|
+
* **Multiple Backend Drivers**: Includes native clients for raw `asyncpg` and `SQLAlchemy` (v2.0 async connections).
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Architecture Schema
|
|
42
|
+
|
|
43
|
+
The schema design isolates graphs by tenant `realm`, ensuring that edges can only link vertices belonging to the exact same realm.
|
|
44
|
+
|
|
45
|
+
```mermaid
|
|
46
|
+
erDiagram
|
|
47
|
+
users {
|
|
48
|
+
text realm PK
|
|
49
|
+
bigserial id PK
|
|
50
|
+
text fqid
|
|
51
|
+
jsonb payload
|
|
52
|
+
timestamptz created_at
|
|
53
|
+
timestamptz updated_at
|
|
54
|
+
}
|
|
55
|
+
posts {
|
|
56
|
+
text realm PK
|
|
57
|
+
bigserial id PK
|
|
58
|
+
text fqid
|
|
59
|
+
jsonb payload
|
|
60
|
+
timestamptz created_at
|
|
61
|
+
timestamptz updated_at
|
|
62
|
+
}
|
|
63
|
+
likes {
|
|
64
|
+
text realm PK, FK
|
|
65
|
+
bigserial id PK
|
|
66
|
+
text fqid
|
|
67
|
+
bigint from_id FK
|
|
68
|
+
bigint to_id FK
|
|
69
|
+
text relation_type
|
|
70
|
+
jsonb payload
|
|
71
|
+
timestamptz created_at
|
|
72
|
+
timestamptz updated_at
|
|
73
|
+
}
|
|
74
|
+
users_audit {
|
|
75
|
+
bigint audit_id PK
|
|
76
|
+
text realm
|
|
77
|
+
text action
|
|
78
|
+
text changed_by
|
|
79
|
+
timestamptz changed_at
|
|
80
|
+
jsonb old_row
|
|
81
|
+
jsonb new_row
|
|
82
|
+
}
|
|
83
|
+
users_data {
|
|
84
|
+
bigserial data_id PK
|
|
85
|
+
text realm FK
|
|
86
|
+
bigint id FK
|
|
87
|
+
jsonb payload
|
|
88
|
+
timestamptz timestamp
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
users ||--o{ likes : "from_id"
|
|
92
|
+
posts ||--o{ likes : "to_id"
|
|
93
|
+
users ||--o{ users_audit : "logs changes"
|
|
94
|
+
users ||--o{ users_data : "appends records"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Installation
|
|
100
|
+
|
|
101
|
+
Define dependencies in your environment:
|
|
102
|
+
```bash
|
|
103
|
+
pip install -r requirements.txt
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Quick Start
|
|
109
|
+
|
|
110
|
+
### 1. Raw `asyncpg` Client (`AsyncPostGraph`)
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
import asyncio
|
|
114
|
+
from post_graph import AsyncPostGraph
|
|
115
|
+
|
|
116
|
+
async def main():
|
|
117
|
+
# Connect to PostgreSQL
|
|
118
|
+
client = AsyncPostGraph(dsn="postgresql://postgres:postgres@localhost:5432/postgres")
|
|
119
|
+
await client.connect()
|
|
120
|
+
|
|
121
|
+
# Create schema (with optional edge-to-vertex cascade deletion parameters)
|
|
122
|
+
await client.create_vertex_table("users")
|
|
123
|
+
await client.create_vertex_table("posts")
|
|
124
|
+
await client.create_edge_table(
|
|
125
|
+
"likes",
|
|
126
|
+
from_vertex_table="users",
|
|
127
|
+
to_vertex_table="posts",
|
|
128
|
+
cascade_delete_from=False, # If True, deleting an edge deletes the source vertex
|
|
129
|
+
cascade_delete_to=False # If True, deleting an edge deletes the target vertex
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Add vertices (isolated in realm 'tenant_1' and audited as user 'admin_alice')
|
|
133
|
+
await client.add_vertex(
|
|
134
|
+
table_name="users",
|
|
135
|
+
realm="tenant_1",
|
|
136
|
+
vertex_id="u_1",
|
|
137
|
+
payload={"username": "alice"},
|
|
138
|
+
user_id="admin_alice"
|
|
139
|
+
)
|
|
140
|
+
await client.add_vertex(
|
|
141
|
+
table_name="posts",
|
|
142
|
+
realm="tenant_1",
|
|
143
|
+
vertex_id="p_1",
|
|
144
|
+
payload={"title": "Hello World"},
|
|
145
|
+
user_id="admin_alice"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Link vertices with an edge (with optional cycle prevention checks)
|
|
149
|
+
await client.add_edge(
|
|
150
|
+
table_name="likes",
|
|
151
|
+
realm="tenant_1",
|
|
152
|
+
edge_id="e_1",
|
|
153
|
+
from_id="u_1",
|
|
154
|
+
to_id="p_1",
|
|
155
|
+
relation_type="like",
|
|
156
|
+
payload={"reaction": "heart"},
|
|
157
|
+
user_id="user_bob",
|
|
158
|
+
check_cycle=True # Optional: raise CyclicReferenceError if a loop would be created
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Query neighbors
|
|
162
|
+
neighbors = await client.get_neighbors(
|
|
163
|
+
realm="tenant_1",
|
|
164
|
+
vertex_table="users",
|
|
165
|
+
vertex_id="u_1",
|
|
166
|
+
edge_tables=["likes"]
|
|
167
|
+
)
|
|
168
|
+
for vertex, edge in neighbors:
|
|
169
|
+
print(f"Connected to post: {vertex.id} with payload: {vertex.payload}")
|
|
170
|
+
|
|
171
|
+
# Delete an entire realm (tenant) from all tables (including audits)
|
|
172
|
+
deleted_rows = await client.delete_realm("tenant_1")
|
|
173
|
+
print(f"Cleared {deleted_rows} rows belonging to tenant_1")
|
|
174
|
+
|
|
175
|
+
await client.close()
|
|
176
|
+
|
|
177
|
+
asyncio.run(main())
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 2. SQLAlchemy Client (`SQLAlchemyPostGraph`)
|
|
181
|
+
|
|
182
|
+
Integrates with your existing SQLAlchemy async architecture:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
186
|
+
from post_graph import SQLAlchemyPostGraph
|
|
187
|
+
|
|
188
|
+
engine = create_async_engine("postgresql+asyncpg://postgres:postgres@localhost:5432/postgres")
|
|
189
|
+
client = SQLAlchemyPostGraph(engine)
|
|
190
|
+
|
|
191
|
+
# All schema creation and graph operation interfaces match the AsyncPostGraph client
|
|
192
|
+
await client.create_vertex_table("users")
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Advanced Graph Operations
|
|
196
|
+
|
|
197
|
+
### Direct Object-Level Traversal
|
|
198
|
+
If a `Vertex` is loaded from a client, you can traverse directly from the object using standard OOP calling patterns.
|
|
199
|
+
|
|
200
|
+
*Note: Since `from` is a reserved keyword in Python, the reverse traversal method is named `.from_()` (with an underscore).*
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
# Load a vertex
|
|
204
|
+
alice_v = await client.get_vertex(table_name="users", realm="tenant_1", vertex_id="u_1")
|
|
205
|
+
|
|
206
|
+
# 1. Forward traversal
|
|
207
|
+
steps = await alice_v.to("follows")
|
|
208
|
+
if steps:
|
|
209
|
+
neighbor_v = steps[0].vertex()
|
|
210
|
+
print(f"Alice follows: {neighbor_v.id}")
|
|
211
|
+
|
|
212
|
+
# 2. Reverse traversal (incoming links)
|
|
213
|
+
rev_steps = await neighbor_v.from_("follows")
|
|
214
|
+
if rev_steps:
|
|
215
|
+
creator_v = rev_steps[0].vertex() # returns Alice!
|
|
216
|
+
print(f"Alice was found by reverse traversal: {creator_v.id}")
|
|
217
|
+
|
|
218
|
+
# 3. Direct Edge Mutations via Objects/Steps
|
|
219
|
+
if rev_steps:
|
|
220
|
+
new_edge = await rev_steps[0].add_edge_to(to_id="p_5", edge_table="likes")
|
|
221
|
+
print(f"Added edge: {new_edge.from_id} -> {new_edge.to_id}")
|
|
222
|
+
|
|
223
|
+
# 4. Direct Vertex/Edge Deletion
|
|
224
|
+
# Deleting a vertex automatically cascade-deletes all connecting edges in the database
|
|
225
|
+
await neighbor_v.delete(user_id="admin_1")
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Path Traversal
|
|
229
|
+
Retrieve all reachable paths up to a specified depth (default 3) starting from a vertex:
|
|
230
|
+
```python
|
|
231
|
+
paths = await client.traverse(
|
|
232
|
+
realm="tenant_1",
|
|
233
|
+
start_table="users",
|
|
234
|
+
start_id="u_1",
|
|
235
|
+
edge_tables=["likes", "follows"],
|
|
236
|
+
max_depth=3
|
|
237
|
+
)
|
|
238
|
+
for p in paths:
|
|
239
|
+
print(f"Path taken: {p['path']} via relations: {p['edge_path']}")
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Shortest Path
|
|
243
|
+
Find the shortest path between two vertices, preventing cycles:
|
|
244
|
+
```python
|
|
245
|
+
sp = await client.shortest_path(
|
|
246
|
+
realm="tenant_1",
|
|
247
|
+
start_table="users",
|
|
248
|
+
start_id="u_1",
|
|
249
|
+
target_table="posts",
|
|
250
|
+
target_id="p_5",
|
|
251
|
+
edge_tables=["follows", "likes"]
|
|
252
|
+
)
|
|
253
|
+
if sp:
|
|
254
|
+
print(f"Shortest path depth: {sp['depth']}, steps: {sp['path']}")
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Shadow Auditing & Session User Mapping
|
|
260
|
+
|
|
261
|
+
Auditing works entirely inside PostgreSQL triggers. To attribute a change to a user:
|
|
262
|
+
1. When calling Python operations (`add_vertex`, `upsert_edge`, `delete_vertex`, etc.), pass the `user_id` keyword argument.
|
|
263
|
+
2. The library executes `SELECT set_config('app.current_user_id', user_id, true)` in the current transaction block.
|
|
264
|
+
3. The database trigger automatically maps this value into the `{table_name}_audit` table.
|
|
265
|
+
A query to `{table_name}_audit` returns the modification history:
|
|
266
|
+
```sql
|
|
267
|
+
SELECT audit_id, realm, action, changed_by, changed_at, old_row, new_row
|
|
268
|
+
FROM users_audit;
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Schema-Per-Realm (Physical Namespace Isolation)
|
|
274
|
+
|
|
275
|
+
By default, `post-graph` uses a single shared set of tables and isolates data logically using a `realm` column. If you require physical data isolation, enable the `schema_per_realm` parameter on client initialization:
|
|
276
|
+
|
|
277
|
+
```python
|
|
278
|
+
# Initialize Async Client with physical namespace isolation
|
|
279
|
+
client = AsyncPostGraph(dsn=db_url, schema_per_realm=True)
|
|
280
|
+
|
|
281
|
+
# Create vertex table under specific realm schema namespace 'tenant_a'
|
|
282
|
+
await client.create_vertex_table("users", realm="tenant_a")
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
When enabled:
|
|
286
|
+
1. Every realm has its own dedicated schema namespace (e.g. `CREATE SCHEMA IF NOT EXISTS "tenant_a"`).
|
|
287
|
+
2. All vertex, edge, and shadow audit tables for a realm are stored physically inside that schema (e.g., `"tenant_a"."users"` and `"tenant_a"."users_audit"`).
|
|
288
|
+
3. Database level triggers are completely isolated per schema namespace.
|
|
289
|
+
4. Schema-specific metadata catalog lookups verify table links locally inside the target schema namespace.
|
|
290
|
+
|