real-ladybug 0.0.1.dev1__cp311-cp311-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of real-ladybug might be problematic. Click here for more details.
- real_ladybug/__init__.py +83 -0
- real_ladybug/_lbug.cp311-win_amd64.pyd +0 -0
- real_ladybug/_lbug.exp +0 -0
- real_ladybug/_lbug.lib +0 -0
- real_ladybug/async_connection.py +226 -0
- real_ladybug/connection.py +323 -0
- real_ladybug/constants.py +7 -0
- real_ladybug/database.py +307 -0
- real_ladybug/prepared_statement.py +51 -0
- real_ladybug/py.typed +0 -0
- real_ladybug/query_result.py +511 -0
- real_ladybug/torch_geometric_feature_store.py +185 -0
- real_ladybug/torch_geometric_graph_store.py +131 -0
- real_ladybug/torch_geometric_result_converter.py +282 -0
- real_ladybug/types.py +39 -0
- real_ladybug-0.0.1.dev1.dist-info/METADATA +88 -0
- real_ladybug-0.0.1.dev1.dist-info/RECORD +114 -0
- real_ladybug-0.0.1.dev1.dist-info/WHEEL +5 -0
- real_ladybug-0.0.1.dev1.dist-info/licenses/LICENSE +21 -0
- real_ladybug-0.0.1.dev1.dist-info/top_level.txt +3 -0
- real_ladybug-0.0.1.dev1.dist-info/zip-safe +1 -0
- real_ladybug-source/scripts/antlr4/hash.py +2 -0
- real_ladybug-source/scripts/antlr4/keywordhandler.py +47 -0
- real_ladybug-source/scripts/collect-extensions.py +68 -0
- real_ladybug-source/scripts/collect-single-file-header.py +126 -0
- real_ladybug-source/scripts/export-dbs.py +101 -0
- real_ladybug-source/scripts/export-import-test.py +345 -0
- real_ladybug-source/scripts/extension/purge-beta.py +34 -0
- real_ladybug-source/scripts/generate-cpp-docs/collect_files.py +122 -0
- real_ladybug-source/scripts/generate-tinysnb.py +34 -0
- real_ladybug-source/scripts/get-clangd-diagnostics.py +233 -0
- real_ladybug-source/scripts/migrate-lbug-db.py +308 -0
- real_ladybug-source/scripts/multiplatform-test-helper/collect-results.py +71 -0
- real_ladybug-source/scripts/multiplatform-test-helper/notify-discord.py +68 -0
- real_ladybug-source/scripts/pip-package/package_tar.py +90 -0
- real_ladybug-source/scripts/pip-package/setup.py +130 -0
- real_ladybug-source/scripts/run-clang-format.py +408 -0
- real_ladybug-source/scripts/setup-extension-repo.py +67 -0
- real_ladybug-source/scripts/test-simsimd-dispatch.py +45 -0
- real_ladybug-source/scripts/update-nightly-build-version.py +81 -0
- real_ladybug-source/third_party/brotli/scripts/dictionary/step-01-download-rfc.py +16 -0
- real_ladybug-source/third_party/brotli/scripts/dictionary/step-02-rfc-to-bin.py +34 -0
- real_ladybug-source/third_party/brotli/scripts/dictionary/step-03-validate-bin.py +35 -0
- real_ladybug-source/third_party/brotli/scripts/dictionary/step-04-generate-java-literals.py +85 -0
- real_ladybug-source/third_party/pybind11/tools/codespell_ignore_lines_from_errors.py +35 -0
- real_ladybug-source/third_party/pybind11/tools/libsize.py +36 -0
- real_ladybug-source/third_party/pybind11/tools/make_changelog.py +63 -0
- real_ladybug-source/tools/python_api/build/real_ladybug/__init__.py +83 -0
- real_ladybug-source/tools/python_api/build/real_ladybug/async_connection.py +226 -0
- real_ladybug-source/tools/python_api/build/real_ladybug/connection.py +323 -0
- real_ladybug-source/tools/python_api/build/real_ladybug/constants.py +7 -0
- real_ladybug-source/tools/python_api/build/real_ladybug/database.py +307 -0
- real_ladybug-source/tools/python_api/build/real_ladybug/prepared_statement.py +51 -0
- real_ladybug-source/tools/python_api/build/real_ladybug/py.typed +0 -0
- real_ladybug-source/tools/python_api/build/real_ladybug/query_result.py +511 -0
- real_ladybug-source/tools/python_api/build/real_ladybug/torch_geometric_feature_store.py +185 -0
- real_ladybug-source/tools/python_api/build/real_ladybug/torch_geometric_graph_store.py +131 -0
- real_ladybug-source/tools/python_api/build/real_ladybug/torch_geometric_result_converter.py +282 -0
- real_ladybug-source/tools/python_api/build/real_ladybug/types.py +39 -0
- real_ladybug-source/tools/python_api/src_py/__init__.py +83 -0
- real_ladybug-source/tools/python_api/src_py/async_connection.py +226 -0
- real_ladybug-source/tools/python_api/src_py/connection.py +323 -0
- real_ladybug-source/tools/python_api/src_py/constants.py +7 -0
- real_ladybug-source/tools/python_api/src_py/database.py +307 -0
- real_ladybug-source/tools/python_api/src_py/prepared_statement.py +51 -0
- real_ladybug-source/tools/python_api/src_py/py.typed +0 -0
- real_ladybug-source/tools/python_api/src_py/query_result.py +511 -0
- real_ladybug-source/tools/python_api/src_py/torch_geometric_feature_store.py +185 -0
- real_ladybug-source/tools/python_api/src_py/torch_geometric_graph_store.py +131 -0
- real_ladybug-source/tools/python_api/src_py/torch_geometric_result_converter.py +282 -0
- real_ladybug-source/tools/python_api/src_py/types.py +39 -0
- real_ladybug-source/tools/python_api/test/conftest.py +230 -0
- real_ladybug-source/tools/python_api/test/disabled_test_extension.py +73 -0
- real_ladybug-source/tools/python_api/test/ground_truth.py +430 -0
- real_ladybug-source/tools/python_api/test/test_arrow.py +694 -0
- real_ladybug-source/tools/python_api/test/test_async_connection.py +159 -0
- real_ladybug-source/tools/python_api/test/test_blob_parameter.py +145 -0
- real_ladybug-source/tools/python_api/test/test_connection.py +49 -0
- real_ladybug-source/tools/python_api/test/test_database.py +234 -0
- real_ladybug-source/tools/python_api/test/test_datatype.py +372 -0
- real_ladybug-source/tools/python_api/test/test_df.py +564 -0
- real_ladybug-source/tools/python_api/test/test_dict.py +112 -0
- real_ladybug-source/tools/python_api/test/test_exception.py +54 -0
- real_ladybug-source/tools/python_api/test/test_fsm.py +227 -0
- real_ladybug-source/tools/python_api/test/test_get_header.py +49 -0
- real_ladybug-source/tools/python_api/test/test_helper.py +8 -0
- real_ladybug-source/tools/python_api/test/test_issue.py +147 -0
- real_ladybug-source/tools/python_api/test/test_iteration.py +96 -0
- real_ladybug-source/tools/python_api/test/test_networkx.py +437 -0
- real_ladybug-source/tools/python_api/test/test_parameter.py +340 -0
- real_ladybug-source/tools/python_api/test/test_prepared_statement.py +117 -0
- real_ladybug-source/tools/python_api/test/test_query_result.py +54 -0
- real_ladybug-source/tools/python_api/test/test_query_result_close.py +44 -0
- real_ladybug-source/tools/python_api/test/test_scan_pandas.py +676 -0
- real_ladybug-source/tools/python_api/test/test_scan_pandas_pyarrow.py +714 -0
- real_ladybug-source/tools/python_api/test/test_scan_polars.py +165 -0
- real_ladybug-source/tools/python_api/test/test_scan_pyarrow.py +167 -0
- real_ladybug-source/tools/python_api/test/test_timeout.py +11 -0
- real_ladybug-source/tools/python_api/test/test_torch_geometric.py +640 -0
- real_ladybug-source/tools/python_api/test/test_torch_geometric_remote_backend.py +111 -0
- real_ladybug-source/tools/python_api/test/test_udf.py +207 -0
- real_ladybug-source/tools/python_api/test/test_version.py +6 -0
- real_ladybug-source/tools/python_api/test/test_wal.py +80 -0
- real_ladybug-source/tools/python_api/test/type_aliases.py +10 -0
- real_ladybug-source/tools/rust_api/update_version.py +47 -0
- real_ladybug-source/tools/shell/test/conftest.py +218 -0
- real_ladybug-source/tools/shell/test/test_helper.py +60 -0
- real_ladybug-source/tools/shell/test/test_shell_basics.py +325 -0
- real_ladybug-source/tools/shell/test/test_shell_commands.py +656 -0
- real_ladybug-source/tools/shell/test/test_shell_control_edit.py +438 -0
- real_ladybug-source/tools/shell/test/test_shell_control_search.py +468 -0
- real_ladybug-source/tools/shell/test/test_shell_esc_edit.py +232 -0
- real_ladybug-source/tools/shell/test/test_shell_esc_search.py +162 -0
- real_ladybug-source/tools/shell/test/test_shell_flags.py +645 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
import real_ladybug as lb
|
|
5
|
+
import pyarrow as pa
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.asyncio
|
|
10
|
+
async def test_async_prepare_and_execute(async_connection_readonly):
|
|
11
|
+
query = "MATCH (a:person) WHERE a.ID = $1 RETURN a.age;"
|
|
12
|
+
result = await async_connection_readonly.execute(query, {"1": 0})
|
|
13
|
+
assert result.has_next()
|
|
14
|
+
assert result.get_next() == [35]
|
|
15
|
+
assert not result.has_next()
|
|
16
|
+
result.close()
|
|
17
|
+
for i in async_connection_readonly.connections_counter:
|
|
18
|
+
assert i == 0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.mark.asyncio
|
|
22
|
+
async def test_async_prepare_and_execute_concurrent(async_connection_readonly):
|
|
23
|
+
num_queries = 100
|
|
24
|
+
query = "RETURN $1;"
|
|
25
|
+
results = await asyncio.gather(*[async_connection_readonly.execute(query, {"1": i}) for i in range(num_queries)])
|
|
26
|
+
for i, result in enumerate(results):
|
|
27
|
+
assert result.has_next()
|
|
28
|
+
assert result.get_next() == [i]
|
|
29
|
+
assert not result.has_next()
|
|
30
|
+
result.close()
|
|
31
|
+
for i in async_connection_readonly.connections_counter:
|
|
32
|
+
assert i == 0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.asyncio
|
|
36
|
+
async def test_async_query(async_connection_readonly):
|
|
37
|
+
query = "MATCH (a:person) WHERE a.ID = 0 RETURN a.age;"
|
|
38
|
+
result = await async_connection_readonly.execute(query)
|
|
39
|
+
assert result.has_next()
|
|
40
|
+
assert result.get_next() == [35]
|
|
41
|
+
assert not result.has_next()
|
|
42
|
+
result.close()
|
|
43
|
+
|
|
44
|
+
query = "MATCH (a:person) WHERE a.ID = 2 RETURN a.age;"
|
|
45
|
+
result = await async_connection_readonly.execute(query)
|
|
46
|
+
assert result.has_next()
|
|
47
|
+
assert result.get_next() == [30]
|
|
48
|
+
assert not result.has_next()
|
|
49
|
+
result.close()
|
|
50
|
+
for i in async_connection_readonly.connections_counter:
|
|
51
|
+
assert i == 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.mark.asyncio
|
|
55
|
+
async def test_async_scan_df(async_connection_readwrite):
|
|
56
|
+
nodes = pa.Table.from_arrays(
|
|
57
|
+
[
|
|
58
|
+
pa.array([1, 2, 3], type=pa.int32()),
|
|
59
|
+
pa.array(["a", "b", "c"], type=pa.string()),
|
|
60
|
+
pa.array([True, False, None], type=pa.bool_()),
|
|
61
|
+
],
|
|
62
|
+
names=["id", "A", "B"],
|
|
63
|
+
)
|
|
64
|
+
await async_connection_readwrite.execute(
|
|
65
|
+
"CREATE NODE TABLE pyarrowtab(id INT32, A STRING, B BOOL, PRIMARY KEY(id))"
|
|
66
|
+
)
|
|
67
|
+
await async_connection_readwrite.execute("COPY pyarrowtab FROM $tab", {"tab": nodes})
|
|
68
|
+
|
|
69
|
+
result = await async_connection_readwrite.execute(
|
|
70
|
+
"MATCH (t:pyarrowtab) RETURN t.id AS id, t.A AS A, t.B AS B ORDER BY t.id"
|
|
71
|
+
)
|
|
72
|
+
assert result.get_next() == [1, "a", True]
|
|
73
|
+
assert result.get_next() == [2, "b", False]
|
|
74
|
+
assert result.get_next() == [3, "c", None]
|
|
75
|
+
result.close()
|
|
76
|
+
|
|
77
|
+
for i in async_connection_readwrite.connections_counter:
|
|
78
|
+
assert i == 0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@pytest.mark.asyncio
|
|
82
|
+
async def test_async_query_concurrent(async_connection_readonly):
|
|
83
|
+
num_queries = 100
|
|
84
|
+
queries = [f"RETURN {i};" for i in range(num_queries)]
|
|
85
|
+
results = await asyncio.gather(*[async_connection_readonly.execute(query) for query in queries])
|
|
86
|
+
for i, result in enumerate(results):
|
|
87
|
+
assert result.has_next()
|
|
88
|
+
assert result.get_next() == [i]
|
|
89
|
+
assert not result.has_next()
|
|
90
|
+
result.close()
|
|
91
|
+
for i in async_connection_readonly.connections_counter:
|
|
92
|
+
assert i == 0
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@pytest.mark.asyncio
|
|
96
|
+
async def test_async_query_multiple_results(async_connection_readonly):
|
|
97
|
+
query = "MATCH (a:person) WHERE a.ID = 0 RETURN a.age; MATCH (a:person) WHERE a.ID = 2 RETURN a.age;"
|
|
98
|
+
results = await async_connection_readonly.execute(query)
|
|
99
|
+
assert len(results) == 2
|
|
100
|
+
result = results[0]
|
|
101
|
+
assert result.has_next()
|
|
102
|
+
assert result.get_next() == [35]
|
|
103
|
+
assert not result.has_next()
|
|
104
|
+
|
|
105
|
+
result = results[1]
|
|
106
|
+
assert result.has_next()
|
|
107
|
+
assert result.get_next() == [30]
|
|
108
|
+
assert not result.has_next()
|
|
109
|
+
for result in results:
|
|
110
|
+
result.close()
|
|
111
|
+
for i in async_connection_readonly.connections_counter:
|
|
112
|
+
assert i == 0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@pytest.mark.asyncio
|
|
116
|
+
async def test_async_connection_create_and_close():
|
|
117
|
+
db = lb.Database(":memory:", buffer_pool_size=2**28)
|
|
118
|
+
async_connection = lb.AsyncConnection(db)
|
|
119
|
+
for _ in range(10):
|
|
120
|
+
res = await async_connection.execute("RETURN 1;")
|
|
121
|
+
assert res.has_next()
|
|
122
|
+
assert res.get_next() == [1]
|
|
123
|
+
assert not res.has_next()
|
|
124
|
+
res.close()
|
|
125
|
+
num_queries = 100
|
|
126
|
+
queries = [f"RETURN {i};" for i in range(num_queries)]
|
|
127
|
+
results = await asyncio.gather(*[async_connection.execute(query) for query in queries])
|
|
128
|
+
for i, result in enumerate(results):
|
|
129
|
+
assert result.has_next()
|
|
130
|
+
assert result.get_next() == [i]
|
|
131
|
+
assert not result.has_next()
|
|
132
|
+
result.close()
|
|
133
|
+
async_connection.close()
|
|
134
|
+
db.close()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_acquire_connection(async_connection_readonly):
|
|
138
|
+
conn = async_connection_readonly.acquire_connection()
|
|
139
|
+
assert conn is not None
|
|
140
|
+
assert async_connection_readonly.connections_counter[0] == 1
|
|
141
|
+
result = conn.execute("RETURN 1;")
|
|
142
|
+
assert result.has_next()
|
|
143
|
+
assert result.get_next() == [1]
|
|
144
|
+
assert not result.has_next()
|
|
145
|
+
result.close()
|
|
146
|
+
async_connection_readonly.release_connection(conn)
|
|
147
|
+
for i in async_connection_readonly.connections_counter:
|
|
148
|
+
assert i == 0
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@pytest.mark.asyncio
|
|
152
|
+
async def test_async_connection_interrupt(async_connection_readonly) -> None:
|
|
153
|
+
query = "UNWIND RANGE(1,1000000) AS x UNWIND RANGE(1, 1000000) AS y RETURN COUNT(x + y);"
|
|
154
|
+
async_connection_readonly.set_query_timeout(100 * 1000)
|
|
155
|
+
task = asyncio.create_task(async_connection_readonly.execute(query))
|
|
156
|
+
time.sleep(5)
|
|
157
|
+
task.cancel()
|
|
158
|
+
with pytest.raises(asyncio.CancelledError):
|
|
159
|
+
await task
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from type_aliases import ConnDB
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_bytes_param(conn_db_empty: ConnDB) -> None:
|
|
10
|
+
conn, _ = conn_db_empty
|
|
11
|
+
conn.execute("CREATE NODE TABLE tab(id SERIAL PRIMARY KEY, data BLOB)")
|
|
12
|
+
|
|
13
|
+
data_0 = b"\x00\x01\x02\x03\xff"
|
|
14
|
+
data_1 = b"testing"
|
|
15
|
+
data_2 = b"A" * 4096 # max size: 4KB, see https://docs.ladybugdb.com/cypher/data-types/#blob
|
|
16
|
+
data_3 = b"" # empty
|
|
17
|
+
data_4 = None # null
|
|
18
|
+
data_5 = "Hello 🌍" # str
|
|
19
|
+
|
|
20
|
+
cases = [
|
|
21
|
+
data_0,
|
|
22
|
+
data_1,
|
|
23
|
+
data_2,
|
|
24
|
+
data_3,
|
|
25
|
+
data_4,
|
|
26
|
+
data_5.encode("utf-8"), # to bytes
|
|
27
|
+
]
|
|
28
|
+
for data in cases:
|
|
29
|
+
conn.execute("CREATE (t:tab {data: $data})", {"data": data})
|
|
30
|
+
|
|
31
|
+
result = conn.execute("MATCH (t:tab) RETURN t.data ORDER BY t.id")
|
|
32
|
+
assert result.get_next() == [data_0]
|
|
33
|
+
assert result.get_next() == [data_1]
|
|
34
|
+
assert result.get_next() == [data_2]
|
|
35
|
+
assert result.get_next() == [data_3]
|
|
36
|
+
assert result.get_next() == [data_4]
|
|
37
|
+
assert result.get_next()[0].decode("utf-8") == data_5
|
|
38
|
+
result.close()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_bytes_param_backwards_compatibility(conn_db_empty: ConnDB) -> None:
|
|
42
|
+
conn, _ = conn_db_empty
|
|
43
|
+
conn.execute("CREATE NODE TABLE tab(id SERIAL PRIMARY KEY, data BLOB)")
|
|
44
|
+
|
|
45
|
+
binary = b"backwards_compatibility"
|
|
46
|
+
string = "".join(f"\\x{b:02x}" for b in binary) # to \xHH format
|
|
47
|
+
|
|
48
|
+
assert isinstance(string, str)
|
|
49
|
+
assert re.match(r"^(\\x[0-9a-f]{2})+$", string)
|
|
50
|
+
|
|
51
|
+
conn.execute("CREATE (t:tab {data: BLOB($string)})", {"string": string})
|
|
52
|
+
|
|
53
|
+
result = conn.execute("MATCH (t:tab) RETURN t.data")
|
|
54
|
+
assert result.get_next() == [binary]
|
|
55
|
+
result.close()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_bytes_param_where_clause(conn_db_empty: ConnDB) -> None:
|
|
59
|
+
conn, _ = conn_db_empty
|
|
60
|
+
conn.execute("CREATE NODE TABLE tab(id INT64 PRIMARY KEY, data BLOB)")
|
|
61
|
+
|
|
62
|
+
data = b"where_clause"
|
|
63
|
+
|
|
64
|
+
conn.execute("CREATE (t:tab {id: 0, data: $data})", {"data": b"some data"})
|
|
65
|
+
conn.execute("CREATE (t:tab {id: 1, data: $data})", {"data": data})
|
|
66
|
+
conn.execute("CREATE (t:tab {id: 2, data: $data})", {"data": b"some other data"})
|
|
67
|
+
|
|
68
|
+
result = conn.execute("MATCH (t:tab) WHERE t.data = $search RETURN t.id", {"search": data})
|
|
69
|
+
assert result.get_next() == [1]
|
|
70
|
+
assert not result.has_next()
|
|
71
|
+
result.close()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_bytes_param_update(conn_db_empty: ConnDB) -> None:
|
|
75
|
+
conn, _ = conn_db_empty
|
|
76
|
+
conn.execute("CREATE NODE TABLE tab(id SERIAL PRIMARY KEY, data BLOB)")
|
|
77
|
+
|
|
78
|
+
initial = b"initial"
|
|
79
|
+
updated = b"updated"
|
|
80
|
+
|
|
81
|
+
conn.execute("CREATE (t:tab {data: $data})", {"data": initial})
|
|
82
|
+
conn.execute("MATCH (t:tab) SET t.data = $new_data", {"new_data": updated})
|
|
83
|
+
|
|
84
|
+
result = conn.execute("MATCH (t:tab) RETURN t.data")
|
|
85
|
+
assert result.get_next() == [updated]
|
|
86
|
+
result.close()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_bytes_param_mixed_types(conn_db_empty: ConnDB) -> None:
|
|
90
|
+
conn, _ = conn_db_empty
|
|
91
|
+
conn.execute("CREATE NODE TABLE tab(id SERIAL PRIMARY KEY, data BLOB, name STRING, value DOUBLE)")
|
|
92
|
+
|
|
93
|
+
data = b"data"
|
|
94
|
+
name = "name"
|
|
95
|
+
value = 3.14
|
|
96
|
+
|
|
97
|
+
params = {"data": data, "name": name, "value": value}
|
|
98
|
+
conn.execute("CREATE (t:tab {data: $data, name: $name, value: $value})", params)
|
|
99
|
+
|
|
100
|
+
result = conn.execute("MATCH (t:tab) RETURN t.data, t.name, t.value")
|
|
101
|
+
assert result.get_next() == [data, name, value]
|
|
102
|
+
result.close()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_bytes_param_relationship(conn_db_empty: ConnDB) -> None:
|
|
106
|
+
conn, _ = conn_db_empty
|
|
107
|
+
conn.execute("CREATE NODE TABLE person(name STRING PRIMARY KEY)")
|
|
108
|
+
conn.execute("CREATE REL TABLE rel(FROM person TO person, data BLOB)")
|
|
109
|
+
|
|
110
|
+
conn.execute("CREATE (p:person {name: 'Alice'})")
|
|
111
|
+
conn.execute("CREATE (p:person {name: 'Bob'})")
|
|
112
|
+
|
|
113
|
+
data = b"relationship"
|
|
114
|
+
|
|
115
|
+
conn.execute(
|
|
116
|
+
"""
|
|
117
|
+
MATCH (p1:person {name: 'Alice'}), (p2:person {name: 'Bob'})
|
|
118
|
+
CREATE (p1)-[r:rel {data: $data}]->(p2)
|
|
119
|
+
""",
|
|
120
|
+
{"data": data},
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
result = conn.execute("MATCH ()-[r:rel]->() RETURN r.data")
|
|
124
|
+
assert result.get_next() == [data]
|
|
125
|
+
result.close()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_bytes_param_udf(conn_db_empty: ConnDB) -> None:
|
|
129
|
+
conn, _ = conn_db_empty
|
|
130
|
+
|
|
131
|
+
def reverse_bytes(data: bytes) -> bytes:
|
|
132
|
+
return data[::-1]
|
|
133
|
+
|
|
134
|
+
conn.create_function("reverse_bytes", reverse_bytes, ["BLOB"], "BLOB")
|
|
135
|
+
|
|
136
|
+
data = b"hello"
|
|
137
|
+
expected = b"olleh"
|
|
138
|
+
|
|
139
|
+
result = conn.execute("RETURN reverse_bytes($data)", {"data": data})
|
|
140
|
+
assert result.get_next() == [expected]
|
|
141
|
+
|
|
142
|
+
result = conn.execute("RETURN reverse_bytes(NULL)")
|
|
143
|
+
assert result.get_next() == [None]
|
|
144
|
+
|
|
145
|
+
result.close()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import real_ladybug as lb
|
|
8
|
+
import pytest
|
|
9
|
+
from type_aliases import ConnDB
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_connection_close(tmp_path: Path) -> None:
|
|
16
|
+
db_path = tmp_path / "test_connection_close.lbug"
|
|
17
|
+
db = lb.Database(database_path=db_path, read_only=False)
|
|
18
|
+
conn = lb.Connection(db)
|
|
19
|
+
conn.close()
|
|
20
|
+
assert conn.is_closed
|
|
21
|
+
pytest.raises(RuntimeError, conn.execute, "RETURN 1")
|
|
22
|
+
db.close()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_connection_close_context_manager(tmp_path: Path) -> None:
|
|
26
|
+
db_path = tmp_path / "test_connection_close_context_manager.lbug"
|
|
27
|
+
with lb.Database(database_path=db_path, read_only=False) as db:
|
|
28
|
+
with lb.Connection(db) as conn:
|
|
29
|
+
pass
|
|
30
|
+
assert conn.is_closed
|
|
31
|
+
pytest.raises(RuntimeError, conn.execute, "RETURN 1")
|
|
32
|
+
assert db.is_closed
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run_long_query(conn):
|
|
36
|
+
query = "UNWIND RANGE(1,1000000) AS x UNWIND RANGE(1, 1000000) AS y RETURN COUNT(x + y);"
|
|
37
|
+
with pytest.raises(RuntimeError) as excinfo:
|
|
38
|
+
conn.execute(query)
|
|
39
|
+
assert "Interrupted" in str(excinfo.value)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_connection_interrupt(conn_db_readwrite: ConnDB) -> None:
|
|
43
|
+
conn, _ = conn_db_readwrite
|
|
44
|
+
execute_thread = threading.Thread(target=run_long_query, args=(conn,))
|
|
45
|
+
execute_thread.start()
|
|
46
|
+
time.sleep(5)
|
|
47
|
+
conn.interrupt()
|
|
48
|
+
execute_thread.join(timeout=100)
|
|
49
|
+
assert not execute_thread.is_alive()
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from textwrap import dedent
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
import real_ladybug as lb
|
|
10
|
+
import pytest
|
|
11
|
+
from conftest import get_db_file_path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def open_database_on_subprocess(tmp_path: Path, build_dir: Path) -> None:
|
|
15
|
+
code = dedent(
|
|
16
|
+
f"""
|
|
17
|
+
import sys
|
|
18
|
+
sys.path.append(r"{build_dir!s}")
|
|
19
|
+
|
|
20
|
+
import real_ladybug as lb
|
|
21
|
+
db = lb.Database(r"{tmp_path!s}")
|
|
22
|
+
print(r"{tmp_path!s}")
|
|
23
|
+
"""
|
|
24
|
+
)
|
|
25
|
+
result = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True)
|
|
26
|
+
if result.returncode != 0:
|
|
27
|
+
raise RuntimeError(result.stderr)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_database_close(tmp_path: Path, build_dir: Path) -> None:
|
|
31
|
+
db_path = tmp_path / "test_database_close.lbug"
|
|
32
|
+
db = lb.Database(database_path=db_path, read_only=False)
|
|
33
|
+
assert not db.is_closed
|
|
34
|
+
assert db._database is not None
|
|
35
|
+
|
|
36
|
+
# Open the database on a subprocess. It should raise an exception.
|
|
37
|
+
with pytest.raises(
|
|
38
|
+
Exception,
|
|
39
|
+
match=r"RuntimeError: IO exception: Could not set lock on file",
|
|
40
|
+
):
|
|
41
|
+
open_database_on_subprocess(db_path, build_dir)
|
|
42
|
+
|
|
43
|
+
db.close()
|
|
44
|
+
assert db.is_closed
|
|
45
|
+
assert db._database is None
|
|
46
|
+
|
|
47
|
+
# Try to close the database again. It should not raise any exceptions.
|
|
48
|
+
db.close()
|
|
49
|
+
assert db.is_closed
|
|
50
|
+
assert db._database is None
|
|
51
|
+
|
|
52
|
+
# Open the database on a subprocess. It should not raise any exceptions.
|
|
53
|
+
# TODO: Enable this test on Windows when the read-only mode is implemented.
|
|
54
|
+
if sys.platform != "win32":
|
|
55
|
+
open_database_on_subprocess(db_path, build_dir)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_database_context_manager(tmp_path: Path, build_dir: Path) -> None:
|
|
59
|
+
db_path = tmp_path / "test_database_context_manager.lbug"
|
|
60
|
+
with lb.Database(database_path=db_path, read_only=False) as db:
|
|
61
|
+
assert not db.is_closed
|
|
62
|
+
assert db._database is not None
|
|
63
|
+
|
|
64
|
+
# Open the database on a subprocess. It should raise an exception.
|
|
65
|
+
# TODO: Enable this test on Windows when the read-only mode is implemented.
|
|
66
|
+
if sys.platform != "win32":
|
|
67
|
+
with pytest.raises(
|
|
68
|
+
RuntimeError,
|
|
69
|
+
match=r"RuntimeError: IO exception: Could not set lock on file",
|
|
70
|
+
):
|
|
71
|
+
open_database_on_subprocess(db_path, build_dir)
|
|
72
|
+
|
|
73
|
+
# Open the database on a subprocess. It should not raise any exceptions.
|
|
74
|
+
# TODO: Enable this test on Windows when the read-only mode is implemented.
|
|
75
|
+
if sys.platform != "win32":
|
|
76
|
+
open_database_on_subprocess(db_path, build_dir)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_in_mem_database_memory_db_path() -> None:
|
|
80
|
+
db = lb.Database(database_path=":memory:")
|
|
81
|
+
assert not db.is_closed
|
|
82
|
+
assert db._database is not None
|
|
83
|
+
|
|
84
|
+
# Open the database on a subprocess. It should raise an exception.
|
|
85
|
+
conn = lb.Connection(db)
|
|
86
|
+
conn.execute("CREATE NODE TABLE person(name STRING, age INT64, PRIMARY KEY(name));")
|
|
87
|
+
conn.execute("CREATE (:person {name: 'Alice', age: 30});")
|
|
88
|
+
conn.execute("CREATE (:person {name: 'Bob', age: 40});")
|
|
89
|
+
result = conn.execute("MATCH (p:person) RETURN p.*")
|
|
90
|
+
assert result.get_num_tuples() == 2
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_in_mem_database_empty_db_path() -> None:
|
|
94
|
+
db = lb.Database()
|
|
95
|
+
assert not db.is_closed
|
|
96
|
+
assert db._database is not None
|
|
97
|
+
|
|
98
|
+
# Open the database on a subprocess. It should raise an exception.
|
|
99
|
+
conn = lb.Connection(db)
|
|
100
|
+
conn.execute("CREATE NODE TABLE person(name STRING, age INT64, PRIMARY KEY(name));")
|
|
101
|
+
conn.execute("CREATE (:person {name: 'Alice', age: 30});")
|
|
102
|
+
conn.execute("CREATE (:person {name: 'Bob', age: 40});")
|
|
103
|
+
result = conn.execute("MATCH (p:person) RETURN p.*")
|
|
104
|
+
assert result.get_num_tuples() == 2
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_in_mem_database_no_db_path() -> None:
|
|
108
|
+
with lb.Database(database_path="") as db:
|
|
109
|
+
assert not db.is_closed
|
|
110
|
+
assert db._database is not None
|
|
111
|
+
|
|
112
|
+
# Open the database on a subprocess. It should raise an exception.
|
|
113
|
+
conn = lb.Connection(db)
|
|
114
|
+
conn.execute("CREATE NODE TABLE person(name STRING, age INT64, PRIMARY KEY(name));")
|
|
115
|
+
conn.execute("CREATE (:person {name: 'Alice', age: 30});")
|
|
116
|
+
conn.execute("CREATE (:person {name: 'Bob', age: 40});")
|
|
117
|
+
with conn.execute("MATCH (p:person) RETURN p.*") as result:
|
|
118
|
+
assert result.get_num_tuples() == 2
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_database_auto_checkpoint_config(tmp_path: Path) -> None:
|
|
122
|
+
with lb.Database(database_path=get_db_file_path(tmp_path), auto_checkpoint=False) as db:
|
|
123
|
+
assert not db.is_closed
|
|
124
|
+
assert db._database is not None
|
|
125
|
+
|
|
126
|
+
conn = lb.Connection(db)
|
|
127
|
+
with conn.execute("CALL current_setting('auto_checkpoint') RETURN *") as result:
|
|
128
|
+
assert result.get_num_tuples() == 1
|
|
129
|
+
assert result.get_next()[0] == "False"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_database_checkpoint_threshold_config(tmp_path: Path) -> None:
|
|
133
|
+
with lb.Database(database_path=get_db_file_path(tmp_path), checkpoint_threshold=1234) as db:
|
|
134
|
+
assert not db.is_closed
|
|
135
|
+
assert db._database is not None
|
|
136
|
+
|
|
137
|
+
conn = lb.Connection(db)
|
|
138
|
+
with conn.execute("CALL current_setting('checkpoint_threshold') RETURN *") as result:
|
|
139
|
+
assert result.get_num_tuples() == 1
|
|
140
|
+
assert result.get_next()[0] == "1234"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_database_throw_on_wal_replay_failure_config(tmp_path: Path) -> None:
|
|
144
|
+
database_path = get_db_file_path(tmp_path)
|
|
145
|
+
wal_file_path = str(database_path) + ".wal"
|
|
146
|
+
with Path.open(wal_file_path, "w") as wal_file:
|
|
147
|
+
wal_file.write("a" * 28)
|
|
148
|
+
with lb.Database(database_path=database_path, throw_on_wal_replay_failure=False) as db:
|
|
149
|
+
assert not db.is_closed
|
|
150
|
+
assert db._database is not None
|
|
151
|
+
|
|
152
|
+
conn = lb.Connection(db)
|
|
153
|
+
with conn.execute("RETURN 1") as result:
|
|
154
|
+
assert result.get_num_tuples() == 1
|
|
155
|
+
assert result.get_next()[0] == 1
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_database_enable_checksums_config(tmp_path: Path) -> None:
|
|
159
|
+
database_path = get_db_file_path(tmp_path)
|
|
160
|
+
# first construct database file with enable_checksums=True
|
|
161
|
+
with lb.Database(database_path=database_path) as db:
|
|
162
|
+
assert not db.is_closed
|
|
163
|
+
assert db._database is not None
|
|
164
|
+
conn = lb.Connection(db)
|
|
165
|
+
|
|
166
|
+
# do some updates to leave a WAL
|
|
167
|
+
conn.execute("call auto_checkpoint=false")
|
|
168
|
+
conn.execute("call force_checkpoint_on_close=false")
|
|
169
|
+
conn.execute("create node table test(id int64 primary key)")
|
|
170
|
+
|
|
171
|
+
# running again with enable_checksums=False should give an error
|
|
172
|
+
with pytest.raises(RuntimeError) as check:
|
|
173
|
+
db = lb.Database(database_path=database_path, enable_checksums=False)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_database_close_order() -> None:
|
|
177
|
+
in_mem_db = lb.Database(database_path=":memory:", buffer_pool_size=1024 * 1024 * 10)
|
|
178
|
+
assert not in_mem_db.is_closed
|
|
179
|
+
assert in_mem_db._database is not None
|
|
180
|
+
|
|
181
|
+
in_mem_conn = lb.Connection(in_mem_db)
|
|
182
|
+
assert not in_mem_conn.is_closed
|
|
183
|
+
assert in_mem_conn._connection is not None
|
|
184
|
+
|
|
185
|
+
query_result = in_mem_conn.execute("RETURN 1+1")
|
|
186
|
+
assert not query_result.is_closed
|
|
187
|
+
assert query_result._query_result is not None
|
|
188
|
+
|
|
189
|
+
assert query_result.get_next()[0] == 2
|
|
190
|
+
# Close the database first, it should not cause crashes or exceptions
|
|
191
|
+
in_mem_db.close()
|
|
192
|
+
|
|
193
|
+
# The query result and connection will be unusable after the database is closed.
|
|
194
|
+
# But calling methods on them should raise exceptions instead of crashing.
|
|
195
|
+
with pytest.raises(Exception, match="Database is closed"):
|
|
196
|
+
in_mem_conn.execute("RETURN 1+1")
|
|
197
|
+
|
|
198
|
+
with pytest.raises(Exception, match="the parent database is closed"):
|
|
199
|
+
query_result.get_next()
|
|
200
|
+
|
|
201
|
+
# Close the connection and query result, they should not raise any exceptions.
|
|
202
|
+
in_mem_conn.close()
|
|
203
|
+
query_result.close()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_database_close_order_with_multiple_statements() -> None:
|
|
207
|
+
in_mem_db = lb.Database(database_path=":memory:", buffer_pool_size=1024 * 1024 * 10)
|
|
208
|
+
assert not in_mem_db.is_closed
|
|
209
|
+
assert in_mem_db._database is not None
|
|
210
|
+
|
|
211
|
+
in_mem_conn = lb.Connection(in_mem_db)
|
|
212
|
+
assert not in_mem_conn.is_closed
|
|
213
|
+
assert in_mem_conn._connection is not None
|
|
214
|
+
|
|
215
|
+
query_results = in_mem_conn.execute("RETURN 1+1; RETURN 2+2; RETURN 3+3;")
|
|
216
|
+
for i, qr in enumerate(query_results):
|
|
217
|
+
assert not qr.is_closed
|
|
218
|
+
assert qr._query_result is not None
|
|
219
|
+
assert qr.get_next()[0] == (i + 1) * 2
|
|
220
|
+
# Close the database first, it should not cause crashes or exceptions
|
|
221
|
+
in_mem_db.close()
|
|
222
|
+
|
|
223
|
+
# The query result and connection will be unusable after the database is closed.
|
|
224
|
+
# But calling methods on them should raise exceptions instead of crashing.
|
|
225
|
+
with pytest.raises(Exception, match="Database is closed"):
|
|
226
|
+
in_mem_conn.execute("RETURN 1+1")
|
|
227
|
+
|
|
228
|
+
with pytest.raises(Exception, match="the parent database is closed"):
|
|
229
|
+
query_results[0].get_next()
|
|
230
|
+
|
|
231
|
+
# Close the connection and query result, they should not raise any exceptions.
|
|
232
|
+
in_mem_conn.close()
|
|
233
|
+
for qr in query_results:
|
|
234
|
+
qr.close()
|