(l/script- :js
{:runtime :basic
:require [[xt.lang.spec-base :as xt]
[xt.db.system.main :as main]
[xt.db.system.impl-common :as impl-common]
[xt.db.system.impl-memory :as impl-memory]]})
(fact:global
{:setup [(l/rt:restart)]
:teardown [(l/rt:stop)]})
xt.db.system
Memory, postgres, sqlite, supabase, and websocket systems.
xt.db.system provides backend implementations for the same database model, from in-memory graphs to postgres, sqlite, Supabase, realtime, and websocket-backed systems.
1 Motivation
The system layer lets tests and applications swap the storage backend while preserving the higher-level database contract. This is why parity tests exist for memory, SQL, and Supabase paths.
2 Internal usage
The main system namespaces are exercised by test-lang/xt/db/system/*. POC tests use these implementations to validate worker, shared-worker, adapter, and Supabase flows.
3 Walkthrough
3.1 Creating a system
A memory system needs only a schema and a table lookup. The same ISourceLocal contract is also provided by postgres, sqlite, and supabase implementations.
create a memory system directly or through the factory
^{:refer xt.db.system.impl-memory/impl-memory :added "4.1"}
(!.js
(var schema {"Task" {"id" {"ident" "id" "order" 0 "type" "text"}
"title" {"ident" "title" "order" 1 "type" "text"}}})
(var lookup {"Task" {"position" 0}})
(var impl (impl-memory/impl-memory schema lookup))
(xt/x:get-key impl "::"))
=> "xt.db.system.impl_memory/ImplMemory"
^{:refer xt.db.system.main/create-impl :added "4.1"}
(!.js
(var schema {"Task" {"id" {"ident" "id" "order" 0 "type" "text"}
"title" {"ident" "title" "order" 1 "type" "text"}}})
(var lookup {"Task" {"position" 0}})
(var impl (main/create-impl "memory" {} schema lookup))
(xt/x:get-key impl "::"))
=> "xt.db.system.impl_memory/ImplMemory"
3.2 Adding and querying records
record-add writes raw rows into a table; pull reads them back. Pull accepts a table name, an optional query map, and an optional projection vector.
add records and pull them back
^{:refer xt.db.system.impl-common/record-add :added "4.1"}
(!.js
(var schema {"Task" {"id" {"ident" "id" "order" 0 "type" "text"}
"title" {"ident" "title" "order" 1 "type" "text"}}})
(var lookup {"Task" {"position" 0}})
(var impl (impl-memory/impl-memory schema lookup))
(impl-common/record-add impl "Task" [{"id" "T-1" "title" "First"}
{"id" "T-2" "title" "Second"}])
(impl-common/pull impl ["Task"]))
=> (just [(just {"id" "T-1" "title" "First"})
(just {"id" "T-2" "title" "Second"})]
:in-any-order)
^{:refer xt.db.system.impl-common/pull :added "4.1"}
(!.js
(var schema {"Task" {"id" {"ident" "id" "order" 0 "type" "text"}
"title" {"ident" "title" "order" 1 "type" "text"}}})
(var lookup {"Task" {"position" 0}})
(var impl (impl-memory/impl-memory schema lookup))
(impl-common/record-add impl "Task" [{"id" "T-1" "title" "First"}])
(impl-common/pull impl ["Task" {"id" "T-1"} ["id" "title"]]))
=> [{"id" "T-1" "title" "First"}]
3.3 Sync payloads and listeners
Real-world clients receive db/sync payloads. sync-process-payload applies those payloads and notifies any listener whose guard matches the changed table.
apply a sync payload and listen for changes
^{:refer xt.db.system.impl-common/sync-process-payload :added "4.1.4"}
(!.js
(var schema {"Task" {"id" {"ident" "id" "order" 0 "type" "text"}
"title" {"ident" "title" "order" 1 "type" "text"}}})
(var lookup {"Task" {"position" 0}})
(var impl (impl-memory/impl-memory schema lookup))
(impl-common/sync-process-payload impl {"db/sync" {"Task" [{"id" "T-3" "title" "Synced"}]}})
(impl-common/pull impl ["Task"]))
=> [{"id" "T-3" "title" "Synced"}]
^{:refer xt.db.system.impl-common/add-db-listener :added "4.1"}
(!.js
(var schema {"Task" {"id" {"ident" "id" "order" 0 "type" "text"}
"title" {"ident" "title" "order" 1 "type" "text"}}})
(var lookup {"Task" {"position" 0}})
(var impl (impl-memory/impl-memory schema lookup))
(var events [])
(impl-common/add-db-listener
impl
"logger"
{"guard" (fn [table] (return (== table "Task")))
"callback" (fn [event] (xt/x:arr-push events event))})
(impl-common/sync-process-payload impl {"db/sync" {"Task" [{"id" "T-4" "title" "Listened"}]}})
events)
=> [{"db/sync" {"Task" [{"id" "T-4" "title" "Listened"}]}}]
3.4 Removing and clearing records
record-delete removes specific ids. A sync payload can also carry db/remove, and clear-db empties every table.
delete records and clear the system
^{:refer xt.db.system.impl-common/record-delete :added "4.1"}
(!.js
(var schema {"Task" {"id" {"ident" "id" "order" 0 "type" "text"}
"title" {"ident" "title" "order" 1 "type" "text"}}})
(var lookup {"Task" {"position" 0}})
(var impl (impl-memory/impl-memory schema lookup))
(impl-common/record-add impl "Task" [{"id" "T-5" "title" "Delete me"}])
(impl-common/record-delete impl "Task" ["T-5"])
(impl-common/pull impl ["Task"]))
=> []
^{:refer xt.db.system.impl-common/sync-process-payload :added "4.1.4"}
(!.js
(var schema {"Task" {"id" {"ident" "id" "order" 0 "type" "text"}
"title" {"ident" "title" "order" 1 "type" "text"}}})
(var lookup {"Task" {"position" 0}})
(var impl (impl-memory/impl-memory schema lookup))
(impl-common/sync-process-payload impl {"db/sync" {"Task" [{"id" "T-6" "title" "Remove me"}]}})
(impl-common/sync-process-payload impl {"db/remove" {"Task" [{"id" "T-6"}]}})
(impl-common/pull impl ["Task"]))
=> []
^{:refer xt.db.system.impl-common/clear-db :added "4.1"}
(!.js
(var schema {"Task" {"id" {"ident" "id" "order" 0 "type" "text"}
"title" {"ident" "title" "order" 1 "type" "text"}}})
(var lookup {"Task" {"position" 0}})
(var impl (impl-memory/impl-memory schema lookup))
(impl-common/record-add impl "Task" [{"id" "T-7" "title" "Cleared"}])
(impl-common/clear-db impl)
[(impl-common/pull impl ["Task"])
(xt/x:len (xt/x:obj-keys (xt/x:get-key impl "rows")))])
=> [[] 0]