runic-py 0.3.0__tar.gz → 0.3.2__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.
Files changed (70) hide show
  1. {runic_py-0.3.0 → runic_py-0.3.2}/.gitignore +7 -0
  2. {runic_py-0.3.0 → runic_py-0.3.2}/PKG-INFO +17 -14
  3. {runic_py-0.3.0 → runic_py-0.3.2}/README.md +5 -4
  4. {runic_py-0.3.0 → runic_py-0.3.2}/pyproject.toml +32 -23
  5. {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/adapters/__init__.py +3 -1
  6. {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/adapters/_base.py +33 -1
  7. {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/adapters/arcadedb.py +8 -60
  8. {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/adapters/falkordb.py +6 -0
  9. {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/adapters/memgraph.py +12 -59
  10. {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/adapters/neo4j.py +8 -57
  11. {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/cli.py +12 -14
  12. {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/context.py +37 -4
  13. {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/operations.py +21 -25
  14. {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/schema.py +61 -72
  15. {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/script.py +3 -3
  16. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/core/descriptors.py +9 -29
  17. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/core/models.py +11 -22
  18. runic_py-0.3.2/runic/orm/query/_compiler.py +210 -0
  19. runic_py-0.3.2/runic/orm/query/_decoder.py +115 -0
  20. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/query/builder.py +4 -252
  21. runic_py-0.3.2/runic/orm/repository/_base.py +49 -0
  22. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/repository/async_repository.py +6 -33
  23. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/repository/repository.py +5 -35
  24. runic_py-0.3.2/runic/orm/session/_base.py +213 -0
  25. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/session/async_session.py +15 -161
  26. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/session/session.py +17 -182
  27. {runic_py-0.3.0 → runic_py-0.3.2}/LICENSE.md +0 -0
  28. {runic_py-0.3.0 → runic_py-0.3.2}/runic/__init__.py +0 -0
  29. {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/__init__.py +0 -0
  30. {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/adapters/age.py +0 -0
  31. {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/autogen.py +0 -0
  32. {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/checksum.py +0 -0
  33. {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/config.py +0 -0
  34. {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/exceptions.py +0 -0
  35. {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/introspect.py +0 -0
  36. {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/manifest.py +0 -0
  37. {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/service.py +0 -0
  38. {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/templates/env.py.mako +0 -0
  39. {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/templates/script.py.mako +0 -0
  40. {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/testing.py +0 -0
  41. {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/version.py +0 -0
  42. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/__init__.py +0 -0
  43. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/core/__init__.py +0 -0
  44. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/core/metadata.py +0 -0
  45. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/core/types.py +0 -0
  46. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/driver/__init__.py +0 -0
  47. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/driver/age.py +0 -0
  48. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/driver/arcadedb.py +0 -0
  49. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/driver/bolt.py +0 -0
  50. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/driver/factory.py +0 -0
  51. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/driver/falkordb.py +0 -0
  52. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/driver/memgraph.py +0 -0
  53. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/driver/neo4j.py +0 -0
  54. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/exceptions.py +0 -0
  55. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/mapper/__init__.py +0 -0
  56. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/mapper/mapper.py +0 -0
  57. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/mapper/relationship_loader.py +0 -0
  58. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/mapper/relationship_writer.py +0 -0
  59. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/operations.py +0 -0
  60. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/query/__init__.py +0 -0
  61. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/query/expressions.py +0 -0
  62. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/query/specialised.py +0 -0
  63. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/query/traversal.py +0 -0
  64. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/repository/__init__.py +0 -0
  65. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/repository/cypher.py +0 -0
  66. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/repository/protocol.py +0 -0
  67. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/schema/__init__.py +0 -0
  68. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/schema/index_manager.py +0 -0
  69. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/session/__init__.py +0 -0
  70. {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/session/connection_pool.py +0 -0
@@ -116,3 +116,10 @@ wheels/
116
116
  .runic
117
117
  runic_test/
118
118
  .claude/ralph-loop.local.md
119
+
120
+ # DeepEval run caches (tests/evals suite runs from an isolated venv)
121
+ .deepeval/
122
+ .deepeval-cache.json
123
+ .deepeval_telemetry.txt
124
+ .latest_run_full.json
125
+ .latest_test_run.json
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runic-py
3
- Version: 0.3.0
4
- Summary: OGM and schema migrations for Graph Databases.
3
+ Version: 0.3.2
4
+ Summary: Graph schema migrations and ORM for Cypher-based graph databases.
5
5
  Project-URL: Homepage, https://runic-py.readthedocs.io/en/latest/
6
6
  Project-URL: Repository, https://github.com/jenreh/runic
7
7
  Author: Jens Rehpöhler
@@ -11,25 +11,27 @@ Classifier: Intended Audience :: Developers
11
11
  Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Programming Language :: Python :: 3 :: Only
14
- Classifier: Topic :: Home Automation
14
+ Classifier: Topic :: Database
15
15
  Requires-Python: >=3.14
16
16
  Requires-Dist: mako<2.0.0,>=1.3.0
17
- Requires-Dist: rich<16.0.0,>=15.0.0
17
+ Requires-Dist: rich<16.0.0,>=14.0.0
18
18
  Requires-Dist: typer<1.0.0,>=0.26.0
19
19
  Provides-Extra: age
20
- Requires-Dist: psycopg[binary]>=3.3.4; extra == 'age'
20
+ Requires-Dist: psycopg<4.0.0,>=3.3.4; extra == 'age'
21
+ Provides-Extra: age-binary
22
+ Requires-Dist: psycopg[binary]<4.0.0,>=3.3.4; extra == 'age-binary'
21
23
  Provides-Extra: all
22
24
  Requires-Dist: falkordb<2.0.0,>=1.6.0; extra == 'all'
23
- Requires-Dist: neo4j>=6.2.0; extra == 'all'
24
- Requires-Dist: psycopg[binary]>=3.3.4; extra == 'all'
25
+ Requires-Dist: neo4j<7.0.0,>=6.2.0; extra == 'all'
26
+ Requires-Dist: psycopg[binary]<4.0.0,>=3.3.4; extra == 'all'
25
27
  Provides-Extra: arcadedb
26
- Requires-Dist: neo4j>=6.2.0; extra == 'arcadedb'
28
+ Requires-Dist: neo4j<7.0.0,>=6.2.0; extra == 'arcadedb'
27
29
  Provides-Extra: falkordb
28
30
  Requires-Dist: falkordb<2.0.0,>=1.6.0; extra == 'falkordb'
29
31
  Provides-Extra: memgraph
30
- Requires-Dist: neo4j>=6.2.0; extra == 'memgraph'
32
+ Requires-Dist: neo4j<7.0.0,>=6.2.0; extra == 'memgraph'
31
33
  Provides-Extra: neo4j
32
- Requires-Dist: neo4j>=6.2.0; extra == 'neo4j'
34
+ Requires-Dist: neo4j<7.0.0,>=6.2.0; extra == 'neo4j'
33
35
  Description-Content-Type: text/markdown
34
36
 
35
37
  <div align="center">
@@ -39,7 +41,8 @@ Description-Content-Type: text/markdown
39
41
 
40
42
  **Graph schema migrations and ORM for Cypher-based graph databases.**
41
43
 
42
- ![Version](https://img.shields.io/badge/version-0.3.0-blue)
44
+ ![Version](https://img.shields.io/badge/version-0.3.2-blue)
45
+ [![PyPI](https://img.shields.io/pypi/v/runic-py.svg)](https://pypi.org/project/runic-py/)
43
46
  [![Python](https://img.shields.io/badge/python-3.14%2B-orange)](https://www.python.org)
44
47
  [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE.md)
45
48
 
@@ -294,9 +297,9 @@ if min_age > 0:
294
297
  stmt = stmt.where(User.age >= min_age)
295
298
 
296
299
  with Session(driver) as session:
297
- users: list[User] = session.scalars(stmt) # list[User]
298
- user: User | None = session.scalar(stmt) # User | None
299
- n: int = session.count(stmt) # int
300
+ users: list[User] = session.scalars(stmt) # list[User]
301
+ user: User | None = session.scalar(stmt) # User | None
302
+ n: int = session.count(stmt) # int
300
303
  ```
301
304
 
302
305
  The same `stmt` is reusable — pass it to multiple sessions or execute it
@@ -5,7 +5,8 @@
5
5
 
6
6
  **Graph schema migrations and ORM for Cypher-based graph databases.**
7
7
 
8
- ![Version](https://img.shields.io/badge/version-0.3.0-blue)
8
+ ![Version](https://img.shields.io/badge/version-0.3.2-blue)
9
+ [![PyPI](https://img.shields.io/pypi/v/runic-py.svg)](https://pypi.org/project/runic-py/)
9
10
  [![Python](https://img.shields.io/badge/python-3.14%2B-orange)](https://www.python.org)
10
11
  [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE.md)
11
12
 
@@ -260,9 +261,9 @@ if min_age > 0:
260
261
  stmt = stmt.where(User.age >= min_age)
261
262
 
262
263
  with Session(driver) as session:
263
- users: list[User] = session.scalars(stmt) # list[User]
264
- user: User | None = session.scalar(stmt) # User | None
265
- n: int = session.count(stmt) # int
264
+ users: list[User] = session.scalars(stmt) # list[User]
265
+ user: User | None = session.scalar(stmt) # User | None
266
+ n: int = session.count(stmt) # int
266
267
  ```
267
268
 
268
269
  The same `stmt` is reusable — pass it to multiple sessions or execute it
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "runic-py"
3
- version = "0.3.0"
4
- description = "OGM and schema migrations for Graph Databases."
3
+ version = "0.3.2"
4
+ description = "Graph schema migrations and ORM for Cypher-based graph databases."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14"
7
7
  license = { text = "MIT" }
@@ -12,47 +12,48 @@ classifiers = [
12
12
  "License :: OSI Approved :: MIT License",
13
13
  "Programming Language :: Python :: 3",
14
14
  "Programming Language :: Python :: 3 :: Only",
15
- "Topic :: Home Automation",
15
+ "Topic :: Database",
16
16
  ]
17
17
 
18
18
  dependencies = [
19
19
  "typer>=0.26.0,<1.0.0",
20
- "rich>=15.0.0,<16.0.0",
20
+ "rich>=14.0.0,<16.0.0",
21
21
  "mako>=1.3.0,<2.0.0",
22
22
  ]
23
23
 
24
24
  [project.optional-dependencies]
25
25
  falkordb = ["falkordb>=1.6.0,<2.0.0"]
26
- neo4j = ["neo4j>=6.2.0"]
27
- memgraph = ["neo4j>=6.2.0"]
28
- arcadedb = ["neo4j>=6.2.0"]
29
- age = ["psycopg[binary]>=3.3.4"]
26
+ neo4j = ["neo4j>=6.2.0,<7.0.0"]
27
+ memgraph = ["neo4j>=6.2.0,<7.0.0"]
28
+ arcadedb = ["neo4j>=6.2.0,<7.0.0"]
29
+ age = ["psycopg>=3.3.4,<4.0.0"]
30
+ age-binary = ["psycopg[binary]>=3.3.4,<4.0.0"]
30
31
  all = [
31
32
  "falkordb>=1.6.0,<2.0.0",
32
- "neo4j>=6.2.0",
33
- "psycopg[binary]>=3.3.4",
33
+ "neo4j>=6.2.0,<7.0.0",
34
+ "psycopg[binary]>=3.3.4,<4.0.0",
34
35
  ]
35
-
36
36
  [dependency-groups]
37
37
  dev = [
38
38
  "bump-my-version==1.3.0",
39
+ "deepeval==4.0.5",
39
40
  "detect-secrets==1.5.0",
41
+ "falkordb>=1.6.1",
42
+ "falkordblite==0.10.0",
43
+ "furo==2025.12.19",
44
+ "neo4j==6.2.0",
40
45
  "pre-commit==4.6.0",
41
- "pytest==9.0.3",
46
+ "psycopg[binary]==3.3.4",
42
47
  "pytest-asyncio==1.4.0",
43
48
  "pytest-cov==7.1.0",
44
49
  "pytest-httpserver==1.1.5",
50
+ "pytest==9.0.3",
51
+ "python-dotenv==1.2.2",
45
52
  "ruff==0.15.16",
46
- "falkordblite==0.10.0",
47
- "sphinx==9.1.0",
48
- "furo==2025.12.19",
49
53
  "sphinx-copybutton==0.5.2",
50
54
  "sphinx-design==0.7.0",
51
- "ty>=0.0.44",
52
- "python-dotenv>=1.2.2",
53
- "falkordb>=1.6.0,<2.0.0",
54
- "neo4j>=6.2.0",
55
- "psycopg[binary]>=3.3.4",
55
+ "sphinx==9.1.0",
56
+ "ty==0.0.44",
56
57
  ]
57
58
 
58
59
  [project.scripts]
@@ -91,7 +92,7 @@ exclude = [
91
92
 
92
93
  [tool.bumpversion]
93
94
  commit = false
94
- current_version = "0.3.0"
95
+ current_version = "0.3.2"
95
96
  tag = false
96
97
  allow-dirty = false
97
98
 
@@ -107,8 +108,13 @@ replace = '![Version](https://img.shields.io/badge/version-{new_version}-blue)'
107
108
 
108
109
  [[tool.bumpversion.files]]
109
110
  filename = "docs/source/index.rst"
110
- search = 'Documentation for version: {current_version}.'
111
- replace = 'Documentation for version: {new_version}.'
111
+ search = '.. |Github| image:: https://img.shields.io/badge/release-{current_version}-yellow?logo=github'
112
+ replace = '.. |Github| image:: https://img.shields.io/badge/release-{new_version}-yellow?logo=github'
113
+
114
+ [[tool.bumpversion.files]]
115
+ filename = "docs/source/index.rst"
116
+ search = ' :target: https://github.com/jenreh/runic/tree/v{current_version}'
117
+ replace = ' :target: https://github.com/jenreh/runic/tree/v{new_version}'
112
118
 
113
119
  [[tool.bumpversion.files]]
114
120
  filename = "docs/source/conf.py"
@@ -243,6 +249,9 @@ known-local-folder = ["sonos"]
243
249
  [tool.pytest.ini_options]
244
250
  cache_dir = ".cache/pytest"
245
251
  testpaths = ["tests"]
252
+ # Evals (tests/evals) are excluded from the default suite; run explicitly with
253
+ # `uv run pytest tests/evals`.
254
+ addopts = "--ignore=tests/evals"
246
255
  asyncio_mode = "auto"
247
256
  markers = [
248
257
  "integration: requires a live graph backend (embedded FalkorDB via redislite by default; other backends via RUNIC_TEST_BACKENDS env var + docker-compose.test.yml)",
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Any, Protocol, runtime_checkable
4
4
 
5
- from runic.migrate.introspect import LiveSchema
5
+ from runic.migrate.introspect import LiveSchema, SchemaSnapshot
6
6
 
7
7
 
8
8
  @runtime_checkable
@@ -37,6 +37,7 @@ class GraphAdapter(Protocol):
37
37
 
38
38
  # Schema introspection
39
39
  def read_live_schema(self) -> LiveSchema: ...
40
+ def introspect_schema(self) -> SchemaSnapshot: ...
40
41
 
41
42
  # Schema DDL
42
43
  def create_range_index(
@@ -74,6 +75,7 @@ class GraphAdapter(Protocol):
74
75
  def delete_graph(self) -> None: ...
75
76
 
76
77
  # Snapshots
78
+ def supports_snapshots(self) -> bool: ...
77
79
  def snapshot(self, snap_name: str) -> None: ...
78
80
  def restore_snapshot(self, snap_name: str) -> None: ...
79
81
  def snapshot_exists(self, snap_name: str) -> bool: ...
@@ -11,7 +11,7 @@ import logging
11
11
  from abc import ABC, abstractmethod
12
12
  from typing import Any, Protocol, runtime_checkable
13
13
 
14
- from runic.migrate.introspect import LiveSchema
14
+ from runic.migrate.introspect import LiveSchema, SchemaSnapshot
15
15
  from runic.orm.schema.index_manager import IndexSpec
16
16
 
17
17
  log = logging.getLogger(__name__)
@@ -124,6 +124,19 @@ class GraphAdapterBase(ABC):
124
124
  """Execute a read-only Cypher query and return the result."""
125
125
  ...
126
126
 
127
+ # ------------------------------------------------------------------
128
+ # DDL execution
129
+ # ------------------------------------------------------------------
130
+
131
+ def _execute_ddl(self, cypher: str) -> Any:
132
+ """Log and execute a single DDL statement, propagating any error.
133
+
134
+ Shared by the Bolt-based adapters so a failed schema operation aborts
135
+ the migration rather than leaving the schema silently incomplete.
136
+ """
137
+ log.info("%s DDL: %s", self._backend_name, cypher)
138
+ return self.run_query(cypher)
139
+
127
140
  # ------------------------------------------------------------------
128
141
  # Version tracking
129
142
  # ------------------------------------------------------------------
@@ -154,6 +167,17 @@ class GraphAdapterBase(ABC):
154
167
  constraints=[],
155
168
  )
156
169
 
170
+ def introspect_schema(self) -> SchemaSnapshot:
171
+ """Snapshot the live schema for baseline / autogenerate.
172
+
173
+ Defaults to raising; backends with full index/constraint introspection
174
+ (currently FalkorDB) override this.
175
+ """
176
+ raise NotImplementedError(
177
+ f"{self._backend_name} does not support schema introspection for "
178
+ "baseline/autogenerate."
179
+ )
180
+
157
181
  def get_existing_specs(self) -> set[IndexSpec]:
158
182
  return set()
159
183
 
@@ -169,6 +193,14 @@ class GraphAdapterBase(ABC):
169
193
  )
170
194
  self.run_query("MATCH (n) DETACH DELETE n")
171
195
 
196
+ def supports_snapshots(self) -> bool:
197
+ """Whether this adapter can take and restore pre-migration snapshots.
198
+
199
+ Defaults to ``False``; backends with native graph-copy support (e.g.
200
+ FalkorDB) override this to ``True``.
201
+ """
202
+ return False
203
+
172
204
  def snapshot(self, snap_name: str) -> None:
173
205
  raise NotImplementedError(
174
206
  f"{self._backend_name} snapshots are not supported via runic migrate."
@@ -84,44 +84,20 @@ class ArcadeDBAdapter(GraphAdapterBase, GraphAdapter):
84
84
  # ------------------------------------------------------------------
85
85
 
86
86
  def create_vertex_type(self, label: str) -> None:
87
- cypher = f"CREATE VERTEX TYPE `{label}` IF NOT EXISTS"
88
- log.info("ArcadeDB DDL: %s", cypher)
89
- try:
90
- self.run_query(cypher)
91
- except Exception as exc:
92
- log.warning("ArcadeDB create_vertex_type failed for %s: %s", label, exc)
87
+ self._execute_ddl(f"CREATE VERTEX TYPE `{label}` IF NOT EXISTS")
93
88
 
94
89
  def create_edge_type(self, type_name: str) -> None:
95
- cypher = f"CREATE EDGE TYPE `{type_name}` IF NOT EXISTS"
96
- log.info("ArcadeDB DDL: %s", cypher)
97
- try:
98
- self.run_query(cypher)
99
- except Exception as exc:
100
- log.warning("ArcadeDB create_edge_type failed for %s: %s", type_name, exc)
90
+ self._execute_ddl(f"CREATE EDGE TYPE `{type_name}` IF NOT EXISTS")
101
91
 
102
92
  # ------------------------------------------------------------------
103
93
  # DDL — indexes
104
94
  # ------------------------------------------------------------------
105
95
 
106
96
  def create_range_index(self, label: str, prop: str, *, rel: bool = False) -> None: # noqa: ARG002
107
- cypher = f"CREATE INDEX ON `{label}` ({prop})"
108
- log.info("ArcadeDB: %s", cypher)
109
- try:
110
- self.run_query(cypher)
111
- except Exception as exc:
112
- log.warning(
113
- "ArcadeDB create_range_index failed for %s.%s: %s", label, prop, exc
114
- )
97
+ self._execute_ddl(f"CREATE INDEX ON `{label}` ({prop})")
115
98
 
116
99
  def drop_range_index(self, label: str, prop: str, *, rel: bool = False) -> None: # noqa: ARG002
117
- cypher = f"DROP INDEX ON `{label}` ({prop})"
118
- log.info("ArcadeDB: %s", cypher)
119
- try:
120
- self.run_query(cypher)
121
- except Exception as exc:
122
- log.warning(
123
- "ArcadeDB drop_range_index failed for %s.%s: %s", label, prop, exc
124
- )
100
+ self._execute_ddl(f"DROP INDEX ON `{label}` ({prop})")
125
101
 
126
102
  def get_existing_specs(self) -> set[IndexSpec]:
127
103
  return set()
@@ -134,25 +110,11 @@ class ArcadeDBAdapter(GraphAdapterBase, GraphAdapter):
134
110
  stopwords: list[str] | None = None, # noqa: ARG002
135
111
  ) -> None:
136
112
  props_str = ", ".join(props)
137
- cypher = f"CREATE FULLTEXT INDEX ON `{label}` ({props_str})"
138
- log.info("ArcadeDB DDL: %s", cypher)
139
- try:
140
- self.run_query(cypher)
141
- except Exception as exc:
142
- log.warning(
143
- "ArcadeDB create_fulltext_index failed for %s %s: %s", label, props, exc
144
- )
113
+ self._execute_ddl(f"CREATE FULLTEXT INDEX ON `{label}` ({props_str})")
145
114
 
146
115
  def drop_fulltext_index(self, label: str, *props: str) -> None:
147
116
  props_str = ", ".join(props)
148
- cypher = f"DROP INDEX ON `{label}` ({props_str})"
149
- log.info("ArcadeDB DDL: %s", cypher)
150
- try:
151
- self.run_query(cypher)
152
- except Exception as exc:
153
- log.warning(
154
- "ArcadeDB drop_fulltext_index failed for %s %s: %s", label, props, exc
155
- )
117
+ self._execute_ddl(f"DROP INDEX ON `{label}` ({props_str})")
156
118
 
157
119
  def create_vector_index(
158
120
  self,
@@ -189,14 +151,7 @@ class ArcadeDBAdapter(GraphAdapterBase, GraphAdapter):
189
151
  ) -> None:
190
152
  if kind == "UNIQUE" and entity == "NODE" and len(props) == 1:
191
153
  prop = props[0]
192
- cypher = f"CREATE INDEX ON `{label}` ({prop}) UNIQUE"
193
- log.info("ArcadeDB DDL: %s", cypher)
194
- try:
195
- self.run_query(cypher)
196
- except Exception as exc:
197
- log.warning(
198
- "ArcadeDB create_constraint failed for %s.%s: %s", label, prop, exc
199
- )
154
+ self._execute_ddl(f"CREATE INDEX ON `{label}` ({prop}) UNIQUE")
200
155
  else:
201
156
  log.warning(
202
157
  "ArcadeDB create_constraint: unsupported kind=%s entity=%s label=%s props=%s",
@@ -211,14 +166,7 @@ class ArcadeDBAdapter(GraphAdapterBase, GraphAdapter):
211
166
  ) -> None:
212
167
  if kind == "UNIQUE" and entity == "NODE" and len(props) == 1:
213
168
  prop = props[0]
214
- cypher = f"DROP INDEX ON `{label}` ({prop}) UNIQUE"
215
- log.info("ArcadeDB DDL: %s", cypher)
216
- try:
217
- self.run_query(cypher)
218
- except Exception as exc:
219
- log.warning(
220
- "ArcadeDB drop_constraint failed for %s.%s: %s", label, prop, exc
221
- )
169
+ self._execute_ddl(f"DROP INDEX ON `{label}` ({prop}) UNIQUE")
222
170
  else:
223
171
  log.warning(
224
172
  "ArcadeDB drop_constraint: unsupported kind=%s entity=%s label=%s props=%s",
@@ -160,6 +160,9 @@ class FalkorDBAdapter(GraphAdapter):
160
160
  def read_live_schema(self) -> LiveSchema:
161
161
  return introspect.read_live_schema(self._graph)
162
162
 
163
+ def introspect_schema(self) -> introspect.SchemaSnapshot:
164
+ return introspect.introspect_graph(self._graph)
165
+
163
166
  def get_existing_specs(self) -> set[IndexSpec]:
164
167
  try:
165
168
  schema = self.read_live_schema()
@@ -328,6 +331,9 @@ class FalkorDBAdapter(GraphAdapter):
328
331
  # Snapshots
329
332
  # ------------------------------------------------------------------
330
333
 
334
+ def supports_snapshots(self) -> bool:
335
+ return True
336
+
331
337
  def snapshot(self, snap_name: str) -> None:
332
338
  # GRAPH.COPY fails on an empty key; initialize graph if it doesn't exist yet
333
339
  if self._graph.name not in self._db.list_graphs():
@@ -95,24 +95,10 @@ class MemgraphAdapter(GraphAdapterBase, GraphAdapter):
95
95
  # ------------------------------------------------------------------
96
96
 
97
97
  def create_range_index(self, label: str, prop: str, *, rel: bool = False) -> None: # noqa: ARG002
98
- cypher = f"CREATE INDEX ON :{label}({prop})"
99
- log.info("Memgraph DDL: %s", cypher)
100
- try:
101
- self.run_query(cypher)
102
- except Exception as exc:
103
- log.warning(
104
- "Memgraph create_range_index failed for %s.%s: %s", label, prop, exc
105
- )
98
+ self._execute_ddl(f"CREATE INDEX ON :{label}({prop})")
106
99
 
107
100
  def drop_range_index(self, label: str, prop: str, *, rel: bool = False) -> None: # noqa: ARG002
108
- cypher = f"DROP INDEX ON :{label}({prop})"
109
- log.info("Memgraph DDL: %s", cypher)
110
- try:
111
- self.run_query(cypher)
112
- except Exception as exc:
113
- log.warning(
114
- "Memgraph drop_range_index failed for %s.%s: %s", label, prop, exc
115
- )
101
+ self._execute_ddl(f"DROP INDEX ON :{label}({prop})")
116
102
 
117
103
  def create_fulltext_index(
118
104
  self,
@@ -122,8 +108,6 @@ class MemgraphAdapter(GraphAdapterBase, GraphAdapter):
122
108
  stopwords: list[str] | None = None, # noqa: ARG002
123
109
  ) -> None:
124
110
  # Memgraph TEXT INDEX is whole-label; index name = label (matches MemgraphDialect)
125
- cypher = f"CREATE TEXT INDEX {label} ON :{label}"
126
- log.info("Memgraph DDL: %s", cypher)
127
111
  if len(props) > 1:
128
112
  log.warning(
129
113
  "Memgraph text indexes cover the full label — "
@@ -131,18 +115,10 @@ class MemgraphAdapter(GraphAdapterBase, GraphAdapter):
131
115
  props,
132
116
  label,
133
117
  )
134
- try:
135
- self.run_query(cypher)
136
- except Exception as exc:
137
- log.warning("Memgraph create_fulltext_index failed for %s: %s", label, exc)
118
+ self._execute_ddl(f"CREATE TEXT INDEX {label} ON :{label}")
138
119
 
139
120
  def drop_fulltext_index(self, label: str, *props: str) -> None: # noqa: ARG002
140
- cypher = f"DROP TEXT INDEX {label}"
141
- log.info("Memgraph DDL: %s", cypher)
142
- try:
143
- self.run_query(cypher)
144
- except Exception as exc:
145
- log.warning("Memgraph drop_fulltext_index failed for %s: %s", label, exc)
121
+ self._execute_ddl(f"DROP TEXT INDEX {label}")
146
122
 
147
123
  def create_vector_index(
148
124
  self,
@@ -168,23 +144,10 @@ class MemgraphAdapter(GraphAdapterBase, GraphAdapter):
168
144
  f'{{"dimension": {dimension}, "capacity": 1000, "metric": "{similarity}", '
169
145
  f'"resize_coefficient": 2, "m": {m}, "ef_construction": {ef_construction}}}'
170
146
  )
171
- log.info("Memgraph DDL: %s", cypher)
172
- try:
173
- self.run_query(cypher)
174
- except Exception as exc:
175
- log.warning(
176
- "Memgraph create_vector_index failed for %s.%s: %s", label, prop, exc
177
- )
147
+ self._execute_ddl(cypher)
178
148
 
179
149
  def drop_vector_index(self, label: str, prop: str) -> None:
180
- cypher = f"DROP VECTOR INDEX {label}_{prop}"
181
- log.info("Memgraph DDL: %s", cypher)
182
- try:
183
- self.run_query(cypher)
184
- except Exception as exc:
185
- log.warning(
186
- "Memgraph drop_vector_index failed for %s.%s: %s", label, prop, exc
187
- )
150
+ self._execute_ddl(f"DROP VECTOR INDEX {label}_{prop}")
188
151
 
189
152
  # ------------------------------------------------------------------
190
153
  # DDL — constraints
@@ -195,14 +158,9 @@ class MemgraphAdapter(GraphAdapterBase, GraphAdapter):
195
158
  ) -> None:
196
159
  if kind == "UNIQUE" and entity == "NODE" and len(props) == 1:
197
160
  prop = props[0]
198
- cypher = f"CREATE CONSTRAINT ON (n:{label}) ASSERT n.{prop} IS UNIQUE"
199
- log.info("Memgraph DDL: %s", cypher)
200
- try:
201
- self.run_query(cypher)
202
- except Exception as exc:
203
- log.warning(
204
- "Memgraph create_constraint failed for %s.%s: %s", label, prop, exc
205
- )
161
+ self._execute_ddl(
162
+ f"CREATE CONSTRAINT ON (n:{label}) ASSERT n.{prop} IS UNIQUE"
163
+ )
206
164
  else:
207
165
  log.warning(
208
166
  "Memgraph create_constraint: unsupported kind=%s entity=%s label=%s props=%s",
@@ -217,14 +175,9 @@ class MemgraphAdapter(GraphAdapterBase, GraphAdapter):
217
175
  ) -> None:
218
176
  if kind == "UNIQUE" and entity == "NODE" and len(props) == 1:
219
177
  prop = props[0]
220
- cypher = f"DROP CONSTRAINT ON (n:{label}) ASSERT n.{prop} IS UNIQUE"
221
- log.info("Memgraph DDL: %s", cypher)
222
- try:
223
- self.run_query(cypher)
224
- except Exception as exc:
225
- log.warning(
226
- "Memgraph drop_constraint failed for %s.%s: %s", label, prop, exc
227
- )
178
+ self._execute_ddl(
179
+ f"DROP CONSTRAINT ON (n:{label}) ASSERT n.{prop} IS UNIQUE"
180
+ )
228
181
  else:
229
182
  log.warning(
230
183
  "Memgraph drop_constraint: unsupported kind=%s entity=%s label=%s props=%s",
@@ -97,21 +97,10 @@ class Neo4jAdapter(GraphAdapterBase, GraphAdapter):
97
97
  def create_range_index(self, label: str, prop: str, *, rel: bool = False) -> None:
98
98
  entity = f"()-[n:{label}]->()" if rel else f"(n:{label})"
99
99
  cypher = f"CREATE INDEX {label}_{prop} IF NOT EXISTS FOR {entity} ON (n.{prop})"
100
- log.info("Neo4j DDL: %s", cypher)
101
- try:
102
- self.run_query(cypher)
103
- except Exception as exc:
104
- log.warning(
105
- "Neo4j create_range_index failed for %s.%s: %s", label, prop, exc
106
- )
100
+ self._execute_ddl(cypher)
107
101
 
108
102
  def drop_range_index(self, label: str, prop: str, *, rel: bool = False) -> None: # noqa: ARG002
109
- cypher = f"DROP INDEX {label}_{prop} IF EXISTS"
110
- log.info("Neo4j DDL: %s", cypher)
111
- try:
112
- self.run_query(cypher)
113
- except Exception as exc:
114
- log.warning("Neo4j drop_range_index failed for %s.%s: %s", label, prop, exc)
103
+ self._execute_ddl(f"DROP INDEX {label}_{prop} IF EXISTS")
115
104
 
116
105
  def create_fulltext_index(
117
106
  self,
@@ -125,21 +114,10 @@ class Neo4jAdapter(GraphAdapterBase, GraphAdapter):
125
114
  f"CREATE FULLTEXT INDEX {label} IF NOT EXISTS "
126
115
  f"FOR (n:{label}) ON EACH [{prop_list}]"
127
116
  )
128
- log.info("Neo4j DDL: %s", cypher)
129
- try:
130
- self.run_query(cypher)
131
- except Exception as exc:
132
- log.warning(
133
- "Neo4j create_fulltext_index failed for %s %s: %s", label, props, exc
134
- )
117
+ self._execute_ddl(cypher)
135
118
 
136
119
  def drop_fulltext_index(self, label: str, *props: str) -> None: # noqa: ARG002
137
- cypher = f"DROP INDEX {label} IF EXISTS"
138
- log.info("Neo4j DDL: %s", cypher)
139
- try:
140
- self.run_query(cypher)
141
- except Exception as exc:
142
- log.warning("Neo4j drop_fulltext_index failed for %s: %s", label, exc)
120
+ self._execute_ddl(f"DROP INDEX {label} IF EXISTS")
143
121
 
144
122
  def create_vector_index(
145
123
  self,
@@ -165,23 +143,10 @@ class Neo4jAdapter(GraphAdapterBase, GraphAdapter):
165
143
  f"OPTIONS {{indexConfig: {{`vector.dimensions`: {dimension}, "
166
144
  f"`vector.similarity_function`: '{similarity}'}}}}"
167
145
  )
168
- log.info("Neo4j DDL: %s", cypher)
169
- try:
170
- self.run_query(cypher)
171
- except Exception as exc:
172
- log.warning(
173
- "Neo4j create_vector_index failed for %s.%s: %s", label, prop, exc
174
- )
146
+ self._execute_ddl(cypher)
175
147
 
176
148
  def drop_vector_index(self, label: str, prop: str) -> None:
177
- cypher = f"DROP INDEX {label}_{prop} IF EXISTS"
178
- log.info("Neo4j DDL: %s", cypher)
179
- try:
180
- self.run_query(cypher)
181
- except Exception as exc:
182
- log.warning(
183
- "Neo4j drop_vector_index failed for %s.%s: %s", label, prop, exc
184
- )
149
+ self._execute_ddl(f"DROP INDEX {label}_{prop} IF EXISTS")
185
150
 
186
151
  # ------------------------------------------------------------------
187
152
  # DDL — constraints
@@ -193,17 +158,10 @@ class Neo4jAdapter(GraphAdapterBase, GraphAdapter):
193
158
  if kind == "UNIQUE" and entity == "NODE" and len(props) == 1:
194
159
  prop = props[0]
195
160
  name = f"{label}_{prop}_unique"
196
- cypher = (
161
+ self._execute_ddl(
197
162
  f"CREATE CONSTRAINT {name} IF NOT EXISTS "
198
163
  f"FOR (n:{label}) REQUIRE n.{prop} IS UNIQUE"
199
164
  )
200
- log.info("Neo4j DDL: %s", cypher)
201
- try:
202
- self.run_query(cypher)
203
- except Exception as exc:
204
- log.warning(
205
- "Neo4j create_constraint failed for %s.%s: %s", label, prop, exc
206
- )
207
165
  else:
208
166
  log.warning(
209
167
  "Neo4j create_constraint: unsupported kind=%s entity=%s label=%s props=%s",
@@ -219,14 +177,7 @@ class Neo4jAdapter(GraphAdapterBase, GraphAdapter):
219
177
  if kind == "UNIQUE" and entity == "NODE" and len(props) == 1:
220
178
  prop = props[0]
221
179
  name = f"{label}_{prop}_unique"
222
- cypher = f"DROP CONSTRAINT {name} IF EXISTS"
223
- log.info("Neo4j DDL: %s", cypher)
224
- try:
225
- self.run_query(cypher)
226
- except Exception as exc:
227
- log.warning(
228
- "Neo4j drop_constraint failed for %s.%s: %s", label, prop, exc
229
- )
180
+ self._execute_ddl(f"DROP CONSTRAINT {name} IF EXISTS")
230
181
  else:
231
182
  log.warning(
232
183
  "Neo4j drop_constraint: unsupported kind=%s entity=%s label=%s props=%s",