Skip to content

Commit 392966c

Browse files
committed
Heterogenous and homogenous tuples #364
1 parent 100ab86 commit 392966c

6 files changed

Lines changed: 685 additions & 36 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# WIP
2+
3+
- Support heterogeneous (`:db/tupleTypes`) and homogeneous (`:db/tupleType`) tuples #364
4+
15
# 1.7.8 - Oct 11, 2025
26

37
- Skip storage writes for no-op transactions #492 via @jxonas

‎CLAUDE.md‎

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
DataScript is an immutable in-memory database and Datalog query engine implemented in Clojure and ClojureScript. It's designed to run in the browser and JVM, providing a lightweight, ephemeral database with Datalog querying capabilities similar to Datomic.
8+
9+
## Key Commands
10+
11+
### Testing
12+
13+
```bash
14+
# Run Clojure tests
15+
./script/test_clj.sh
16+
# or: clojure -X:dev user/-test-main
17+
18+
# Run ClojureScript tests
19+
./script/test_cljs.sh
20+
21+
# Run all tests
22+
./script/test_all.sh
23+
24+
# Run Datomic compatibility tests (requires datomic-free setup)
25+
./script/test_datomic.sh
26+
# or: clj -M:datomic
27+
```
28+
29+
### Benchmarking
30+
31+
```bash
32+
# Run Clojure benchmarks
33+
./script/bench_clj.sh
34+
# or: clojure -A:bench -M -m datascript.bench.datascript
35+
36+
# Run ClojureScript benchmarks
37+
./script/bench_cljs.sh
38+
39+
# Run Datomic benchmarks (requires datomic-free setup)
40+
./script/bench_datomic.sh
41+
42+
# Run all benchmarks
43+
./script/bench_all.sh
44+
```
45+
46+
### Development
47+
48+
```bash
49+
# Start a REPL
50+
./script/repl.sh
51+
52+
# Build ClojureScript release
53+
lein with-profile test cljsbuild once release
54+
```
55+
56+
## Architecture Overview
57+
58+
### Core Components
59+
60+
**Database Layer (`datascript.db`):**
61+
- `Datom` records represent facts as [entity attribute value transaction] tuples
62+
- `DB` is the immutable database value with three indexes: EAVT, AEVT, AVET
63+
- Uses persistent sorted sets for index storage (`me.tonsky.persistent-sorted-set`)
64+
- Transaction IDs start at `0x20000000` (tx0), entity IDs start at 0 (e0)
65+
- Schema is optional and only needed for special behaviors (cardinality many, unique attributes, ref types)
66+
67+
**Query Engine (`datascript.query`):**
68+
- Implements Datalog query language
69+
- Queries are parsed, cached (LRU cache of 100), and executed against relations
70+
- Supports rules, aggregates, predicates, negation, and multiple data sources
71+
- `Relation` records store query intermediate results as {attrs tuples}
72+
- `Context` holds relations, sources, and rules during query execution
73+
74+
**Connection Management (`datascript.conn`):**
75+
- Connections are atoms holding the latest DB value
76+
- Access current DB with `@conn`, not `(d/db conn)` as in Datomic
77+
- Transaction reports include `:db-before`, `:db-after`, `:tx-data`, `:tempids`, `:tx-meta`
78+
- Supports transaction listeners via `listen!`
79+
80+
**Entity API (`datascript.impl.entity`):**
81+
- Lazy map-like entities for navigating database
82+
- Entities cache accessed attributes
83+
- Reverse references via `_` prefix (`:_ref`)
84+
- Component references return single entity instead of collection
85+
86+
**Storage (`datascript.storage`):**
87+
- Protocol-based persistent storage for databases
88+
- Serializes indexes as persistent sorted sets with addressable chunks
89+
- Supports custom storage backends via `IStorage` protocol
90+
91+
**Pull API (`datascript.pull-api`, `datascript.pull-parser`):**
92+
- EQL-style pattern-based entity graph extraction
93+
- Supports recursive patterns, attribute limits, default values
94+
95+
### Key Differences from Datomic
96+
97+
- Entities use `-1`, `-2` for tempids instead of `#db/id[:db.part/user -100]`
98+
- No `:db/txInstant` annotations by default
99+
- Custom query functions in ClojureScript must be passed as source (no `resolve`)
100+
- Transaction functions called as `[:db.fn/call f args]` where `f` is function reference
101+
- Additional `:db.fn/retractAttribute` built-in function
102+
- Schemas are not queryable and attributes don't need pre-declaration
103+
104+
### File Organization
105+
106+
- `src/datascript/` - Core implementation (.cljc files for JVM + JS)
107+
- `core.cljc` - Public API entry point
108+
- `db.cljc` - Database, datom, and index implementation
109+
- `query.cljc` - Datalog query engine
110+
- `pull_api.cljc`, `pull_parser.cljc` - Pull API
111+
- `storage.clj/.cljs` - Persistent storage (platform-specific)
112+
- `conn.cljc` - Connection and transaction handling
113+
- `parser.cljc` - Query/rule parsing
114+
- `built_ins.cljc` - Built-in query functions and aggregates
115+
116+
- `test/datascript/test/` - Test suite (.cljc files)
117+
- Tests use `.cljc` files for cross-platform testing
118+
- Acceptance tests demonstrate usage: `test/datascript/test/*.cljc`
119+
120+
- `bench/` - Benchmarking code
121+
- `bench_datomic/` - Datomic compatibility checks
122+
- `script/` - Build and test scripts
123+
124+
### Development Notes
125+
126+
- Uses `.cljc` files for cross-platform (JVM + ClojureScript) code
127+
- Custom macros in `datascript.db` like `defn+` handle CLJS efficiency
128+
- `*warn-on-reflection*` enabled for JVM performance
129+
- Storage implementation differs between Clojure (`.clj`) and ClojureScript (`.cljs`)
130+
- shadow-cljs users must add `:compiler-options {:externs ["datascript/externs.js"]}`
131+
132+
### Aliases (deps.edn)
133+
134+
- `:dev` - Development with REPL tools
135+
- `:test` - Test dependencies
136+
- `:bench` - Benchmarking tools (criterium, profiler)
137+
- `:datomic` - Datomic compatibility testing
138+
- `:cljs` - ClojureScript compilation
139+
- `:1.9`, `:1.10`, `:1.11.1`, `:1.12` - Override Clojure versions

‎docs/tuples.md‎

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1-
# Composite tuples
1+
# Tuples
22

33
A tuple is a collection of scalar values, represented in memory as a Clojure vector.
44

5+
There are three kinds of tuples:
6+
7+
- [Composite tuples](#composite-tuples) (`:db/tupleAttrs`) are derived from other attributes of the same entity and managed by DataScript;
8+
- [Heterogeneous tuples](#heterogeneous-tuples) (`:db/tupleTypes`) are fixed-length tuples with a declared type per slot, asserted by you;
9+
- [Homogeneous tuples](#homogeneous-tuples) (`:db/tupleType`) are variable-length tuples with a single declared element type, asserted by you.
10+
11+
# Composite tuples
12+
513
Composite tuples are applicable:
614

715
- when a domain entity has a multi-attribute key;
@@ -100,4 +108,74 @@ If you mark tuple attribute as `:db/unique :db.unique/identity`, you might use i
100108
; => {:db/id 4, :a "a", :b "b", :a+b ["a" "b"]}
101109
```
102110

103-
Composite tuples are like normal attributes in most cases. Feel free to use them in lookup refs, upserts, queries, index access.
111+
Composite tuples are like normal attributes in most cases. Feel free to use them in lookup refs, upserts, queries, index access.
112+
113+
# Heterogeneous tuples
114+
115+
Heterogeneous tuples have a fixed length, with a type declared per slot:
116+
117+
```
118+
(def conn (d/create-conn
119+
{:player/location {:db/valueType :db.type/tuple
120+
:db/tupleTypes [:db.type/long :db.type/long]}}))
121+
```
122+
123+
Unlike composite tuples, you assert and retract them yourself:
124+
125+
```
126+
(d/transact! conn
127+
[{:player/handle "Argent Adept"
128+
:player/location [100 0]}])
129+
```
130+
131+
DataScript validates that the value is a vector of the declared length. `nil` is a legal value for any slot:
132+
133+
```
134+
(d/transact! conn [[:db/add 1 :player/location [100 nil]]])
135+
```
136+
137+
Slot types other than `:db.type/ref` are not enforced — DataScript is dynamically typed and treats them as documentation. Datomic’s limits (max 8 elements, strings ≤ 256 chars) are not enforced either. Keep the types in each slot consistent though, otherwise index sorting might fail.
138+
139+
`:db.type/ref` slots are special: entity ids, lookup refs, idents, tempids and `:db/current-tx` in them are resolved the same way ref attribute values are:
140+
141+
```
142+
(def conn (d/create-conn
143+
{:name {:db/unique :db.unique/identity}
144+
:ref+long {:db/valueType :db.type/tuple
145+
:db/tupleTypes [:db.type/ref :db.type/long]}}))
146+
147+
(d/transact! conn
148+
[{:db/id -1 :name "Ivan"}
149+
[:db/add 2 :ref+long [-1 7]]]) ; tempid, resolves to Ivan’s eid
150+
151+
(d/transact! conn
152+
[[:db/add 2 :ref+long [[:name "Ivan"] 8]]]) ; lookup ref works too
153+
```
154+
155+
Refs inside tuples are resolved on write, but are not full-fledged references otherwise: they don’t show up in reverse index, don’t get retracted when the referenced entity is retracted (the eid stays in the tuple, dangling), can’t be `:db/isComponent`, and entity API returns them as plain numbers, not entities. Same is true in Datomic.
156+
157+
Unlike composite tuples, heterogeneous and homogeneous tuples are not indexed by default. Add `:db/index true` or `:db/unique` if you want AVET access. Unique declared tuples work in upserts and lookup refs, same as composite ones:
158+
159+
```
160+
(d/entity (d/db conn) [:ref+long [[:name "Ivan"] 8]])
161+
```
162+
163+
Unlike Datomic, heterogeneous and homogeneous tuples can be `:db.cardinality/many`. Note that in map form multiple values must be wrapped in an extra vector, otherwise a single tuple will be interpreted as multiple values:
164+
165+
```
166+
(d/transact! conn [{:db/id 1, :locations [[100 0] [200 300]]}]) ; two tuples
167+
```
168+
169+
# Homogeneous tuples
170+
171+
Homogeneous tuples have variable length and a single type declared for all elements:
172+
173+
```
174+
(def conn (d/create-conn
175+
{:keywords {:db/valueType :db.type/tuple
176+
:db/tupleType :db.type/keyword}}))
177+
178+
(d/transact! conn [[:db/add 1 :keywords [:a :b :c]]])
179+
```
180+
181+
Everything said about heterogeneous tuples applies: only the vector shape is validated, `:db.type/ref` element type gets full ref resolution in every slot, indexing is opt-in. Note that vectors sort by length first, so `index-range` over different-length tuples is not lexicographic.

‎script/nrepl.sh‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/bin/bash
2+
set -o errexit -o nounset -o pipefail
3+
cd "`dirname $0`/.."
4+
5+
clojure -Sdeps '{:deps {nrepl/nrepl {:mvn/version "1.3.1"}}}' -M:1.12:dev -m nrepl.cmdline

0 commit comments

Comments
 (0)