runic-py 0.3.0__tar.gz → 0.3.3__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.3}/.gitignore +11 -4
  2. {runic_py-0.3.0 → runic_py-0.3.3}/PKG-INFO +19 -15
  3. {runic_py-0.3.0 → runic_py-0.3.3}/README.md +6 -5
  4. {runic_py-0.3.0 → runic_py-0.3.3}/pyproject.toml +36 -24
  5. {runic_py-0.3.0 → runic_py-0.3.3}/runic/migrate/adapters/__init__.py +3 -1
  6. {runic_py-0.3.0 → runic_py-0.3.3}/runic/migrate/adapters/_base.py +33 -1
  7. {runic_py-0.3.0 → runic_py-0.3.3}/runic/migrate/adapters/arcadedb.py +8 -60
  8. {runic_py-0.3.0 → runic_py-0.3.3}/runic/migrate/adapters/falkordb.py +6 -0
  9. {runic_py-0.3.0 → runic_py-0.3.3}/runic/migrate/adapters/memgraph.py +12 -59
  10. {runic_py-0.3.0 → runic_py-0.3.3}/runic/migrate/adapters/neo4j.py +8 -57
  11. {runic_py-0.3.0 → runic_py-0.3.3}/runic/migrate/cli.py +12 -14
  12. {runic_py-0.3.0 → runic_py-0.3.3}/runic/migrate/context.py +37 -4
  13. {runic_py-0.3.0 → runic_py-0.3.3}/runic/migrate/operations.py +21 -25
  14. {runic_py-0.3.0 → runic_py-0.3.3}/runic/migrate/schema.py +61 -72
  15. {runic_py-0.3.0 → runic_py-0.3.3}/runic/migrate/script.py +3 -3
  16. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/core/descriptors.py +9 -29
  17. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/core/models.py +11 -22
  18. runic_py-0.3.3/runic/orm/query/_compiler.py +210 -0
  19. runic_py-0.3.3/runic/orm/query/_decoder.py +115 -0
  20. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/query/builder.py +4 -252
  21. runic_py-0.3.3/runic/orm/repository/_base.py +49 -0
  22. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/repository/async_repository.py +6 -33
  23. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/repository/repository.py +5 -35
  24. runic_py-0.3.3/runic/orm/session/_base.py +213 -0
  25. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/session/async_session.py +15 -161
  26. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/session/session.py +17 -182
  27. {runic_py-0.3.0 → runic_py-0.3.3}/LICENSE.md +0 -0
  28. {runic_py-0.3.0 → runic_py-0.3.3}/runic/__init__.py +0 -0
  29. {runic_py-0.3.0 → runic_py-0.3.3}/runic/migrate/__init__.py +0 -0
  30. {runic_py-0.3.0 → runic_py-0.3.3}/runic/migrate/adapters/age.py +0 -0
  31. {runic_py-0.3.0 → runic_py-0.3.3}/runic/migrate/autogen.py +0 -0
  32. {runic_py-0.3.0 → runic_py-0.3.3}/runic/migrate/checksum.py +0 -0
  33. {runic_py-0.3.0 → runic_py-0.3.3}/runic/migrate/config.py +0 -0
  34. {runic_py-0.3.0 → runic_py-0.3.3}/runic/migrate/exceptions.py +0 -0
  35. {runic_py-0.3.0 → runic_py-0.3.3}/runic/migrate/introspect.py +0 -0
  36. {runic_py-0.3.0 → runic_py-0.3.3}/runic/migrate/manifest.py +0 -0
  37. {runic_py-0.3.0 → runic_py-0.3.3}/runic/migrate/service.py +0 -0
  38. {runic_py-0.3.0 → runic_py-0.3.3}/runic/migrate/templates/env.py.mako +0 -0
  39. {runic_py-0.3.0 → runic_py-0.3.3}/runic/migrate/templates/script.py.mako +0 -0
  40. {runic_py-0.3.0 → runic_py-0.3.3}/runic/migrate/testing.py +0 -0
  41. {runic_py-0.3.0 → runic_py-0.3.3}/runic/migrate/version.py +0 -0
  42. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/__init__.py +0 -0
  43. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/core/__init__.py +0 -0
  44. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/core/metadata.py +0 -0
  45. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/core/types.py +0 -0
  46. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/driver/__init__.py +0 -0
  47. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/driver/age.py +0 -0
  48. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/driver/arcadedb.py +0 -0
  49. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/driver/bolt.py +0 -0
  50. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/driver/factory.py +0 -0
  51. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/driver/falkordb.py +0 -0
  52. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/driver/memgraph.py +0 -0
  53. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/driver/neo4j.py +0 -0
  54. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/exceptions.py +0 -0
  55. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/mapper/__init__.py +0 -0
  56. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/mapper/mapper.py +0 -0
  57. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/mapper/relationship_loader.py +0 -0
  58. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/mapper/relationship_writer.py +0 -0
  59. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/operations.py +0 -0
  60. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/query/__init__.py +0 -0
  61. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/query/expressions.py +0 -0
  62. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/query/specialised.py +0 -0
  63. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/query/traversal.py +0 -0
  64. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/repository/__init__.py +0 -0
  65. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/repository/cypher.py +0 -0
  66. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/repository/protocol.py +0 -0
  67. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/schema/__init__.py +0 -0
  68. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/schema/index_manager.py +0 -0
  69. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/session/__init__.py +0 -0
  70. {runic_py-0.3.0 → runic_py-0.3.3}/runic/orm/session/connection_pool.py +0 -0
@@ -1,8 +1,12 @@
1
1
  __pycache__/
2
2
  __pypackages__/
3
3
  .cache
4
+ .claude/ralph-loop.local.md
4
5
  .coverage
5
6
  .coverage.*
7
+ .deepeval_telemetry.txt
8
+ .deepeval-cache.json
9
+ .deepeval/
6
10
  .dmypy.json
7
11
  .DS_Store
8
12
  .eggs/
@@ -13,6 +17,8 @@ __pypackages__/
13
17
  .idea/
14
18
  .installed.cfg
15
19
  .ipynb_checkpoints
20
+ .latest_run_full.json
21
+ .latest_test_run.json
16
22
  .mypy_cache/
17
23
  .nox/
18
24
  .pdm.toml
@@ -23,10 +29,12 @@ __pypackages__/
23
29
  .python_packages
24
30
  .pytype/
25
31
  .ropeproject
32
+ .runic
26
33
  .scrapy
27
34
  .spyderproject
28
35
  .spyproject
29
36
  .states
37
+ .task/
30
38
  .tox/
31
39
  .venv
32
40
  .venv.mac
@@ -57,11 +65,13 @@ __pypackages__/
57
65
  *$py.class
58
66
  /site
59
67
  /vectorstore/
68
+ # DeepEval run caches (tests/evals suite runs from an isolated venv)
60
69
  aila-storage/
61
70
  assets/external/
62
71
  build/
63
72
  celerybeat-schedule
64
73
  celerybeat.pid
74
+ CLAUDE.local.md
65
75
  configuration/config.abaz009.yaml
66
76
  configuration/config.bubb001.yaml
67
77
  configuration/config.stie104.yaml
@@ -98,6 +108,7 @@ pip-delete-this-directory.txt
98
108
  pip-log.txt
99
109
  Pipfile
100
110
  profile_default/
111
+ runic_test/
101
112
  sdist/
102
113
  share/python-wheels/
103
114
  sketchpad/
@@ -112,7 +123,3 @@ var/
112
123
  venv.bak/
113
124
  venv/
114
125
  wheels/
115
- .task/
116
- .runic
117
- runic_test/
118
- .claude/ralph-loop.local.md
@@ -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.3
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,28 @@ 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
+ Requires-Dist: shellingham<2.0.0,>=1.5.0
18
19
  Requires-Dist: typer<1.0.0,>=0.26.0
19
20
  Provides-Extra: age
20
- Requires-Dist: psycopg[binary]>=3.3.4; extra == 'age'
21
+ Requires-Dist: psycopg<4.0.0,>=3.3.4; extra == 'age'
22
+ Provides-Extra: age-binary
23
+ Requires-Dist: psycopg[binary]<4.0.0,>=3.3.4; extra == 'age-binary'
21
24
  Provides-Extra: all
22
25
  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'
26
+ Requires-Dist: neo4j<7.0.0,>=6.2.0; extra == 'all'
27
+ Requires-Dist: psycopg[binary]<4.0.0,>=3.3.4; extra == 'all'
25
28
  Provides-Extra: arcadedb
26
- Requires-Dist: neo4j>=6.2.0; extra == 'arcadedb'
29
+ Requires-Dist: neo4j<7.0.0,>=6.2.0; extra == 'arcadedb'
27
30
  Provides-Extra: falkordb
28
31
  Requires-Dist: falkordb<2.0.0,>=1.6.0; extra == 'falkordb'
29
32
  Provides-Extra: memgraph
30
- Requires-Dist: neo4j>=6.2.0; extra == 'memgraph'
33
+ Requires-Dist: neo4j<7.0.0,>=6.2.0; extra == 'memgraph'
31
34
  Provides-Extra: neo4j
32
- Requires-Dist: neo4j>=6.2.0; extra == 'neo4j'
35
+ Requires-Dist: neo4j<7.0.0,>=6.2.0; extra == 'neo4j'
33
36
  Description-Content-Type: text/markdown
34
37
 
35
38
  <div align="center">
@@ -39,7 +42,8 @@ Description-Content-Type: text/markdown
39
42
 
40
43
  **Graph schema migrations and ORM for Cypher-based graph databases.**
41
44
 
42
- ![Version](https://img.shields.io/badge/version-0.3.0-blue)
45
+ ![Version](https://img.shields.io/badge/version-0.3.3-blue)
46
+ [![PyPI](https://img.shields.io/pypi/v/runic-py.svg)](https://pypi.org/project/runic-py/)
43
47
  [![Python](https://img.shields.io/badge/python-3.14%2B-orange)](https://www.python.org)
44
48
  [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE.md)
45
49
 
@@ -294,9 +298,9 @@ if min_age > 0:
294
298
  stmt = stmt.where(User.age >= min_age)
295
299
 
296
300
  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
301
+ users: list[User] = session.scalars(stmt) # list[User]
302
+ user: User | None = session.scalar(stmt) # User | None
303
+ n: int = session.count(stmt) # int
300
304
  ```
301
305
 
302
306
  The same `stmt` is reusable — pass it to multiple sessions or execute it
@@ -354,4 +358,4 @@ class Article(Node, labels=["Article"]):
354
358
 
355
359
  ## Documentation
356
360
 
357
- Full conceptual overview, async usage, advanced CLI flags, and API reference at the complete [Runic Documentation](https://runic-py.readthedocs.io/latest/).
361
+ Full conceptual overview, async usage, advanced CLI flags, and API reference at the complete [Runic Documentation](https://runic-py.readthedocs.io/en/latest).
@@ -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.3-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
@@ -320,4 +321,4 @@ class Article(Node, labels=["Article"]):
320
321
 
321
322
  ## Documentation
322
323
 
323
- Full conceptual overview, async usage, advanced CLI flags, and API reference at the complete [Runic Documentation](https://runic-py.readthedocs.io/latest/).
324
+ Full conceptual overview, async usage, advanced CLI flags, and API reference at the complete [Runic Documentation](https://runic-py.readthedocs.io/en/latest).
@@ -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.3"
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,49 @@ 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
+ "shellingham>=1.5.0,<2.0.0",
22
23
  ]
23
24
 
24
25
  [project.optional-dependencies]
25
26
  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"]
27
+ neo4j = ["neo4j>=6.2.0,<7.0.0"]
28
+ memgraph = ["neo4j>=6.2.0,<7.0.0"]
29
+ arcadedb = ["neo4j>=6.2.0,<7.0.0"]
30
+ age = ["psycopg>=3.3.4,<4.0.0"]
31
+ age-binary = ["psycopg[binary]>=3.3.4,<4.0.0"]
30
32
  all = [
31
33
  "falkordb>=1.6.0,<2.0.0",
32
- "neo4j>=6.2.0",
33
- "psycopg[binary]>=3.3.4",
34
+ "neo4j>=6.2.0,<7.0.0",
35
+ "psycopg[binary]>=3.3.4,<4.0.0",
34
36
  ]
35
-
36
37
  [dependency-groups]
37
38
  dev = [
38
39
  "bump-my-version==1.3.0",
40
+ "deepeval==4.0.5",
39
41
  "detect-secrets==1.5.0",
42
+ "falkordb>=1.6.1",
43
+ "falkordblite==0.10.0",
44
+ "furo==2025.12.19",
45
+ "neo4j==6.2.0",
40
46
  "pre-commit==4.6.0",
41
- "pytest==9.0.3",
47
+ "psycopg[binary]==3.3.4",
42
48
  "pytest-asyncio==1.4.0",
43
49
  "pytest-cov==7.1.0",
44
50
  "pytest-httpserver==1.1.5",
51
+ "pytest==9.0.3",
52
+ "python-dotenv==1.2.2",
45
53
  "ruff==0.15.16",
46
- "falkordblite==0.10.0",
47
- "sphinx==9.1.0",
48
- "furo==2025.12.19",
49
54
  "sphinx-copybutton==0.5.2",
50
55
  "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",
56
+ "sphinx==9.1.0",
57
+ "ty==0.0.44",
56
58
  ]
57
59
 
58
60
  [project.scripts]
@@ -91,7 +93,7 @@ exclude = [
91
93
 
92
94
  [tool.bumpversion]
93
95
  commit = false
94
- current_version = "0.3.0"
96
+ current_version = "0.3.3"
95
97
  tag = false
96
98
  allow-dirty = false
97
99
 
@@ -107,8 +109,13 @@ replace = '![Version](https://img.shields.io/badge/version-{new_version}-blue)'
107
109
 
108
110
  [[tool.bumpversion.files]]
109
111
  filename = "docs/source/index.rst"
110
- search = 'Documentation for version: {current_version}.'
111
- replace = 'Documentation for version: {new_version}.'
112
+ search = '.. |Github| image:: https://img.shields.io/badge/release-{current_version}-yellow?logo=github'
113
+ replace = '.. |Github| image:: https://img.shields.io/badge/release-{new_version}-yellow?logo=github'
114
+
115
+ [[tool.bumpversion.files]]
116
+ filename = "docs/source/index.rst"
117
+ search = ' :target: https://github.com/jenreh/runic/tree/v{current_version}'
118
+ replace = ' :target: https://github.com/jenreh/runic/tree/v{new_version}'
112
119
 
113
120
  [[tool.bumpversion.files]]
114
121
  filename = "docs/source/conf.py"
@@ -243,6 +250,9 @@ known-local-folder = ["sonos"]
243
250
  [tool.pytest.ini_options]
244
251
  cache_dir = ".cache/pytest"
245
252
  testpaths = ["tests"]
253
+ # Evals (tests/evals) are excluded from the default suite; run explicitly with
254
+ # `uv run pytest tests/evals`.
255
+ addopts = "--ignore=tests/evals"
246
256
  asyncio_mode = "auto"
247
257
  markers = [
248
258
  "integration: requires a live graph backend (embedded FalkorDB via redislite by default; other backends via RUNIC_TEST_BACKENDS env var + docker-compose.test.yml)",
@@ -262,6 +272,8 @@ exclude = [
262
272
  "build",
263
273
  "dist",
264
274
  "examples",
275
+ "skill",
276
+ "tests/evals",
265
277
  ]
266
278
 
267
279
  [tool.ty.rules]
@@ -273,7 +285,7 @@ cache_dir = ".cache/mypy"
273
285
 
274
286
  [tool.coverage.run]
275
287
  branch = true
276
- source = ["app", "runic"]
288
+ source = ["runic"]
277
289
  omit = ["runic/adapters/*"]
278
290
 
279
291
  [tool.coverage.report]
@@ -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",