graflo 1.3.7__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.
Potentially problematic release.
This version of graflo might be problematic. Click here for more details.
- graflo/README.md +18 -0
- graflo/__init__.py +70 -0
- graflo/architecture/__init__.py +38 -0
- graflo/architecture/actor.py +1276 -0
- graflo/architecture/actor_util.py +450 -0
- graflo/architecture/edge.py +418 -0
- graflo/architecture/onto.py +376 -0
- graflo/architecture/onto_sql.py +54 -0
- graflo/architecture/resource.py +163 -0
- graflo/architecture/schema.py +135 -0
- graflo/architecture/transform.py +292 -0
- graflo/architecture/util.py +89 -0
- graflo/architecture/vertex.py +562 -0
- graflo/caster.py +736 -0
- graflo/cli/__init__.py +14 -0
- graflo/cli/ingest.py +203 -0
- graflo/cli/manage_dbs.py +197 -0
- graflo/cli/plot_schema.py +132 -0
- graflo/cli/xml2json.py +93 -0
- graflo/data_source/__init__.py +48 -0
- graflo/data_source/api.py +339 -0
- graflo/data_source/base.py +95 -0
- graflo/data_source/factory.py +304 -0
- graflo/data_source/file.py +148 -0
- graflo/data_source/memory.py +70 -0
- graflo/data_source/registry.py +82 -0
- graflo/data_source/sql.py +183 -0
- graflo/db/__init__.py +44 -0
- graflo/db/arango/__init__.py +22 -0
- graflo/db/arango/conn.py +1025 -0
- graflo/db/arango/query.py +180 -0
- graflo/db/arango/util.py +88 -0
- graflo/db/conn.py +377 -0
- graflo/db/connection/__init__.py +6 -0
- graflo/db/connection/config_mapping.py +18 -0
- graflo/db/connection/onto.py +717 -0
- graflo/db/connection/wsgi.py +29 -0
- graflo/db/manager.py +119 -0
- graflo/db/neo4j/__init__.py +16 -0
- graflo/db/neo4j/conn.py +639 -0
- graflo/db/postgres/__init__.py +37 -0
- graflo/db/postgres/conn.py +948 -0
- graflo/db/postgres/fuzzy_matcher.py +281 -0
- graflo/db/postgres/heuristics.py +133 -0
- graflo/db/postgres/inference_utils.py +428 -0
- graflo/db/postgres/resource_mapping.py +273 -0
- graflo/db/postgres/schema_inference.py +372 -0
- graflo/db/postgres/types.py +148 -0
- graflo/db/postgres/util.py +87 -0
- graflo/db/tigergraph/__init__.py +9 -0
- graflo/db/tigergraph/conn.py +2365 -0
- graflo/db/tigergraph/onto.py +26 -0
- graflo/db/util.py +49 -0
- graflo/filter/__init__.py +21 -0
- graflo/filter/onto.py +525 -0
- graflo/logging.conf +22 -0
- graflo/onto.py +312 -0
- graflo/plot/__init__.py +17 -0
- graflo/plot/plotter.py +616 -0
- graflo/util/__init__.py +23 -0
- graflo/util/chunker.py +807 -0
- graflo/util/merge.py +150 -0
- graflo/util/misc.py +37 -0
- graflo/util/onto.py +422 -0
- graflo/util/transform.py +454 -0
- graflo-1.3.7.dist-info/METADATA +243 -0
- graflo-1.3.7.dist-info/RECORD +70 -0
- graflo-1.3.7.dist-info/WHEEL +4 -0
- graflo-1.3.7.dist-info/entry_points.txt +5 -0
- graflo-1.3.7.dist-info/licenses/LICENSE +126 -0
graflo/db/neo4j/conn.py
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
"""Neo4j connection implementation for graph database operations.
|
|
2
|
+
|
|
3
|
+
This module implements the Connection interface for Neo4j, providing
|
|
4
|
+
specific functionality for graph operations in Neo4j. It handles:
|
|
5
|
+
- Node and relationship management
|
|
6
|
+
- Cypher query execution
|
|
7
|
+
- Index creation and management
|
|
8
|
+
- Batch operations
|
|
9
|
+
- Graph traversal and pattern matching
|
|
10
|
+
|
|
11
|
+
Key Features:
|
|
12
|
+
- Label-based node organization
|
|
13
|
+
- Relationship type management
|
|
14
|
+
- Property indices
|
|
15
|
+
- Cypher query execution
|
|
16
|
+
- Batch node and relationship operations
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
>>> conn = Neo4jConnection(config)
|
|
20
|
+
>>> conn.init_db(schema, clean_start=True)
|
|
21
|
+
>>> conn.upsert_docs_batch(docs, "User", match_keys=["email"])
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
|
|
26
|
+
from neo4j import GraphDatabase
|
|
27
|
+
|
|
28
|
+
from graflo.architecture.edge import Edge
|
|
29
|
+
from graflo.architecture.onto import Index
|
|
30
|
+
from graflo.architecture.schema import Schema
|
|
31
|
+
from graflo.architecture.vertex import VertexConfig
|
|
32
|
+
from graflo.db.conn import Connection
|
|
33
|
+
from graflo.filter.onto import Expression
|
|
34
|
+
from graflo.onto import AggregationType, DBFlavor, ExpressionFlavor
|
|
35
|
+
|
|
36
|
+
from ..connection.onto import Neo4jConfig
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Neo4jConnection(Connection):
|
|
42
|
+
"""Neo4j-specific implementation of the Connection interface.
|
|
43
|
+
|
|
44
|
+
This class provides Neo4j-specific implementations for all database
|
|
45
|
+
operations, including node management, relationship operations, and
|
|
46
|
+
Cypher query execution. It uses the Neo4j Python driver for all operations.
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
flavor: Database flavor identifier (NEO4J)
|
|
50
|
+
conn: Neo4j session instance
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
flavor = DBFlavor.NEO4J
|
|
54
|
+
|
|
55
|
+
def __init__(self, config: Neo4jConfig):
|
|
56
|
+
"""Initialize Neo4j connection.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
config: Neo4j connection configuration containing URL and credentials
|
|
60
|
+
"""
|
|
61
|
+
super().__init__()
|
|
62
|
+
# Store config for later use
|
|
63
|
+
self.config = config
|
|
64
|
+
# Ensure url is not None - GraphDatabase.driver requires a non-None URI
|
|
65
|
+
if config.url is None:
|
|
66
|
+
raise ValueError("Neo4j connection requires a URL to be configured")
|
|
67
|
+
self._driver = GraphDatabase.driver(
|
|
68
|
+
uri=config.url, auth=(config.username, config.password)
|
|
69
|
+
)
|
|
70
|
+
self.conn = self._driver.session()
|
|
71
|
+
|
|
72
|
+
def execute(self, query, **kwargs):
|
|
73
|
+
"""Execute a Cypher query.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
query: Cypher query string to execute
|
|
77
|
+
**kwargs: Additional query parameters
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Result: Neo4j query result
|
|
81
|
+
"""
|
|
82
|
+
cursor = self.conn.run(query, **kwargs)
|
|
83
|
+
return cursor
|
|
84
|
+
|
|
85
|
+
def close(self):
|
|
86
|
+
"""Close the Neo4j connection and session."""
|
|
87
|
+
# Close session first, then the underlying driver
|
|
88
|
+
try:
|
|
89
|
+
self.conn.close()
|
|
90
|
+
finally:
|
|
91
|
+
# Ensure the driver is also closed to release resources
|
|
92
|
+
self._driver.close()
|
|
93
|
+
|
|
94
|
+
def create_database(self, name: str):
|
|
95
|
+
"""Create a new Neo4j database.
|
|
96
|
+
|
|
97
|
+
Note: This operation is only supported in Neo4j Enterprise Edition.
|
|
98
|
+
Community Edition only supports one database per instance.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
name: Name of the database to create
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
self.execute(f"CREATE DATABASE {name}")
|
|
105
|
+
logger.info(f"Successfully created Neo4j database '{name}'")
|
|
106
|
+
except Exception as e:
|
|
107
|
+
raise e
|
|
108
|
+
|
|
109
|
+
def delete_database(self, name: str):
|
|
110
|
+
"""Delete a Neo4j database.
|
|
111
|
+
|
|
112
|
+
Note: This operation is only supported in Neo4j Enterprise Edition.
|
|
113
|
+
As a fallback, it deletes all nodes and relationships.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
name: Name of the database to delete (unused, deletes all data)
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
self.execute("MATCH (n) DETACH DELETE n")
|
|
120
|
+
logger.info("Successfully cleaned Neo4j database")
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logger.error(
|
|
123
|
+
f"Failed to clean Neo4j database: {e}",
|
|
124
|
+
exc_info=True,
|
|
125
|
+
)
|
|
126
|
+
raise
|
|
127
|
+
|
|
128
|
+
def define_vertex_indices(self, vertex_config: VertexConfig):
|
|
129
|
+
"""Define indices for vertex labels.
|
|
130
|
+
|
|
131
|
+
Creates indices for each vertex label based on the configuration.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
vertex_config: Vertex configuration containing index definitions
|
|
135
|
+
"""
|
|
136
|
+
for c in vertex_config.vertex_set:
|
|
137
|
+
for index_obj in vertex_config.indexes(c):
|
|
138
|
+
self._add_index(c, index_obj)
|
|
139
|
+
|
|
140
|
+
def define_edge_indices(self, edges: list[Edge]):
|
|
141
|
+
"""Define indices for relationship types.
|
|
142
|
+
|
|
143
|
+
Creates indices for each relationship type based on the configuration.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
edges: List of edge configurations containing index definitions
|
|
147
|
+
"""
|
|
148
|
+
for edge in edges:
|
|
149
|
+
for index_obj in edge.indexes:
|
|
150
|
+
if edge.relation is not None:
|
|
151
|
+
self._add_index(edge.relation, index_obj, is_vertex_index=False)
|
|
152
|
+
|
|
153
|
+
def _add_index(self, obj_name, index: Index, is_vertex_index=True):
|
|
154
|
+
"""Add an index to a label or relationship type.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
obj_name: Label or relationship type name
|
|
158
|
+
index: Index configuration to create
|
|
159
|
+
is_vertex_index: If True, create index on nodes, otherwise on relationships
|
|
160
|
+
"""
|
|
161
|
+
fields_str = ", ".join([f"x.{f}" for f in index.fields])
|
|
162
|
+
fields_str2 = "_".join(index.fields)
|
|
163
|
+
index_name = f"{obj_name}_{fields_str2}"
|
|
164
|
+
if is_vertex_index:
|
|
165
|
+
formula = f"(x:{obj_name})"
|
|
166
|
+
else:
|
|
167
|
+
formula = f"()-[x:{obj_name}]-()"
|
|
168
|
+
|
|
169
|
+
q = f"CREATE INDEX {index_name} IF NOT EXISTS FOR {formula} ON ({fields_str});"
|
|
170
|
+
|
|
171
|
+
self.execute(q)
|
|
172
|
+
|
|
173
|
+
def define_schema(self, schema: Schema):
|
|
174
|
+
"""Define collections based on schema.
|
|
175
|
+
|
|
176
|
+
Note: This is a no-op in Neo4j as collections are implicit.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
schema: Schema containing collection definitions
|
|
180
|
+
"""
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
def define_vertex_collections(self, schema: Schema):
|
|
184
|
+
"""Define vertex collections based on schema.
|
|
185
|
+
|
|
186
|
+
Note: This is a no-op in Neo4j as vertex collections are implicit.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
schema: Schema containing vertex definitions
|
|
190
|
+
"""
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
def define_edge_collections(self, edges: list[Edge]):
|
|
194
|
+
"""Define edge collections based on schema.
|
|
195
|
+
|
|
196
|
+
Note: This is a no-op in Neo4j as edge collections are implicit.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
edges: List of edge configurations
|
|
200
|
+
"""
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
def delete_graph_structure(self, vertex_types=(), graph_names=(), delete_all=False):
|
|
204
|
+
"""Delete graph structure (nodes and relationships) from Neo4j.
|
|
205
|
+
|
|
206
|
+
In Neo4j:
|
|
207
|
+
- Labels: Categories for nodes (equivalent to vertex types)
|
|
208
|
+
- Relationship Types: Types of relationships (equivalent to edge types)
|
|
209
|
+
- No explicit "graph" concept - all nodes/relationships are in the database
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
vertex_types: Label names to delete nodes for
|
|
213
|
+
graph_names: Unused in Neo4j (no explicit graph concept)
|
|
214
|
+
delete_all: If True, delete all nodes and relationships
|
|
215
|
+
"""
|
|
216
|
+
cnames = vertex_types
|
|
217
|
+
if cnames:
|
|
218
|
+
for c in cnames:
|
|
219
|
+
q = f"MATCH (n:{c}) DELETE n"
|
|
220
|
+
self.execute(q)
|
|
221
|
+
else:
|
|
222
|
+
q = "MATCH (n) DELETE n"
|
|
223
|
+
self.execute(q)
|
|
224
|
+
|
|
225
|
+
def init_db(self, schema: Schema, clean_start):
|
|
226
|
+
"""Initialize Neo4j with the given schema.
|
|
227
|
+
|
|
228
|
+
Checks if the database exists and creates it if it doesn't.
|
|
229
|
+
Uses schema.general.name if database is not set in config.
|
|
230
|
+
Note: Database creation is only supported in Neo4j Enterprise Edition.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
schema: Schema containing graph structure definitions
|
|
234
|
+
clean_start: If True, delete all existing data before initialization
|
|
235
|
+
"""
|
|
236
|
+
# Determine database name: use config.database if set, otherwise use schema.general.name
|
|
237
|
+
db_name = self.config.database
|
|
238
|
+
if not db_name:
|
|
239
|
+
db_name = schema.general.name
|
|
240
|
+
# Update config for subsequent operations
|
|
241
|
+
self.config.database = db_name
|
|
242
|
+
|
|
243
|
+
# Check if database exists and create it if it doesn't
|
|
244
|
+
# Note: This only works in Neo4j Enterprise Edition
|
|
245
|
+
# For Community Edition, we'll try to create it but it may fail gracefully
|
|
246
|
+
# Community Edition only allows one database per instance
|
|
247
|
+
try:
|
|
248
|
+
# Try to check if database exists (Enterprise feature)
|
|
249
|
+
try:
|
|
250
|
+
result = self.execute("SHOW DATABASES")
|
|
251
|
+
# Neo4j result is a cursor-like object, iterate to get records
|
|
252
|
+
databases = []
|
|
253
|
+
for record in result:
|
|
254
|
+
# Record structure may vary, try common field names
|
|
255
|
+
if hasattr(record, "get"):
|
|
256
|
+
db_name_field = (
|
|
257
|
+
record.get("name")
|
|
258
|
+
or record.get("database")
|
|
259
|
+
or record.get("db")
|
|
260
|
+
)
|
|
261
|
+
else:
|
|
262
|
+
# If record is a dict-like object, try direct access
|
|
263
|
+
db_name_field = getattr(record, "name", None) or getattr(
|
|
264
|
+
record, "database", None
|
|
265
|
+
)
|
|
266
|
+
if db_name_field:
|
|
267
|
+
databases.append(db_name_field)
|
|
268
|
+
|
|
269
|
+
if db_name not in databases:
|
|
270
|
+
logger.info(
|
|
271
|
+
f"Database '{db_name}' does not exist, attempting to create it..."
|
|
272
|
+
)
|
|
273
|
+
try:
|
|
274
|
+
self.create_database(db_name)
|
|
275
|
+
logger.info(f"Successfully created database '{db_name}'")
|
|
276
|
+
except Exception as create_error:
|
|
277
|
+
logger.info(
|
|
278
|
+
f"Neo4j Community Edition? Could not create database '{db_name}': {create_error}. "
|
|
279
|
+
f"This may be Neo4j Community Edition which only supports one database per instance.",
|
|
280
|
+
exc_info=True,
|
|
281
|
+
)
|
|
282
|
+
# Continue with default database for Community Edition
|
|
283
|
+
except Exception as show_error:
|
|
284
|
+
# If SHOW DATABASES fails (Community Edition or older versions), try to create anyway
|
|
285
|
+
logger.debug(
|
|
286
|
+
f"Could not check database existence (may be Community Edition): {show_error}"
|
|
287
|
+
)
|
|
288
|
+
try:
|
|
289
|
+
self.create_database(db_name)
|
|
290
|
+
logger.info(f"Successfully created database '{db_name}'")
|
|
291
|
+
except Exception as create_error:
|
|
292
|
+
logger.info(
|
|
293
|
+
f"Neo4j Community Edition? Could not create database '{db_name}': {create_error}. "
|
|
294
|
+
f"This may be Neo4j Community Edition which only supports one database per instance. "
|
|
295
|
+
f"Continuing with default database.",
|
|
296
|
+
exc_info=True,
|
|
297
|
+
)
|
|
298
|
+
# Continue with default database for Community Edition
|
|
299
|
+
except Exception as e:
|
|
300
|
+
logger.error(
|
|
301
|
+
f"Error during database initialization for '{db_name}': {e}",
|
|
302
|
+
exc_info=True,
|
|
303
|
+
)
|
|
304
|
+
# Don't raise - allow operation to continue with default database
|
|
305
|
+
logger.warning(
|
|
306
|
+
"Continuing with default database due to initialization error"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
if clean_start:
|
|
311
|
+
try:
|
|
312
|
+
self.delete_database("")
|
|
313
|
+
logger.debug(f"Cleaned database '{db_name}' for fresh start")
|
|
314
|
+
except Exception as clean_error:
|
|
315
|
+
logger.warning(
|
|
316
|
+
f"Error during clean_start for database '{db_name}': {clean_error}",
|
|
317
|
+
exc_info=True,
|
|
318
|
+
)
|
|
319
|
+
# Continue - may be first run or already clean
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
self.define_indexes(schema)
|
|
323
|
+
logger.debug(f"Defined indexes for database '{db_name}'")
|
|
324
|
+
except Exception as index_error:
|
|
325
|
+
logger.error(
|
|
326
|
+
f"Failed to define indexes for database '{db_name}': {index_error}",
|
|
327
|
+
exc_info=True,
|
|
328
|
+
)
|
|
329
|
+
raise
|
|
330
|
+
except Exception as e:
|
|
331
|
+
logger.error(
|
|
332
|
+
f"Error during database schema initialization for '{db_name}': {e}",
|
|
333
|
+
exc_info=True,
|
|
334
|
+
)
|
|
335
|
+
raise
|
|
336
|
+
|
|
337
|
+
def upsert_docs_batch(self, docs, class_name, match_keys, **kwargs):
|
|
338
|
+
"""Upsert a batch of nodes using Cypher.
|
|
339
|
+
|
|
340
|
+
Performs an upsert operation on a batch of nodes, using the specified
|
|
341
|
+
match keys to determine whether to update existing nodes or create new ones.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
docs: List of node documents to upsert
|
|
345
|
+
class_name: Label to upsert into
|
|
346
|
+
match_keys: Keys to match for upsert operation
|
|
347
|
+
**kwargs: Additional options:
|
|
348
|
+
- dry: If True, don't execute the query
|
|
349
|
+
"""
|
|
350
|
+
dry = kwargs.pop("dry", False)
|
|
351
|
+
|
|
352
|
+
index_str = ", ".join([f"{k}: row.{k}" for k in match_keys])
|
|
353
|
+
q = f"""
|
|
354
|
+
WITH $batch AS batch
|
|
355
|
+
UNWIND batch as row
|
|
356
|
+
MERGE (n:{class_name} {{ {index_str} }})
|
|
357
|
+
ON MATCH set n += row
|
|
358
|
+
ON CREATE set n += row
|
|
359
|
+
"""
|
|
360
|
+
if not dry:
|
|
361
|
+
self.execute(q, batch=docs)
|
|
362
|
+
|
|
363
|
+
def insert_edges_batch(
|
|
364
|
+
self,
|
|
365
|
+
docs_edges,
|
|
366
|
+
source_class,
|
|
367
|
+
target_class,
|
|
368
|
+
relation_name,
|
|
369
|
+
collection_name=None,
|
|
370
|
+
match_keys_source=("_key",),
|
|
371
|
+
match_keys_target=("_key",),
|
|
372
|
+
filter_uniques=True,
|
|
373
|
+
uniq_weight_fields=None,
|
|
374
|
+
uniq_weight_collections=None,
|
|
375
|
+
upsert_option=False,
|
|
376
|
+
head=None,
|
|
377
|
+
**kwargs,
|
|
378
|
+
):
|
|
379
|
+
"""Insert a batch of relationships using Cypher.
|
|
380
|
+
|
|
381
|
+
Creates relationships between source and target nodes, with support for
|
|
382
|
+
property matching and unique constraints.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
docs_edges: List of edge documents in format [{__source: source_doc, __target: target_doc}]
|
|
386
|
+
source_class: Source node label
|
|
387
|
+
target_class: Target node label
|
|
388
|
+
relation_name: Relationship type name
|
|
389
|
+
collection_name: Unused in Neo4j
|
|
390
|
+
match_keys_source: Keys to match source nodes
|
|
391
|
+
match_keys_target: Keys to match target nodes
|
|
392
|
+
filter_uniques: Unused in Neo4j
|
|
393
|
+
uniq_weight_fields: Unused in Neo4j
|
|
394
|
+
uniq_weight_collections: Unused in Neo4j
|
|
395
|
+
upsert_option: Unused in Neo4j
|
|
396
|
+
head: Optional limit on number of relationships to insert
|
|
397
|
+
**kwargs: Additional options:
|
|
398
|
+
- dry: If True, don't execute the query
|
|
399
|
+
"""
|
|
400
|
+
dry = kwargs.pop("dry", False)
|
|
401
|
+
|
|
402
|
+
source_match_str = [f"source.{key} = row[0].{key}" for key in match_keys_source]
|
|
403
|
+
target_match_str = [f"target.{key} = row[1].{key}" for key in match_keys_target]
|
|
404
|
+
|
|
405
|
+
match_clause = "WHERE " + " AND ".join(source_match_str + target_match_str)
|
|
406
|
+
|
|
407
|
+
q = f"""
|
|
408
|
+
WITH $batch AS batch
|
|
409
|
+
UNWIND batch as row
|
|
410
|
+
MATCH (source:{source_class}),
|
|
411
|
+
(target:{target_class}) {match_clause}
|
|
412
|
+
MERGE (source)-[r:{relation_name}]->(target)
|
|
413
|
+
SET r += row[2]
|
|
414
|
+
|
|
415
|
+
"""
|
|
416
|
+
if not dry:
|
|
417
|
+
self.execute(q, batch=docs_edges)
|
|
418
|
+
|
|
419
|
+
def insert_return_batch(self, docs, class_name):
|
|
420
|
+
"""Insert nodes and return their properties.
|
|
421
|
+
|
|
422
|
+
Note: Not implemented in Neo4j.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
docs: Documents to insert
|
|
426
|
+
class_name: Label to insert into
|
|
427
|
+
|
|
428
|
+
Raises:
|
|
429
|
+
NotImplementedError: This method is not implemented for Neo4j
|
|
430
|
+
"""
|
|
431
|
+
raise NotImplementedError()
|
|
432
|
+
|
|
433
|
+
def fetch_docs(
|
|
434
|
+
self,
|
|
435
|
+
class_name,
|
|
436
|
+
filters: list | dict | None = None,
|
|
437
|
+
limit: int | None = None,
|
|
438
|
+
return_keys: list | None = None,
|
|
439
|
+
unset_keys: list | None = None,
|
|
440
|
+
**kwargs,
|
|
441
|
+
):
|
|
442
|
+
"""Fetch nodes from a label.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
class_name: Label to fetch from
|
|
446
|
+
filters: Query filters
|
|
447
|
+
limit: Maximum number of nodes to return
|
|
448
|
+
return_keys: Keys to return
|
|
449
|
+
unset_keys: Unused in Neo4j
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
list: Fetched nodes
|
|
453
|
+
"""
|
|
454
|
+
if filters is not None:
|
|
455
|
+
ff = Expression.from_dict(filters)
|
|
456
|
+
filter_clause = f"WHERE {ff(doc_name='n', kind=DBFlavor.NEO4J)}"
|
|
457
|
+
else:
|
|
458
|
+
filter_clause = ""
|
|
459
|
+
|
|
460
|
+
if return_keys is not None:
|
|
461
|
+
keep_clause_ = ", ".join([f".{item}" for item in return_keys])
|
|
462
|
+
keep_clause = f"{{ {keep_clause_} }}"
|
|
463
|
+
else:
|
|
464
|
+
keep_clause = ""
|
|
465
|
+
|
|
466
|
+
if limit is not None and isinstance(limit, int):
|
|
467
|
+
limit_clause = f"LIMIT {limit}"
|
|
468
|
+
else:
|
|
469
|
+
limit_clause = ""
|
|
470
|
+
|
|
471
|
+
q = (
|
|
472
|
+
f"MATCH (n:{class_name})"
|
|
473
|
+
f" {filter_clause}"
|
|
474
|
+
f" RETURN n {keep_clause}"
|
|
475
|
+
f" {limit_clause}"
|
|
476
|
+
)
|
|
477
|
+
cursor = self.execute(q)
|
|
478
|
+
r = [item["n"] for item in cursor.data()]
|
|
479
|
+
return r
|
|
480
|
+
|
|
481
|
+
# TODO test
|
|
482
|
+
def fetch_edges(
|
|
483
|
+
self,
|
|
484
|
+
from_type: str,
|
|
485
|
+
from_id: str,
|
|
486
|
+
edge_type: str | None = None,
|
|
487
|
+
to_type: str | None = None,
|
|
488
|
+
to_id: str | None = None,
|
|
489
|
+
filters: list | dict | None = None,
|
|
490
|
+
limit: int | None = None,
|
|
491
|
+
return_keys: list | None = None,
|
|
492
|
+
unset_keys: list | None = None,
|
|
493
|
+
**kwargs,
|
|
494
|
+
):
|
|
495
|
+
"""Fetch edges from Neo4j using Cypher.
|
|
496
|
+
|
|
497
|
+
Args:
|
|
498
|
+
from_type: Source node label
|
|
499
|
+
from_id: Source node ID (property name depends on match_keys used)
|
|
500
|
+
edge_type: Optional relationship type to filter by
|
|
501
|
+
to_type: Optional target node label to filter by
|
|
502
|
+
to_id: Optional target node ID to filter by
|
|
503
|
+
filters: Additional query filters
|
|
504
|
+
limit: Maximum number of edges to return
|
|
505
|
+
return_keys: Keys to return (projection)
|
|
506
|
+
unset_keys: Keys to exclude (projection) - not supported in Neo4j
|
|
507
|
+
**kwargs: Additional parameters
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
list: List of fetched edges
|
|
511
|
+
"""
|
|
512
|
+
# Build Cypher query to fetch edges
|
|
513
|
+
# Match source node first
|
|
514
|
+
source_match = f"(source:{from_type} {{id: '{from_id}'}})"
|
|
515
|
+
|
|
516
|
+
# Build relationship pattern
|
|
517
|
+
if edge_type:
|
|
518
|
+
rel_pattern = f"-[r:{edge_type}]->"
|
|
519
|
+
else:
|
|
520
|
+
rel_pattern = "-[r]->"
|
|
521
|
+
|
|
522
|
+
# Build target node match
|
|
523
|
+
if to_type:
|
|
524
|
+
target_match = f"(target:{to_type})"
|
|
525
|
+
else:
|
|
526
|
+
target_match = "(target)"
|
|
527
|
+
|
|
528
|
+
# Add target ID filter if provided
|
|
529
|
+
where_clauses = []
|
|
530
|
+
if to_id:
|
|
531
|
+
where_clauses.append(f"target.id = '{to_id}'")
|
|
532
|
+
|
|
533
|
+
# Add additional filters if provided
|
|
534
|
+
if filters is not None:
|
|
535
|
+
from graflo.filter.onto import Expression
|
|
536
|
+
|
|
537
|
+
ff = Expression.from_dict(filters)
|
|
538
|
+
filter_clause = ff(doc_name="r", kind=ExpressionFlavor.NEO4J)
|
|
539
|
+
where_clauses.append(filter_clause)
|
|
540
|
+
|
|
541
|
+
where_clause = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else ""
|
|
542
|
+
|
|
543
|
+
# Build return clause
|
|
544
|
+
if return_keys is not None:
|
|
545
|
+
return_clause = ", ".join([f"r.{key} as {key}" for key in return_keys])
|
|
546
|
+
return_clause = f"RETURN {return_clause}"
|
|
547
|
+
else:
|
|
548
|
+
return_clause = "RETURN r"
|
|
549
|
+
|
|
550
|
+
limit_clause = f"LIMIT {limit}" if limit else ""
|
|
551
|
+
|
|
552
|
+
query = f"""
|
|
553
|
+
MATCH {source_match}{rel_pattern}{target_match}
|
|
554
|
+
{where_clause}
|
|
555
|
+
{return_clause}
|
|
556
|
+
{limit_clause}
|
|
557
|
+
"""
|
|
558
|
+
|
|
559
|
+
cursor = self.execute(query)
|
|
560
|
+
result = [item["r"] for item in cursor.data()]
|
|
561
|
+
|
|
562
|
+
# Note: unset_keys is not supported in Neo4j as we can't modify the result structure
|
|
563
|
+
# after the query
|
|
564
|
+
|
|
565
|
+
return result
|
|
566
|
+
|
|
567
|
+
def fetch_present_documents(
|
|
568
|
+
self,
|
|
569
|
+
batch,
|
|
570
|
+
class_name,
|
|
571
|
+
match_keys,
|
|
572
|
+
keep_keys,
|
|
573
|
+
flatten=False,
|
|
574
|
+
filters: list | dict | None = None,
|
|
575
|
+
):
|
|
576
|
+
"""Fetch nodes that exist in the database.
|
|
577
|
+
|
|
578
|
+
Note: Not implemented in Neo4j.
|
|
579
|
+
|
|
580
|
+
Args:
|
|
581
|
+
batch: Batch of documents to check
|
|
582
|
+
class_name: Label to check in
|
|
583
|
+
match_keys: Keys to match nodes
|
|
584
|
+
keep_keys: Keys to keep in result
|
|
585
|
+
flatten: Unused in Neo4j
|
|
586
|
+
filters: Additional query filters
|
|
587
|
+
|
|
588
|
+
Raises:
|
|
589
|
+
NotImplementedError: This method is not implemented for Neo4j
|
|
590
|
+
"""
|
|
591
|
+
raise NotImplementedError
|
|
592
|
+
|
|
593
|
+
def aggregate(
|
|
594
|
+
self,
|
|
595
|
+
class_name,
|
|
596
|
+
aggregation_function: AggregationType,
|
|
597
|
+
discriminant: str | None = None,
|
|
598
|
+
aggregated_field: str | None = None,
|
|
599
|
+
filters: list | dict | None = None,
|
|
600
|
+
):
|
|
601
|
+
"""Perform aggregation on nodes.
|
|
602
|
+
|
|
603
|
+
Note: Not implemented in Neo4j.
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
class_name: Label to aggregate
|
|
607
|
+
aggregation_function: Type of aggregation to perform
|
|
608
|
+
discriminant: Field to group by
|
|
609
|
+
aggregated_field: Field to aggregate
|
|
610
|
+
filters: Query filters
|
|
611
|
+
|
|
612
|
+
Raises:
|
|
613
|
+
NotImplementedError: This method is not implemented for Neo4j
|
|
614
|
+
"""
|
|
615
|
+
raise NotImplementedError
|
|
616
|
+
|
|
617
|
+
def keep_absent_documents(
|
|
618
|
+
self,
|
|
619
|
+
batch,
|
|
620
|
+
class_name,
|
|
621
|
+
match_keys,
|
|
622
|
+
keep_keys,
|
|
623
|
+
filters: list | dict | None = None,
|
|
624
|
+
):
|
|
625
|
+
"""Keep nodes that don't exist in the database.
|
|
626
|
+
|
|
627
|
+
Note: Not implemented in Neo4j.
|
|
628
|
+
|
|
629
|
+
Args:
|
|
630
|
+
batch: Batch of documents to check
|
|
631
|
+
class_name: Label to check in
|
|
632
|
+
match_keys: Keys to match nodes
|
|
633
|
+
keep_keys: Keys to keep in result
|
|
634
|
+
filters: Additional query filters
|
|
635
|
+
|
|
636
|
+
Raises:
|
|
637
|
+
NotImplementedError: This method is not implemented for Neo4j
|
|
638
|
+
"""
|
|
639
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""PostgreSQL database implementation.
|
|
2
|
+
|
|
3
|
+
This package provides PostgreSQL-specific implementations for schema introspection
|
|
4
|
+
and connection management. It focuses on reading and analyzing 3NF schemas to identify
|
|
5
|
+
vertex-like and edge-like tables, and inferring graflo Schema objects.
|
|
6
|
+
|
|
7
|
+
Key Components:
|
|
8
|
+
- PostgresConnection: PostgreSQL connection and schema introspection implementation
|
|
9
|
+
- PostgresSchemaInferencer: Infers graflo Schema from PostgreSQL schemas
|
|
10
|
+
- PostgresResourceMapper: Maps PostgreSQL tables to graflo Resources
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
>>> from graflo.db.postgres.heuristics import infer_schema_from_postgres >>> from graflo.db.postgres import PostgresConnection
|
|
14
|
+
>>> from graflo.db.connection.onto import PostgresConfig
|
|
15
|
+
>>> config = PostgresConfig.from_docker_env()
|
|
16
|
+
>>> conn = PostgresConnection(config)
|
|
17
|
+
>>> schema = infer_schema_from_postgres(conn, schema_name="public")
|
|
18
|
+
>>> conn.close()
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from .conn import PostgresConnection
|
|
22
|
+
from .heuristics import (
|
|
23
|
+
create_patterns_from_postgres,
|
|
24
|
+
create_resources_from_postgres,
|
|
25
|
+
infer_schema_from_postgres,
|
|
26
|
+
)
|
|
27
|
+
from .resource_mapping import PostgresResourceMapper
|
|
28
|
+
from .schema_inference import PostgresSchemaInferencer
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"PostgresConnection",
|
|
32
|
+
"PostgresSchemaInferencer",
|
|
33
|
+
"PostgresResourceMapper",
|
|
34
|
+
"infer_schema_from_postgres",
|
|
35
|
+
"create_resources_from_postgres",
|
|
36
|
+
"create_patterns_from_postgres",
|
|
37
|
+
]
|