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.
Files changed (324) hide show
  1. package/.circleci/config.yml +405 -0
  2. package/.circleci/scripts/gen_ci.clj +194 -0
  3. package/.cirrus.yml +60 -0
  4. package/.clj-kondo/babashka/sci/config.edn +1 -0
  5. package/.clj-kondo/babashka/sci/sci/core.clj +9 -0
  6. package/.clj-kondo/config.edn +95 -0
  7. package/.dir-locals.el +2 -0
  8. package/.github/FUNDING.yml +3 -0
  9. package/.github/ISSUE_TEMPLATE/1-bug-report.yml +68 -0
  10. package/.github/ISSUE_TEMPLATE/2-feature-request.yml +28 -0
  11. package/.github/ISSUE_TEMPLATE/config.yml +6 -0
  12. package/.github/pull_request_template.md +24 -0
  13. package/.github/workflows/native-image.yml +84 -0
  14. package/LICENSE +203 -0
  15. package/README.md +273 -0
  16. package/bb/deps.edn +9 -0
  17. package/bb/resources/github-fingerprints +3 -0
  18. package/bb/resources/native-image-tests/run-bb-pod-tests.clj +162 -0
  19. package/bb/resources/native-image-tests/run-libdatahike-tests +12 -0
  20. package/bb/resources/native-image-tests/run-native-image-tests +74 -0
  21. package/bb/resources/native-image-tests/run-python-tests +22 -0
  22. package/bb/resources/native-image-tests/testconfig.attr-refs.edn +6 -0
  23. package/bb/resources/native-image-tests/testconfig.edn +5 -0
  24. package/bb/resources/template/.settings/org.eclipse.jdt.apt.core.prefs +2 -0
  25. package/bb/resources/template/.settings/org.eclipse.jdt.core.prefs +9 -0
  26. package/bb/resources/template/.settings/org.eclipse.m2e.core.prefs +4 -0
  27. package/bb/resources/template/pom.xml +22 -0
  28. package/bb/src/tools/build.clj +132 -0
  29. package/bb/src/tools/clj_kondo.clj +32 -0
  30. package/bb/src/tools/deploy.clj +26 -0
  31. package/bb/src/tools/examples.clj +19 -0
  32. package/bb/src/tools/npm.clj +100 -0
  33. package/bb/src/tools/python.clj +14 -0
  34. package/bb/src/tools/release.clj +94 -0
  35. package/bb/src/tools/test.clj +148 -0
  36. package/bb/src/tools/version.clj +47 -0
  37. package/bb.edn +269 -0
  38. package/benchmark/src/benchmark/cli.clj +195 -0
  39. package/benchmark/src/benchmark/compare.clj +157 -0
  40. package/benchmark/src/benchmark/config.clj +316 -0
  41. package/benchmark/src/benchmark/measure.clj +187 -0
  42. package/benchmark/src/benchmark/store.clj +190 -0
  43. package/benchmark/test/benchmark/measure_test.clj +156 -0
  44. package/build.clj +30 -0
  45. package/config.edn +49 -0
  46. package/deps.edn +138 -0
  47. package/dev/sandbox.clj +82 -0
  48. package/dev/sandbox.cljs +127 -0
  49. package/dev/sandbox_benchmarks.clj +27 -0
  50. package/dev/sandbox_client.clj +87 -0
  51. package/dev/sandbox_transact_bench.clj +109 -0
  52. package/dev/user.clj +79 -0
  53. package/doc/README.md +96 -0
  54. package/doc/adl/README.md +6 -0
  55. package/doc/adl/adr-000-adr.org +28 -0
  56. package/doc/adl/adr-001-attribute-references.org +15 -0
  57. package/doc/adl/adr-002-build-tooling.org +54 -0
  58. package/doc/adl/adr-003-db-meta-data.md +52 -0
  59. package/doc/adl/adr-004-github-flow.md +40 -0
  60. package/doc/adl/adr-XYZ-template.md +30 -0
  61. package/doc/adl/index.org +3 -0
  62. package/doc/assets/datahike-logo.svg +3 -0
  63. package/doc/assets/datahiking-invoice.org +85 -0
  64. package/doc/assets/hhtree2.png +0 -0
  65. package/doc/assets/network_topology.svg +624 -0
  66. package/doc/assets/perf.png +0 -0
  67. package/doc/assets/schema_mindmap.mm +132 -0
  68. package/doc/assets/schema_mindmap.svg +970 -0
  69. package/doc/assets/temporal_index.mm +74 -0
  70. package/doc/backend-development.md +78 -0
  71. package/doc/bb-pod.md +89 -0
  72. package/doc/benchmarking.md +360 -0
  73. package/doc/bindings/edn-conversion.md +383 -0
  74. package/doc/cli.md +162 -0
  75. package/doc/cljdoc.edn +27 -0
  76. package/doc/cljs-support.md +133 -0
  77. package/doc/config.md +406 -0
  78. package/doc/contributing.md +114 -0
  79. package/doc/datalog-vs-sql.md +210 -0
  80. package/doc/datomic_differences.md +109 -0
  81. package/doc/development/pull-api-ns.md +186 -0
  82. package/doc/development/pull-frame-state-diagram.jpg +0 -0
  83. package/doc/distributed.md +566 -0
  84. package/doc/entity_spec.md +92 -0
  85. package/doc/gc.md +273 -0
  86. package/doc/java-api.md +808 -0
  87. package/doc/javascript-api.md +421 -0
  88. package/doc/libdatahike.md +86 -0
  89. package/doc/logging_and_error_handling.md +43 -0
  90. package/doc/norms.md +66 -0
  91. package/doc/schema-migration.md +85 -0
  92. package/doc/schema.md +287 -0
  93. package/doc/storage-backends.md +363 -0
  94. package/doc/store-id-refactoring.md +596 -0
  95. package/doc/time_variance.md +325 -0
  96. package/doc/unstructured.md +167 -0
  97. package/doc/versioning.md +261 -0
  98. package/examples/basic/README.md +19 -0
  99. package/examples/basic/deps.edn +6 -0
  100. package/examples/basic/docker-compose.yml +13 -0
  101. package/examples/basic/src/examples/core.clj +60 -0
  102. package/examples/basic/src/examples/schema.clj +155 -0
  103. package/examples/basic/src/examples/store.clj +60 -0
  104. package/examples/basic/src/examples/time_travel.clj +185 -0
  105. package/examples/java/.settings/org.eclipse.core.resources.prefs +3 -0
  106. package/examples/java/.settings/org.eclipse.jdt.apt.core.prefs +2 -0
  107. package/examples/java/.settings/org.eclipse.jdt.core.prefs +9 -0
  108. package/examples/java/.settings/org.eclipse.m2e.core.prefs +4 -0
  109. package/examples/java/README.md +162 -0
  110. package/examples/java/pom.xml +62 -0
  111. package/examples/java/src/main/java/examples/QuickStart.java +115 -0
  112. package/examples/java/src/main/java/examples/SchemaExample.java +148 -0
  113. package/examples/java/src/main/java/examples/TimeTravelExample.java +121 -0
  114. package/flake.lock +27 -0
  115. package/flake.nix +27 -0
  116. package/http-server/datahike/http/middleware.clj +75 -0
  117. package/http-server/datahike/http/server.clj +269 -0
  118. package/java/src/datahike/java/Database.java +274 -0
  119. package/java/src/datahike/java/Datahike.java +281 -0
  120. package/java/src/datahike/java/DatahikeGeneratedTest.java +349 -0
  121. package/java/src/datahike/java/DatahikeTest.java +370 -0
  122. package/java/src/datahike/java/EDN.java +170 -0
  123. package/java/src/datahike/java/IEntity.java +11 -0
  124. package/java/src/datahike/java/Keywords.java +161 -0
  125. package/java/src/datahike/java/SchemaFlexibility.java +52 -0
  126. package/java/src/datahike/java/Util.java +219 -0
  127. package/karma.conf.js +19 -0
  128. package/libdatahike/compile-cpp +7 -0
  129. package/libdatahike/src/datahike/impl/LibDatahikeBase.java +203 -0
  130. package/libdatahike/src/datahike/impl/libdatahike.clj +59 -0
  131. package/libdatahike/src/test_cpp.cpp +61 -0
  132. package/npm-package/PUBLISHING.md +140 -0
  133. package/npm-package/README.md +226 -0
  134. package/npm-package/package.template.json +34 -0
  135. package/npm-package/test-isomorphic.ts +281 -0
  136. package/npm-package/test.js +557 -0
  137. package/npm-package/typescript-test.ts +70 -0
  138. package/package.json +16 -0
  139. package/pydatahike/README.md +569 -0
  140. package/pydatahike/pyproject.toml +91 -0
  141. package/pydatahike/setup.py +42 -0
  142. package/pydatahike/src/datahike/__init__.py +134 -0
  143. package/pydatahike/src/datahike/_native.py +250 -0
  144. package/pydatahike/src/datahike/_version.py +2 -0
  145. package/pydatahike/src/datahike/database.py +722 -0
  146. package/pydatahike/src/datahike/edn.py +311 -0
  147. package/pydatahike/src/datahike/py.typed +0 -0
  148. package/pydatahike/tests/conftest.py +17 -0
  149. package/pydatahike/tests/test_basic.py +170 -0
  150. package/pydatahike/tests/test_database.py +51 -0
  151. package/pydatahike/tests/test_edn_conversion.py +299 -0
  152. package/pydatahike/tests/test_query.py +99 -0
  153. package/pydatahike/tests/test_schema.py +55 -0
  154. package/resources/clj-kondo.exports/io.replikativ/datahike/config.edn +5 -0
  155. package/resources/example_server.edn +4 -0
  156. package/shadow-cljs.edn +56 -0
  157. package/src/data_readers.clj +7 -0
  158. package/src/datahike/api/impl.cljc +176 -0
  159. package/src/datahike/api/specification.cljc +633 -0
  160. package/src/datahike/api/types.cljc +261 -0
  161. package/src/datahike/api.cljc +41 -0
  162. package/src/datahike/array.cljc +99 -0
  163. package/src/datahike/cli.clj +166 -0
  164. package/src/datahike/cljs.cljs +6 -0
  165. package/src/datahike/codegen/cli.clj +406 -0
  166. package/src/datahike/codegen/clj_kondo.clj +291 -0
  167. package/src/datahike/codegen/java.clj +403 -0
  168. package/src/datahike/codegen/naming.cljc +33 -0
  169. package/src/datahike/codegen/native.clj +559 -0
  170. package/src/datahike/codegen/pod.clj +488 -0
  171. package/src/datahike/codegen/python.clj +838 -0
  172. package/src/datahike/codegen/report.clj +55 -0
  173. package/src/datahike/codegen/typescript.clj +262 -0
  174. package/src/datahike/codegen/validation.clj +145 -0
  175. package/src/datahike/config.cljc +294 -0
  176. package/src/datahike/connections.cljc +16 -0
  177. package/src/datahike/connector.cljc +265 -0
  178. package/src/datahike/constants.cljc +142 -0
  179. package/src/datahike/core.cljc +297 -0
  180. package/src/datahike/datom.cljc +459 -0
  181. package/src/datahike/db/interface.cljc +119 -0
  182. package/src/datahike/db/search.cljc +305 -0
  183. package/src/datahike/db/transaction.cljc +937 -0
  184. package/src/datahike/db/utils.cljc +338 -0
  185. package/src/datahike/db.cljc +956 -0
  186. package/src/datahike/experimental/unstructured.cljc +126 -0
  187. package/src/datahike/experimental/versioning.cljc +172 -0
  188. package/src/datahike/externs.js +31 -0
  189. package/src/datahike/gc.cljc +69 -0
  190. package/src/datahike/http/client.clj +188 -0
  191. package/src/datahike/http/writer.clj +79 -0
  192. package/src/datahike/impl/entity.cljc +218 -0
  193. package/src/datahike/index/interface.cljc +93 -0
  194. package/src/datahike/index/persistent_set.cljc +469 -0
  195. package/src/datahike/index/utils.cljc +44 -0
  196. package/src/datahike/index.cljc +32 -0
  197. package/src/datahike/js/api.cljs +172 -0
  198. package/src/datahike/js/api_macros.clj +22 -0
  199. package/src/datahike/js.cljs +163 -0
  200. package/src/datahike/json.cljc +209 -0
  201. package/src/datahike/lru.cljc +146 -0
  202. package/src/datahike/migrate.clj +39 -0
  203. package/src/datahike/norm/norm.clj +245 -0
  204. package/src/datahike/online_gc.cljc +252 -0
  205. package/src/datahike/pod.clj +155 -0
  206. package/src/datahike/pull_api.cljc +325 -0
  207. package/src/datahike/query.cljc +1945 -0
  208. package/src/datahike/query_stats.cljc +88 -0
  209. package/src/datahike/readers.cljc +62 -0
  210. package/src/datahike/remote.cljc +218 -0
  211. package/src/datahike/schema.cljc +228 -0
  212. package/src/datahike/schema_cache.cljc +42 -0
  213. package/src/datahike/spec.cljc +101 -0
  214. package/src/datahike/store.cljc +80 -0
  215. package/src/datahike/tools.cljc +308 -0
  216. package/src/datahike/transit.cljc +80 -0
  217. package/src/datahike/writer.cljc +239 -0
  218. package/src/datahike/writing.cljc +362 -0
  219. package/src/deps.cljs +1 -0
  220. package/src-hitchhiker-tree/datahike/index/hitchhiker_tree/insert.cljc +76 -0
  221. package/src-hitchhiker-tree/datahike/index/hitchhiker_tree/upsert.cljc +128 -0
  222. package/src-hitchhiker-tree/datahike/index/hitchhiker_tree.cljc +213 -0
  223. package/test/datahike/backward_compatibility_test/src/backward_test.clj +37 -0
  224. package/test/datahike/integration_test/config_record_file_test.clj +14 -0
  225. package/test/datahike/integration_test/config_record_test.clj +14 -0
  226. package/test/datahike/integration_test/depr_config_uri_test.clj +15 -0
  227. package/test/datahike/integration_test/return_map_test.clj +62 -0
  228. package/test/datahike/integration_test.cljc +67 -0
  229. package/test/datahike/norm/norm_test.clj +124 -0
  230. package/test/datahike/norm/resources/naming-and-sorting-test/001-a1-example.edn +5 -0
  231. package/test/datahike/norm/resources/naming-and-sorting-test/002-a2-example.edn +5 -0
  232. package/test/datahike/norm/resources/naming-and-sorting-test/003-tx-fn-test.edn +1 -0
  233. package/test/datahike/norm/resources/naming-and-sorting-test/004-tx-data-and-tx-fn-test.edn +5 -0
  234. package/test/datahike/norm/resources/naming-and-sorting-test/01-transact-basic-characters.edn +2 -0
  235. package/test/datahike/norm/resources/naming-and-sorting-test/02 add occupation.edn +5 -0
  236. package/test/datahike/norm/resources/naming-and-sorting-test/checksums.edn +12 -0
  237. package/test/datahike/norm/resources/simple-test/001-a1-example.edn +5 -0
  238. package/test/datahike/norm/resources/simple-test/002-a2-example.edn +5 -0
  239. package/test/datahike/norm/resources/simple-test/checksums.edn +4 -0
  240. package/test/datahike/norm/resources/tx-data-and-tx-fn-test/first/001-a1-example.edn +5 -0
  241. package/test/datahike/norm/resources/tx-data-and-tx-fn-test/first/002-a2-example.edn +5 -0
  242. package/test/datahike/norm/resources/tx-data-and-tx-fn-test/first/003-tx-fn-test.edn +1 -0
  243. package/test/datahike/norm/resources/tx-data-and-tx-fn-test/first/checksums.edn +6 -0
  244. package/test/datahike/norm/resources/tx-data-and-tx-fn-test/second/004-tx-data-and-tx-fn-test.edn +5 -0
  245. package/test/datahike/norm/resources/tx-data-and-tx-fn-test/second/checksums.edn +2 -0
  246. package/test/datahike/norm/resources/tx-fn-test/first/001-a1-example.edn +5 -0
  247. package/test/datahike/norm/resources/tx-fn-test/first/002-a2-example.edn +5 -0
  248. package/test/datahike/norm/resources/tx-fn-test/first/checksums.edn +4 -0
  249. package/test/datahike/norm/resources/tx-fn-test/second/003-tx-fn-test.edn +1 -0
  250. package/test/datahike/norm/resources/tx-fn-test/second/checksums.edn +2 -0
  251. package/test/datahike/test/api_test.cljc +895 -0
  252. package/test/datahike/test/array_test.cljc +40 -0
  253. package/test/datahike/test/attribute_refs/datoms_test.cljc +140 -0
  254. package/test/datahike/test/attribute_refs/db_test.cljc +42 -0
  255. package/test/datahike/test/attribute_refs/differences_test.cljc +515 -0
  256. package/test/datahike/test/attribute_refs/entity_test.cljc +89 -0
  257. package/test/datahike/test/attribute_refs/pull_api_test.cljc +320 -0
  258. package/test/datahike/test/attribute_refs/query_find_specs_test.cljc +59 -0
  259. package/test/datahike/test/attribute_refs/query_fns_test.cljc +130 -0
  260. package/test/datahike/test/attribute_refs/query_interop_test.cljc +47 -0
  261. package/test/datahike/test/attribute_refs/query_not_test.cljc +193 -0
  262. package/test/datahike/test/attribute_refs/query_or_test.cljc +137 -0
  263. package/test/datahike/test/attribute_refs/query_pull_test.cljc +156 -0
  264. package/test/datahike/test/attribute_refs/query_rules_test.cljc +176 -0
  265. package/test/datahike/test/attribute_refs/query_test.cljc +241 -0
  266. package/test/datahike/test/attribute_refs/temporal_search.cljc +22 -0
  267. package/test/datahike/test/attribute_refs/transact_test.cljc +220 -0
  268. package/test/datahike/test/attribute_refs/utils.cljc +128 -0
  269. package/test/datahike/test/cache_test.cljc +38 -0
  270. package/test/datahike/test/components_test.cljc +92 -0
  271. package/test/datahike/test/config_test.cljc +158 -0
  272. package/test/datahike/test/core_test.cljc +105 -0
  273. package/test/datahike/test/datom_test.cljc +44 -0
  274. package/test/datahike/test/db_test.cljc +54 -0
  275. package/test/datahike/test/entity_spec_test.cljc +159 -0
  276. package/test/datahike/test/entity_test.cljc +103 -0
  277. package/test/datahike/test/explode_test.cljc +143 -0
  278. package/test/datahike/test/filter_test.cljc +75 -0
  279. package/test/datahike/test/gc_test.cljc +159 -0
  280. package/test/datahike/test/http/server_test.clj +192 -0
  281. package/test/datahike/test/http/writer_test.clj +86 -0
  282. package/test/datahike/test/ident_test.cljc +32 -0
  283. package/test/datahike/test/index_test.cljc +345 -0
  284. package/test/datahike/test/insert.cljc +125 -0
  285. package/test/datahike/test/java_bindings_test.clj +6 -0
  286. package/test/datahike/test/listen_test.cljc +41 -0
  287. package/test/datahike/test/lookup_refs_test.cljc +266 -0
  288. package/test/datahike/test/lru_test.cljc +27 -0
  289. package/test/datahike/test/migrate_test.clj +297 -0
  290. package/test/datahike/test/model/core.cljc +376 -0
  291. package/test/datahike/test/model/invariant.cljc +142 -0
  292. package/test/datahike/test/model/rng.cljc +82 -0
  293. package/test/datahike/test/model_test.clj +217 -0
  294. package/test/datahike/test/nodejs_test.cljs +262 -0
  295. package/test/datahike/test/online_gc_test.cljc +475 -0
  296. package/test/datahike/test/pod_test.clj +369 -0
  297. package/test/datahike/test/pull_api_test.cljc +474 -0
  298. package/test/datahike/test/purge_test.cljc +144 -0
  299. package/test/datahike/test/query_aggregates_test.cljc +101 -0
  300. package/test/datahike/test/query_find_specs_test.cljc +52 -0
  301. package/test/datahike/test/query_fns_test.cljc +523 -0
  302. package/test/datahike/test/query_interop_test.cljc +47 -0
  303. package/test/datahike/test/query_not_test.cljc +189 -0
  304. package/test/datahike/test/query_or_test.cljc +158 -0
  305. package/test/datahike/test/query_pull_test.cljc +147 -0
  306. package/test/datahike/test/query_rules_test.cljc +248 -0
  307. package/test/datahike/test/query_stats_test.cljc +218 -0
  308. package/test/datahike/test/query_test.cljc +984 -0
  309. package/test/datahike/test/schema_test.cljc +424 -0
  310. package/test/datahike/test/specification_test.cljc +30 -0
  311. package/test/datahike/test/store_test.cljc +78 -0
  312. package/test/datahike/test/stress_test.cljc +57 -0
  313. package/test/datahike/test/time_variance_test.cljc +518 -0
  314. package/test/datahike/test/tools_test.clj +134 -0
  315. package/test/datahike/test/transact_test.cljc +518 -0
  316. package/test/datahike/test/tuples_test.cljc +564 -0
  317. package/test/datahike/test/unstructured_test.cljc +291 -0
  318. package/test/datahike/test/upsert_impl_test.cljc +205 -0
  319. package/test/datahike/test/upsert_test.cljc +363 -0
  320. package/test/datahike/test/utils.cljc +110 -0
  321. package/test/datahike/test/validation_test.cljc +48 -0
  322. package/test/datahike/test/versioning_test.cljc +56 -0
  323. package/test/datahike/test.cljc +66 -0
  324. 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