datahike-browser-tests 1.0.0
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.
- package/.circleci/config.yml +405 -0
- package/.circleci/scripts/gen_ci.clj +194 -0
- package/.cirrus.yml +60 -0
- package/.clj-kondo/babashka/sci/config.edn +1 -0
- package/.clj-kondo/babashka/sci/sci/core.clj +9 -0
- package/.clj-kondo/config.edn +95 -0
- package/.dir-locals.el +2 -0
- package/.github/FUNDING.yml +3 -0
- package/.github/ISSUE_TEMPLATE/1-bug-report.yml +68 -0
- package/.github/ISSUE_TEMPLATE/2-feature-request.yml +28 -0
- package/.github/ISSUE_TEMPLATE/config.yml +6 -0
- package/.github/pull_request_template.md +24 -0
- package/.github/workflows/native-image.yml +84 -0
- package/LICENSE +203 -0
- package/README.md +273 -0
- package/bb/deps.edn +9 -0
- package/bb/resources/github-fingerprints +3 -0
- package/bb/resources/native-image-tests/run-bb-pod-tests.clj +162 -0
- package/bb/resources/native-image-tests/run-libdatahike-tests +12 -0
- package/bb/resources/native-image-tests/run-native-image-tests +74 -0
- package/bb/resources/native-image-tests/run-python-tests +22 -0
- package/bb/resources/native-image-tests/testconfig.attr-refs.edn +6 -0
- package/bb/resources/native-image-tests/testconfig.edn +5 -0
- package/bb/resources/template/.settings/org.eclipse.jdt.apt.core.prefs +2 -0
- package/bb/resources/template/.settings/org.eclipse.jdt.core.prefs +9 -0
- package/bb/resources/template/.settings/org.eclipse.m2e.core.prefs +4 -0
- package/bb/resources/template/pom.xml +22 -0
- package/bb/src/tools/build.clj +132 -0
- package/bb/src/tools/clj_kondo.clj +32 -0
- package/bb/src/tools/deploy.clj +26 -0
- package/bb/src/tools/examples.clj +19 -0
- package/bb/src/tools/npm.clj +100 -0
- package/bb/src/tools/python.clj +14 -0
- package/bb/src/tools/release.clj +94 -0
- package/bb/src/tools/test.clj +148 -0
- package/bb/src/tools/version.clj +47 -0
- package/bb.edn +269 -0
- package/benchmark/src/benchmark/cli.clj +195 -0
- package/benchmark/src/benchmark/compare.clj +157 -0
- package/benchmark/src/benchmark/config.clj +316 -0
- package/benchmark/src/benchmark/measure.clj +187 -0
- package/benchmark/src/benchmark/store.clj +190 -0
- package/benchmark/test/benchmark/measure_test.clj +156 -0
- package/build.clj +30 -0
- package/config.edn +49 -0
- package/deps.edn +138 -0
- package/dev/sandbox.clj +82 -0
- package/dev/sandbox.cljs +127 -0
- package/dev/sandbox_benchmarks.clj +27 -0
- package/dev/sandbox_client.clj +87 -0
- package/dev/sandbox_transact_bench.clj +109 -0
- package/dev/user.clj +79 -0
- package/doc/README.md +96 -0
- package/doc/adl/README.md +6 -0
- package/doc/adl/adr-000-adr.org +28 -0
- package/doc/adl/adr-001-attribute-references.org +15 -0
- package/doc/adl/adr-002-build-tooling.org +54 -0
- package/doc/adl/adr-003-db-meta-data.md +52 -0
- package/doc/adl/adr-004-github-flow.md +40 -0
- package/doc/adl/adr-XYZ-template.md +30 -0
- package/doc/adl/index.org +3 -0
- package/doc/assets/datahike-logo.svg +3 -0
- package/doc/assets/datahiking-invoice.org +85 -0
- package/doc/assets/hhtree2.png +0 -0
- package/doc/assets/network_topology.svg +624 -0
- package/doc/assets/perf.png +0 -0
- package/doc/assets/schema_mindmap.mm +132 -0
- package/doc/assets/schema_mindmap.svg +970 -0
- package/doc/assets/temporal_index.mm +74 -0
- package/doc/backend-development.md +78 -0
- package/doc/bb-pod.md +89 -0
- package/doc/benchmarking.md +360 -0
- package/doc/bindings/edn-conversion.md +383 -0
- package/doc/cli.md +162 -0
- package/doc/cljdoc.edn +27 -0
- package/doc/cljs-support.md +133 -0
- package/doc/config.md +406 -0
- package/doc/contributing.md +114 -0
- package/doc/datalog-vs-sql.md +210 -0
- package/doc/datomic_differences.md +109 -0
- package/doc/development/pull-api-ns.md +186 -0
- package/doc/development/pull-frame-state-diagram.jpg +0 -0
- package/doc/distributed.md +566 -0
- package/doc/entity_spec.md +92 -0
- package/doc/gc.md +273 -0
- package/doc/java-api.md +808 -0
- package/doc/javascript-api.md +421 -0
- package/doc/libdatahike.md +86 -0
- package/doc/logging_and_error_handling.md +43 -0
- package/doc/norms.md +66 -0
- package/doc/schema-migration.md +85 -0
- package/doc/schema.md +287 -0
- package/doc/storage-backends.md +363 -0
- package/doc/store-id-refactoring.md +596 -0
- package/doc/time_variance.md +325 -0
- package/doc/unstructured.md +167 -0
- package/doc/versioning.md +261 -0
- package/examples/basic/README.md +19 -0
- package/examples/basic/deps.edn +6 -0
- package/examples/basic/docker-compose.yml +13 -0
- package/examples/basic/src/examples/core.clj +60 -0
- package/examples/basic/src/examples/schema.clj +155 -0
- package/examples/basic/src/examples/store.clj +60 -0
- package/examples/basic/src/examples/time_travel.clj +185 -0
- package/examples/java/.settings/org.eclipse.core.resources.prefs +3 -0
- package/examples/java/.settings/org.eclipse.jdt.apt.core.prefs +2 -0
- package/examples/java/.settings/org.eclipse.jdt.core.prefs +9 -0
- package/examples/java/.settings/org.eclipse.m2e.core.prefs +4 -0
- package/examples/java/README.md +162 -0
- package/examples/java/pom.xml +62 -0
- package/examples/java/src/main/java/examples/QuickStart.java +115 -0
- package/examples/java/src/main/java/examples/SchemaExample.java +148 -0
- package/examples/java/src/main/java/examples/TimeTravelExample.java +121 -0
- package/flake.lock +27 -0
- package/flake.nix +27 -0
- package/http-server/datahike/http/middleware.clj +75 -0
- package/http-server/datahike/http/server.clj +269 -0
- package/java/src/datahike/java/Database.java +274 -0
- package/java/src/datahike/java/Datahike.java +281 -0
- package/java/src/datahike/java/DatahikeGeneratedTest.java +349 -0
- package/java/src/datahike/java/DatahikeTest.java +370 -0
- package/java/src/datahike/java/EDN.java +170 -0
- package/java/src/datahike/java/IEntity.java +11 -0
- package/java/src/datahike/java/Keywords.java +161 -0
- package/java/src/datahike/java/SchemaFlexibility.java +52 -0
- package/java/src/datahike/java/Util.java +219 -0
- package/karma.conf.js +19 -0
- package/libdatahike/compile-cpp +7 -0
- package/libdatahike/src/datahike/impl/LibDatahikeBase.java +203 -0
- package/libdatahike/src/datahike/impl/libdatahike.clj +59 -0
- package/libdatahike/src/test_cpp.cpp +61 -0
- package/npm-package/PUBLISHING.md +140 -0
- package/npm-package/README.md +226 -0
- package/npm-package/package.template.json +34 -0
- package/npm-package/test-isomorphic.ts +281 -0
- package/npm-package/test.js +557 -0
- package/npm-package/typescript-test.ts +70 -0
- package/package.json +16 -0
- package/pydatahike/README.md +569 -0
- package/pydatahike/pyproject.toml +91 -0
- package/pydatahike/setup.py +42 -0
- package/pydatahike/src/datahike/__init__.py +134 -0
- package/pydatahike/src/datahike/_native.py +250 -0
- package/pydatahike/src/datahike/_version.py +2 -0
- package/pydatahike/src/datahike/database.py +722 -0
- package/pydatahike/src/datahike/edn.py +311 -0
- package/pydatahike/src/datahike/py.typed +0 -0
- package/pydatahike/tests/conftest.py +17 -0
- package/pydatahike/tests/test_basic.py +170 -0
- package/pydatahike/tests/test_database.py +51 -0
- package/pydatahike/tests/test_edn_conversion.py +299 -0
- package/pydatahike/tests/test_query.py +99 -0
- package/pydatahike/tests/test_schema.py +55 -0
- package/resources/clj-kondo.exports/io.replikativ/datahike/config.edn +5 -0
- package/resources/example_server.edn +4 -0
- package/shadow-cljs.edn +56 -0
- package/src/data_readers.clj +7 -0
- package/src/datahike/api/impl.cljc +176 -0
- package/src/datahike/api/specification.cljc +633 -0
- package/src/datahike/api/types.cljc +261 -0
- package/src/datahike/api.cljc +41 -0
- package/src/datahike/array.cljc +99 -0
- package/src/datahike/cli.clj +166 -0
- package/src/datahike/cljs.cljs +6 -0
- package/src/datahike/codegen/cli.clj +406 -0
- package/src/datahike/codegen/clj_kondo.clj +291 -0
- package/src/datahike/codegen/java.clj +403 -0
- package/src/datahike/codegen/naming.cljc +33 -0
- package/src/datahike/codegen/native.clj +559 -0
- package/src/datahike/codegen/pod.clj +488 -0
- package/src/datahike/codegen/python.clj +838 -0
- package/src/datahike/codegen/report.clj +55 -0
- package/src/datahike/codegen/typescript.clj +262 -0
- package/src/datahike/codegen/validation.clj +145 -0
- package/src/datahike/config.cljc +294 -0
- package/src/datahike/connections.cljc +16 -0
- package/src/datahike/connector.cljc +265 -0
- package/src/datahike/constants.cljc +142 -0
- package/src/datahike/core.cljc +297 -0
- package/src/datahike/datom.cljc +459 -0
- package/src/datahike/db/interface.cljc +119 -0
- package/src/datahike/db/search.cljc +305 -0
- package/src/datahike/db/transaction.cljc +937 -0
- package/src/datahike/db/utils.cljc +338 -0
- package/src/datahike/db.cljc +956 -0
- package/src/datahike/experimental/unstructured.cljc +126 -0
- package/src/datahike/experimental/versioning.cljc +172 -0
- package/src/datahike/externs.js +31 -0
- package/src/datahike/gc.cljc +69 -0
- package/src/datahike/http/client.clj +188 -0
- package/src/datahike/http/writer.clj +79 -0
- package/src/datahike/impl/entity.cljc +218 -0
- package/src/datahike/index/interface.cljc +93 -0
- package/src/datahike/index/persistent_set.cljc +469 -0
- package/src/datahike/index/utils.cljc +44 -0
- package/src/datahike/index.cljc +32 -0
- package/src/datahike/js/api.cljs +172 -0
- package/src/datahike/js/api_macros.clj +22 -0
- package/src/datahike/js.cljs +163 -0
- package/src/datahike/json.cljc +209 -0
- package/src/datahike/lru.cljc +146 -0
- package/src/datahike/migrate.clj +39 -0
- package/src/datahike/norm/norm.clj +245 -0
- package/src/datahike/online_gc.cljc +252 -0
- package/src/datahike/pod.clj +155 -0
- package/src/datahike/pull_api.cljc +325 -0
- package/src/datahike/query.cljc +1945 -0
- package/src/datahike/query_stats.cljc +88 -0
- package/src/datahike/readers.cljc +62 -0
- package/src/datahike/remote.cljc +218 -0
- package/src/datahike/schema.cljc +228 -0
- package/src/datahike/schema_cache.cljc +42 -0
- package/src/datahike/spec.cljc +101 -0
- package/src/datahike/store.cljc +80 -0
- package/src/datahike/tools.cljc +308 -0
- package/src/datahike/transit.cljc +80 -0
- package/src/datahike/writer.cljc +239 -0
- package/src/datahike/writing.cljc +362 -0
- package/src/deps.cljs +1 -0
- package/src-hitchhiker-tree/datahike/index/hitchhiker_tree/insert.cljc +76 -0
- package/src-hitchhiker-tree/datahike/index/hitchhiker_tree/upsert.cljc +128 -0
- package/src-hitchhiker-tree/datahike/index/hitchhiker_tree.cljc +213 -0
- package/test/datahike/backward_compatibility_test/src/backward_test.clj +37 -0
- package/test/datahike/integration_test/config_record_file_test.clj +14 -0
- package/test/datahike/integration_test/config_record_test.clj +14 -0
- package/test/datahike/integration_test/depr_config_uri_test.clj +15 -0
- package/test/datahike/integration_test/return_map_test.clj +62 -0
- package/test/datahike/integration_test.cljc +67 -0
- package/test/datahike/norm/norm_test.clj +124 -0
- package/test/datahike/norm/resources/naming-and-sorting-test/001-a1-example.edn +5 -0
- package/test/datahike/norm/resources/naming-and-sorting-test/002-a2-example.edn +5 -0
- package/test/datahike/norm/resources/naming-and-sorting-test/003-tx-fn-test.edn +1 -0
- package/test/datahike/norm/resources/naming-and-sorting-test/004-tx-data-and-tx-fn-test.edn +5 -0
- package/test/datahike/norm/resources/naming-and-sorting-test/01-transact-basic-characters.edn +2 -0
- package/test/datahike/norm/resources/naming-and-sorting-test/02 add occupation.edn +5 -0
- package/test/datahike/norm/resources/naming-and-sorting-test/checksums.edn +12 -0
- package/test/datahike/norm/resources/simple-test/001-a1-example.edn +5 -0
- package/test/datahike/norm/resources/simple-test/002-a2-example.edn +5 -0
- package/test/datahike/norm/resources/simple-test/checksums.edn +4 -0
- package/test/datahike/norm/resources/tx-data-and-tx-fn-test/first/001-a1-example.edn +5 -0
- package/test/datahike/norm/resources/tx-data-and-tx-fn-test/first/002-a2-example.edn +5 -0
- package/test/datahike/norm/resources/tx-data-and-tx-fn-test/first/003-tx-fn-test.edn +1 -0
- package/test/datahike/norm/resources/tx-data-and-tx-fn-test/first/checksums.edn +6 -0
- package/test/datahike/norm/resources/tx-data-and-tx-fn-test/second/004-tx-data-and-tx-fn-test.edn +5 -0
- package/test/datahike/norm/resources/tx-data-and-tx-fn-test/second/checksums.edn +2 -0
- package/test/datahike/norm/resources/tx-fn-test/first/001-a1-example.edn +5 -0
- package/test/datahike/norm/resources/tx-fn-test/first/002-a2-example.edn +5 -0
- package/test/datahike/norm/resources/tx-fn-test/first/checksums.edn +4 -0
- package/test/datahike/norm/resources/tx-fn-test/second/003-tx-fn-test.edn +1 -0
- package/test/datahike/norm/resources/tx-fn-test/second/checksums.edn +2 -0
- package/test/datahike/test/api_test.cljc +895 -0
- package/test/datahike/test/array_test.cljc +40 -0
- package/test/datahike/test/attribute_refs/datoms_test.cljc +140 -0
- package/test/datahike/test/attribute_refs/db_test.cljc +42 -0
- package/test/datahike/test/attribute_refs/differences_test.cljc +515 -0
- package/test/datahike/test/attribute_refs/entity_test.cljc +89 -0
- package/test/datahike/test/attribute_refs/pull_api_test.cljc +320 -0
- package/test/datahike/test/attribute_refs/query_find_specs_test.cljc +59 -0
- package/test/datahike/test/attribute_refs/query_fns_test.cljc +130 -0
- package/test/datahike/test/attribute_refs/query_interop_test.cljc +47 -0
- package/test/datahike/test/attribute_refs/query_not_test.cljc +193 -0
- package/test/datahike/test/attribute_refs/query_or_test.cljc +137 -0
- package/test/datahike/test/attribute_refs/query_pull_test.cljc +156 -0
- package/test/datahike/test/attribute_refs/query_rules_test.cljc +176 -0
- package/test/datahike/test/attribute_refs/query_test.cljc +241 -0
- package/test/datahike/test/attribute_refs/temporal_search.cljc +22 -0
- package/test/datahike/test/attribute_refs/transact_test.cljc +220 -0
- package/test/datahike/test/attribute_refs/utils.cljc +128 -0
- package/test/datahike/test/cache_test.cljc +38 -0
- package/test/datahike/test/components_test.cljc +92 -0
- package/test/datahike/test/config_test.cljc +158 -0
- package/test/datahike/test/core_test.cljc +105 -0
- package/test/datahike/test/datom_test.cljc +44 -0
- package/test/datahike/test/db_test.cljc +54 -0
- package/test/datahike/test/entity_spec_test.cljc +159 -0
- package/test/datahike/test/entity_test.cljc +103 -0
- package/test/datahike/test/explode_test.cljc +143 -0
- package/test/datahike/test/filter_test.cljc +75 -0
- package/test/datahike/test/gc_test.cljc +159 -0
- package/test/datahike/test/http/server_test.clj +192 -0
- package/test/datahike/test/http/writer_test.clj +86 -0
- package/test/datahike/test/ident_test.cljc +32 -0
- package/test/datahike/test/index_test.cljc +345 -0
- package/test/datahike/test/insert.cljc +125 -0
- package/test/datahike/test/java_bindings_test.clj +6 -0
- package/test/datahike/test/listen_test.cljc +41 -0
- package/test/datahike/test/lookup_refs_test.cljc +266 -0
- package/test/datahike/test/lru_test.cljc +27 -0
- package/test/datahike/test/migrate_test.clj +297 -0
- package/test/datahike/test/model/core.cljc +376 -0
- package/test/datahike/test/model/invariant.cljc +142 -0
- package/test/datahike/test/model/rng.cljc +82 -0
- package/test/datahike/test/model_test.clj +217 -0
- package/test/datahike/test/nodejs_test.cljs +262 -0
- package/test/datahike/test/online_gc_test.cljc +475 -0
- package/test/datahike/test/pod_test.clj +369 -0
- package/test/datahike/test/pull_api_test.cljc +474 -0
- package/test/datahike/test/purge_test.cljc +144 -0
- package/test/datahike/test/query_aggregates_test.cljc +101 -0
- package/test/datahike/test/query_find_specs_test.cljc +52 -0
- package/test/datahike/test/query_fns_test.cljc +523 -0
- package/test/datahike/test/query_interop_test.cljc +47 -0
- package/test/datahike/test/query_not_test.cljc +189 -0
- package/test/datahike/test/query_or_test.cljc +158 -0
- package/test/datahike/test/query_pull_test.cljc +147 -0
- package/test/datahike/test/query_rules_test.cljc +248 -0
- package/test/datahike/test/query_stats_test.cljc +218 -0
- package/test/datahike/test/query_test.cljc +984 -0
- package/test/datahike/test/schema_test.cljc +424 -0
- package/test/datahike/test/specification_test.cljc +30 -0
- package/test/datahike/test/store_test.cljc +78 -0
- package/test/datahike/test/stress_test.cljc +57 -0
- package/test/datahike/test/time_variance_test.cljc +518 -0
- package/test/datahike/test/tools_test.clj +134 -0
- package/test/datahike/test/transact_test.cljc +518 -0
- package/test/datahike/test/tuples_test.cljc +564 -0
- package/test/datahike/test/unstructured_test.cljc +291 -0
- package/test/datahike/test/upsert_impl_test.cljc +205 -0
- package/test/datahike/test/upsert_test.cljc +363 -0
- package/test/datahike/test/utils.cljc +110 -0
- package/test/datahike/test/validation_test.cljc +48 -0
- package/test/datahike/test/versioning_test.cljc +56 -0
- package/test/datahike/test.cljc +66 -0
- package/tests.edn +24 -0
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
"""High-level Datahike database interface.
|
|
2
|
+
|
|
3
|
+
Provides a Pythonic wrapper around the low-level FFI bindings with:
|
|
4
|
+
- Configuration as Python dicts/kwargs instead of EDN strings
|
|
5
|
+
- Auto-serialization of Python objects to JSON/EDN
|
|
6
|
+
- Simplified query interface with smart defaults
|
|
7
|
+
- Automatic result unwrapping
|
|
8
|
+
- Time-travel query support
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
>>> from datahike import Database
|
|
12
|
+
>>>
|
|
13
|
+
>>> db = Database(backend='mem', id='test')
|
|
14
|
+
>>> db.create()
|
|
15
|
+
>>>
|
|
16
|
+
>>> db.transact([{"name": "Alice", "age": 30}])
|
|
17
|
+
>>> results = db.q('[:find ?name :where [?e :name ?name]]')
|
|
18
|
+
>>> print(results) # [['Alice']]
|
|
19
|
+
>>>
|
|
20
|
+
>>> db.delete()
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
from typing import Any, Dict, List, Optional, Union, Tuple, Iterator
|
|
25
|
+
from contextlib import contextmanager
|
|
26
|
+
|
|
27
|
+
from ._native import OutputFormat, InputFormat
|
|
28
|
+
from . import generated as _gen
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# =============================================================================
|
|
32
|
+
# EDN Conversion
|
|
33
|
+
# =============================================================================
|
|
34
|
+
|
|
35
|
+
class EDNType:
|
|
36
|
+
"""Base class for explicit EDN types."""
|
|
37
|
+
def __init__(self, value: Any):
|
|
38
|
+
self.value = value
|
|
39
|
+
|
|
40
|
+
def to_edn(self) -> str:
|
|
41
|
+
"""Convert to EDN string representation."""
|
|
42
|
+
raise NotImplementedError
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _key_to_keyword(key: str) -> str:
|
|
46
|
+
"""Convert Python dict key to EDN keyword.
|
|
47
|
+
|
|
48
|
+
Keys are always keywordized:
|
|
49
|
+
"name" → :name
|
|
50
|
+
"person/name" → :person/name
|
|
51
|
+
":db/id" → :db/id (strip leading :)
|
|
52
|
+
"keep-history?" → :keep-history?
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
key: Dictionary key string
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
EDN keyword string
|
|
59
|
+
"""
|
|
60
|
+
# Strip leading : if present (convenience)
|
|
61
|
+
if key.startswith(':'):
|
|
62
|
+
return key
|
|
63
|
+
else:
|
|
64
|
+
return f':{key}'
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _value_to_edn(v: Any) -> str:
|
|
68
|
+
"""Convert Python value to EDN representation.
|
|
69
|
+
|
|
70
|
+
Rules:
|
|
71
|
+
- EDNType instances: use their to_edn() method
|
|
72
|
+
- str starting with '<STRING>': forced string (strip marker, quote)
|
|
73
|
+
- str starting with '\\:': escaped colon → literal string
|
|
74
|
+
- str starting with ':': keyword
|
|
75
|
+
- str without ':': string (quoted)
|
|
76
|
+
- int, float: number
|
|
77
|
+
- bool: true/false
|
|
78
|
+
- None: nil
|
|
79
|
+
- list: vector (recursive)
|
|
80
|
+
- dict: map (recursive)
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
v: Python value to convert
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
EDN string representation
|
|
87
|
+
"""
|
|
88
|
+
# Check for explicit EDN types first
|
|
89
|
+
if isinstance(v, EDNType):
|
|
90
|
+
return v.to_edn()
|
|
91
|
+
|
|
92
|
+
# Check for forced string marker
|
|
93
|
+
if isinstance(v, str) and v.startswith('<STRING>'):
|
|
94
|
+
# Forced string - strip marker and quote
|
|
95
|
+
return f'"{v[8:]}"'
|
|
96
|
+
|
|
97
|
+
# Regular value conversion
|
|
98
|
+
if isinstance(v, str):
|
|
99
|
+
if v.startswith('\\:'):
|
|
100
|
+
# Escaped colon - literal string with :
|
|
101
|
+
# "\\:active" → ":active" (remove escape, quote as string)
|
|
102
|
+
return f'"{v[1:]}"'
|
|
103
|
+
elif v.startswith(':'):
|
|
104
|
+
# Unescaped colon - keyword
|
|
105
|
+
return v
|
|
106
|
+
else:
|
|
107
|
+
# Regular string - quote it
|
|
108
|
+
# Escape any quotes inside the string
|
|
109
|
+
escaped = v.replace('\\', '\\\\').replace('"', '\\"')
|
|
110
|
+
return f'"{escaped}"'
|
|
111
|
+
|
|
112
|
+
elif isinstance(v, bool):
|
|
113
|
+
# Must check bool before int (bool is subclass of int in Python)
|
|
114
|
+
return 'true' if v else 'false'
|
|
115
|
+
|
|
116
|
+
elif isinstance(v, (int, float)):
|
|
117
|
+
return str(v)
|
|
118
|
+
|
|
119
|
+
elif v is None:
|
|
120
|
+
return 'nil'
|
|
121
|
+
|
|
122
|
+
elif isinstance(v, list):
|
|
123
|
+
items = [_value_to_edn(item) for item in v]
|
|
124
|
+
return '[' + ' '.join(items) + ']'
|
|
125
|
+
|
|
126
|
+
elif isinstance(v, dict):
|
|
127
|
+
return _dict_to_edn(v)
|
|
128
|
+
|
|
129
|
+
else:
|
|
130
|
+
# Fallback: stringify
|
|
131
|
+
return str(v)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _dict_to_edn(d: Dict[str, Any]) -> str:
|
|
135
|
+
"""Convert Python dict to EDN map.
|
|
136
|
+
|
|
137
|
+
Keys are keywordized, values are converted recursively.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
d: Python dictionary
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
EDN map string
|
|
144
|
+
"""
|
|
145
|
+
items = []
|
|
146
|
+
for k, v in d.items():
|
|
147
|
+
edn_key = _key_to_keyword(k)
|
|
148
|
+
edn_val = _value_to_edn(v)
|
|
149
|
+
items.append(f'{edn_key} {edn_val}')
|
|
150
|
+
return '{' + ' '.join(items) + '}'
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _python_to_edn(data: Union[List, Dict, Any]) -> str:
|
|
154
|
+
"""Convert Python data structure to EDN string.
|
|
155
|
+
|
|
156
|
+
Main entry point for EDN conversion.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
data: Python list, dict, or primitive value
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
EDN string representation
|
|
163
|
+
"""
|
|
164
|
+
if isinstance(data, list):
|
|
165
|
+
items = [_python_to_edn(item) for item in data]
|
|
166
|
+
return '[' + ' '.join(items) + ']'
|
|
167
|
+
elif isinstance(data, dict):
|
|
168
|
+
return _dict_to_edn(data)
|
|
169
|
+
else:
|
|
170
|
+
return _value_to_edn(data)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# =============================================================================
|
|
174
|
+
# Database Class
|
|
175
|
+
# =============================================================================
|
|
176
|
+
|
|
177
|
+
class Database:
|
|
178
|
+
"""High-level Datahike database interface.
|
|
179
|
+
|
|
180
|
+
Wraps low-level API with Pythonic conveniences:
|
|
181
|
+
- Config as dict/kwargs instead of EDN strings
|
|
182
|
+
- Auto-serialize Python objects to JSON
|
|
183
|
+
- Simplified query interface
|
|
184
|
+
- Auto-unwrap result sets
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
config: Database configuration as dict or EDN string
|
|
188
|
+
**kwargs: Config as keyword arguments (for simple store configs)
|
|
189
|
+
|
|
190
|
+
Examples:
|
|
191
|
+
# Nested dict (recommended for complex configs)
|
|
192
|
+
db = Database({"store": {"backend": ":memory", "id": "test"}})
|
|
193
|
+
|
|
194
|
+
# Flat kwargs (convenience for simple store config)
|
|
195
|
+
db = Database(backend=':memory', id='test')
|
|
196
|
+
|
|
197
|
+
# EDN string (escape hatch)
|
|
198
|
+
db = Database('{:store {:backend :memory :id "test"}}')
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
def __init__(
|
|
202
|
+
self,
|
|
203
|
+
config: Union[str, Dict[str, Any], None] = None,
|
|
204
|
+
**kwargs
|
|
205
|
+
):
|
|
206
|
+
if isinstance(config, str):
|
|
207
|
+
# EDN string passthrough
|
|
208
|
+
self._config = config
|
|
209
|
+
|
|
210
|
+
elif isinstance(config, dict):
|
|
211
|
+
# Dict config - convert to EDN
|
|
212
|
+
self._config = _dict_to_edn(config)
|
|
213
|
+
|
|
214
|
+
elif kwargs:
|
|
215
|
+
# Flat kwargs - wrap in :store
|
|
216
|
+
store_config = kwargs
|
|
217
|
+
full_config = {"store": store_config}
|
|
218
|
+
self._config = _dict_to_edn(full_config)
|
|
219
|
+
|
|
220
|
+
else:
|
|
221
|
+
raise ValueError(
|
|
222
|
+
"Must provide config as dict, EDN string, or keyword arguments"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def config(self) -> str:
|
|
227
|
+
"""Get EDN config string (for low-level API compatibility)."""
|
|
228
|
+
return self._config
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def memory(id: str) -> 'Database':
|
|
232
|
+
"""Create in-memory database configuration.
|
|
233
|
+
|
|
234
|
+
Convenience factory for the most common use case.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
id: Database identifier. **Must be a UUID string** (use str(uuid.uuid4())).
|
|
238
|
+
This is required by the konserve store and is essential for distributed
|
|
239
|
+
database tracking.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Database instance with memory backend
|
|
243
|
+
|
|
244
|
+
Example:
|
|
245
|
+
>>> import uuid
|
|
246
|
+
>>> db = Database.memory(str(uuid.uuid4()))
|
|
247
|
+
>>> db.create()
|
|
248
|
+
"""
|
|
249
|
+
return Database({"store": {"backend": ":memory", "id": id}})
|
|
250
|
+
|
|
251
|
+
@staticmethod
|
|
252
|
+
def file(path: str) -> 'Database':
|
|
253
|
+
"""Create file-based database configuration.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
path: File system path for database
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Database instance with file backend
|
|
260
|
+
|
|
261
|
+
Example:
|
|
262
|
+
>>> db = Database.file('/tmp/mydb')
|
|
263
|
+
>>> db.create()
|
|
264
|
+
"""
|
|
265
|
+
return Database({"store": {"backend": ":file", "path": path}})
|
|
266
|
+
|
|
267
|
+
def create(self) -> None:
|
|
268
|
+
"""Create this database.
|
|
269
|
+
|
|
270
|
+
Raises:
|
|
271
|
+
DatahikeException: If database creation fails
|
|
272
|
+
"""
|
|
273
|
+
_gen.create_database(self._config)
|
|
274
|
+
|
|
275
|
+
def delete(self) -> None:
|
|
276
|
+
"""Delete this database.
|
|
277
|
+
|
|
278
|
+
Raises:
|
|
279
|
+
DatahikeException: If database deletion fails
|
|
280
|
+
"""
|
|
281
|
+
_gen.delete_database(self._config)
|
|
282
|
+
|
|
283
|
+
def exists(self) -> bool:
|
|
284
|
+
"""Check if database exists.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
True if database exists, False otherwise
|
|
288
|
+
"""
|
|
289
|
+
return _gen.database_exists(self._config)
|
|
290
|
+
|
|
291
|
+
def transact(
|
|
292
|
+
self,
|
|
293
|
+
data: Union[str, List[Dict], Dict],
|
|
294
|
+
input_format: str = 'json'
|
|
295
|
+
) -> Any:
|
|
296
|
+
"""Transact data into database.
|
|
297
|
+
|
|
298
|
+
Automatically serializes Python objects to JSON or EDN.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
data: Transaction data as Python list/dict or EDN/JSON string
|
|
302
|
+
input_format: 'json' (default) or 'edn'
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Transaction result
|
|
306
|
+
|
|
307
|
+
Raises:
|
|
308
|
+
DatahikeException: If transaction fails
|
|
309
|
+
ValueError: If format is invalid
|
|
310
|
+
|
|
311
|
+
Examples:
|
|
312
|
+
# Python objects (auto-serialized to JSON)
|
|
313
|
+
db.transact([
|
|
314
|
+
{"name": "Alice", "age": 30},
|
|
315
|
+
{"name": "Bob", "age": 25}
|
|
316
|
+
])
|
|
317
|
+
|
|
318
|
+
# Single entity
|
|
319
|
+
db.transact({"name": "Charlie", "age": 35})
|
|
320
|
+
|
|
321
|
+
# With entity ID (update)
|
|
322
|
+
db.transact([{":db/id": 1, "age": 31}])
|
|
323
|
+
|
|
324
|
+
# EDN string (passthrough)
|
|
325
|
+
db.transact('[{:name "Alice"}]', input_format='edn')
|
|
326
|
+
"""
|
|
327
|
+
if isinstance(data, str):
|
|
328
|
+
# Already serialized
|
|
329
|
+
tx_data = data
|
|
330
|
+
|
|
331
|
+
elif isinstance(data, (list, dict)):
|
|
332
|
+
# Python objects - serialize based on format
|
|
333
|
+
if input_format == 'json':
|
|
334
|
+
# Serialize to JSON
|
|
335
|
+
tx_data = json.dumps(data)
|
|
336
|
+
elif input_format == 'edn':
|
|
337
|
+
# Convert Python → EDN
|
|
338
|
+
tx_data = _python_to_edn(data)
|
|
339
|
+
else:
|
|
340
|
+
raise ValueError(
|
|
341
|
+
f"Unknown format: {input_format}. "
|
|
342
|
+
f"Expected 'json' or 'edn'"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
else:
|
|
346
|
+
raise TypeError(f"Cannot transact type: {type(data)}")
|
|
347
|
+
|
|
348
|
+
# Call low-level API
|
|
349
|
+
return _gen.transact(self._config, tx_data, input_format=input_format)
|
|
350
|
+
|
|
351
|
+
def q(
|
|
352
|
+
self,
|
|
353
|
+
query: str,
|
|
354
|
+
*args,
|
|
355
|
+
inputs: Optional[List] = None,
|
|
356
|
+
output_format: OutputFormat = 'cbor',
|
|
357
|
+
unwrap: bool = True
|
|
358
|
+
) -> Any:
|
|
359
|
+
"""Execute Datalog query with smart defaults.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
query: Datalog query string
|
|
363
|
+
*args: Additional inputs (other DBs, params)
|
|
364
|
+
inputs: Override input list completely
|
|
365
|
+
output_format: 'json', 'edn', or 'cbor' (default)
|
|
366
|
+
unwrap: Auto-unwrap '!set' wrapper (default True)
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
Query results (list of tuples by default)
|
|
370
|
+
|
|
371
|
+
Raises:
|
|
372
|
+
DatahikeException: If query fails
|
|
373
|
+
|
|
374
|
+
Examples:
|
|
375
|
+
# Simple - implicit 'db' input
|
|
376
|
+
results = db.q('[:find ?name :where [?e :name ?name]]')
|
|
377
|
+
|
|
378
|
+
# With parameter
|
|
379
|
+
results = db.q(
|
|
380
|
+
'[:find ?e :in $ ?name :where [?e :name ?name]]',
|
|
381
|
+
('param', '"Alice"')
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# Multiple databases
|
|
385
|
+
results = db.q(
|
|
386
|
+
'[:find ?name :in $ $2 :where ...]',
|
|
387
|
+
other_db # Another Database instance
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
# Get raw result (no unwrap)
|
|
391
|
+
raw = db.q(query, unwrap=False)
|
|
392
|
+
"""
|
|
393
|
+
# Build inputs list
|
|
394
|
+
if inputs is None:
|
|
395
|
+
# Start with this database as primary input
|
|
396
|
+
query_inputs = [('db', self._config)]
|
|
397
|
+
|
|
398
|
+
# Process additional args
|
|
399
|
+
for arg in args:
|
|
400
|
+
if isinstance(arg, Database):
|
|
401
|
+
# Another Database instance
|
|
402
|
+
query_inputs.append(('db', arg.config))
|
|
403
|
+
elif isinstance(arg, DatabaseSnapshot):
|
|
404
|
+
# Snapshot with specific input format
|
|
405
|
+
query_inputs.append((arg._input_format, arg._config))
|
|
406
|
+
elif isinstance(arg, tuple):
|
|
407
|
+
# Direct input tuple like ('param', 'value')
|
|
408
|
+
query_inputs.append(arg)
|
|
409
|
+
else:
|
|
410
|
+
# Assume it's a parameter value
|
|
411
|
+
query_inputs.append(('param', str(arg)))
|
|
412
|
+
else:
|
|
413
|
+
# User provided complete inputs list
|
|
414
|
+
query_inputs = inputs
|
|
415
|
+
|
|
416
|
+
# Execute low-level query
|
|
417
|
+
result = _gen.q(query, query_inputs, output_format=output_format)
|
|
418
|
+
|
|
419
|
+
# Auto-unwrap if requested
|
|
420
|
+
if unwrap and isinstance(result, (list, tuple)):
|
|
421
|
+
# Check for CBOR '!set' wrapper
|
|
422
|
+
if len(result) == 2 and result[0] == '!set':
|
|
423
|
+
return result[1]
|
|
424
|
+
|
|
425
|
+
return result
|
|
426
|
+
|
|
427
|
+
def pull(
|
|
428
|
+
self,
|
|
429
|
+
pattern: Union[str, List],
|
|
430
|
+
eid: int,
|
|
431
|
+
input_format: InputFormat = 'db',
|
|
432
|
+
output_format: OutputFormat = 'cbor'
|
|
433
|
+
) -> Optional[Dict[str, Any]]:
|
|
434
|
+
"""Pull entity by pattern.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
pattern: Pull pattern as string or list
|
|
438
|
+
eid: Entity ID
|
|
439
|
+
input_format: 'db', 'history', 'since:<ts>', 'asof:<ts>'
|
|
440
|
+
output_format: Output format
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
Entity dict or None if not found
|
|
444
|
+
|
|
445
|
+
Examples:
|
|
446
|
+
# Pull with pattern
|
|
447
|
+
entity = db.pull('[:name :age]', 1)
|
|
448
|
+
|
|
449
|
+
# Pull all attributes
|
|
450
|
+
entity = db.pull('[*]', 1)
|
|
451
|
+
|
|
452
|
+
# Pull with relationships
|
|
453
|
+
entity = db.pull('[:name {:friends [:name]}]', 1)
|
|
454
|
+
"""
|
|
455
|
+
if isinstance(pattern, list):
|
|
456
|
+
# Convert Python list to EDN-like string
|
|
457
|
+
# Simple conversion for common cases
|
|
458
|
+
pattern = str(pattern).replace("'", ":")
|
|
459
|
+
|
|
460
|
+
return _gen.pull(
|
|
461
|
+
self._config,
|
|
462
|
+
pattern,
|
|
463
|
+
eid,
|
|
464
|
+
input_format=input_format,
|
|
465
|
+
output_format=output_format
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
def pull_many(
|
|
469
|
+
self,
|
|
470
|
+
pattern: Union[str, List],
|
|
471
|
+
eids: List[int],
|
|
472
|
+
input_format: InputFormat = 'db',
|
|
473
|
+
output_format: OutputFormat = 'cbor'
|
|
474
|
+
) -> List[Dict[str, Any]]:
|
|
475
|
+
"""Pull multiple entities by pattern.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
pattern: Pull pattern
|
|
479
|
+
eids: List of entity IDs
|
|
480
|
+
input_format: Input format
|
|
481
|
+
output_format: Output format
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
List of entity dicts
|
|
485
|
+
"""
|
|
486
|
+
if isinstance(pattern, list):
|
|
487
|
+
pattern = str(pattern).replace("'", ":")
|
|
488
|
+
|
|
489
|
+
# Convert Python list to EDN
|
|
490
|
+
eids_edn = str(eids)
|
|
491
|
+
|
|
492
|
+
return _gen.pull_many(
|
|
493
|
+
self._config,
|
|
494
|
+
pattern,
|
|
495
|
+
eids_edn,
|
|
496
|
+
input_format=input_format,
|
|
497
|
+
output_format=output_format
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
def entity(
|
|
501
|
+
self,
|
|
502
|
+
eid: int,
|
|
503
|
+
input_format: InputFormat = 'db',
|
|
504
|
+
output_format: OutputFormat = 'cbor'
|
|
505
|
+
) -> Optional[Dict[str, Any]]:
|
|
506
|
+
"""Get entity by ID.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
eid: Entity ID
|
|
510
|
+
input_format: Input format
|
|
511
|
+
output_format: Output format
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
Entity dict or None
|
|
515
|
+
"""
|
|
516
|
+
return _gen.entity(
|
|
517
|
+
self._config,
|
|
518
|
+
eid,
|
|
519
|
+
input_format=input_format,
|
|
520
|
+
output_format=output_format
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
def schema(
|
|
524
|
+
self,
|
|
525
|
+
output_format: OutputFormat = 'cbor'
|
|
526
|
+
) -> Any:
|
|
527
|
+
"""Get database schema.
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
output_format: Output format
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
Schema data
|
|
534
|
+
"""
|
|
535
|
+
return _gen.schema(self._config, output_format=output_format)
|
|
536
|
+
|
|
537
|
+
def as_of(self, timestamp_ms: int) -> 'DatabaseSnapshot':
|
|
538
|
+
"""Get database snapshot at specific timestamp.
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
timestamp_ms: Unix timestamp in milliseconds
|
|
542
|
+
|
|
543
|
+
Returns:
|
|
544
|
+
DatabaseSnapshot that queries at that point in time
|
|
545
|
+
|
|
546
|
+
Example:
|
|
547
|
+
>>> past = db.as_of(1234567890)
|
|
548
|
+
>>> results = past.q('[:find ?name :where [?e :name ?name]]')
|
|
549
|
+
"""
|
|
550
|
+
return DatabaseSnapshot(self._config, f'asof:{timestamp_ms}')
|
|
551
|
+
|
|
552
|
+
def since(self, timestamp_ms: int) -> 'DatabaseSnapshot':
|
|
553
|
+
"""Get changes since timestamp.
|
|
554
|
+
|
|
555
|
+
Args:
|
|
556
|
+
timestamp_ms: Unix timestamp in milliseconds
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
DatabaseSnapshot showing changes since that time
|
|
560
|
+
"""
|
|
561
|
+
return DatabaseSnapshot(self._config, f'since:{timestamp_ms}')
|
|
562
|
+
|
|
563
|
+
@property
|
|
564
|
+
def history(self) -> 'DatabaseSnapshot':
|
|
565
|
+
"""Get full history view (all transactions).
|
|
566
|
+
|
|
567
|
+
Returns:
|
|
568
|
+
DatabaseSnapshot with complete history
|
|
569
|
+
|
|
570
|
+
Example:
|
|
571
|
+
>>> all_changes = db.history.q('[:find ?name :where [?e :name ?name]]')
|
|
572
|
+
"""
|
|
573
|
+
return DatabaseSnapshot(self._config, 'history')
|
|
574
|
+
|
|
575
|
+
def __enter__(self):
|
|
576
|
+
"""Context manager entry - create database."""
|
|
577
|
+
self.create()
|
|
578
|
+
return self
|
|
579
|
+
|
|
580
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
581
|
+
"""Context manager exit - delete database."""
|
|
582
|
+
try:
|
|
583
|
+
self.delete()
|
|
584
|
+
except Exception:
|
|
585
|
+
pass # Suppress cleanup errors
|
|
586
|
+
return False
|
|
587
|
+
|
|
588
|
+
def __repr__(self):
|
|
589
|
+
return f'Database({self._config})'
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
# =============================================================================
|
|
593
|
+
# Database Snapshot (Time Travel)
|
|
594
|
+
# =============================================================================
|
|
595
|
+
|
|
596
|
+
class DatabaseSnapshot:
|
|
597
|
+
"""Read-only snapshot of database at a point in time.
|
|
598
|
+
|
|
599
|
+
This is a lightweight object that overrides the input format
|
|
600
|
+
when executing queries, enabling time-travel queries.
|
|
601
|
+
|
|
602
|
+
Args:
|
|
603
|
+
config: EDN config string (from parent Database)
|
|
604
|
+
input_format: 'db', 'history', 'asof:<ts>', 'since:<ts>'
|
|
605
|
+
"""
|
|
606
|
+
|
|
607
|
+
def __init__(self, config: str, input_format: str):
|
|
608
|
+
self._config = config
|
|
609
|
+
self._input_format = input_format
|
|
610
|
+
|
|
611
|
+
def q(
|
|
612
|
+
self,
|
|
613
|
+
query: str,
|
|
614
|
+
*args,
|
|
615
|
+
output_format: OutputFormat = 'cbor',
|
|
616
|
+
unwrap: bool = True
|
|
617
|
+
) -> Any:
|
|
618
|
+
"""Query this snapshot.
|
|
619
|
+
|
|
620
|
+
Works like Database.q() but uses snapshot's input format.
|
|
621
|
+
|
|
622
|
+
Args:
|
|
623
|
+
query: Datalog query string
|
|
624
|
+
*args: Additional inputs
|
|
625
|
+
output_format: Output format
|
|
626
|
+
unwrap: Auto-unwrap results
|
|
627
|
+
|
|
628
|
+
Returns:
|
|
629
|
+
Query results
|
|
630
|
+
"""
|
|
631
|
+
# Build inputs with our format
|
|
632
|
+
query_inputs = [(self._input_format, self._config)]
|
|
633
|
+
|
|
634
|
+
# Add any additional inputs from args
|
|
635
|
+
for arg in args:
|
|
636
|
+
if isinstance(arg, Database):
|
|
637
|
+
query_inputs.append(('db', arg.config))
|
|
638
|
+
elif isinstance(arg, DatabaseSnapshot):
|
|
639
|
+
query_inputs.append((arg._input_format, arg._config))
|
|
640
|
+
elif isinstance(arg, tuple):
|
|
641
|
+
query_inputs.append(arg)
|
|
642
|
+
else:
|
|
643
|
+
query_inputs.append(('param', str(arg)))
|
|
644
|
+
|
|
645
|
+
# Execute query
|
|
646
|
+
result = _gen.q(query, query_inputs, output_format=output_format)
|
|
647
|
+
|
|
648
|
+
# Auto-unwrap
|
|
649
|
+
if unwrap and isinstance(result, (list, tuple)):
|
|
650
|
+
if len(result) == 2 and result[0] == '!set':
|
|
651
|
+
return result[1]
|
|
652
|
+
|
|
653
|
+
return result
|
|
654
|
+
|
|
655
|
+
def pull(
|
|
656
|
+
self,
|
|
657
|
+
pattern: Union[str, List],
|
|
658
|
+
eid: int,
|
|
659
|
+
output_format: OutputFormat = 'cbor'
|
|
660
|
+
) -> Optional[Dict[str, Any]]:
|
|
661
|
+
"""Pull from this snapshot.
|
|
662
|
+
|
|
663
|
+
Args:
|
|
664
|
+
pattern: Pull pattern
|
|
665
|
+
eid: Entity ID
|
|
666
|
+
output_format: Output format
|
|
667
|
+
|
|
668
|
+
Returns:
|
|
669
|
+
Entity dict or None
|
|
670
|
+
"""
|
|
671
|
+
if isinstance(pattern, list):
|
|
672
|
+
pattern = str(pattern).replace("'", ":")
|
|
673
|
+
|
|
674
|
+
return _gen.pull(
|
|
675
|
+
self._config,
|
|
676
|
+
pattern,
|
|
677
|
+
eid,
|
|
678
|
+
input_format=self._input_format,
|
|
679
|
+
output_format=output_format
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
def __repr__(self):
|
|
683
|
+
return f'DatabaseSnapshot({self._input_format}, {self._config})'
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
# =============================================================================
|
|
687
|
+
# Convenience Context Manager
|
|
688
|
+
# =============================================================================
|
|
689
|
+
|
|
690
|
+
@contextmanager
|
|
691
|
+
def database(config: Union[str, Dict[str, Any], None] = None, **kwargs) -> Iterator[Database]:
|
|
692
|
+
"""Context manager for database lifecycle.
|
|
693
|
+
|
|
694
|
+
Automatically creates database on entry and deletes on exit.
|
|
695
|
+
Useful for tests and temporary databases.
|
|
696
|
+
|
|
697
|
+
Args:
|
|
698
|
+
config: Database configuration
|
|
699
|
+
**kwargs: Config as keyword arguments
|
|
700
|
+
|
|
701
|
+
Yields:
|
|
702
|
+
Database instance
|
|
703
|
+
|
|
704
|
+
Example:
|
|
705
|
+
>>> with database(backend=':memory', id='test') as db:
|
|
706
|
+
... db.transact([{'name': 'Alice'}])
|
|
707
|
+
... result = db.q('[:find ?name :where [?e :name ?name]]')
|
|
708
|
+
... print(result)
|
|
709
|
+
"""
|
|
710
|
+
db = Database(config, **kwargs) if config or kwargs else None
|
|
711
|
+
if db is None:
|
|
712
|
+
raise ValueError("Must provide config")
|
|
713
|
+
|
|
714
|
+
db.create()
|
|
715
|
+
try:
|
|
716
|
+
yield db
|
|
717
|
+
finally:
|
|
718
|
+
try:
|
|
719
|
+
db.delete()
|
|
720
|
+
except Exception:
|
|
721
|
+
# Suppress cleanup errors - database may already be deleted
|
|
722
|
+
pass
|