DataScript Documentation

repository·master·Indexed 26 days ago

https://github.com/tonsky/datascript

An immutable in-memory database and Datalog query engine designed for Clojure and ClojureScript, optimized for managing complex application state in client-side applications. It features a triple-store model, support for recursive rules, aggregates, and a Pull API. DataScript can be used in Clojure, ClojureScript, and vanilla JavaScript via npm, CDN, or RequireJS. It provides multiple serialization methods, including EDN strings, serialization-friendly structures, and the IStorage protocol for custom, incremental storage.

Tokens
4.2K
Snippets
10
Records
23
Agent score
91%

What's inside DataScript

  1. Compare DataScript with Datomic

    master

    While inspired by Datomic, DataScript has several key architectural differences:

    • Runtime: Runs in both the browser and the JVM.
    • Schema: Simplified and not queryable. Attributes do not need to be declared in advance; keywords are used literally as attribute values (no integer IDs).
    • Memory Management: Designed to operate in constant space for interactive apps. Unlike Datomic, it does not keep full history by default (preventing monotonic growth), though you can implement history tracking manually.
    • Simplicity: No schema migrations, no full-text search, and no partitions.
    • Values: Any type can be used for values.
  2. Understand DataScript core features

    master

    DataScript is an immutable, triple-store database designed for interactive, long-living applications (like browsers). Key features include:

    • Immutable Database: Each DB is an immutable value. New DBs are created on top of old ones, while old ones remain valid.
    • Data Model: Uses a triple store model with EAVT, AEVT, and AVET indexes. Supports multi-valued attributes via :db/cardinality :db.cardinality/many.
    • Mutation: Database updates are performed via transact!.
    • Query Engine: Supports implicit joins, parameterized queries via :in, predicates, user functions, negation, disjunction, rules (including recursive rules), aggregates, and find specifications.
    • Pull API: Includes a Pull API for data retrieval.
    • Advanced Lookups: Supports direct index lookup/iteration via datoms and seek-datoms, filtered databases via filter, and lookup refs.
    • Constraints: Supports unique constraints and upsert operations.
  3. Use DataScript from vanilla JavaScript

    master

    When using DataScript in a JavaScript environment, follow these rules for queries, entities, and transactions:

    Queries

    • Pass queries and rules as EDN strings.
    • The results of the q function are returned as regular JavaScript arrays.

    Entities

    • Entities returned by the entity call are lazy (similar to Clojure).
    • Access properties using .get("prop"), .get(":db/id"), or the .db property.
    • Entities implement the ECMAScript 6 Map interface (supporting has, get, keys, etc.).

    Transactions

    • Use strings for namespaced keywords, such as ":db/id" or ":db/add".
    • Use regular JavaScript arrays and objects to pass data to transact and db_with.

    Transaction Reports

    • report.tempids uses string keys (e.g., "-1" for entity tempid -1). Use resolve_tempid to map these to actual IDs.
  4. Define and use composite tuples

    master

    Composite tuples are collections of scalar values (represented as Clojure vectors) used for multi-attribute keys or query optimization. They are managed entirely by DataScript; you do not assert or retract them directly. Instead, DataScript automatically populates or updates the tuple whenever the constituent attributes are modified.

    Rules for tuple attributes:

    • Must be of cardinality one.
    • Cannot reference cardinality many attributes.
    • Cannot reference other tuple attributes.
    • Are indexed by default.

    To define a composite tuple, use the :db/tupleAttrs key in your schema to list the constituent attributes.

  5. Configure shadow-cljs for DataScript

    master

    If you are using shadow-cljs, you must add the DataScript externs to your compiler options to ensure proper interop.

    Add the following to your build configuration:

    :compiler-options {:externs ["datascript/externs.js"]}
    :compiler-options {:externs ["datascript/externs.js"]}
  6. Install DataScript in JavaScript

    master

    DataScript can be used in vanilla JavaScript environments via a CDN script tag, as a CommonJS module via npm, or as a RequireJS module.

    CDN (HTML):

    <script src="https://github.com/tonsky/datascript/releases/download/1.7.8/datascript-1.7.8.min.js"></script>

    npm (CommonJS):

    npm install datascript
    var ds = require('datascript');

    RequireJS:

    require(['datascript'], function(ds) { ... });
    <script src="https://github.com/tonsky/datascript/releases/download/1.7.8/datascript-1.7.8.min.js"></script>
    
    # or as a CommonJS module
    npm install datascript
    
    var ds = require('datascript');
    
    # or as a RequireJS module
    require(['datascript'], function(ds) { ... });
  7. Enforce uniqueness using composite tuples

    master

    You can use composite tuples to enforce uniqueness constraints across multiple attributes.

    Uniqueness by Value (:db.unique/value)

    Marking a tuple attribute with :db/unique :db.unique/value ensures that no two entities have the same combination of values for the constituent attributes.

    Uniqueness by Identity (:db.unique/identity)

    Marking a tuple attribute with :db/unique :db.unique/identity allows you to use the tuple as a lookup reference to find an entity.

    Example of uniqueness by identity:

    ;; Schema with tuple-based identity
    {:a+b {:db/tupleAttrs [:a :b] :db/unique :db.unique/identity}}
    
    ;; Using the tuple to look up an entity
    (d/entity (d/db conn) [:a+b ["a" "b"]])
  8. Perform Garbage Collection on storage

    master

    Because incremental storage reuses nodes and leaves old ones behind, garbage accumulates. Use d/collect-garbage to clean up unreferenced nodes. This requires your IStorage implementation to support -list-addresses and -delete.

    (d/collect-garbage storage)
  9. Serialize DataScript using serializable/from-serializable

    master
    A faster serialization method that allows you to use your own serialization format (like JSON via Cheshire or Transit). It converts the database into a serialization-friendly data structure that avoids keywords to ensure compatibility with formats like JSON.
  10. Manage composite tuple lifecycle

    master

    DataScript handles the lifecycle of composite tuples automatically based on the underlying attributes:

    1. Automatic Population: When you assert attributes that form a tuple, the tuple is created. If an attribute is missing, its position in the tuple vector will be nil.
    2. Automatic Updates: Changing any constituent attribute automatically updates the tuple value.
    3. Automatic Retraction: If all attributes making up a tuple are retracted, the tuple attribute is also retracted.
    4. Restriction: You cannot directly modify or assert a tuple attribute. Attempting to do so will result in a clojure.lang.ExceptionInfo: Can’t modify tuple attrs directly.

    Example of automatic population and update:

    ;; Schema definition
    {:a+b+c {:db/tupleAttrs [:a :b :c]}}
    
    ;; Asserting constituent attributes automatically populates the tuple
    (d/transact! conn [{:db/id 1, :a "a", :b "b"}])
    ;; Result: {:db/id 1, :a "a", :b "b", :a+b+c ["a" "b" nil]}
    
    ;; Updating constituent attributes updates the tuple
    (d/transact! conn [{:db/id 1, :a "A", :b "B", :c "c"}])
    ;; Result: {:db/id 1, :a "A", :b "B", :c "c", :a+b+c ["A" "B" "c"]}
  11. Return maps from queries using :keys, :syms, or :strs

    master

    By default, queries return sets of tuples. To return a set of maps instead, use one of the following return-map keywords followed by a list of keys:

    • :keys: Uses symbols as keys.
    • :syms: Uses symbols as keys.
    • :strs: Forces the map to use strings as keys.

    Requirements:

    • The number of keys provided must exactly match the number of elements in the :find clause.
    • Return maps are only compatible with normal :find and tuple-returning :find queries.
    • If using a tuple-returning :find (e.g., [:find [?a ?b]]), the result will be a single map containing the tuple elements mapped to the specified keys.