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.
- {runic_py-0.3.0 → runic_py-0.3.2}/.gitignore +7 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/PKG-INFO +17 -14
- {runic_py-0.3.0 → runic_py-0.3.2}/README.md +5 -4
- {runic_py-0.3.0 → runic_py-0.3.2}/pyproject.toml +32 -23
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/adapters/__init__.py +3 -1
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/adapters/_base.py +33 -1
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/adapters/arcadedb.py +8 -60
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/adapters/falkordb.py +6 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/adapters/memgraph.py +12 -59
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/adapters/neo4j.py +8 -57
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/cli.py +12 -14
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/context.py +37 -4
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/operations.py +21 -25
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/schema.py +61 -72
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/script.py +3 -3
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/core/descriptors.py +9 -29
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/core/models.py +11 -22
- runic_py-0.3.2/runic/orm/query/_compiler.py +210 -0
- runic_py-0.3.2/runic/orm/query/_decoder.py +115 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/query/builder.py +4 -252
- runic_py-0.3.2/runic/orm/repository/_base.py +49 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/repository/async_repository.py +6 -33
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/repository/repository.py +5 -35
- runic_py-0.3.2/runic/orm/session/_base.py +213 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/session/async_session.py +15 -161
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/session/session.py +17 -182
- {runic_py-0.3.0 → runic_py-0.3.2}/LICENSE.md +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/__init__.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/__init__.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/adapters/age.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/autogen.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/checksum.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/config.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/exceptions.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/introspect.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/manifest.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/service.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/templates/env.py.mako +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/templates/script.py.mako +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/testing.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/migrate/version.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/__init__.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/core/__init__.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/core/metadata.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/core/types.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/driver/__init__.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/driver/age.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/driver/arcadedb.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/driver/bolt.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/driver/factory.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/driver/falkordb.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/driver/memgraph.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/driver/neo4j.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/exceptions.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/mapper/__init__.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/mapper/mapper.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/mapper/relationship_loader.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/mapper/relationship_writer.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/operations.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/query/__init__.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/query/expressions.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/query/specialised.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/query/traversal.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/repository/__init__.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/repository/cypher.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/repository/protocol.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/schema/__init__.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/schema/index_manager.py +0 -0
- {runic_py-0.3.0 → runic_py-0.3.2}/runic/orm/session/__init__.py +0 -0
- {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.
|
|
4
|
-
Summary:
|
|
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 ::
|
|
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,>=
|
|
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
|
|
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
|
|
24
|
-
Requires-Dist: psycopg[binary]
|
|
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
|
|
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
|
|
32
|
+
Requires-Dist: neo4j<7.0.0,>=6.2.0; extra == 'memgraph'
|
|
31
33
|
Provides-Extra: neo4j
|
|
32
|
-
Requires-Dist: 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
|
-

|
|
45
|
+
[](https://pypi.org/project/runic-py/)
|
|
43
46
|
[](https://www.python.org)
|
|
44
47
|
[](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]
|
|
298
|
-
user:
|
|
299
|
-
n:
|
|
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
|
-

|
|
9
|
+
[](https://pypi.org/project/runic-py/)
|
|
9
10
|
[](https://www.python.org)
|
|
10
11
|
[](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]
|
|
264
|
-
user:
|
|
265
|
-
n:
|
|
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.
|
|
4
|
-
description = "
|
|
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 ::
|
|
15
|
+
"Topic :: Database",
|
|
16
16
|
]
|
|
17
17
|
|
|
18
18
|
dependencies = [
|
|
19
19
|
"typer>=0.26.0,<1.0.0",
|
|
20
|
-
"rich>=
|
|
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
|
|
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
|
-
"
|
|
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
|
-
"
|
|
52
|
-
"
|
|
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.
|
|
95
|
+
current_version = "0.3.2"
|
|
95
96
|
tag = false
|
|
96
97
|
allow-dirty = false
|
|
97
98
|
|
|
@@ -107,8 +108,13 @@ replace = ''
|
|
|
107
108
|
|
|
108
109
|
[[tool.bumpversion.files]]
|
|
109
110
|
filename = "docs/source/index.rst"
|
|
110
|
-
search = '
|
|
111
|
-
replace = '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|