post-graph 0.3.2__tar.gz → 0.4.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.3.2 → post_graph-0.4.0}/PKG-INFO +1 -1
- {post_graph-0.3.2 → post_graph-0.4.0}/post_graph/__init__.py +5 -2
- {post_graph-0.3.2 → post_graph-0.4.0}/post_graph/client_asyncpg.py +77 -33
- {post_graph-0.3.2 → post_graph-0.4.0}/post_graph/client_sqlalchemy.py +123 -55
- {post_graph-0.3.2 → post_graph-0.4.0}/post_graph/errors.py +5 -0
- {post_graph-0.3.2 → post_graph-0.4.0}/post_graph/models.py +10 -6
- {post_graph-0.3.2 → post_graph-0.4.0}/pyproject.toml +1 -1
- {post_graph-0.3.2 → post_graph-0.4.0}/.github/workflows/publish.yml +0 -0
- {post_graph-0.3.2 → post_graph-0.4.0}/.gitignore +0 -0
- {post_graph-0.3.2 → post_graph-0.4.0}/LICENSE +0 -0
- {post_graph-0.3.2 → post_graph-0.4.0}/README.md +0 -0
- {post_graph-0.3.2 → post_graph-0.4.0}/demo.py +0 -0
- {post_graph-0.3.2 → post_graph-0.4.0}/demo_schema_per_realm.py +0 -0
- {post_graph-0.3.2 → post_graph-0.4.0}/demo_space.py +0 -0
- {post_graph-0.3.2 → post_graph-0.4.0}/demo_transaction.py +0 -0
- {post_graph-0.3.2 → post_graph-0.4.0}/demo_uuid.py +0 -0
- {post_graph-0.3.2 → post_graph-0.4.0}/demo_vector.py +0 -0
- {post_graph-0.3.2 → post_graph-0.4.0}/greek_gods.py +0 -0
- {post_graph-0.3.2 → post_graph-0.4.0}/requirements.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: post-graph
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: High-performance PostgreSQL-backed graph database library supporting multi-tenant realms, space sub-grouping, schema-per-realm, shadow auditing, and append-only data history.
|
|
5
5
|
Project-URL: Homepage, https://github.com/crajah/post-graph
|
|
6
6
|
Project-URL: Repository, https://github.com/crajah/post-graph
|
|
@@ -5,16 +5,17 @@ from post_graph.errors import (
|
|
|
5
5
|
TableExistsError,
|
|
6
6
|
TableNotFoundError,
|
|
7
7
|
CyclicReferenceError,
|
|
8
|
+
ReservedSpaceError,
|
|
8
9
|
)
|
|
9
10
|
from post_graph.models import Vertex, Edge, TraversalStep
|
|
10
|
-
from post_graph.client_asyncpg import AsyncPostGraph
|
|
11
|
+
from post_graph.client_asyncpg import AsyncPostGraph, RESERVED_SPACE_ALL
|
|
11
12
|
|
|
12
13
|
try:
|
|
13
14
|
from post_graph.client_sqlalchemy import SQLAlchemyPostGraph
|
|
14
15
|
except ImportError:
|
|
15
16
|
SQLAlchemyPostGraph = None
|
|
16
17
|
|
|
17
|
-
__version__ = "0.
|
|
18
|
+
__version__ = "0.4.0"
|
|
18
19
|
|
|
19
20
|
__all__ = [
|
|
20
21
|
"__version__",
|
|
@@ -24,9 +25,11 @@ __all__ = [
|
|
|
24
25
|
"TableExistsError",
|
|
25
26
|
"TableNotFoundError",
|
|
26
27
|
"CyclicReferenceError",
|
|
28
|
+
"ReservedSpaceError",
|
|
27
29
|
"Vertex",
|
|
28
30
|
"Edge",
|
|
29
31
|
"TraversalStep",
|
|
30
32
|
"AsyncPostGraph",
|
|
31
33
|
"SQLAlchemyPostGraph",
|
|
34
|
+
"RESERVED_SPACE_ALL",
|
|
32
35
|
]
|
|
@@ -10,12 +10,16 @@ from post_graph.errors import (
|
|
|
10
10
|
TableExistsError,
|
|
11
11
|
TableNotFoundError,
|
|
12
12
|
PostGraphError,
|
|
13
|
+
ReservedSpaceError,
|
|
13
14
|
)
|
|
14
15
|
from post_graph.models import Vertex, Edge, DataRecord
|
|
15
16
|
|
|
16
17
|
logger = logging.getLogger("post_graph")
|
|
17
18
|
|
|
18
19
|
|
|
20
|
+
RESERVED_SPACE_ALL = "__all__"
|
|
21
|
+
|
|
22
|
+
|
|
19
23
|
class AsyncPostGraph:
|
|
20
24
|
def __init__(
|
|
21
25
|
self,
|
|
@@ -338,6 +342,7 @@ class AsyncPostGraph:
|
|
|
338
342
|
CREATE TABLE IF NOT EXISTS {table_ref} (
|
|
339
343
|
realm TEXT NOT NULL,
|
|
340
344
|
id BIGSERIAL,
|
|
345
|
+
space VARCHAR(255) DEFAULT 'default' NOT NULL,
|
|
341
346
|
uuid UUID DEFAULT gen_random_uuid() NOT NULL,
|
|
342
347
|
fqid TEXT GENERATED ALWAYS AS (realm || '/' || '{from_vertex_table}-{to_vertex_table}' || '/' || id::text) STORED NOT NULL,
|
|
343
348
|
from_id BIGINT NOT NULL,
|
|
@@ -352,6 +357,8 @@ class AsyncPostGraph:
|
|
|
352
357
|
);
|
|
353
358
|
"""
|
|
354
359
|
await self._execute(query)
|
|
360
|
+
await self._execute(f"ALTER TABLE {table_ref} ADD COLUMN IF NOT EXISTS space VARCHAR(255) DEFAULT 'default';")
|
|
361
|
+
await self._execute(f'CREATE INDEX IF NOT EXISTS "idx_{table_name}_space" ON {table_ref} (realm, space);')
|
|
355
362
|
await self._execute(f"ALTER TABLE {table_ref} ADD COLUMN IF NOT EXISTS fqid TEXT GENERATED ALWAYS AS (realm || '/' || '{from_vertex_table}-{to_vertex_table}' || '/' || id::text) STORED;")
|
|
356
363
|
await self._execute(f"ALTER TABLE {table_ref} ADD COLUMN IF NOT EXISTS uuid UUID DEFAULT gen_random_uuid();")
|
|
357
364
|
await self._execute(f'CREATE UNIQUE INDEX IF NOT EXISTS "idx_{table_name}_uuid" ON {table_ref} (uuid);')
|
|
@@ -361,6 +368,7 @@ class AsyncPostGraph:
|
|
|
361
368
|
CREATE TABLE IF NOT EXISTS {audit_table_ref} (
|
|
362
369
|
audit_id BIGSERIAL PRIMARY KEY,
|
|
363
370
|
realm TEXT NOT NULL,
|
|
371
|
+
space VARCHAR(255) DEFAULT 'default',
|
|
364
372
|
action TEXT NOT NULL,
|
|
365
373
|
changed_by TEXT,
|
|
366
374
|
changed_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
@@ -376,12 +384,15 @@ class AsyncPostGraph:
|
|
|
376
384
|
data_id BIGSERIAL PRIMARY KEY,
|
|
377
385
|
realm TEXT NOT NULL,
|
|
378
386
|
id BIGINT NOT NULL,
|
|
387
|
+
space VARCHAR(255) DEFAULT 'default' NOT NULL,
|
|
379
388
|
payload JSONB NOT NULL DEFAULT '{{}}'::jsonb,
|
|
380
389
|
timestamp TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
381
390
|
FOREIGN KEY (realm, id) REFERENCES {table_ref}(realm, id) ON DELETE CASCADE
|
|
382
391
|
);
|
|
383
392
|
"""
|
|
384
393
|
await self._execute(data_query)
|
|
394
|
+
await self._execute(f"ALTER TABLE {data_table_ref} ADD COLUMN IF NOT EXISTS space VARCHAR(255) DEFAULT 'default';")
|
|
395
|
+
await self._execute(f'CREATE INDEX IF NOT EXISTS "idx_{table_name}_data_space" ON {data_table_ref} (realm, space);')
|
|
385
396
|
await self._execute(f'CREATE INDEX IF NOT EXISTS "idx_{table_name}_data_id" ON {data_table_ref} (realm, id);')
|
|
386
397
|
await self._execute(f'CREATE INDEX IF NOT EXISTS "idx_{table_name}_data_payload" ON {data_table_ref} USING gin (payload);')
|
|
387
398
|
|
|
@@ -451,6 +462,8 @@ class AsyncPostGraph:
|
|
|
451
462
|
) -> Vertex:
|
|
452
463
|
"""Add a new vertex. Raises TableExistsError if it already exists."""
|
|
453
464
|
self._validate_identifier(table_name)
|
|
465
|
+
if space == RESERVED_SPACE_ALL:
|
|
466
|
+
raise ReservedSpaceError(f"'{RESERVED_SPACE_ALL}' is a reserved space name and cannot be used for creation. It is only valid as a query-time filter.")
|
|
454
467
|
payload_json = json.dumps(payload or {})
|
|
455
468
|
table_ref = self._get_table_ref(table_name, realm)
|
|
456
469
|
vec_str = f"[{','.join(str(x) for x in embedding)}]" if embedding else None
|
|
@@ -531,6 +544,8 @@ class AsyncPostGraph:
|
|
|
531
544
|
) -> Vertex:
|
|
532
545
|
"""Upsert a vertex (merges payload JSONB on conflict)."""
|
|
533
546
|
self._validate_identifier(table_name)
|
|
547
|
+
if space == RESERVED_SPACE_ALL:
|
|
548
|
+
raise ReservedSpaceError(f"'{RESERVED_SPACE_ALL}' is a reserved space name and cannot be used for creation. It is only valid as a query-time filter.")
|
|
534
549
|
payload_json = json.dumps(payload or {})
|
|
535
550
|
table_ref = self._get_table_ref(table_name, realm)
|
|
536
551
|
vec_str = f"[{','.join(str(x) for x in embedding)}]" if embedding else None
|
|
@@ -616,7 +631,7 @@ class AsyncPostGraph:
|
|
|
616
631
|
|
|
617
632
|
async def _op(conn):
|
|
618
633
|
query = f"""
|
|
619
|
-
SELECT t.realm, t.id, t.fqid, t.payload, t.created_at, t.updated_at,
|
|
634
|
+
SELECT t.realm, t.id, t.space, t.fqid, t.payload, t.created_at, t.updated_at,
|
|
620
635
|
t.uuid::text AS uuid_text,
|
|
621
636
|
to_jsonb(t)->>'embedding' AS embedding_text
|
|
622
637
|
FROM {table_ref} t
|
|
@@ -634,6 +649,7 @@ class AsyncPostGraph:
|
|
|
634
649
|
return Vertex(
|
|
635
650
|
realm=row['realm'],
|
|
636
651
|
id=str(row['id']),
|
|
652
|
+
space=row.get('space') or 'default',
|
|
637
653
|
fqid=row['fqid'],
|
|
638
654
|
payload=row['payload'] if isinstance(row['payload'], dict) else json.loads(row['payload']),
|
|
639
655
|
created_at=row['created_at'],
|
|
@@ -666,7 +682,7 @@ class AsyncPostGraph:
|
|
|
666
682
|
async def _op(conn):
|
|
667
683
|
params = [realm]
|
|
668
684
|
space_clause = ""
|
|
669
|
-
if space:
|
|
685
|
+
if space and space != RESERVED_SPACE_ALL:
|
|
670
686
|
params.append(space)
|
|
671
687
|
space_clause = f" AND (t.space = ${len(params)} OR (${len(params)} = 'default' AND (t.space IS NULL OR t.space = 'default')))"
|
|
672
688
|
|
|
@@ -721,7 +737,7 @@ class AsyncPostGraph:
|
|
|
721
737
|
|
|
722
738
|
async def _op(conn):
|
|
723
739
|
query = f"""
|
|
724
|
-
SELECT t.realm, t.id, t.fqid, t.payload, t.created_at, t.updated_at,
|
|
740
|
+
SELECT t.realm, t.id, t.space, t.fqid, t.payload, t.created_at, t.updated_at,
|
|
725
741
|
t.uuid::text AS uuid_text,
|
|
726
742
|
to_jsonb(t)->>'embedding' AS embedding_text
|
|
727
743
|
FROM {table_ref} t
|
|
@@ -739,6 +755,7 @@ class AsyncPostGraph:
|
|
|
739
755
|
return Vertex(
|
|
740
756
|
realm=row['realm'],
|
|
741
757
|
id=str(row['id']),
|
|
758
|
+
space=row.get('space') or 'default',
|
|
742
759
|
fqid=row['fqid'],
|
|
743
760
|
payload=row['payload'] if isinstance(row['payload'], dict) else json.loads(row['payload']),
|
|
744
761
|
created_at=row['created_at'],
|
|
@@ -796,15 +813,22 @@ class AsyncPostGraph:
|
|
|
796
813
|
if search_data_table and scope == "main":
|
|
797
814
|
scope = "data"
|
|
798
815
|
|
|
816
|
+
effective_space = space if space and space != RESERVED_SPACE_ALL else None
|
|
817
|
+
space_filter = ""
|
|
818
|
+
if effective_space:
|
|
819
|
+
space_filter = f" AND v.space = $4" if scope in ("data", "both") else f" AND t.space = $4"
|
|
820
|
+
space_filter_d = f" AND d.space = $4" if effective_space else ""
|
|
821
|
+
space_filter_combined = f" AND space = $4" if effective_space else ""
|
|
822
|
+
|
|
799
823
|
if scope == "data":
|
|
800
824
|
query = f"""
|
|
801
|
-
SELECT v.realm, v.id, v.fqid, v.payload, v.created_at, v.updated_at,
|
|
825
|
+
SELECT v.realm, v.id, v.space, v.fqid, v.payload, v.created_at, v.updated_at,
|
|
802
826
|
to_jsonb(v)->>'embedding' AS embedding_text,
|
|
803
827
|
MIN(d.embedding {op} $2::vector) AS distance
|
|
804
828
|
FROM {data_table_ref} d
|
|
805
829
|
JOIN {table_ref} v ON d.realm = v.realm AND d.id = v.id
|
|
806
|
-
WHERE d.realm = $1 AND d.embedding IS NOT NULL
|
|
807
|
-
GROUP BY v.realm, v.id, v.fqid, v.payload, v.created_at, v.updated_at, to_jsonb(v)
|
|
830
|
+
WHERE d.realm = $1 AND d.embedding IS NOT NULL{space_filter_d}
|
|
831
|
+
GROUP BY v.realm, v.id, v.space, v.fqid, v.payload, v.created_at, v.updated_at, to_jsonb(v)
|
|
808
832
|
ORDER BY distance ASC
|
|
809
833
|
LIMIT $3
|
|
810
834
|
"""
|
|
@@ -813,20 +837,20 @@ class AsyncPostGraph:
|
|
|
813
837
|
WITH combined AS (
|
|
814
838
|
SELECT realm, id, (embedding {op} $2::vector) AS distance
|
|
815
839
|
FROM {table_ref}
|
|
816
|
-
WHERE realm = $1 AND embedding IS NOT NULL
|
|
840
|
+
WHERE realm = $1 AND embedding IS NOT NULL{space_filter_combined}
|
|
817
841
|
|
|
818
842
|
UNION ALL
|
|
819
843
|
|
|
820
844
|
SELECT realm, id, (embedding {op} $2::vector) AS distance
|
|
821
845
|
FROM {data_table_ref}
|
|
822
|
-
WHERE realm = $1 AND embedding IS NOT NULL
|
|
846
|
+
WHERE realm = $1 AND embedding IS NOT NULL{space_filter_combined}
|
|
823
847
|
),
|
|
824
848
|
best AS (
|
|
825
849
|
SELECT realm, id, MIN(distance) AS distance
|
|
826
850
|
FROM combined
|
|
827
851
|
GROUP BY realm, id
|
|
828
852
|
)
|
|
829
|
-
SELECT v.realm, v.id, v.fqid, v.payload, v.created_at, v.updated_at,
|
|
853
|
+
SELECT v.realm, v.id, v.space, v.fqid, v.payload, v.created_at, v.updated_at,
|
|
830
854
|
to_jsonb(v)->>'embedding' AS embedding_text,
|
|
831
855
|
b.distance
|
|
832
856
|
FROM best b
|
|
@@ -836,18 +860,22 @@ class AsyncPostGraph:
|
|
|
836
860
|
"""
|
|
837
861
|
else:
|
|
838
862
|
query = f"""
|
|
839
|
-
SELECT t.realm, t.id, t.fqid, t.payload, t.created_at, t.updated_at,
|
|
863
|
+
SELECT t.realm, t.id, t.space, t.fqid, t.payload, t.created_at, t.updated_at,
|
|
840
864
|
to_jsonb(t)->>'embedding' AS embedding_text,
|
|
841
865
|
(t.embedding {op} $2::vector) AS distance
|
|
842
866
|
FROM {table_ref} t
|
|
843
|
-
WHERE t.realm = $1 AND t.embedding IS NOT NULL
|
|
867
|
+
WHERE t.realm = $1 AND t.embedding IS NOT NULL{space_filter}
|
|
844
868
|
ORDER BY t.embedding {op} $2::vector ASC
|
|
845
869
|
LIMIT $3
|
|
846
870
|
"""
|
|
847
871
|
|
|
872
|
+
fetch_args = [realm, vec_str, top_k]
|
|
873
|
+
if effective_space:
|
|
874
|
+
fetch_args.append(effective_space)
|
|
875
|
+
|
|
848
876
|
async def _op(conn):
|
|
849
877
|
try:
|
|
850
|
-
rows = await conn.fetch(query,
|
|
878
|
+
rows = await conn.fetch(query, *fetch_args)
|
|
851
879
|
except asyncpg.UndefinedTableError:
|
|
852
880
|
raise TableNotFoundError(f"Vertex table '{table_name}' does not exist.")
|
|
853
881
|
except asyncpg.UndefinedColumnError:
|
|
@@ -862,6 +890,7 @@ class AsyncPostGraph:
|
|
|
862
890
|
v = Vertex(
|
|
863
891
|
realm=r['realm'],
|
|
864
892
|
id=str(r['id']),
|
|
893
|
+
space=r.get('space') or 'default',
|
|
865
894
|
fqid=r['fqid'],
|
|
866
895
|
payload=r['payload'] if isinstance(r['payload'], dict) else json.loads(r['payload']),
|
|
867
896
|
created_at=r['created_at'],
|
|
@@ -909,12 +938,16 @@ class AsyncPostGraph:
|
|
|
909
938
|
edge_id: Optional[Union[str, int]] = None,
|
|
910
939
|
payload: Optional[Dict[str, Any]] = None,
|
|
911
940
|
user_id: Optional[str] = None,
|
|
912
|
-
check_cycle: Union[bool, List[str]] = False
|
|
941
|
+
check_cycle: Union[bool, List[str]] = False,
|
|
942
|
+
space: Optional[str] = "default"
|
|
913
943
|
) -> Edge:
|
|
914
944
|
"""Add a new edge. Raises TableExistsError if it already exists."""
|
|
915
945
|
self._validate_identifier(table_name)
|
|
946
|
+
if space == RESERVED_SPACE_ALL:
|
|
947
|
+
raise ReservedSpaceError(f"'{RESERVED_SPACE_ALL}' is a reserved space name and cannot be used for creation. It is only valid as a query-time filter.")
|
|
916
948
|
payload_json = json.dumps(payload or {})
|
|
917
949
|
table_ref = self._get_table_ref(table_name, realm)
|
|
950
|
+
eff_space = space or "default"
|
|
918
951
|
|
|
919
952
|
async def _op(conn):
|
|
920
953
|
nonlocal edge_id
|
|
@@ -954,12 +987,12 @@ class AsyncPostGraph:
|
|
|
954
987
|
)
|
|
955
988
|
|
|
956
989
|
query = f"""
|
|
957
|
-
INSERT INTO {table_ref} (realm, id, from_id, to_id, relation_type, payload)
|
|
958
|
-
VALUES ($1, $2, $3, $4, $5, $6::jsonb)
|
|
959
|
-
RETURNING realm, id, fqid, from_id, to_id, relation_type, payload, created_at, updated_at, uuid::text AS uuid_text
|
|
990
|
+
INSERT INTO {table_ref} (realm, id, space, from_id, to_id, relation_type, payload)
|
|
991
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)
|
|
992
|
+
RETURNING realm, id, space, fqid, from_id, to_id, relation_type, payload, created_at, updated_at, uuid::text AS uuid_text
|
|
960
993
|
"""
|
|
961
994
|
try:
|
|
962
|
-
row = await conn.fetchrow(query, realm, e_id_int, from_id_int, to_id_int, relation_type, payload_json)
|
|
995
|
+
row = await conn.fetchrow(query, realm, e_id_int, eff_space, from_id_int, to_id_int, relation_type, payload_json)
|
|
963
996
|
if edge_id is not None:
|
|
964
997
|
await conn.execute(
|
|
965
998
|
f"SELECT setval(pg_get_serial_sequence('{table_ref_pg}', 'id'), (SELECT COALESCE(MAX(id), 1) FROM {table_ref}))"
|
|
@@ -971,6 +1004,7 @@ class AsyncPostGraph:
|
|
|
971
1004
|
from_id=str(row['from_id']),
|
|
972
1005
|
to_id=str(row['to_id']),
|
|
973
1006
|
relation_type=row['relation_type'],
|
|
1007
|
+
space=row.get('space') or 'default',
|
|
974
1008
|
payload=row['payload'] if isinstance(row['payload'], dict) else json.loads(row['payload']),
|
|
975
1009
|
created_at=row['created_at'],
|
|
976
1010
|
updated_at=row['updated_at'],
|
|
@@ -999,12 +1033,16 @@ class AsyncPostGraph:
|
|
|
999
1033
|
edge_id: Optional[Union[str, int]] = None,
|
|
1000
1034
|
payload: Optional[Dict[str, Any]] = None,
|
|
1001
1035
|
user_id: Optional[str] = None,
|
|
1002
|
-
check_cycle: Union[bool, List[str]] = False
|
|
1036
|
+
check_cycle: Union[bool, List[str]] = False,
|
|
1037
|
+
space: Optional[str] = "default"
|
|
1003
1038
|
) -> Edge:
|
|
1004
1039
|
"""Upsert an edge (merges payload JSONB on conflict)."""
|
|
1005
1040
|
self._validate_identifier(table_name)
|
|
1041
|
+
if space == RESERVED_SPACE_ALL:
|
|
1042
|
+
raise ReservedSpaceError(f"'{RESERVED_SPACE_ALL}' is a reserved space name and cannot be used for creation. It is only valid as a query-time filter.")
|
|
1006
1043
|
payload_json = json.dumps(payload or {})
|
|
1007
1044
|
table_ref = self._get_table_ref(table_name, realm)
|
|
1045
|
+
eff_space = space or "default"
|
|
1008
1046
|
|
|
1009
1047
|
async def _op(conn):
|
|
1010
1048
|
nonlocal edge_id
|
|
@@ -1045,18 +1083,19 @@ class AsyncPostGraph:
|
|
|
1045
1083
|
)
|
|
1046
1084
|
|
|
1047
1085
|
query = f"""
|
|
1048
|
-
INSERT INTO {table_ref} (realm, id, from_id, to_id, relation_type, payload)
|
|
1049
|
-
VALUES ($1, $2, $3, $4, $5, $6::jsonb)
|
|
1086
|
+
INSERT INTO {table_ref} (realm, id, space, from_id, to_id, relation_type, payload)
|
|
1087
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)
|
|
1050
1088
|
ON CONFLICT (realm, id) DO UPDATE
|
|
1051
|
-
SET
|
|
1089
|
+
SET space = EXCLUDED.space,
|
|
1090
|
+
from_id = EXCLUDED.from_id,
|
|
1052
1091
|
to_id = EXCLUDED.to_id,
|
|
1053
1092
|
relation_type = EXCLUDED.relation_type,
|
|
1054
1093
|
payload = {table_ref}.payload || EXCLUDED.payload,
|
|
1055
1094
|
updated_at = CURRENT_TIMESTAMP
|
|
1056
|
-
RETURNING realm, id, fqid, from_id, to_id, relation_type, payload, created_at, updated_at, uuid::text AS uuid_text
|
|
1095
|
+
RETURNING realm, id, space, fqid, from_id, to_id, relation_type, payload, created_at, updated_at, uuid::text AS uuid_text
|
|
1057
1096
|
"""
|
|
1058
1097
|
try:
|
|
1059
|
-
row = await conn.fetchrow(query, realm, e_id_int, from_id_int, to_id_int, relation_type, payload_json)
|
|
1098
|
+
row = await conn.fetchrow(query, realm, e_id_int, eff_space, from_id_int, to_id_int, relation_type, payload_json)
|
|
1060
1099
|
return Edge(
|
|
1061
1100
|
realm=row['realm'],
|
|
1062
1101
|
id=str(row['id']),
|
|
@@ -1064,6 +1103,7 @@ class AsyncPostGraph:
|
|
|
1064
1103
|
from_id=str(row['from_id']),
|
|
1065
1104
|
to_id=str(row['to_id']),
|
|
1066
1105
|
relation_type=row['relation_type'],
|
|
1106
|
+
space=row.get('space') or 'default',
|
|
1067
1107
|
payload=row['payload'] if isinstance(row['payload'], dict) else json.loads(row['payload']),
|
|
1068
1108
|
created_at=row['created_at'],
|
|
1069
1109
|
updated_at=row['updated_at'],
|
|
@@ -1088,8 +1128,8 @@ class AsyncPostGraph:
|
|
|
1088
1128
|
|
|
1089
1129
|
table_ref = self._get_table_ref(table_name, realm)
|
|
1090
1130
|
query = f"""
|
|
1091
|
-
SELECT realm, id, fqid, from_id, to_id, relation_type, payload, created_at, updated_at, uuid::text AS uuid_text
|
|
1092
|
-
FROM {table_ref}
|
|
1131
|
+
SELECT realm, id, space, fqid, from_id, to_id, relation_type, payload, created_at, updated_at, uuid::text AS uuid_text
|
|
1132
|
+
FROM {table_ref}
|
|
1093
1133
|
WHERE realm = $1 AND ((CASE WHEN $2 ~ '^[0-9]+$' THEN id = $2::bigint ELSE FALSE END) OR fqid = $2)
|
|
1094
1134
|
"""
|
|
1095
1135
|
try:
|
|
@@ -1103,6 +1143,7 @@ class AsyncPostGraph:
|
|
|
1103
1143
|
from_id=str(row['from_id']),
|
|
1104
1144
|
to_id=str(row['to_id']),
|
|
1105
1145
|
relation_type=row['relation_type'],
|
|
1146
|
+
space=row.get('space') or 'default',
|
|
1106
1147
|
payload=row['payload'] if isinstance(row['payload'], dict) else json.loads(row['payload']),
|
|
1107
1148
|
created_at=row['created_at'],
|
|
1108
1149
|
updated_at=row['updated_at'],
|
|
@@ -1119,8 +1160,8 @@ class AsyncPostGraph:
|
|
|
1119
1160
|
table_ref = self._get_table_ref(table_name, realm)
|
|
1120
1161
|
uuid_str = str(uuid).strip()
|
|
1121
1162
|
query = f"""
|
|
1122
|
-
SELECT realm, id, fqid, from_id, to_id, relation_type, payload, created_at, updated_at, uuid::text AS uuid_text
|
|
1123
|
-
FROM {table_ref}
|
|
1163
|
+
SELECT realm, id, space, fqid, from_id, to_id, relation_type, payload, created_at, updated_at, uuid::text AS uuid_text
|
|
1164
|
+
FROM {table_ref}
|
|
1124
1165
|
WHERE realm = $1 AND uuid = $2::uuid
|
|
1125
1166
|
"""
|
|
1126
1167
|
try:
|
|
@@ -1134,6 +1175,7 @@ class AsyncPostGraph:
|
|
|
1134
1175
|
from_id=str(row['from_id']),
|
|
1135
1176
|
to_id=str(row['to_id']),
|
|
1136
1177
|
relation_type=row['relation_type'],
|
|
1178
|
+
space=row.get('space') or 'default',
|
|
1137
1179
|
payload=row['payload'] if isinstance(row['payload'], dict) else json.loads(row['payload']),
|
|
1138
1180
|
created_at=row['created_at'],
|
|
1139
1181
|
updated_at=row['updated_at'],
|
|
@@ -1280,9 +1322,9 @@ class AsyncPostGraph:
|
|
|
1280
1322
|
queries.append((
|
|
1281
1323
|
to_ref,
|
|
1282
1324
|
f"""
|
|
1283
|
-
SELECT
|
|
1284
|
-
v.id AS v_id, v.fqid AS v_fqid, v.payload AS v_payload, v.created_at AS v_created_at, v.updated_at AS v_updated_at,
|
|
1285
|
-
e.id AS e_id, e.fqid AS e_fqid, e.from_id AS e_from, e.to_id AS e_to, e.relation_type AS e_rel, e.payload AS e_payload, e.created_at AS e_created_at, e.updated_at AS e_updated_at
|
|
1325
|
+
SELECT
|
|
1326
|
+
v.id AS v_id, v.space AS v_space, v.fqid AS v_fqid, v.payload AS v_payload, v.created_at AS v_created_at, v.updated_at AS v_updated_at,
|
|
1327
|
+
e.id AS e_id, e.space AS e_space, e.fqid AS e_fqid, e.from_id AS e_from, e.to_id AS e_to, e.relation_type AS e_rel, e.payload AS e_payload, e.created_at AS e_created_at, e.updated_at AS e_updated_at
|
|
1286
1328
|
FROM {edge_ref} e
|
|
1287
1329
|
JOIN {to_ref_ref} v ON e.realm = v.realm AND e.to_id = v.id
|
|
1288
1330
|
WHERE e.realm = $1 AND e.from_id = $2
|
|
@@ -1296,9 +1338,9 @@ class AsyncPostGraph:
|
|
|
1296
1338
|
queries.append((
|
|
1297
1339
|
from_ref,
|
|
1298
1340
|
f"""
|
|
1299
|
-
SELECT
|
|
1300
|
-
v.id AS v_id, v.fqid AS v_fqid, v.payload AS v_payload, v.created_at AS v_created_at, v.updated_at AS v_updated_at,
|
|
1301
|
-
e.id AS e_id, e.fqid AS e_fqid, e.from_id AS e_from, e.to_id AS e_to, e.relation_type AS e_rel, e.payload AS e_payload, e.created_at AS e_created_at, e.updated_at AS e_updated_at
|
|
1341
|
+
SELECT
|
|
1342
|
+
v.id AS v_id, v.space AS v_space, v.fqid AS v_fqid, v.payload AS v_payload, v.created_at AS v_created_at, v.updated_at AS v_updated_at,
|
|
1343
|
+
e.id AS e_id, e.space AS e_space, e.fqid AS e_fqid, e.from_id AS e_from, e.to_id AS e_to, e.relation_type AS e_rel, e.payload AS e_payload, e.created_at AS e_created_at, e.updated_at AS e_updated_at
|
|
1302
1344
|
FROM {edge_ref} e
|
|
1303
1345
|
JOIN {from_ref_ref} v ON e.realm = v.realm AND e.from_id = v.id
|
|
1304
1346
|
WHERE e.realm = $1 AND e.to_id = $2
|
|
@@ -1311,6 +1353,7 @@ class AsyncPostGraph:
|
|
|
1311
1353
|
v = Vertex(
|
|
1312
1354
|
realm=realm,
|
|
1313
1355
|
id=str(r['v_id']),
|
|
1356
|
+
space=r.get('v_space') or 'default',
|
|
1314
1357
|
fqid=r['v_fqid'],
|
|
1315
1358
|
payload=r['v_payload'] if isinstance(r['v_payload'], dict) else json.loads(r['v_payload']),
|
|
1316
1359
|
created_at=r['v_created_at'],
|
|
@@ -1325,6 +1368,7 @@ class AsyncPostGraph:
|
|
|
1325
1368
|
from_id=str(r['e_from']),
|
|
1326
1369
|
to_id=str(r['e_to']),
|
|
1327
1370
|
relation_type=r['e_rel'],
|
|
1371
|
+
space=r.get('e_space') or 'default',
|
|
1328
1372
|
payload=r['e_payload'] if isinstance(r['e_payload'], dict) else json.loads(r['e_payload']),
|
|
1329
1373
|
created_at=r['e_created_at'],
|
|
1330
1374
|
updated_at=r['e_updated_at'],
|
|
@@ -12,11 +12,14 @@ from post_graph.errors import (
|
|
|
12
12
|
TableExistsError,
|
|
13
13
|
TableNotFoundError,
|
|
14
14
|
PostGraphError,
|
|
15
|
+
ReservedSpaceError,
|
|
15
16
|
)
|
|
16
17
|
from post_graph.models import Vertex, Edge, DataRecord
|
|
17
18
|
|
|
18
19
|
logger = logging.getLogger("post_graph")
|
|
19
20
|
|
|
21
|
+
RESERVED_SPACE_ALL = "__all__"
|
|
22
|
+
|
|
20
23
|
|
|
21
24
|
class SQLAlchemyPostGraph:
|
|
22
25
|
def __init__(self, engine_or_connection: Union[AsyncEngine, AsyncConnection], schema_per_realm: bool = False):
|
|
@@ -210,6 +213,7 @@ class SQLAlchemyPostGraph:
|
|
|
210
213
|
CREATE TABLE IF NOT EXISTS {audit_table_ref} (
|
|
211
214
|
audit_id BIGSERIAL PRIMARY KEY,
|
|
212
215
|
realm TEXT NOT NULL,
|
|
216
|
+
space VARCHAR(255) DEFAULT 'default',
|
|
213
217
|
action TEXT NOT NULL,
|
|
214
218
|
changed_by TEXT,
|
|
215
219
|
changed_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
@@ -219,6 +223,24 @@ class SQLAlchemyPostGraph:
|
|
|
219
223
|
"""
|
|
220
224
|
await conn.execute(text(audit_query))
|
|
221
225
|
|
|
226
|
+
# 3. Create append-only data table
|
|
227
|
+
data_query = f"""
|
|
228
|
+
CREATE TABLE IF NOT EXISTS {data_table_ref} (
|
|
229
|
+
data_id BIGSERIAL PRIMARY KEY,
|
|
230
|
+
realm TEXT NOT NULL,
|
|
231
|
+
id BIGINT NOT NULL,
|
|
232
|
+
space VARCHAR(255) DEFAULT 'default' NOT NULL,
|
|
233
|
+
payload JSONB NOT NULL DEFAULT '{{}}'::jsonb,
|
|
234
|
+
timestamp TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
235
|
+
FOREIGN KEY (realm, id) REFERENCES {table_ref}(realm, id) ON DELETE CASCADE
|
|
236
|
+
);
|
|
237
|
+
"""
|
|
238
|
+
await conn.execute(text(data_query))
|
|
239
|
+
await conn.execute(text(f"ALTER TABLE {data_table_ref} ADD COLUMN IF NOT EXISTS space VARCHAR(255) DEFAULT 'default';"))
|
|
240
|
+
await conn.execute(text(f'CREATE INDEX IF NOT EXISTS "idx_{table_name}_data_space" ON {data_table_ref} (realm, space);'))
|
|
241
|
+
await conn.execute(text(f'CREATE INDEX IF NOT EXISTS "idx_{table_name}_data_id" ON {data_table_ref} (realm, id);'))
|
|
242
|
+
await conn.execute(text(f'CREATE INDEX IF NOT EXISTS "idx_{table_name}_data_payload" ON {data_table_ref} USING gin (payload);'))
|
|
243
|
+
|
|
222
244
|
await conn.execute(text(f'DROP TRIGGER IF EXISTS "update_{table_name}_modtime" ON {table_ref};'))
|
|
223
245
|
await conn.execute(text(f"""
|
|
224
246
|
CREATE TRIGGER "update_{table_name}_modtime"
|
|
@@ -292,6 +314,7 @@ class SQLAlchemyPostGraph:
|
|
|
292
314
|
CREATE TABLE IF NOT EXISTS {table_ref} (
|
|
293
315
|
realm TEXT NOT NULL,
|
|
294
316
|
id BIGSERIAL,
|
|
317
|
+
space VARCHAR(255) DEFAULT 'default' NOT NULL,
|
|
295
318
|
uuid UUID DEFAULT gen_random_uuid() NOT NULL,
|
|
296
319
|
fqid TEXT GENERATED ALWAYS AS (realm || '/' || '{from_vertex_table}-{to_vertex_table}' || '/' || id::text) STORED NOT NULL,
|
|
297
320
|
from_id BIGINT NOT NULL,
|
|
@@ -306,6 +329,8 @@ class SQLAlchemyPostGraph:
|
|
|
306
329
|
);
|
|
307
330
|
"""
|
|
308
331
|
await conn.execute(text(query))
|
|
332
|
+
await conn.execute(text(f"ALTER TABLE {table_ref} ADD COLUMN IF NOT EXISTS space VARCHAR(255) DEFAULT 'default';"))
|
|
333
|
+
await conn.execute(text(f'CREATE INDEX IF NOT EXISTS "idx_{table_name}_space" ON {table_ref} (realm, space);'))
|
|
309
334
|
await conn.execute(text(f"ALTER TABLE {table_ref} ADD COLUMN IF NOT EXISTS fqid TEXT GENERATED ALWAYS AS (realm || '/' || '{from_vertex_table}-{to_vertex_table}' || '/' || id::text) STORED;"))
|
|
310
335
|
await conn.execute(text(f"ALTER TABLE {table_ref} ADD COLUMN IF NOT EXISTS uuid UUID DEFAULT gen_random_uuid();"))
|
|
311
336
|
await conn.execute(text(f'CREATE UNIQUE INDEX IF NOT EXISTS "idx_{table_name}_uuid" ON {table_ref} (uuid);'))
|
|
@@ -315,6 +340,7 @@ class SQLAlchemyPostGraph:
|
|
|
315
340
|
CREATE TABLE IF NOT EXISTS {audit_table_ref} (
|
|
316
341
|
audit_id BIGSERIAL PRIMARY KEY,
|
|
317
342
|
realm TEXT NOT NULL,
|
|
343
|
+
space VARCHAR(255) DEFAULT 'default',
|
|
318
344
|
action TEXT NOT NULL,
|
|
319
345
|
changed_by TEXT,
|
|
320
346
|
changed_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
@@ -330,12 +356,15 @@ class SQLAlchemyPostGraph:
|
|
|
330
356
|
data_id BIGSERIAL PRIMARY KEY,
|
|
331
357
|
realm TEXT NOT NULL,
|
|
332
358
|
id BIGINT NOT NULL,
|
|
359
|
+
space VARCHAR(255) DEFAULT 'default' NOT NULL,
|
|
333
360
|
payload JSONB NOT NULL DEFAULT '{{}}'::jsonb,
|
|
334
361
|
timestamp TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
335
362
|
FOREIGN KEY (realm, id) REFERENCES {table_ref}(realm, id) ON DELETE CASCADE
|
|
336
363
|
);
|
|
337
364
|
"""
|
|
338
365
|
await conn.execute(text(data_query))
|
|
366
|
+
await conn.execute(text(f"ALTER TABLE {data_table_ref} ADD COLUMN IF NOT EXISTS space VARCHAR(255) DEFAULT 'default';"))
|
|
367
|
+
await conn.execute(text(f'CREATE INDEX IF NOT EXISTS "idx_{table_name}_data_space" ON {data_table_ref} (realm, space);'))
|
|
339
368
|
await conn.execute(text(f'CREATE INDEX IF NOT EXISTS "idx_{table_name}_data_id" ON {data_table_ref} (realm, id);'))
|
|
340
369
|
await conn.execute(text(f'CREATE INDEX IF NOT EXISTS "idx_{table_name}_data_payload" ON {data_table_ref} USING gin (payload);'))
|
|
341
370
|
|
|
@@ -406,13 +435,17 @@ class SQLAlchemyPostGraph:
|
|
|
406
435
|
vertex_id: Optional[Union[str, int]] = None,
|
|
407
436
|
payload: Optional[Dict[str, Any]] = None,
|
|
408
437
|
embedding: Optional[List[float]] = None,
|
|
409
|
-
user_id: Optional[str] = None
|
|
438
|
+
user_id: Optional[str] = None,
|
|
439
|
+
space: Optional[str] = "default"
|
|
410
440
|
) -> Vertex:
|
|
411
441
|
"""Add a new vertex. Raises TableExistsError if it already exists."""
|
|
412
442
|
self._validate_identifier(table_name)
|
|
443
|
+
if space == RESERVED_SPACE_ALL:
|
|
444
|
+
raise ReservedSpaceError(f"'{RESERVED_SPACE_ALL}' is a reserved space name and cannot be used for creation. It is only valid as a query-time filter.")
|
|
413
445
|
payload_json = json.dumps(payload or {})
|
|
414
446
|
table_ref = self._get_table_ref(table_name, realm)
|
|
415
447
|
vec_str = f"[{','.join(str(x) for x in embedding)}]" if embedding else None
|
|
448
|
+
eff_space = space or "default"
|
|
416
449
|
|
|
417
450
|
async def _op(conn):
|
|
418
451
|
nonlocal vertex_id
|
|
@@ -427,18 +460,18 @@ class SQLAlchemyPostGraph:
|
|
|
427
460
|
|
|
428
461
|
if vec_str:
|
|
429
462
|
query = f"""
|
|
430
|
-
INSERT INTO {table_ref} (realm, id, payload, embedding)
|
|
431
|
-
VALUES (:realm, :id, CAST(:payload AS JSONB), CAST(:vec AS vector))
|
|
432
|
-
RETURNING realm, id, fqid, payload, created_at, updated_at, uuid::text AS uuid_text, CAST(embedding AS TEXT) AS embedding_text
|
|
463
|
+
INSERT INTO {table_ref} (realm, id, space, payload, embedding)
|
|
464
|
+
VALUES (:realm, :id, :space, CAST(:payload AS JSONB), CAST(:vec AS vector))
|
|
465
|
+
RETURNING realm, id, space, fqid, payload, created_at, updated_at, uuid::text AS uuid_text, CAST(embedding AS TEXT) AS embedding_text
|
|
433
466
|
"""
|
|
434
|
-
kwargs = {"realm": realm, "id": v_id_int, "payload": payload_json, "vec": vec_str}
|
|
467
|
+
kwargs = {"realm": realm, "id": v_id_int, "space": eff_space, "payload": payload_json, "vec": vec_str}
|
|
435
468
|
else:
|
|
436
469
|
query = f"""
|
|
437
|
-
INSERT INTO {table_ref} (realm, id, payload)
|
|
438
|
-
VALUES (:realm, :id, CAST(:payload AS JSONB))
|
|
439
|
-
RETURNING realm, id, fqid, payload, created_at, updated_at, uuid::text AS uuid_text
|
|
470
|
+
INSERT INTO {table_ref} (realm, id, space, payload)
|
|
471
|
+
VALUES (:realm, :id, :space, CAST(:payload AS JSONB))
|
|
472
|
+
RETURNING realm, id, space, fqid, payload, created_at, updated_at, uuid::text AS uuid_text
|
|
440
473
|
"""
|
|
441
|
-
kwargs = {"realm": realm, "id": v_id_int, "payload": payload_json}
|
|
474
|
+
kwargs = {"realm": realm, "id": v_id_int, "space": eff_space, "payload": payload_json}
|
|
442
475
|
|
|
443
476
|
try:
|
|
444
477
|
row = await self._fetchrow(conn, query, **kwargs)
|
|
@@ -452,6 +485,7 @@ class SQLAlchemyPostGraph:
|
|
|
452
485
|
return Vertex(
|
|
453
486
|
realm=row['realm'],
|
|
454
487
|
id=str(row['id']),
|
|
488
|
+
space=row.get('space') or 'default',
|
|
455
489
|
fqid=row['fqid'],
|
|
456
490
|
payload=row['payload'] if isinstance(row['payload'], dict) else json.loads(row['payload']),
|
|
457
491
|
created_at=row['created_at'],
|
|
@@ -481,13 +515,17 @@ class SQLAlchemyPostGraph:
|
|
|
481
515
|
vertex_id: Optional[Union[str, int]] = None,
|
|
482
516
|
payload: Optional[Dict[str, Any]] = None,
|
|
483
517
|
embedding: Optional[List[float]] = None,
|
|
484
|
-
user_id: Optional[str] = None
|
|
518
|
+
user_id: Optional[str] = None,
|
|
519
|
+
space: Optional[str] = "default"
|
|
485
520
|
) -> Vertex:
|
|
486
521
|
"""Upsert a vertex (merges payload JSONB on conflict)."""
|
|
487
522
|
self._validate_identifier(table_name)
|
|
523
|
+
if space == RESERVED_SPACE_ALL:
|
|
524
|
+
raise ReservedSpaceError(f"'{RESERVED_SPACE_ALL}' is a reserved space name and cannot be used for creation. It is only valid as a query-time filter.")
|
|
488
525
|
payload_json = json.dumps(payload or {})
|
|
489
526
|
table_ref = self._get_table_ref(table_name, realm)
|
|
490
527
|
vec_str = f"[{','.join(str(x) for x in embedding)}]" if embedding else None
|
|
528
|
+
eff_space = space or "default"
|
|
491
529
|
|
|
492
530
|
async def _op(conn):
|
|
493
531
|
nonlocal vertex_id
|
|
@@ -502,25 +540,27 @@ class SQLAlchemyPostGraph:
|
|
|
502
540
|
|
|
503
541
|
if vec_str:
|
|
504
542
|
query = f"""
|
|
505
|
-
INSERT INTO {table_ref} (realm, id, payload, embedding)
|
|
506
|
-
VALUES (:realm, :id, CAST(:payload AS JSONB), CAST(:vec AS vector))
|
|
543
|
+
INSERT INTO {table_ref} (realm, id, space, payload, embedding)
|
|
544
|
+
VALUES (:realm, :id, :space, CAST(:payload AS JSONB), CAST(:vec AS vector))
|
|
507
545
|
ON CONFLICT (realm, id) DO UPDATE
|
|
508
|
-
SET
|
|
546
|
+
SET space = EXCLUDED.space,
|
|
547
|
+
payload = {table_ref}.payload || EXCLUDED.payload,
|
|
509
548
|
embedding = EXCLUDED.embedding,
|
|
510
549
|
updated_at = CURRENT_TIMESTAMP
|
|
511
|
-
RETURNING realm, id, fqid, payload, created_at, updated_at, uuid::text AS uuid_text, CAST(embedding AS TEXT) AS embedding_text
|
|
550
|
+
RETURNING realm, id, space, fqid, payload, created_at, updated_at, uuid::text AS uuid_text, CAST(embedding AS TEXT) AS embedding_text
|
|
512
551
|
"""
|
|
513
|
-
kwargs = {"realm": realm, "id": v_id_int, "payload": payload_json, "vec": vec_str}
|
|
552
|
+
kwargs = {"realm": realm, "id": v_id_int, "space": eff_space, "payload": payload_json, "vec": vec_str}
|
|
514
553
|
else:
|
|
515
554
|
query = f"""
|
|
516
|
-
INSERT INTO {table_ref} (realm, id, payload)
|
|
517
|
-
VALUES (:realm, :id, CAST(:payload AS JSONB))
|
|
555
|
+
INSERT INTO {table_ref} (realm, id, space, payload)
|
|
556
|
+
VALUES (:realm, :id, :space, CAST(:payload AS JSONB))
|
|
518
557
|
ON CONFLICT (realm, id) DO UPDATE
|
|
519
|
-
SET
|
|
558
|
+
SET space = EXCLUDED.space,
|
|
559
|
+
payload = {table_ref}.payload || EXCLUDED.payload,
|
|
520
560
|
updated_at = CURRENT_TIMESTAMP
|
|
521
|
-
RETURNING realm, id, fqid, payload, created_at, updated_at, uuid::text AS uuid_text
|
|
561
|
+
RETURNING realm, id, space, fqid, payload, created_at, updated_at, uuid::text AS uuid_text
|
|
522
562
|
"""
|
|
523
|
-
kwargs = {"realm": realm, "id": v_id_int, "payload": payload_json}
|
|
563
|
+
kwargs = {"realm": realm, "id": v_id_int, "space": eff_space, "payload": payload_json}
|
|
524
564
|
|
|
525
565
|
try:
|
|
526
566
|
row = await self._fetchrow(conn, query, **kwargs)
|
|
@@ -530,6 +570,7 @@ class SQLAlchemyPostGraph:
|
|
|
530
570
|
return Vertex(
|
|
531
571
|
realm=row['realm'],
|
|
532
572
|
id=str(row['id']),
|
|
573
|
+
space=row.get('space') or 'default',
|
|
533
574
|
fqid=row['fqid'],
|
|
534
575
|
payload=row['payload'] if isinstance(row['payload'], dict) else json.loads(row['payload']),
|
|
535
576
|
created_at=row['created_at'],
|
|
@@ -563,7 +604,7 @@ class SQLAlchemyPostGraph:
|
|
|
563
604
|
|
|
564
605
|
async def _op(conn):
|
|
565
606
|
query = f"""
|
|
566
|
-
SELECT t.realm, t.id, t.fqid, t.payload, t.created_at, t.updated_at,
|
|
607
|
+
SELECT t.realm, t.id, t.space, t.fqid, t.payload, t.created_at, t.updated_at,
|
|
567
608
|
t.uuid::text AS uuid_text,
|
|
568
609
|
to_jsonb(t)->>'embedding' AS embedding_text
|
|
569
610
|
FROM {table_ref} t
|
|
@@ -581,6 +622,7 @@ class SQLAlchemyPostGraph:
|
|
|
581
622
|
return Vertex(
|
|
582
623
|
realm=row['realm'],
|
|
583
624
|
id=str(row['id']),
|
|
625
|
+
space=row.get('space') or 'default',
|
|
584
626
|
fqid=row['fqid'],
|
|
585
627
|
payload=row['payload'] if isinstance(row['payload'], dict) else json.loads(row['payload']),
|
|
586
628
|
created_at=row['created_at'],
|
|
@@ -615,7 +657,7 @@ class SQLAlchemyPostGraph:
|
|
|
615
657
|
async def _op(conn):
|
|
616
658
|
params = {"realm": realm}
|
|
617
659
|
space_clause = ""
|
|
618
|
-
if space:
|
|
660
|
+
if space and space != RESERVED_SPACE_ALL:
|
|
619
661
|
params["space"] = space
|
|
620
662
|
space_clause = " AND (t.space = :space OR (:space = 'default' AND (t.space IS NULL OR t.space = 'default')))"
|
|
621
663
|
|
|
@@ -671,7 +713,7 @@ class SQLAlchemyPostGraph:
|
|
|
671
713
|
|
|
672
714
|
async def _op(conn):
|
|
673
715
|
query = f"""
|
|
674
|
-
SELECT t.realm, t.id, t.fqid, t.payload, t.created_at, t.updated_at,
|
|
716
|
+
SELECT t.realm, t.id, t.space, t.fqid, t.payload, t.created_at, t.updated_at,
|
|
675
717
|
t.uuid::text AS uuid_text,
|
|
676
718
|
to_jsonb(t)->>'embedding' AS embedding_text
|
|
677
719
|
FROM {table_ref} t
|
|
@@ -689,6 +731,7 @@ class SQLAlchemyPostGraph:
|
|
|
689
731
|
return Vertex(
|
|
690
732
|
realm=row['realm'],
|
|
691
733
|
id=str(row['id']),
|
|
734
|
+
space=row.get('space') or 'default',
|
|
692
735
|
fqid=row['fqid'],
|
|
693
736
|
payload=row['payload'] if isinstance(row['payload'], dict) else json.loads(row['payload']),
|
|
694
737
|
created_at=row['created_at'],
|
|
@@ -736,15 +779,20 @@ class SQLAlchemyPostGraph:
|
|
|
736
779
|
if search_data_table and scope == "main":
|
|
737
780
|
scope = "data"
|
|
738
781
|
|
|
782
|
+
effective_space = space if space and space != RESERVED_SPACE_ALL else None
|
|
783
|
+
space_filter_d = " AND d.space = :space" if effective_space else ""
|
|
784
|
+
space_filter_t = " AND t.space = :space" if effective_space else ""
|
|
785
|
+
space_filter_combined = " AND space = :space" if effective_space else ""
|
|
786
|
+
|
|
739
787
|
if scope == "data":
|
|
740
788
|
query = f"""
|
|
741
|
-
SELECT v.realm, v.id, v.fqid, v.payload, v.created_at, v.updated_at,
|
|
789
|
+
SELECT v.realm, v.id, v.space, v.fqid, v.payload, v.created_at, v.updated_at,
|
|
742
790
|
to_jsonb(v)->>'embedding' AS embedding_text,
|
|
743
791
|
MIN(d.embedding {op} CAST(:vec AS vector)) AS distance
|
|
744
792
|
FROM {data_table_ref} d
|
|
745
793
|
JOIN {table_ref} v ON d.realm = v.realm AND d.id = v.id
|
|
746
|
-
WHERE d.realm = :realm AND d.embedding IS NOT NULL
|
|
747
|
-
GROUP BY v.realm, v.id, v.fqid, v.payload, v.created_at, v.updated_at, to_jsonb(v)
|
|
794
|
+
WHERE d.realm = :realm AND d.embedding IS NOT NULL{space_filter_d}
|
|
795
|
+
GROUP BY v.realm, v.id, v.space, v.fqid, v.payload, v.created_at, v.updated_at, to_jsonb(v)
|
|
748
796
|
ORDER BY distance ASC
|
|
749
797
|
LIMIT :top_k
|
|
750
798
|
"""
|
|
@@ -753,20 +801,20 @@ class SQLAlchemyPostGraph:
|
|
|
753
801
|
WITH combined AS (
|
|
754
802
|
SELECT realm, id, (embedding {op} CAST(:vec AS vector)) AS distance
|
|
755
803
|
FROM {table_ref}
|
|
756
|
-
WHERE realm = :realm AND embedding IS NOT NULL
|
|
804
|
+
WHERE realm = :realm AND embedding IS NOT NULL{space_filter_combined}
|
|
757
805
|
|
|
758
806
|
UNION ALL
|
|
759
807
|
|
|
760
808
|
SELECT realm, id, (embedding {op} CAST(:vec AS vector)) AS distance
|
|
761
809
|
FROM {data_table_ref}
|
|
762
|
-
WHERE realm = :realm AND embedding IS NOT NULL
|
|
810
|
+
WHERE realm = :realm AND embedding IS NOT NULL{space_filter_combined}
|
|
763
811
|
),
|
|
764
812
|
best AS (
|
|
765
813
|
SELECT realm, id, MIN(distance) AS distance
|
|
766
814
|
FROM combined
|
|
767
815
|
GROUP BY realm, id
|
|
768
816
|
)
|
|
769
|
-
SELECT v.realm, v.id, v.fqid, v.payload, v.created_at, v.updated_at,
|
|
817
|
+
SELECT v.realm, v.id, v.space, v.fqid, v.payload, v.created_at, v.updated_at,
|
|
770
818
|
to_jsonb(v)->>'embedding' AS embedding_text,
|
|
771
819
|
b.distance
|
|
772
820
|
FROM best b
|
|
@@ -776,18 +824,22 @@ class SQLAlchemyPostGraph:
|
|
|
776
824
|
"""
|
|
777
825
|
else:
|
|
778
826
|
query = f"""
|
|
779
|
-
SELECT t.realm, t.id, t.fqid, t.payload, t.created_at, t.updated_at,
|
|
827
|
+
SELECT t.realm, t.id, t.space, t.fqid, t.payload, t.created_at, t.updated_at,
|
|
780
828
|
CAST(t.embedding AS TEXT) AS embedding_text,
|
|
781
829
|
(t.embedding {op} CAST(:vec AS vector)) AS distance
|
|
782
830
|
FROM {table_ref} t
|
|
783
|
-
WHERE t.realm = :realm AND t.embedding IS NOT NULL
|
|
831
|
+
WHERE t.realm = :realm AND t.embedding IS NOT NULL{space_filter_t}
|
|
784
832
|
ORDER BY t.embedding {op} CAST(:vec AS vector) ASC
|
|
785
833
|
LIMIT :top_k
|
|
786
834
|
"""
|
|
787
835
|
|
|
836
|
+
fetch_params = {"realm": realm, "vec": vec_str, "top_k": top_k}
|
|
837
|
+
if effective_space:
|
|
838
|
+
fetch_params["space"] = effective_space
|
|
839
|
+
|
|
788
840
|
async def _op(conn):
|
|
789
841
|
try:
|
|
790
|
-
rows = await self._fetch(conn, query,
|
|
842
|
+
rows = await self._fetch(conn, query, **fetch_params)
|
|
791
843
|
except ProgrammingError as e:
|
|
792
844
|
if "does not exist" in str(e).lower():
|
|
793
845
|
raise TableNotFoundError(f"Vertex table '{table_name}' does not exist.")
|
|
@@ -802,6 +854,7 @@ class SQLAlchemyPostGraph:
|
|
|
802
854
|
v = Vertex(
|
|
803
855
|
realm=r['realm'],
|
|
804
856
|
id=str(r['id']),
|
|
857
|
+
space=r.get('space') or 'default',
|
|
805
858
|
fqid=r['fqid'],
|
|
806
859
|
payload=r['payload'] if isinstance(r['payload'], dict) else json.loads(r['payload']),
|
|
807
860
|
created_at=r['created_at'],
|
|
@@ -851,12 +904,16 @@ class SQLAlchemyPostGraph:
|
|
|
851
904
|
edge_id: Optional[Union[str, int]] = None,
|
|
852
905
|
payload: Optional[Dict[str, Any]] = None,
|
|
853
906
|
user_id: Optional[str] = None,
|
|
854
|
-
check_cycle: Union[bool, List[str]] = False
|
|
907
|
+
check_cycle: Union[bool, List[str]] = False,
|
|
908
|
+
space: Optional[str] = "default"
|
|
855
909
|
) -> Edge:
|
|
856
910
|
"""Add a new edge. Raises TableExistsError if it already exists."""
|
|
857
911
|
self._validate_identifier(table_name)
|
|
912
|
+
if space == RESERVED_SPACE_ALL:
|
|
913
|
+
raise ReservedSpaceError(f"'{RESERVED_SPACE_ALL}' is a reserved space name and cannot be used for creation. It is only valid as a query-time filter.")
|
|
858
914
|
payload_json = json.dumps(payload or {})
|
|
859
915
|
table_ref = self._get_table_ref(table_name, realm)
|
|
916
|
+
eff_space = space or "default"
|
|
860
917
|
|
|
861
918
|
async def _op(conn):
|
|
862
919
|
nonlocal edge_id
|
|
@@ -896,14 +953,14 @@ class SQLAlchemyPostGraph:
|
|
|
896
953
|
)
|
|
897
954
|
|
|
898
955
|
query = f"""
|
|
899
|
-
INSERT INTO {table_ref} (realm, id, from_id, to_id, relation_type, payload)
|
|
900
|
-
VALUES (:realm, :id, :from_id, :to_id, :relation_type, CAST(:payload AS JSONB))
|
|
901
|
-
RETURNING realm, id, fqid, from_id, to_id, relation_type, payload, created_at, updated_at, uuid::text AS uuid_text
|
|
956
|
+
INSERT INTO {table_ref} (realm, id, space, from_id, to_id, relation_type, payload)
|
|
957
|
+
VALUES (:realm, :id, :space, :from_id, :to_id, :relation_type, CAST(:payload AS JSONB))
|
|
958
|
+
RETURNING realm, id, space, fqid, from_id, to_id, relation_type, payload, created_at, updated_at, uuid::text AS uuid_text
|
|
902
959
|
"""
|
|
903
960
|
try:
|
|
904
961
|
row = await self._fetchrow(
|
|
905
962
|
conn, query,
|
|
906
|
-
realm=realm, id=e_id_int, from_id=from_id_int, to_id=to_id_int,
|
|
963
|
+
realm=realm, id=e_id_int, space=eff_space, from_id=from_id_int, to_id=to_id_int,
|
|
907
964
|
relation_type=relation_type, payload=payload_json
|
|
908
965
|
)
|
|
909
966
|
if edge_id is not None:
|
|
@@ -917,6 +974,7 @@ class SQLAlchemyPostGraph:
|
|
|
917
974
|
from_id=str(row['from_id']),
|
|
918
975
|
to_id=str(row['to_id']),
|
|
919
976
|
relation_type=row['relation_type'],
|
|
977
|
+
space=row.get('space') or 'default',
|
|
920
978
|
payload=row['payload'] if isinstance(row['payload'], dict) else json.loads(row['payload']),
|
|
921
979
|
created_at=row['created_at'],
|
|
922
980
|
updated_at=row['updated_at'],
|
|
@@ -949,12 +1007,16 @@ class SQLAlchemyPostGraph:
|
|
|
949
1007
|
edge_id: Optional[Union[str, int]] = None,
|
|
950
1008
|
payload: Optional[Dict[str, Any]] = None,
|
|
951
1009
|
user_id: Optional[str] = None,
|
|
952
|
-
check_cycle: Union[bool, List[str]] = False
|
|
1010
|
+
check_cycle: Union[bool, List[str]] = False,
|
|
1011
|
+
space: Optional[str] = "default"
|
|
953
1012
|
) -> Edge:
|
|
954
1013
|
"""Upsert an edge (merges payload JSONB on conflict)."""
|
|
955
1014
|
self._validate_identifier(table_name)
|
|
1015
|
+
if space == RESERVED_SPACE_ALL:
|
|
1016
|
+
raise ReservedSpaceError(f"'{RESERVED_SPACE_ALL}' is a reserved space name and cannot be used for creation. It is only valid as a query-time filter.")
|
|
956
1017
|
payload_json = json.dumps(payload or {})
|
|
957
1018
|
table_ref = self._get_table_ref(table_name, realm)
|
|
1019
|
+
eff_space = space or "default"
|
|
958
1020
|
|
|
959
1021
|
async def _op(conn):
|
|
960
1022
|
nonlocal edge_id
|
|
@@ -992,20 +1054,21 @@ class SQLAlchemyPostGraph:
|
|
|
992
1054
|
raise CyclicReferenceError(f"Adding edge from '{from_id}' to '{to_id}' in table '{table_name}' would create a cycle.")
|
|
993
1055
|
|
|
994
1056
|
query = f"""
|
|
995
|
-
INSERT INTO {table_ref} (realm, id, from_id, to_id, relation_type, payload)
|
|
996
|
-
VALUES (:realm, :id, :from_id, :to_id, :relation_type, CAST(:payload AS JSONB))
|
|
1057
|
+
INSERT INTO {table_ref} (realm, id, space, from_id, to_id, relation_type, payload)
|
|
1058
|
+
VALUES (:realm, :id, :space, :from_id, :to_id, :relation_type, CAST(:payload AS JSONB))
|
|
997
1059
|
ON CONFLICT (realm, id) DO UPDATE
|
|
998
|
-
SET
|
|
1060
|
+
SET space = EXCLUDED.space,
|
|
1061
|
+
from_id = EXCLUDED.from_id,
|
|
999
1062
|
to_id = EXCLUDED.to_id,
|
|
1000
1063
|
relation_type = EXCLUDED.relation_type,
|
|
1001
1064
|
payload = {table_ref}.payload || EXCLUDED.payload,
|
|
1002
1065
|
updated_at = CURRENT_TIMESTAMP
|
|
1003
|
-
RETURNING realm, id, fqid, from_id, to_id, relation_type, payload, created_at, updated_at, uuid::text AS uuid_text
|
|
1066
|
+
RETURNING realm, id, space, fqid, from_id, to_id, relation_type, payload, created_at, updated_at, uuid::text AS uuid_text
|
|
1004
1067
|
"""
|
|
1005
1068
|
try:
|
|
1006
1069
|
row = await self._fetchrow(
|
|
1007
1070
|
conn, query,
|
|
1008
|
-
realm=realm, id=e_id_int, from_id=from_id_int, to_id=to_id_int,
|
|
1071
|
+
realm=realm, id=e_id_int, space=eff_space, from_id=from_id_int, to_id=to_id_int,
|
|
1009
1072
|
relation_type=relation_type, payload=payload_json
|
|
1010
1073
|
)
|
|
1011
1074
|
return Edge(
|
|
@@ -1015,6 +1078,7 @@ class SQLAlchemyPostGraph:
|
|
|
1015
1078
|
from_id=str(row['from_id']),
|
|
1016
1079
|
to_id=str(row['to_id']),
|
|
1017
1080
|
relation_type=row['relation_type'],
|
|
1081
|
+
space=row.get('space') or 'default',
|
|
1018
1082
|
payload=row['payload'] if isinstance(row['payload'], dict) else json.loads(row['payload']),
|
|
1019
1083
|
created_at=row['created_at'],
|
|
1020
1084
|
updated_at=row['updated_at'],
|
|
@@ -1043,11 +1107,11 @@ class SQLAlchemyPostGraph:
|
|
|
1043
1107
|
|
|
1044
1108
|
table_ref = self._get_table_ref(table_name, realm)
|
|
1045
1109
|
query = f"""
|
|
1046
|
-
SELECT realm, id, fqid, from_id, to_id, relation_type, payload, created_at, updated_at, uuid::text AS uuid_text
|
|
1047
|
-
FROM {table_ref}
|
|
1110
|
+
SELECT realm, id, space, fqid, from_id, to_id, relation_type, payload, created_at, updated_at, uuid::text AS uuid_text
|
|
1111
|
+
FROM {table_ref}
|
|
1048
1112
|
WHERE realm = :realm AND ((CASE WHEN :id ~ '^[0-9]+$' THEN id = CAST(:id AS BIGINT) ELSE FALSE END) OR fqid = :id)
|
|
1049
1113
|
"""
|
|
1050
|
-
|
|
1114
|
+
|
|
1051
1115
|
async def _op(conn):
|
|
1052
1116
|
try:
|
|
1053
1117
|
row = await self._fetchrow(conn, query, realm=realm, id=e_str)
|
|
@@ -1060,6 +1124,7 @@ class SQLAlchemyPostGraph:
|
|
|
1060
1124
|
from_id=str(row['from_id']),
|
|
1061
1125
|
to_id=str(row['to_id']),
|
|
1062
1126
|
relation_type=row['relation_type'],
|
|
1127
|
+
space=row.get('space') or 'default',
|
|
1063
1128
|
payload=row['payload'] if isinstance(row['payload'], dict) else json.loads(row['payload']),
|
|
1064
1129
|
created_at=row['created_at'],
|
|
1065
1130
|
updated_at=row['updated_at'],
|
|
@@ -1084,11 +1149,11 @@ class SQLAlchemyPostGraph:
|
|
|
1084
1149
|
table_ref = self._get_table_ref(table_name, realm)
|
|
1085
1150
|
uuid_str = str(uuid).strip()
|
|
1086
1151
|
query = f"""
|
|
1087
|
-
SELECT realm, id, fqid, from_id, to_id, relation_type, payload, created_at, updated_at, uuid::text AS uuid_text
|
|
1088
|
-
FROM {table_ref}
|
|
1152
|
+
SELECT realm, id, space, fqid, from_id, to_id, relation_type, payload, created_at, updated_at, uuid::text AS uuid_text
|
|
1153
|
+
FROM {table_ref}
|
|
1089
1154
|
WHERE realm = :realm AND uuid = CAST(:uuid AS UUID)
|
|
1090
1155
|
"""
|
|
1091
|
-
|
|
1156
|
+
|
|
1092
1157
|
async def _op(conn):
|
|
1093
1158
|
try:
|
|
1094
1159
|
row = await self._fetchrow(conn, query, realm=realm, uuid=uuid_str)
|
|
@@ -1101,6 +1166,7 @@ class SQLAlchemyPostGraph:
|
|
|
1101
1166
|
from_id=str(row['from_id']),
|
|
1102
1167
|
to_id=str(row['to_id']),
|
|
1103
1168
|
relation_type=row['relation_type'],
|
|
1169
|
+
space=row.get('space') or 'default',
|
|
1104
1170
|
payload=row['payload'] if isinstance(row['payload'], dict) else json.loads(row['payload']),
|
|
1105
1171
|
created_at=row['created_at'],
|
|
1106
1172
|
updated_at=row['updated_at'],
|
|
@@ -1252,9 +1318,9 @@ class SQLAlchemyPostGraph:
|
|
|
1252
1318
|
queries.append((
|
|
1253
1319
|
to_ref,
|
|
1254
1320
|
f"""
|
|
1255
|
-
SELECT
|
|
1256
|
-
v.id AS v_id, v.fqid AS v_fqid, v.payload AS v_payload, v.created_at AS v_created_at, v.updated_at AS v_updated_at,
|
|
1257
|
-
e.id AS e_id, e.fqid AS e_fqid, e.from_id AS e_from, e.to_id AS e_to, e.relation_type AS e_rel, e.payload AS e_payload, e.created_at AS e_created_at, e.updated_at AS e_updated_at
|
|
1321
|
+
SELECT
|
|
1322
|
+
v.id AS v_id, v.space AS v_space, v.fqid AS v_fqid, v.payload AS v_payload, v.created_at AS v_created_at, v.updated_at AS v_updated_at,
|
|
1323
|
+
e.id AS e_id, e.space AS e_space, e.fqid AS e_fqid, e.from_id AS e_from, e.to_id AS e_to, e.relation_type AS e_rel, e.payload AS e_payload, e.created_at AS e_created_at, e.updated_at AS e_updated_at
|
|
1258
1324
|
FROM {edge_ref} e
|
|
1259
1325
|
JOIN {to_ref_ref} v ON e.realm = v.realm AND e.to_id = v.id
|
|
1260
1326
|
WHERE e.realm = :realm AND e.from_id = :vertex_id
|
|
@@ -1268,9 +1334,9 @@ class SQLAlchemyPostGraph:
|
|
|
1268
1334
|
queries.append((
|
|
1269
1335
|
from_ref,
|
|
1270
1336
|
f"""
|
|
1271
|
-
SELECT
|
|
1272
|
-
v.id AS v_id, v.fqid AS v_fqid, v.payload AS v_payload, v.created_at AS v_created_at, v.updated_at AS v_updated_at,
|
|
1273
|
-
e.id AS e_id, e.fqid AS e_fqid, e.from_id AS e_from, e.to_id AS e_to, e.relation_type AS e_rel, e.payload AS e_payload, e.created_at AS e_created_at, e.updated_at AS e_updated_at
|
|
1337
|
+
SELECT
|
|
1338
|
+
v.id AS v_id, v.space AS v_space, v.fqid AS v_fqid, v.payload AS v_payload, v.created_at AS v_created_at, v.updated_at AS v_updated_at,
|
|
1339
|
+
e.id AS e_id, e.space AS e_space, e.fqid AS e_fqid, e.from_id AS e_from, e.to_id AS e_to, e.relation_type AS e_rel, e.payload AS e_payload, e.created_at AS e_created_at, e.updated_at AS e_updated_at
|
|
1274
1340
|
FROM {edge_ref} e
|
|
1275
1341
|
JOIN {from_ref_ref} v ON e.realm = v.realm AND e.from_id = v.id
|
|
1276
1342
|
WHERE e.realm = :realm AND e.to_id = :vertex_id
|
|
@@ -1283,6 +1349,7 @@ class SQLAlchemyPostGraph:
|
|
|
1283
1349
|
v = Vertex(
|
|
1284
1350
|
realm=realm,
|
|
1285
1351
|
id=str(r['v_id']),
|
|
1352
|
+
space=r.get('v_space') or 'default',
|
|
1286
1353
|
fqid=r['v_fqid'],
|
|
1287
1354
|
payload=r['v_payload'] if isinstance(r['v_payload'], dict) else json.loads(r['v_payload']),
|
|
1288
1355
|
created_at=r['v_created_at'],
|
|
@@ -1297,6 +1364,7 @@ class SQLAlchemyPostGraph:
|
|
|
1297
1364
|
from_id=str(r['e_from']),
|
|
1298
1365
|
to_id=str(r['e_to']),
|
|
1299
1366
|
relation_type=r['e_rel'],
|
|
1367
|
+
space=r.get('e_space') or 'default',
|
|
1300
1368
|
payload=r['e_payload'] if isinstance(r['e_payload'], dict) else json.loads(r['e_payload']),
|
|
1301
1369
|
created_at=r['e_created_at'],
|
|
1302
1370
|
updated_at=r['e_updated_at'],
|
|
@@ -26,3 +26,8 @@ class TableNotFoundError(PostGraphError):
|
|
|
26
26
|
class CyclicReferenceError(PostGraphError):
|
|
27
27
|
"""Raised when adding an edge would introduce a cyclic reference in the graph."""
|
|
28
28
|
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ReservedSpaceError(PostGraphError):
|
|
32
|
+
"""Raised when a reserved space name like '__all__' is used in a write operation."""
|
|
33
|
+
pass
|
|
@@ -69,13 +69,14 @@ class Vertex:
|
|
|
69
69
|
edge_table: str,
|
|
70
70
|
edge_id: Optional[str] = None,
|
|
71
71
|
payload: Optional[Dict[str, Any]] = None,
|
|
72
|
-
user_id: Optional[str] = None
|
|
72
|
+
user_id: Optional[str] = None,
|
|
73
|
+
space: Optional[str] = None
|
|
73
74
|
) -> 'Edge':
|
|
74
75
|
"""Create a new outgoing edge from this vertex to another vertex."""
|
|
75
76
|
if not self._client:
|
|
76
77
|
from post_graph.errors import PostGraphError
|
|
77
78
|
raise PostGraphError("Vertex was not loaded with a database client reference. Edge creation is unavailable.")
|
|
78
|
-
|
|
79
|
+
|
|
79
80
|
return await self._client.add_edge(
|
|
80
81
|
table_name=edge_table,
|
|
81
82
|
realm=self.realm,
|
|
@@ -84,7 +85,8 @@ class Vertex:
|
|
|
84
85
|
to_id=to_id,
|
|
85
86
|
relation_type=edge_table, # Default relation_type to the edge table name
|
|
86
87
|
payload=payload,
|
|
87
|
-
user_id=user_id
|
|
88
|
+
user_id=user_id,
|
|
89
|
+
space=space or self.space
|
|
88
90
|
)
|
|
89
91
|
|
|
90
92
|
async def add_edge_from(
|
|
@@ -93,13 +95,14 @@ class Vertex:
|
|
|
93
95
|
edge_table: str,
|
|
94
96
|
edge_id: Optional[str] = None,
|
|
95
97
|
payload: Optional[Dict[str, Any]] = None,
|
|
96
|
-
user_id: Optional[str] = None
|
|
98
|
+
user_id: Optional[str] = None,
|
|
99
|
+
space: Optional[str] = None
|
|
97
100
|
) -> 'Edge':
|
|
98
101
|
"""Create a new incoming edge from another vertex to this vertex."""
|
|
99
102
|
if not self._client:
|
|
100
103
|
from post_graph.errors import PostGraphError
|
|
101
104
|
raise PostGraphError("Vertex was not loaded with a database client reference. Edge creation is unavailable.")
|
|
102
|
-
|
|
105
|
+
|
|
103
106
|
return await self._client.add_edge(
|
|
104
107
|
table_name=edge_table,
|
|
105
108
|
realm=self.realm,
|
|
@@ -108,7 +111,8 @@ class Vertex:
|
|
|
108
111
|
to_id=self.id,
|
|
109
112
|
relation_type=edge_table,
|
|
110
113
|
payload=payload,
|
|
111
|
-
user_id=user_id
|
|
114
|
+
user_id=user_id,
|
|
115
|
+
space=space or self.space
|
|
112
116
|
)
|
|
113
117
|
|
|
114
118
|
async def delete(self, user_id: Optional[str] = None) -> bool:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "post-graph"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "High-performance PostgreSQL-backed graph database library supporting multi-tenant realms, space sub-grouping, schema-per-realm, shadow auditing, and append-only data history."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|