real-ladybug 0.0.1.dev1__cp312-cp312-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.

Files changed (114) hide show
  1. real_ladybug/__init__.py +83 -0
  2. real_ladybug/_lbug.cp312-win_amd64.pyd +0 -0
  3. real_ladybug/_lbug.exp +0 -0
  4. real_ladybug/_lbug.lib +0 -0
  5. real_ladybug/async_connection.py +226 -0
  6. real_ladybug/connection.py +323 -0
  7. real_ladybug/constants.py +7 -0
  8. real_ladybug/database.py +307 -0
  9. real_ladybug/prepared_statement.py +51 -0
  10. real_ladybug/py.typed +0 -0
  11. real_ladybug/query_result.py +511 -0
  12. real_ladybug/torch_geometric_feature_store.py +185 -0
  13. real_ladybug/torch_geometric_graph_store.py +131 -0
  14. real_ladybug/torch_geometric_result_converter.py +282 -0
  15. real_ladybug/types.py +39 -0
  16. real_ladybug-0.0.1.dev1.dist-info/METADATA +88 -0
  17. real_ladybug-0.0.1.dev1.dist-info/RECORD +114 -0
  18. real_ladybug-0.0.1.dev1.dist-info/WHEEL +5 -0
  19. real_ladybug-0.0.1.dev1.dist-info/licenses/LICENSE +21 -0
  20. real_ladybug-0.0.1.dev1.dist-info/top_level.txt +3 -0
  21. real_ladybug-0.0.1.dev1.dist-info/zip-safe +1 -0
  22. real_ladybug-source/scripts/antlr4/hash.py +2 -0
  23. real_ladybug-source/scripts/antlr4/keywordhandler.py +47 -0
  24. real_ladybug-source/scripts/collect-extensions.py +68 -0
  25. real_ladybug-source/scripts/collect-single-file-header.py +126 -0
  26. real_ladybug-source/scripts/export-dbs.py +101 -0
  27. real_ladybug-source/scripts/export-import-test.py +345 -0
  28. real_ladybug-source/scripts/extension/purge-beta.py +34 -0
  29. real_ladybug-source/scripts/generate-cpp-docs/collect_files.py +122 -0
  30. real_ladybug-source/scripts/generate-tinysnb.py +34 -0
  31. real_ladybug-source/scripts/get-clangd-diagnostics.py +233 -0
  32. real_ladybug-source/scripts/migrate-lbug-db.py +308 -0
  33. real_ladybug-source/scripts/multiplatform-test-helper/collect-results.py +71 -0
  34. real_ladybug-source/scripts/multiplatform-test-helper/notify-discord.py +68 -0
  35. real_ladybug-source/scripts/pip-package/package_tar.py +90 -0
  36. real_ladybug-source/scripts/pip-package/setup.py +130 -0
  37. real_ladybug-source/scripts/run-clang-format.py +408 -0
  38. real_ladybug-source/scripts/setup-extension-repo.py +67 -0
  39. real_ladybug-source/scripts/test-simsimd-dispatch.py +45 -0
  40. real_ladybug-source/scripts/update-nightly-build-version.py +81 -0
  41. real_ladybug-source/third_party/brotli/scripts/dictionary/step-01-download-rfc.py +16 -0
  42. real_ladybug-source/third_party/brotli/scripts/dictionary/step-02-rfc-to-bin.py +34 -0
  43. real_ladybug-source/third_party/brotli/scripts/dictionary/step-03-validate-bin.py +35 -0
  44. real_ladybug-source/third_party/brotli/scripts/dictionary/step-04-generate-java-literals.py +85 -0
  45. real_ladybug-source/third_party/pybind11/tools/codespell_ignore_lines_from_errors.py +35 -0
  46. real_ladybug-source/third_party/pybind11/tools/libsize.py +36 -0
  47. real_ladybug-source/third_party/pybind11/tools/make_changelog.py +63 -0
  48. real_ladybug-source/tools/python_api/build/real_ladybug/__init__.py +83 -0
  49. real_ladybug-source/tools/python_api/build/real_ladybug/async_connection.py +226 -0
  50. real_ladybug-source/tools/python_api/build/real_ladybug/connection.py +323 -0
  51. real_ladybug-source/tools/python_api/build/real_ladybug/constants.py +7 -0
  52. real_ladybug-source/tools/python_api/build/real_ladybug/database.py +307 -0
  53. real_ladybug-source/tools/python_api/build/real_ladybug/prepared_statement.py +51 -0
  54. real_ladybug-source/tools/python_api/build/real_ladybug/py.typed +0 -0
  55. real_ladybug-source/tools/python_api/build/real_ladybug/query_result.py +511 -0
  56. real_ladybug-source/tools/python_api/build/real_ladybug/torch_geometric_feature_store.py +185 -0
  57. real_ladybug-source/tools/python_api/build/real_ladybug/torch_geometric_graph_store.py +131 -0
  58. real_ladybug-source/tools/python_api/build/real_ladybug/torch_geometric_result_converter.py +282 -0
  59. real_ladybug-source/tools/python_api/build/real_ladybug/types.py +39 -0
  60. real_ladybug-source/tools/python_api/src_py/__init__.py +83 -0
  61. real_ladybug-source/tools/python_api/src_py/async_connection.py +226 -0
  62. real_ladybug-source/tools/python_api/src_py/connection.py +323 -0
  63. real_ladybug-source/tools/python_api/src_py/constants.py +7 -0
  64. real_ladybug-source/tools/python_api/src_py/database.py +307 -0
  65. real_ladybug-source/tools/python_api/src_py/prepared_statement.py +51 -0
  66. real_ladybug-source/tools/python_api/src_py/py.typed +0 -0
  67. real_ladybug-source/tools/python_api/src_py/query_result.py +511 -0
  68. real_ladybug-source/tools/python_api/src_py/torch_geometric_feature_store.py +185 -0
  69. real_ladybug-source/tools/python_api/src_py/torch_geometric_graph_store.py +131 -0
  70. real_ladybug-source/tools/python_api/src_py/torch_geometric_result_converter.py +282 -0
  71. real_ladybug-source/tools/python_api/src_py/types.py +39 -0
  72. real_ladybug-source/tools/python_api/test/conftest.py +230 -0
  73. real_ladybug-source/tools/python_api/test/disabled_test_extension.py +73 -0
  74. real_ladybug-source/tools/python_api/test/ground_truth.py +430 -0
  75. real_ladybug-source/tools/python_api/test/test_arrow.py +694 -0
  76. real_ladybug-source/tools/python_api/test/test_async_connection.py +159 -0
  77. real_ladybug-source/tools/python_api/test/test_blob_parameter.py +145 -0
  78. real_ladybug-source/tools/python_api/test/test_connection.py +49 -0
  79. real_ladybug-source/tools/python_api/test/test_database.py +234 -0
  80. real_ladybug-source/tools/python_api/test/test_datatype.py +372 -0
  81. real_ladybug-source/tools/python_api/test/test_df.py +564 -0
  82. real_ladybug-source/tools/python_api/test/test_dict.py +112 -0
  83. real_ladybug-source/tools/python_api/test/test_exception.py +54 -0
  84. real_ladybug-source/tools/python_api/test/test_fsm.py +227 -0
  85. real_ladybug-source/tools/python_api/test/test_get_header.py +49 -0
  86. real_ladybug-source/tools/python_api/test/test_helper.py +8 -0
  87. real_ladybug-source/tools/python_api/test/test_issue.py +147 -0
  88. real_ladybug-source/tools/python_api/test/test_iteration.py +96 -0
  89. real_ladybug-source/tools/python_api/test/test_networkx.py +437 -0
  90. real_ladybug-source/tools/python_api/test/test_parameter.py +340 -0
  91. real_ladybug-source/tools/python_api/test/test_prepared_statement.py +117 -0
  92. real_ladybug-source/tools/python_api/test/test_query_result.py +54 -0
  93. real_ladybug-source/tools/python_api/test/test_query_result_close.py +44 -0
  94. real_ladybug-source/tools/python_api/test/test_scan_pandas.py +676 -0
  95. real_ladybug-source/tools/python_api/test/test_scan_pandas_pyarrow.py +714 -0
  96. real_ladybug-source/tools/python_api/test/test_scan_polars.py +165 -0
  97. real_ladybug-source/tools/python_api/test/test_scan_pyarrow.py +167 -0
  98. real_ladybug-source/tools/python_api/test/test_timeout.py +11 -0
  99. real_ladybug-source/tools/python_api/test/test_torch_geometric.py +640 -0
  100. real_ladybug-source/tools/python_api/test/test_torch_geometric_remote_backend.py +111 -0
  101. real_ladybug-source/tools/python_api/test/test_udf.py +207 -0
  102. real_ladybug-source/tools/python_api/test/test_version.py +6 -0
  103. real_ladybug-source/tools/python_api/test/test_wal.py +80 -0
  104. real_ladybug-source/tools/python_api/test/type_aliases.py +10 -0
  105. real_ladybug-source/tools/rust_api/update_version.py +47 -0
  106. real_ladybug-source/tools/shell/test/conftest.py +218 -0
  107. real_ladybug-source/tools/shell/test/test_helper.py +60 -0
  108. real_ladybug-source/tools/shell/test/test_shell_basics.py +325 -0
  109. real_ladybug-source/tools/shell/test/test_shell_commands.py +656 -0
  110. real_ladybug-source/tools/shell/test/test_shell_control_edit.py +438 -0
  111. real_ladybug-source/tools/shell/test/test_shell_control_search.py +468 -0
  112. real_ladybug-source/tools/shell/test/test_shell_esc_edit.py +232 -0
  113. real_ladybug-source/tools/shell/test/test_shell_esc_search.py +162 -0
  114. 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()