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,566 @@
|
|
|
1
|
+
# Distributed Architecture
|
|
2
|
+
|
|
3
|
+
Datahike's architecture is built on **immutable persistent data structures** that enable efficient distribution and collaboration. The database is fundamentally designed around two complementary approaches:
|
|
4
|
+
|
|
5
|
+
1. **Distributed Index Space (DIS)**: Share persistent indices across processes—readers access data directly without database connections
|
|
6
|
+
2. **Remote Procedure Calls (RPC)**: Centralize computation on a server for shared caching and simplified deployment
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
# Distributed Index Space (DIS)
|
|
11
|
+
|
|
12
|
+
**Distributed Index Space is Datahike's key architectural advantage.** It enables massive read scalability and powers collaborative systems by treating database snapshots as immutable values that can be shared like files.
|
|
13
|
+
|
|
14
|
+
## How it works
|
|
15
|
+
|
|
16
|
+
Datahike builds on **copy-on-write persistent data structures** where changes create new structure sharing most data with previous versions. When you transact to a database:
|
|
17
|
+
|
|
18
|
+
1. New index nodes are written to the shared [storage backend](storage-backends.md) (S3, JDBC, file, etc.)
|
|
19
|
+
2. A new root pointer is published atomically
|
|
20
|
+
3. Readers pick up the new snapshot on next access—no active connections needed
|
|
21
|
+
|
|
22
|
+
This is similar to [Datomic](https://datomic.com), but **Datahike connections are lightweight and require no communication by default**. If you only need to read from a database (e.g., a dataset provided by a third party), you just need read access to the storage—no server setup required.
|
|
23
|
+
|
|
24
|
+
## Scaling and collaboration
|
|
25
|
+
|
|
26
|
+
The DIS model provides fundamental advantages for distributed systems:
|
|
27
|
+
|
|
28
|
+
- **Massive read scaling**: Add readers without coordination—they access persistent indices directly
|
|
29
|
+
- **Zero connection overhead**: No connection pooling, no network round-trips for reads
|
|
30
|
+
- **Snapshot isolation**: Each reader sees a consistent point-in-time view
|
|
31
|
+
- **Efficient sharding**: Create one database per logical unit (e.g., per customer, per project)—readers can join across databases locally
|
|
32
|
+
- **Offline-first capable**: Readers can cache indices locally and sync differentially when online
|
|
33
|
+
|
|
34
|
+
This architecture enables collaborative systems where multiple processes share access to evolving datasets without centralized coordination. The same design principles that enable DIS (immutability, structural sharing) also support more advanced distribution patterns including CRDT-based merge strategies (see [replikativ](https://github.com/replikativ/replikativ)) and peer-to-peer synchronization (demonstrated with [dat-sync](https://github.com/replikativ/dat-sync)).
|
|
35
|
+
|
|
36
|
+
These capabilities are valuable even in centralized production environments: differential sync reduces bandwidth, immutable snapshots simplify caching and recovery, and the architecture naturally handles network partitions.
|
|
37
|
+
|
|
38
|
+
## Single writer model
|
|
39
|
+
|
|
40
|
+
Datahike uses a **single-writer, multiple-reader** model—the same architectural choice as Datomic, Datalevin, and XTDB. While multiple readers can access indices concurrently via DIS, write operations are serialized through a single writer process to ensure strong consistency and linearizable transactions.
|
|
41
|
+
|
|
42
|
+
To provide distributed write access, you configure a writer endpoint (HTTP server or Kabel WebSocket). The writer:
|
|
43
|
+
- Serializes all transactions for strong consistency guarantees
|
|
44
|
+
- Publishes new index snapshots to the shared storage backend
|
|
45
|
+
- Allows unlimited readers to access the updated indices via DIS
|
|
46
|
+
|
|
47
|
+
**All readers continue to access data locally** from the distributed storage (shared filesystem, JDBC, S3, etc.) without connecting to the writer—they only contact it to submit transactions. This model is supported by all Datahike clients: JVM, Node.js, browser, CLI, Babashka pod, and libdatahike.
|
|
48
|
+
|
|
49
|
+
The client setup is simple, you just add a `:writer` entry in the configuration
|
|
50
|
+
for your database, e.g.
|
|
51
|
+
|
|
52
|
+
```clojure
|
|
53
|
+
{:store {:backend :file
|
|
54
|
+
:id #uuid "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
|
55
|
+
:path "/shared/filesystem/store"}
|
|
56
|
+
:keep-history? true
|
|
57
|
+
:schema-flexibility :read
|
|
58
|
+
:writer {:backend :datahike-server
|
|
59
|
+
:url "http://localhost:4444"
|
|
60
|
+
:token "securerandompassword"}}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
You can now use the normal `datahike.api` as usual and all operations changing a
|
|
64
|
+
database, e.g. `create-database`, `delete-database` and `transact` are sent to
|
|
65
|
+
the server while all other calls are executed locally.
|
|
66
|
+
|
|
67
|
+
### AWS lambda
|
|
68
|
+
|
|
69
|
+
An example setup to run Datahike distributed in AWS lambda without a server can
|
|
70
|
+
be found [here](https://github.com/viesti/clj-lambda-datahike). It configures a
|
|
71
|
+
singleton lambda for write operations while reader lambdas can be run multiple
|
|
72
|
+
times and scale out. This setup can be upgraded later to use dedicated servers
|
|
73
|
+
through EC2 instances.
|
|
74
|
+
|
|
75
|
+
### Streaming writer (Kabel)
|
|
76
|
+
|
|
77
|
+
**Beta feature - please try it out and provide feedback.**
|
|
78
|
+
|
|
79
|
+
The Kabel writer provides **real-time reactive updates** via WebSockets, complementing the HTTP server's REST API. Where HTTP server is ideal for conventional REST integrations (including non-Clojure clients), Kabel enables live synchronization where clients receive database updates as they happen, without polling.
|
|
80
|
+
|
|
81
|
+
The stack consists of:
|
|
82
|
+
|
|
83
|
+
- [kabel](https://github.com/replikativ/kabel) - WebSocket transport with middleware support
|
|
84
|
+
- [distributed-scope](https://github.com/replikativ/distributed-scope) - Remote function invocation with Clojure semantics
|
|
85
|
+
- [konserve-sync](https://github.com/replikativ/konserve-sync) - Differential store synchronization (only transmits changed data)
|
|
86
|
+
|
|
87
|
+
This setup is particularly useful for browser clients where storage backends cannot be shared directly, and for applications requiring reactive UIs that update automatically when data changes on the server (see [JavaScript API](javascript-api.md)).
|
|
88
|
+
|
|
89
|
+
#### Server setup
|
|
90
|
+
|
|
91
|
+
The server owns the database and handles all write operations. It uses a file
|
|
92
|
+
backend and broadcasts updates to connected clients via konserve-sync.
|
|
93
|
+
|
|
94
|
+
```clojure
|
|
95
|
+
(ns my-app.server
|
|
96
|
+
(:require [datahike.api :as d]
|
|
97
|
+
[datahike.kabel.handlers :as handlers]
|
|
98
|
+
[datahike.kabel.fressian-handlers :as fh]
|
|
99
|
+
[kabel.peer :as peer]
|
|
100
|
+
[kabel.http-kit :refer [create-http-kit-handler!]]
|
|
101
|
+
[konserve-sync.core :as sync]
|
|
102
|
+
[is.simm.distributed-scope :refer [remote-middleware invoke-on-peer]]
|
|
103
|
+
[superv.async :refer [S go-try <?]]
|
|
104
|
+
[clojure.core.async :refer [<!!]]))
|
|
105
|
+
|
|
106
|
+
(def server-id #uuid "aaaaaaaa-0000-0000-0000-000000000001")
|
|
107
|
+
(def server-url "ws://localhost:47296")
|
|
108
|
+
|
|
109
|
+
;; Fressian middleware with Datahike type handlers for serialization
|
|
110
|
+
(defn datahike-fressian-middleware [peer-config]
|
|
111
|
+
(kabel.middleware.fressian/fressian
|
|
112
|
+
(atom fh/read-handlers)
|
|
113
|
+
(atom fh/write-handlers)
|
|
114
|
+
peer-config))
|
|
115
|
+
|
|
116
|
+
;; Store config factory - maps client store UUID to server-side file store
|
|
117
|
+
;; Browsers use TieredStore (memory + IndexedDB), but the server uses file backend
|
|
118
|
+
;; The store-id parameter is the UUID from the client's :store :id field
|
|
119
|
+
(defn store-config-fn [store-id _client-config]
|
|
120
|
+
{:backend :file
|
|
121
|
+
:path (str "/var/data/datahike/" store-id)
|
|
122
|
+
:id store-id})
|
|
123
|
+
|
|
124
|
+
(defn start-server! []
|
|
125
|
+
(let [;; Create kabel server peer with middleware stack:
|
|
126
|
+
;; - sync/server-middleware: handles konserve-sync replication
|
|
127
|
+
;; - remote-middleware: handles distributed-scope RPC
|
|
128
|
+
;; - datahike-fressian-middleware: serializes Datahike types
|
|
129
|
+
server (peer/server-peer
|
|
130
|
+
S
|
|
131
|
+
(create-http-kit-handler! S server-url server-id)
|
|
132
|
+
server-id
|
|
133
|
+
(comp (sync/server-middleware) remote-middleware)
|
|
134
|
+
datahike-fressian-middleware)]
|
|
135
|
+
|
|
136
|
+
;; Start server and enable remote function invocation
|
|
137
|
+
(<!! (peer/start server))
|
|
138
|
+
(invoke-on-peer server)
|
|
139
|
+
|
|
140
|
+
;; Register global Datahike handlers for create-database, delete-database, transact
|
|
141
|
+
;; The :store-config-fn translates client config to server-side store config
|
|
142
|
+
(handlers/register-global-handlers! server {:store-config-fn store-config-fn})
|
|
143
|
+
|
|
144
|
+
server))
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
#### Browser client setup
|
|
148
|
+
|
|
149
|
+
Browser clients use a TieredStore combining fast in-memory access with
|
|
150
|
+
persistent IndexedDB storage. The KabelWriter sends transactions to the server,
|
|
151
|
+
and konserve-sync replicates updates back to the client's store.
|
|
152
|
+
|
|
153
|
+
**Store IDs**: Store IDs should be UUIDs for distributed coordination. Use a
|
|
154
|
+
fixed UUID when multiple clients need to share the same database, or generate a
|
|
155
|
+
unique UUID with `(random-uuid)` for ephemeral/test databases.
|
|
156
|
+
|
|
157
|
+
```clojure
|
|
158
|
+
(ns my-app.client
|
|
159
|
+
(:require [cljs.core.async :refer [<! timeout alts!] :refer-macros [go]]
|
|
160
|
+
[datahike.api :as d]
|
|
161
|
+
[datahike.kabel.fressian-handlers :refer [datahike-fressian-middleware]]
|
|
162
|
+
[is.simm.distributed-scope :as ds]
|
|
163
|
+
[kabel.peer :as peer]
|
|
164
|
+
[konserve-sync.core :as sync]
|
|
165
|
+
[superv.async :refer [S] :refer-macros [go-try <?]]))
|
|
166
|
+
|
|
167
|
+
(def server-url "ws://localhost:47296")
|
|
168
|
+
(def server-id #uuid "aaaaaaaa-0000-0000-0000-000000000001")
|
|
169
|
+
(def client-id #uuid "bbbbbbbb-0000-0000-0000-000000000002")
|
|
170
|
+
|
|
171
|
+
(defonce client-peer (atom nil))
|
|
172
|
+
|
|
173
|
+
(defn init-peer! []
|
|
174
|
+
;; Create client peer with middleware stack (innermost runs first):
|
|
175
|
+
;; - ds/remote-middleware: handles distributed-scope RPC responses
|
|
176
|
+
;; - sync/client-middleware: handles konserve-sync replication
|
|
177
|
+
(let [peer-atom (peer/client-peer
|
|
178
|
+
S
|
|
179
|
+
client-id
|
|
180
|
+
(comp ds/remote-middleware (sync/client-middleware))
|
|
181
|
+
datahike-fressian-middleware)]
|
|
182
|
+
;; Start invocation loop for handling remote calls
|
|
183
|
+
(ds/invoke-on-peer peer-atom)
|
|
184
|
+
(reset! client-peer peer-atom)))
|
|
185
|
+
|
|
186
|
+
(defn example []
|
|
187
|
+
;; go-try/<? from superv.async propagate errors through async channels
|
|
188
|
+
;; Use go/<! if you prefer manual error handling
|
|
189
|
+
(go-try S
|
|
190
|
+
;; Connect to server via distributed-scope
|
|
191
|
+
(<? S (ds/connect-distributed-scope S @client-peer server-url))
|
|
192
|
+
|
|
193
|
+
(let [store-id (random-uuid)
|
|
194
|
+
db-name (str "db-" store-id)
|
|
195
|
+
;; TieredStore: memory frontend for fast reads, IndexedDB for persistence
|
|
196
|
+
;; The server uses file backend - store-config-fn handles this translation
|
|
197
|
+
;; Note: All :id values must match for konserve validation
|
|
198
|
+
config {:store {:backend :tiered
|
|
199
|
+
:frontend-config {:backend :memory :id store-id}
|
|
200
|
+
:backend-config {:backend :indexeddb :name db-name :id store-id}
|
|
201
|
+
:id store-id}
|
|
202
|
+
:writer {:backend :kabel
|
|
203
|
+
:peer-id server-id
|
|
204
|
+
:local-peer @client-peer}
|
|
205
|
+
:schema-flexibility :write
|
|
206
|
+
:keep-history? false}]
|
|
207
|
+
|
|
208
|
+
;; Create database on server (transmitted via distributed-scope RPC)
|
|
209
|
+
(<? S (d/create-database config))
|
|
210
|
+
|
|
211
|
+
;; Connect locally - syncs initial state from server via konserve-sync
|
|
212
|
+
;; TieredStore caches data from IndexedDB into memory before subscribing
|
|
213
|
+
;; so the sync handshake only requests keys newer than cached timestamps
|
|
214
|
+
(let [conn (<? S (d/connect config {:sync? false}))]
|
|
215
|
+
|
|
216
|
+
;; Transact schema - sent to server, then synced back to local store
|
|
217
|
+
(<? S (d/transact! conn [{:db/ident :name
|
|
218
|
+
:db/valueType :db.type/string
|
|
219
|
+
:db/cardinality :db.cardinality/one}]))
|
|
220
|
+
|
|
221
|
+
;; Transact data
|
|
222
|
+
(<? S (d/transact! conn [{:name "Alice"} {:name "Bob"}]))
|
|
223
|
+
|
|
224
|
+
;; Query locally - no network round-trip needed
|
|
225
|
+
(let [db (d/db conn)
|
|
226
|
+
results (d/q '[:find ?name :where [?e :name ?name]] db)]
|
|
227
|
+
(println "Found:" results)) ;; => #{["Alice"] ["Bob"]}
|
|
228
|
+
|
|
229
|
+
;; Clean up
|
|
230
|
+
(d/release conn)
|
|
231
|
+
(<? S (d/delete-database config))))))
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
# Remote Procedure Calls (RPC)
|
|
235
|
+
|
|
236
|
+
In addition to DIS, Datahike supports **remote procedure calls** where all operations (reads and writes) are executed on a server. This approach is complementary to DIS:
|
|
237
|
+
|
|
238
|
+
**Use RPC when:**
|
|
239
|
+
- You want simplified deployment (thin clients, all logic on server)
|
|
240
|
+
- Shared server-side caching benefits multiple clients
|
|
241
|
+
- Clients are resource-constrained (mobile, embedded)
|
|
242
|
+
- You need conventional REST integration with non-Clojure clients
|
|
243
|
+
|
|
244
|
+
**Use DIS when:**
|
|
245
|
+
- Read scalability is critical (unlimited readers without server load)
|
|
246
|
+
- You want offline-capable or low-latency reads
|
|
247
|
+
- Clients need to run custom queries with local functions/closures
|
|
248
|
+
- Network bandwidth or availability is a concern
|
|
249
|
+
|
|
250
|
+
The remote API has the same call signatures as `datahike.api` and is located in `datahike.api.client`. All functionality except `listen!` and `with` is supported. To use it, add `:remote-peer` to your config:
|
|
251
|
+
|
|
252
|
+
```clojure
|
|
253
|
+
{:store {:backend :memory :id "distributed-datahike"}
|
|
254
|
+
:keep-history? true
|
|
255
|
+
:schema-flexibility :read
|
|
256
|
+
:remote-peer {:backend :datahike-server
|
|
257
|
+
:url "http://localhost:4444"
|
|
258
|
+
:token "securerandompassword"}}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
The API will return lightweight remote pointers that follow the same semantics
|
|
262
|
+
as `datahike.api`, but do not support any of Datahike's local functionality,
|
|
263
|
+
i.e. you can only use them with this API.
|
|
264
|
+
|
|
265
|
+
# Hybrid Architecture
|
|
266
|
+
|
|
267
|
+
You can combine DIS and RPC in the same deployment. For example:
|
|
268
|
+
- A set of application servers access a shared database via DIS (direct index access)
|
|
269
|
+
- These servers expose RPC/REST APIs to external clients
|
|
270
|
+
- Internal servers benefit from DIS scalability and local query execution
|
|
271
|
+
- External clients get a simple REST interface without needing Datahike dependencies
|
|
272
|
+
|
|
273
|
+
This pattern is common in production systems where internal services need high-performance data access while external integrations require conventional APIs.
|
|
274
|
+
|
|
275
|
+
# HTTP Server Setup
|
|
276
|
+
|
|
277
|
+
The HTTP server provides a **REST/RPC interface** for conventional integrations with any language or tool that speaks HTTP. Use this when you need request/response semantics rather than reactive updates (for reactive updates, see Kabel above).
|
|
278
|
+
|
|
279
|
+
To build locally, clone the repository and run `bb http-server-uber` to create the jar. Run the server with:
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
java -jar datahike-http-server-VERSION.jar path/to/config.edn
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
The edn configuration file looks like:
|
|
286
|
+
|
|
287
|
+
```clojure
|
|
288
|
+
{:port 4444
|
|
289
|
+
:level :debug
|
|
290
|
+
:dev-mode true
|
|
291
|
+
:token "securerandompassword"}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Port sets the `port` to run the HTTP server under, `level` sets the log-level.
|
|
295
|
+
`dev-mode` deactivates authentication during development and if `token` is
|
|
296
|
+
provided then you need to send this token as the HTTP header "token" to
|
|
297
|
+
authenticate.
|
|
298
|
+
|
|
299
|
+
The server exports a swagger interface on the port and can serialize requests in
|
|
300
|
+
`transit-json`, `edn` and `JSON` with
|
|
301
|
+
[jsonista](https://github.com/metosin/jsonista) tagged literals. The server
|
|
302
|
+
exposes all referentially transparent calls (that don't change given their
|
|
303
|
+
arguments) as GET requests and all requests that depend on input information as
|
|
304
|
+
POST requests. All arguments in both cases are sent as a list *in the request
|
|
305
|
+
body*.
|
|
306
|
+
|
|
307
|
+
### Extended configuration
|
|
308
|
+
|
|
309
|
+
CORS headers can be set, e.g. with adding
|
|
310
|
+
```clojure
|
|
311
|
+
:access-control-allow-origin [#"http://localhost" #"http://localhost:8080"]
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
The server also experimentally supports HTTP caching for GET requests, e.g. by adding
|
|
315
|
+
```clojure
|
|
316
|
+
:cache {:get {:max-age 3600}}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
This should be beneficially in case your HTTP client or proxy supports efficient
|
|
320
|
+
caching and you often run the same queries many times on different queries (e.g.
|
|
321
|
+
to retrieve a daily context in an app against a database only changes with low
|
|
322
|
+
frequency.)
|
|
323
|
+
|
|
324
|
+
# JSON Support (HTTP Server)
|
|
325
|
+
|
|
326
|
+
The HTTP server supports JSON with embedded [tagged literals](https://github.com/metosin/jsonista#tagged-json) for language-agnostic integration. This allows non-Clojure clients (JavaScript, Python, etc.) to interact with Datahike using familiar JSON syntax.
|
|
327
|
+
|
|
328
|
+
When sending HTTP requests to the datahike-server, you can use JSON argument arrays in each method body. Include the "token" header if authentication is enabled.
|
|
329
|
+
|
|
330
|
+
`POST` to "/create-database"
|
|
331
|
+
```javascript
|
|
332
|
+
["{:schema-flexibility :read}"]
|
|
333
|
+
```
|
|
334
|
+
Note that here you can pass the configuration as an `edn` string, which is more concise. If you want to speak JSON directly you would pass
|
|
335
|
+
```
|
|
336
|
+
[{"schema-flexibility": ["!kw", "read"]}]
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
"!kw" annotates a tagged literal here and encodes that "read" is an `edn`
|
|
340
|
+
keyword.
|
|
341
|
+
|
|
342
|
+
The resulting configuration will look like (with random DB name):
|
|
343
|
+
```javascript
|
|
344
|
+
cfg = {
|
|
345
|
+
"keep-history?": true,
|
|
346
|
+
"search-cache-size": 10000,
|
|
347
|
+
"index": [
|
|
348
|
+
"!kw",
|
|
349
|
+
"datahike.index/persistent-set"
|
|
350
|
+
],
|
|
351
|
+
"store": {
|
|
352
|
+
"id": "wiggly-field-vole",
|
|
353
|
+
"backend": [
|
|
354
|
+
"!kw",
|
|
355
|
+
"memory"
|
|
356
|
+
]
|
|
357
|
+
},
|
|
358
|
+
"store-cache-size": 1000,
|
|
359
|
+
"attribute-refs?": false,
|
|
360
|
+
"writer": {
|
|
361
|
+
"backend": [
|
|
362
|
+
"!kw",
|
|
363
|
+
"self"
|
|
364
|
+
]
|
|
365
|
+
},
|
|
366
|
+
"crypto-hash?": false,
|
|
367
|
+
"remote-peer": null,
|
|
368
|
+
"schema-flexibility": [
|
|
369
|
+
"!kw",
|
|
370
|
+
"read"
|
|
371
|
+
],
|
|
372
|
+
"branch": [
|
|
373
|
+
"!kw",
|
|
374
|
+
"db"
|
|
375
|
+
]
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
You can now use this cfg to connect to this database:
|
|
380
|
+
|
|
381
|
+
`POST` to "/connect"
|
|
382
|
+
```javascript
|
|
383
|
+
[cfg]
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
The result will look like:
|
|
387
|
+
|
|
388
|
+
```javascript
|
|
389
|
+
conn = ["!datahike/Connection",[[["!kw","memory"],"wiggly-field-vole"],["!kw","db"]]]
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Finally let's add some data to the database:
|
|
393
|
+
|
|
394
|
+
`POST` to "/transact"
|
|
395
|
+
```javascript
|
|
396
|
+
[conn, [{"name": "Peter", "age": 42}]]
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
The result is a comprehensive transaction record (feel free to ignore the details):
|
|
400
|
+
|
|
401
|
+
```javascript
|
|
402
|
+
[
|
|
403
|
+
"!datahike/TxReport",
|
|
404
|
+
{
|
|
405
|
+
"db-before": [
|
|
406
|
+
"!datahike/DB",
|
|
407
|
+
{
|
|
408
|
+
"store-id": [
|
|
409
|
+
[
|
|
410
|
+
[
|
|
411
|
+
"!kw",
|
|
412
|
+
"memory"
|
|
413
|
+
],
|
|
414
|
+
"wiggly-field-vole"
|
|
415
|
+
],
|
|
416
|
+
[
|
|
417
|
+
"!kw",
|
|
418
|
+
"db"
|
|
419
|
+
]
|
|
420
|
+
],
|
|
421
|
+
"commit-id": [
|
|
422
|
+
"!uuid",
|
|
423
|
+
"2c8f71f9-a3c6-4189-ba0c-e183cc29c672"
|
|
424
|
+
],
|
|
425
|
+
"max-eid": 1,
|
|
426
|
+
"max-tx": 536870913
|
|
427
|
+
}
|
|
428
|
+
],
|
|
429
|
+
"db-after": [
|
|
430
|
+
"!datahike/DB",
|
|
431
|
+
{
|
|
432
|
+
"store-id": [
|
|
433
|
+
[
|
|
434
|
+
[
|
|
435
|
+
"!kw",
|
|
436
|
+
"memory"
|
|
437
|
+
],
|
|
438
|
+
"wiggly-field-vole"
|
|
439
|
+
],
|
|
440
|
+
[
|
|
441
|
+
"!kw",
|
|
442
|
+
"db"
|
|
443
|
+
]
|
|
444
|
+
],
|
|
445
|
+
"commit-id": [
|
|
446
|
+
"!uuid",
|
|
447
|
+
"6ebf8979-cdf0-41f4-b615-30ff81830b0c"
|
|
448
|
+
],
|
|
449
|
+
"max-eid": 2,
|
|
450
|
+
"max-tx": 536870914
|
|
451
|
+
}
|
|
452
|
+
],
|
|
453
|
+
"tx-data": [
|
|
454
|
+
[
|
|
455
|
+
"!datahike/Datom",
|
|
456
|
+
[
|
|
457
|
+
536870914,
|
|
458
|
+
[
|
|
459
|
+
"!kw",
|
|
460
|
+
"db/txInstant"
|
|
461
|
+
],
|
|
462
|
+
[
|
|
463
|
+
"!date",
|
|
464
|
+
"1695952443102"
|
|
465
|
+
],
|
|
466
|
+
536870914,
|
|
467
|
+
true
|
|
468
|
+
]
|
|
469
|
+
],
|
|
470
|
+
[
|
|
471
|
+
"!datahike/Datom",
|
|
472
|
+
[
|
|
473
|
+
2,
|
|
474
|
+
[
|
|
475
|
+
"!kw",
|
|
476
|
+
"age"
|
|
477
|
+
],
|
|
478
|
+
42,
|
|
479
|
+
536870914,
|
|
480
|
+
true
|
|
481
|
+
]
|
|
482
|
+
],
|
|
483
|
+
[
|
|
484
|
+
"!datahike/Datom",
|
|
485
|
+
[
|
|
486
|
+
2,
|
|
487
|
+
[
|
|
488
|
+
"!kw",
|
|
489
|
+
"name"
|
|
490
|
+
],
|
|
491
|
+
"Peter",
|
|
492
|
+
536870914,
|
|
493
|
+
true
|
|
494
|
+
]
|
|
495
|
+
]
|
|
496
|
+
],
|
|
497
|
+
"tempids": {
|
|
498
|
+
"db/current-tx": 536870914
|
|
499
|
+
},
|
|
500
|
+
"tx-meta": {
|
|
501
|
+
"db/txInstant": [
|
|
502
|
+
"!date",
|
|
503
|
+
"1695952443102"
|
|
504
|
+
],
|
|
505
|
+
"db/commitId": [
|
|
506
|
+
"!uuid",
|
|
507
|
+
"6ebf8979-cdf0-41f4-b615-30ff81830b0c"
|
|
508
|
+
]
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
]
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
Note that you can extract the snapshots of the database `db-before` and `db-after` the commit as well as the facts added to the database as `tx-data`.
|
|
515
|
+
|
|
516
|
+
To retrieve the current database for your connection use
|
|
517
|
+
|
|
518
|
+
`POST` to "/db"
|
|
519
|
+
```javascript
|
|
520
|
+
[conn]
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
The result looks like:
|
|
524
|
+
|
|
525
|
+
```javascript
|
|
526
|
+
db = [
|
|
527
|
+
"!datahike/DB",
|
|
528
|
+
{
|
|
529
|
+
"store-id": [
|
|
530
|
+
[
|
|
531
|
+
[
|
|
532
|
+
"!kw",
|
|
533
|
+
"mem"
|
|
534
|
+
],
|
|
535
|
+
"127.0.1.1",
|
|
536
|
+
"wiggly-field-vole"
|
|
537
|
+
],
|
|
538
|
+
[
|
|
539
|
+
"!kw",
|
|
540
|
+
"db"
|
|
541
|
+
]
|
|
542
|
+
],
|
|
543
|
+
"commit-id": [
|
|
544
|
+
"!uuid",
|
|
545
|
+
"6ebf8979-cdf0-41f4-b615-30ff81830b0c"
|
|
546
|
+
],
|
|
547
|
+
"max-eid": 2,
|
|
548
|
+
"max-tx": 536870914
|
|
549
|
+
}
|
|
550
|
+
]
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
You can query this database with the query endpoint. We recommend again using a string to denote the query DSL instead of direct JSON encoding unless you want to manipulate the queries in JSON programmatically.
|
|
554
|
+
|
|
555
|
+
`GET` from "/q"
|
|
556
|
+
```javascript
|
|
557
|
+
["[:find ?n ?a :where [?e :name ?n] [?e :age ?a]]", db]
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
The result set is
|
|
561
|
+
|
|
562
|
+
```javascript
|
|
563
|
+
["!set",[["Peter",42]]]
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
You can also pass strings for pull expressions and to pass configurations to `delete-database` and `database-exists`.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Entity Specs
|
|
2
|
+
|
|
3
|
+
Similar to [Datomic's](https://www.datomic.com/) [entity specs](https://docs.datomic.com/on-prem/schema.html#entity-specs) Datahike supports assertion to ensure properties of transacted entities.
|
|
4
|
+
|
|
5
|
+
**⚠️ Important:** Predicate validation (`:db.entity/preds`) only works in Clojure, not ClojureScript. Attribute validation (`:db.entity/attrs`) works in both.
|
|
6
|
+
|
|
7
|
+
In short: you need to transact a `spec` to the database using `:db/ident` as the identifier, with at least either `:db.entity/attrs` as a list of attributes defined in the schema for ensuring required attributes, or `:db.entity/preds` with a list of fully namespaced symbols that refer to predicate functions that you want to assert. The signature of the predicate function should be `[db eid]` with the database value `db` where the transaction has happened and the entity id `eid` to be asserted.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## Example
|
|
11
|
+
|
|
12
|
+
Imagine you have a database where an account has an email, a holder, and a balance. Each new account should have an `email` and a `holder` as a required attribute and the email should be checked for its standard.
|
|
13
|
+
The next spec should check a new `balance` to not be less than 0.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
Let's fire up a REPL:
|
|
17
|
+
|
|
18
|
+
```clojure
|
|
19
|
+
(require '[datahike.api :as d])
|
|
20
|
+
|
|
21
|
+
;; setup database with schema and connection
|
|
22
|
+
(def schema [{:db/ident :account/email
|
|
23
|
+
:db/valueType :db.type/string
|
|
24
|
+
:db/unique :db.unique/identity
|
|
25
|
+
:db/cardinality :db.cardinality/one}
|
|
26
|
+
{:db/ident :account/holder
|
|
27
|
+
:db/valueType :db.type/string
|
|
28
|
+
:db/cardinality :db.cardinality/one}
|
|
29
|
+
{:db/ident :account/balance
|
|
30
|
+
:db/valueType :db.type/long
|
|
31
|
+
:db/cardinality :db.cardinality/one}])
|
|
32
|
+
|
|
33
|
+
(def cfg {:store {:backend :memory
|
|
34
|
+
:id #uuid "550e8400-e29b-41d4-a716-446655440000"}
|
|
35
|
+
:schema-flexibility :write
|
|
36
|
+
:keep-history? true
|
|
37
|
+
:initial-tx schema})
|
|
38
|
+
|
|
39
|
+
(d/delete-database cfg)
|
|
40
|
+
|
|
41
|
+
(d/create-database cfg)
|
|
42
|
+
|
|
43
|
+
(def conn (d/connect cfg))
|
|
44
|
+
|
|
45
|
+
;; define both predicates (must be fully-qualified symbols matching the namespace)
|
|
46
|
+
(defn is-email? [db eid]
|
|
47
|
+
(if-let [email (:account/email (d/entity db eid))]
|
|
48
|
+
(seq (re-find #"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)" email))
|
|
49
|
+
false))
|
|
50
|
+
|
|
51
|
+
(defn positive-balance? [db eid]
|
|
52
|
+
(if-let [balance (-> (d/entity db eid) :account/balance)]
|
|
53
|
+
(< 0 balance)
|
|
54
|
+
false))
|
|
55
|
+
|
|
56
|
+
;; add the person spec
|
|
57
|
+
;; Note: 'user/is-email? assumes you're in the 'user namespace (default REPL namespace)
|
|
58
|
+
;; If running in a different namespace, use that namespace in the symbol
|
|
59
|
+
(d/transact conn {:tx-data [{:db/ident :person/guard
|
|
60
|
+
:db.entity/attrs [:account/email :account/holder]
|
|
61
|
+
:db.entity/preds ['user/is-email?]}]})
|
|
62
|
+
|
|
63
|
+
(def valid-account {:account/email "emma@datahike.io"
|
|
64
|
+
:account/holder "Emma"})
|
|
65
|
+
|
|
66
|
+
;; add a valid person
|
|
67
|
+
(d/transact conn {:tx-data [(assoc valid-account :db/ensure :person/guard)]})
|
|
68
|
+
|
|
69
|
+
;; add with missing holder, observe exception
|
|
70
|
+
(d/transact conn {:tx-data [{:account/email "benedikt@datahike.io"
|
|
71
|
+
:db/ensure :person/guard}]})
|
|
72
|
+
|
|
73
|
+
;; add with invalid email, observe exception
|
|
74
|
+
(d/transact conn {:tx-data [{:account/email "thekla@datahike"
|
|
75
|
+
:account/holder "Thekla"
|
|
76
|
+
:db/ensure :person/guard}]})
|
|
77
|
+
|
|
78
|
+
;; add the balance spec
|
|
79
|
+
(d/transact conn {:tx-data [{:db/ident :balance/guard
|
|
80
|
+
:db.entity/attrs [:account/balance]
|
|
81
|
+
:db.entity/preds ['user/positive-balance?]}]})
|
|
82
|
+
|
|
83
|
+
;; add valid balance
|
|
84
|
+
(d/transact conn {:tx-data [{:db/id [:account/email (:account/email valid-account)]
|
|
85
|
+
:account/balance 1000
|
|
86
|
+
:db/ensure :balance/guard}]})
|
|
87
|
+
|
|
88
|
+
;; add invalid negative balance, observe exception
|
|
89
|
+
(d/transact conn {:tx-data [{:db/id [:account/email (:account/email valid-account)]
|
|
90
|
+
:account/balance -100
|
|
91
|
+
:db/ensure :balance/guard}]})
|
|
92
|
+
```
|