diff --git a/Cargo.lock b/Cargo.lock
index 98c4ef55..6c5d2ba5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -21,9 +21,9 @@ dependencies = [
[[package]]
name = "actix-http"
-version = "3.12.1"
+version = "3.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93acb4a42f64936f9b8cae4a433b237599dd6eb6ed06124eb67132ef8cc90662"
+checksum = "48e2faa3e7418ed780cca54829d32782a4008a077230f67457caa063415e99c2"
dependencies = [
"actix-codec",
"actix-rt",
@@ -37,7 +37,7 @@ dependencies = [
"derive_more 2.1.1",
"encoding_rs",
"flate2",
- "foldhash",
+ "foldhash 0.2.0",
"futures-core",
"h2 0.3.27",
"http 0.2.12",
@@ -65,7 +65,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
dependencies = [
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -132,9 +132,9 @@ dependencies = [
[[package]]
name = "actix-web"
-version = "4.13.0"
+version = "4.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff87453bc3b56e9b2b23c1cc0b1be8797184accf51d2abe0f8a33ec275d316bf"
+checksum = "df09e2d9239703dd64056359c920c7f3fba6535ec61a0059e0f44e095ffe02b4"
dependencies = [
"actix-codec",
"actix-http",
@@ -151,7 +151,7 @@ dependencies = [
"cookie",
"derive_more 2.1.1",
"encoding_rs",
- "foldhash",
+ "foldhash 0.2.0",
"futures-core",
"futures-util",
"impl-more",
@@ -182,7 +182,7 @@ dependencies = [
"actix-router",
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -249,9 +249,9 @@ checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
[[package]]
name = "alloc-stdlib"
-version = "0.2.2"
+version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
+checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195"
dependencies = [
"alloc-no-stdlib",
]
@@ -338,9 +338,18 @@ dependencies = [
[[package]]
name = "anyhow"
-version = "1.0.102"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
+
+[[package]]
+name = "ar_archive_writer"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+checksum = "4087686b4b0a3427190bae57a1d9a478dbb2d40c5dc1bd6e2b6d797913bdd348"
+dependencies = [
+ "object",
+]
[[package]]
name = "arbitrary"
@@ -353,9 +362,9 @@ dependencies = [
[[package]]
name = "arc-swap"
-version = "1.9.1"
+version = "1.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207"
+checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b"
dependencies = [
"rustversion",
]
@@ -380,9 +389,9 @@ checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
[[package]]
name = "arrayvec"
-version = "0.7.6"
+version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe"
[[package]]
name = "arrow-schema"
@@ -436,7 +445,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -447,7 +456,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -537,15 +546,15 @@ dependencies = [
[[package]]
name = "bitflags"
-version = "2.12.1"
+version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a"
+checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
[[package]]
name = "bitvec"
-version = "1.0.1"
+version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
+checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837"
dependencies = [
"funty",
"radium",
@@ -578,18 +587,18 @@ dependencies = [
[[package]]
name = "block-buffer"
-version = "0.12.0"
+version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be"
+checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
dependencies = [
"hybrid-array",
]
[[package]]
name = "brotli"
-version = "8.0.3"
+version = "8.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8119e4516436f5708bbc474a9d395bf12f1b5395e93a92a56e647ac3388c8610"
+checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@@ -598,9 +607,9 @@ dependencies = [
[[package]]
name = "brotli-decompressor"
-version = "5.0.1"
+version = "5.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924"
+checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@@ -652,7 +661,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -663,9 +672,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
-version = "1.11.1"
+version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
[[package]]
name = "bytestring"
@@ -693,9 +702,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
-version = "1.2.63"
+version = "1.2.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
+checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -717,9 +726,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chacha20"
-version = "0.10.0"
+version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
+checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
@@ -808,7 +817,7 @@ dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -899,19 +908,24 @@ dependencies = [
name = "coln-store"
version = "0.1.0"
dependencies = [
+ "anyhow",
"blake3",
+ "clap",
"coln-flir-rs",
+ "csv",
"future_form",
"hex",
"hexane",
"js-sys",
"leb128",
+ "petgraph 0.8.3",
"postcard",
"rustyline",
"sedimentree_core",
"serde",
"serde_json",
"shlex",
+ "sqlparser",
"subduction_core",
"subduction_crypto",
"subduction_websocket",
@@ -1241,7 +1255,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -1312,7 +1326,7 @@ dependencies = [
"ordered-float 4.6.0",
"ouroboros",
"paste",
- "petgraph",
+ "petgraph 0.6.5",
"pin-project-lite",
"ptr_meta 0.2.0",
"rand 0.8.6",
@@ -1375,7 +1389,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -1404,7 +1418,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
"unicode-xid",
]
@@ -1418,7 +1432,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustc_version",
- "syn 2.0.117",
+ "syn 2.0.118",
"unicode-xid",
]
@@ -1439,7 +1453,7 @@ version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
dependencies = [
- "block-buffer 0.12.0",
+ "block-buffer 0.12.1",
"const-oid 0.10.2",
"crypto-common 0.2.2",
]
@@ -1452,7 +1466,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -1536,7 +1550,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -1654,7 +1668,7 @@ dependencies = [
"prettyplease",
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -1764,6 +1778,12 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
+[[package]]
+name = "fixedbitset"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
+
[[package]]
name = "flate2"
version = "1.1.9"
@@ -1787,6 +1807,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
[[package]]
name = "foreign-types"
version = "0.3.2"
@@ -1835,7 +1861,7 @@ checksum = "6658fc211f4fa8521b72c00efe6626be82dd9d5bde5042137bcb90b02caee84d"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -1894,7 +1920,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -1971,16 +1997,14 @@ dependencies = [
[[package]]
name = "getrandom"
-version = "0.4.2"
+version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
"rand_core 0.10.1",
- "wasip2",
- "wasip3",
]
[[package]]
@@ -2017,9 +2041,9 @@ dependencies = [
[[package]]
name = "h2"
-version = "0.4.14"
+version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733"
+checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
dependencies = [
"atomic-waker",
"bytes",
@@ -2088,7 +2112,7 @@ version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
- "foldhash",
+ "foldhash 0.1.5",
]
[[package]]
@@ -2144,7 +2168,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hexane"
version = "1.0.0-alpha.1"
-source = "git+https://github.com/automerge/automerge?branch=hexane_v1#30b83c03dc9669654153c01fe89f3fb931198324"
+source = "git+https://github.com/automerge/automerge?branch=hexane_v1#583114e24ec5f01e7568503099e840168d3735f3"
dependencies = [
"leb128",
"thiserror 2.0.18",
@@ -2232,9 +2256,9 @@ checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
[[package]]
name = "hybrid-array"
-version = "0.4.12"
+version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da"
+checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c"
dependencies = [
"typenum",
]
@@ -2249,7 +2273,7 @@ dependencies = [
"bytes",
"futures-channel",
"futures-core",
- "h2 0.4.14",
+ "h2 0.4.15",
"http 1.4.2",
"http-body",
"httparse",
@@ -2405,12 +2429,6 @@ dependencies = [
"zerovec",
]
-[[package]]
-name = "id-arena"
-version = "2.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
-
[[package]]
name = "idna"
version = "1.1.0"
@@ -2434,9 +2452,9 @@ dependencies = [
[[package]]
name = "impl-more"
-version = "0.1.9"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2"
+checksum = "35a84fd5aa25fae5c0f4a33d9cac2ca017fc622cbd089be2229993514990f870"
[[package]]
name = "impl-trait-for-tuples"
@@ -2446,7 +2464,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -2527,9 +2545,9 @@ dependencies = [
[[package]]
name = "js-sys"
-version = "0.3.100"
+version = "0.3.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162"
+checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
dependencies = [
"cfg-if",
"futures-util",
@@ -2550,15 +2568,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "leb128"
-version = "0.2.6"
+version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545"
-
-[[package]]
-name = "leb128fmt"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52"
[[package]]
name = "libbz2-rs-sys"
@@ -2618,9 +2630,9 @@ dependencies = [
[[package]]
name = "log"
-version = "0.4.32"
+version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "lru-slab"
@@ -2665,9 +2677,9 @@ dependencies = [
[[package]]
name = "memchr"
-version = "2.8.1"
+version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
+checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
[[package]]
name = "memory-stats"
@@ -2850,7 +2862,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -2913,6 +2925,15 @@ dependencies = [
"libc",
]
+[[package]]
+name = "object"
+version = "0.37.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "object_store"
version = "0.12.5"
@@ -2991,7 +3012,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -3054,7 +3075,7 @@ dependencies = [
"proc-macro2",
"proc-macro2-diagnostics",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -3130,10 +3151,22 @@ version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
dependencies = [
- "fixedbitset",
+ "fixedbitset 0.4.2",
"indexmap",
]
+[[package]]
+name = "petgraph"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
+dependencies = [
+ "fixedbitset 0.5.7",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "serde",
+]
+
[[package]]
name = "pin-project-lite"
version = "0.2.17"
@@ -3240,7 +3273,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -3284,11 +3317,21 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
"version_check",
"yansi",
]
+[[package]]
+name = "psm"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea"
+dependencies = [
+ "ar_archive_writer",
+ "cc",
+]
+
[[package]]
name = "ptr_meta"
version = "0.1.4"
@@ -3341,9 +3384,9 @@ dependencies = [
[[package]]
name = "quick_cache"
-version = "0.6.23"
+version = "0.6.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a3db184a8b66cfe87f0263a1de147a6b554c864d1767c6f7fa4eb0e5497b565"
+checksum = "b9c6658afe513a3b484e3abfdaa0d03ef3c0bbf017542c178dd55f94eb3051f9"
dependencies = [
"ahash 0.8.12",
"equivalent",
@@ -3353,9 +3396,9 @@ dependencies = [
[[package]]
name = "quinn"
-version = "0.11.9"
+version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
+checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
dependencies = [
"bytes",
"cfg_aliases",
@@ -3373,9 +3416,9 @@ dependencies = [
[[package]]
name = "quinn-proto"
-version = "0.11.14"
+version = "0.11.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
+checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e"
dependencies = [
"bytes",
"getrandom 0.3.4",
@@ -3408,9 +3451,9 @@ dependencies = [
[[package]]
name = "quote"
-version = "1.0.45"
+version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
dependencies = [
"proc-macro2",
]
@@ -3472,7 +3515,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
dependencies = [
"chacha20",
- "getrandom 0.4.2",
+ "getrandom 0.4.3",
"rand_core 0.10.1",
]
@@ -3541,6 +3584,26 @@ dependencies = [
"crossbeam-utils",
]
+[[package]]
+name = "recursive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0786a43debb760f491b1bc0269fe5e84155353c67482b9e60d0cfb596054b43e"
+dependencies = [
+ "recursive-proc-macro-impl",
+ "stacker",
+]
+
+[[package]]
+name = "recursive-proc-macro-impl"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76009fbe0614077fc1a2ce255e3a1881a2e3a3527097d5dc6d8212c585e7e38b"
+dependencies = [
+ "quote",
+ "syn 2.0.118",
+]
+
[[package]]
name = "redox_syscall"
version = "0.5.18"
@@ -3604,7 +3667,7 @@ dependencies = [
"bytes",
"futures-core",
"futures-util",
- "h2 0.4.14",
+ "h2 0.4.15",
"http 1.4.2",
"http-body",
"http-body-util",
@@ -3738,9 +3801,9 @@ dependencies = [
[[package]]
name = "rustls"
-version = "0.23.40"
+version = "0.23.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
+checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f"
dependencies = [
"once_cell",
"ring",
@@ -3773,9 +3836,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
-version = "1.14.1"
+version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
+checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
dependencies = [
"web-time",
"zeroize",
@@ -3800,9 +3863,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "rustyline"
-version = "18.0.0"
+version = "18.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4a990b25f351b25139ddc7f21ee3f6f56f86d6846b74ac8fad3a719a287cd4a0"
+checksum = "53f6a737db68eb1a8ccff86b584b2fc13eca6a7bb6f78ebc7c529547e3ab9684"
dependencies = [
"bitflags",
"cfg-if",
@@ -3881,7 +3944,7 @@ dependencies = [
[[package]]
name = "sedimentree_core"
version = "0.13.0"
-source = "git+https://github.com/inkandswitch/subduction#9c95d13aff18c860a1582fdc3536686919f09057"
+source = "git+https://github.com/inkandswitch/subduction#03d322c0313578e232d989697d0f47f772083f25"
dependencies = [
"arbitrary",
"async-lock",
@@ -3950,7 +4013,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -3961,7 +4024,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -4115,18 +4178,18 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
-version = "1.15.1"
+version = "1.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
dependencies = [
"serde",
]
[[package]]
name = "smawk"
-version = "0.3.2"
+version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
+checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100"
[[package]]
name = "snap"
@@ -4173,12 +4236,35 @@ dependencies = [
"der",
]
+[[package]]
+name = "sqlparser"
+version = "0.62.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c6d1b651dc4edf07eead2a0c6c78016ce971bc2c10da5266861b13f25e7cec"
+dependencies = [
+ "log",
+ "recursive",
+]
+
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+[[package]]
+name = "stacker"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "libc",
+ "psm",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "static_assertions"
version = "1.1.0"
@@ -4194,7 +4280,7 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subduction_core"
version = "0.16.0"
-source = "git+https://github.com/inkandswitch/subduction#9c95d13aff18c860a1582fdc3536686919f09057"
+source = "git+https://github.com/inkandswitch/subduction#03d322c0313578e232d989697d0f47f772083f25"
dependencies = [
"async-channel",
"async-lock",
@@ -4216,7 +4302,7 @@ dependencies = [
[[package]]
name = "subduction_crypto"
version = "0.8.0"
-source = "git+https://github.com/inkandswitch/subduction#9c95d13aff18c860a1582fdc3536686919f09057"
+source = "git+https://github.com/inkandswitch/subduction#03d322c0313578e232d989697d0f47f772083f25"
dependencies = [
"ed25519-dalek",
"future_form",
@@ -4230,7 +4316,7 @@ dependencies = [
[[package]]
name = "subduction_websocket"
version = "0.11.0"
-source = "git+https://github.com/inkandswitch/subduction#9c95d13aff18c860a1582fdc3536686919f09057"
+source = "git+https://github.com/inkandswitch/subduction#03d322c0313578e232d989697d0f47f772083f25"
dependencies = [
"async-channel",
"async-lock",
@@ -4270,9 +4356,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.117"
+version = "2.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
dependencies = [
"proc-macro2",
"quote",
@@ -4296,7 +4382,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -4312,7 +4398,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
- "getrandom 0.4.2",
+ "getrandom 0.4.3",
"once_cell",
"rustix",
"windows-sys 0.61.2",
@@ -4374,7 +4460,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -4385,7 +4471,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -4498,7 +4584,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -4600,7 +4686,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -4670,7 +4756,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -4799,17 +4885,17 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
"uuid",
]
[[package]]
name = "uuid"
-version = "1.23.3"
+version = "1.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7"
+checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53"
dependencies = [
- "getrandom 0.4.2",
+ "getrandom 0.4.3",
"js-sys",
"serde_core",
"wasm-bindgen",
@@ -4860,27 +4946,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
-version = "1.0.3+wasi-0.2.9"
+version = "1.0.4+wasi-0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
dependencies = [
- "wit-bindgen 0.57.1",
-]
-
-[[package]]
-name = "wasip3"
-version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
-dependencies = [
- "wit-bindgen 0.51.0",
+ "wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
-version = "0.2.123"
+version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563"
+checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
dependencies = [
"cfg-if",
"once_cell",
@@ -4891,9 +4968,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.73"
+version = "0.4.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "54568702fabf5d4849ce2b90fadfa64168a097eaf4b351ce9df8b687a0086aaf"
+checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -4901,9 +4978,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.123"
+version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc"
+checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -4911,48 +4988,26 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.123"
+version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b"
+checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.123"
+version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92"
+checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
dependencies = [
"unicode-ident",
]
-[[package]]
-name = "wasm-encoder"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
-dependencies = [
- "leb128fmt",
- "wasmparser",
-]
-
-[[package]]
-name = "wasm-metadata"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
-dependencies = [
- "anyhow",
- "indexmap",
- "wasm-encoder",
- "wasmparser",
-]
-
[[package]]
name = "wasm-streams"
version = "0.4.2"
@@ -4966,23 +5021,11 @@ dependencies = [
"web-sys",
]
-[[package]]
-name = "wasmparser"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
-dependencies = [
- "bitflags",
- "hashbrown 0.15.5",
- "indexmap",
- "semver",
-]
-
[[package]]
name = "web-sys"
-version = "0.3.100"
+version = "0.3.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e0871acf327f283dc6da28a1696cdc64fb355ba9f935d052021fa77f35cce69"
+checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -5050,7 +5093,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -5061,7 +5104,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -5244,100 +5287,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
-[[package]]
-name = "wit-bindgen"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
-dependencies = [
- "wit-bindgen-rust-macro",
-]
-
[[package]]
name = "wit-bindgen"
version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
-[[package]]
-name = "wit-bindgen-core"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
-dependencies = [
- "anyhow",
- "heck 0.5.0",
- "wit-parser",
-]
-
-[[package]]
-name = "wit-bindgen-rust"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
-dependencies = [
- "anyhow",
- "heck 0.5.0",
- "indexmap",
- "prettyplease",
- "syn 2.0.117",
- "wasm-metadata",
- "wit-bindgen-core",
- "wit-component",
-]
-
-[[package]]
-name = "wit-bindgen-rust-macro"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
-dependencies = [
- "anyhow",
- "prettyplease",
- "proc-macro2",
- "quote",
- "syn 2.0.117",
- "wit-bindgen-core",
- "wit-bindgen-rust",
-]
-
-[[package]]
-name = "wit-component"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
-dependencies = [
- "anyhow",
- "bitflags",
- "indexmap",
- "log",
- "serde",
- "serde_derive",
- "serde_json",
- "wasm-encoder",
- "wasm-metadata",
- "wasmparser",
- "wit-parser",
-]
-
-[[package]]
-name = "wit-parser"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
-dependencies = [
- "anyhow",
- "id-arena",
- "indexmap",
- "log",
- "semver",
- "serde",
- "serde_derive",
- "serde_json",
- "unicode-xid",
- "wasmparser",
-]
-
[[package]]
name = "writeable"
version = "0.6.3"
@@ -5384,7 +5339,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
"synstructure",
]
@@ -5405,7 +5360,7 @@ checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -5425,28 +5380,28 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
"synstructure",
]
[[package]]
name = "zeroize"
-version = "1.8.2"
+version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
-version = "1.4.3"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
+checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -5479,7 +5434,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 2.0.118",
]
[[package]]
@@ -5511,9 +5466,9 @@ dependencies = [
[[package]]
name = "zlib-rs"
-version = "0.6.3"
+version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513"
+checksum = "977347db8caa080403f6b6b7c1cda9479a8e869316f7e13a59b19076a40f94e3"
[[package]]
name = "zmij"
diff --git a/packages/coln-js-runtime/src/rust/handles.rs b/packages/coln-js-runtime/src/rust/handles.rs
index 4efe2be1..c1e3a6a5 100644
--- a/packages/coln-js-runtime/src/rust/handles.rs
+++ b/packages/coln-js-runtime/src/rust/handles.rs
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: 2026 Coln contributors
//
// SPDX-License-Identifier: Apache-2.0 OR MIT
+
use coln_flir_rs::ir;
use coln_store::{
commit::hash::CommitHash as StoreCommitHash,
diff --git a/packages/coln-store/.gitignore b/packages/coln-store/.gitignore
index def30136..26eb7d1f 100644
--- a/packages/coln-store/.gitignore
+++ b/packages/coln-store/.gitignore
@@ -64,3 +64,9 @@ tmp/
.claude/
.codex
.agent/
+
+# colnstore
+*.colnstore
+
+# compressed data
+*.zst
diff --git a/packages/coln-store/AGENTS.md b/packages/coln-store/AGENTS.md
index 1bcb9258..051a67c4 100644
--- a/packages/coln-store/AGENTS.md
+++ b/packages/coln-store/AGENTS.md
@@ -79,7 +79,7 @@ Quick examples:
## Required Validation
-Run these checks for any non-trivial change:
+Run these checks for non-trivial change when you are told to. Otherwise there is no need to run checks.
`just check`
@@ -95,9 +95,9 @@ Use this sequence for your first change:
1. Read `src/lib.rs`, and the relevant module files.
2. Implement the smallest possible code change.
3. Add or update tests that fail before and pass after.
-4. Run `cargo test --workspace --all-targets`.
-5. Run `cargo clippy --workspace --all-targets --all-features -- -D warnings`.
-6. Run `cargo fmt --all --check`.
+4. Run `cargo test -p coln-store --all-targets`.
+5. Run `cargo clippy -p coln-store --all-targets --all-features -- -D warnings`.
+6. Run `cargo fmt -p coln-store --check`.
7. Update docs if public API or user-facing behavior changed.
Example scopes that are good first tasks:
@@ -135,9 +135,9 @@ Before coding:
Before submitting:
-1. Passing `cargo test --workspace --all-targets`.
-2. Passing `cargo clippy --workspace --all-targets --all-features -- -D warnings`.
-3. Passing `cargo fmt --all --check`.
+1. Passing `cargo test -p coln-store --all-targets`.
+2. Passing `cargo clippy -p coln-store --all-targets --all-features -- -D warnings`.
+3. Passing `cargo fmt -p coln-store --check`.
4. Tests added or updated where behavior changed.
5. Docs updated where public behavior changed.
@@ -184,6 +184,6 @@ Use this review format:
Suggested PR checklist:
- [ ] Tests added or updated for behavior changes
-- [ ] `cargo test --workspace --all-targets` passes
-- [ ] `cargo clippy --workspace --all-targets --all-features -- -D warnings` passes
-- [ ] `cargo fmt --all --check` passes
+- [ ] `cargo test -p coln-store --all-targets` passes
+- [ ] `cargo clippy -p coln-store --all-targets --all-features -- -D warnings` passes
+- [ ] `cargo fmt -p coln-store --check` passes
diff --git a/packages/coln-store/Cargo.toml b/packages/coln-store/Cargo.toml
index f538707c..2db5fa30 100644
--- a/packages/coln-store/Cargo.toml
+++ b/packages/coln-store/Cargo.toml
@@ -18,18 +18,23 @@ exclude = [
]
[dependencies]
+anyhow.workspace = true
blake3 = "1.8.5"
+clap = { version = "4.6.1", features = ["derive"], optional = true }
coln-flir-rs = { path = "../coln-flir-rs" }
+csv = "1.4.0"
# coln-query = { path = "../coln-query" }
hex = "0.4.3"
hexane = { git = "https://github.com/automerge/automerge", branch = "hexane_v1", package = "hexane" }
js-sys = { version = "0.3.83", optional = true }
leb128 = "0.2.6"
+petgraph = "0.8.3"
postcard = { version = "1.1.3", features = ["alloc"] }
rustyline = { version = "18.0.0", optional = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
shlex = "2.0.1"
+sqlparser = "0.62.0"
thiserror = "2.0.18"
tracing = { workspace = true }
tracing-subscriber = { workspace = true, features = [
@@ -64,7 +69,7 @@ tokio = { version = "1.52.3", features = [
[features]
default = ["native"]
-native = ["dep:rustyline", "dep:tracing-subscriber"]
+native = ["dep:rustyline", "dep:tracing-subscriber", "dep:clap"]
wasm = ["dep:js-sys"]
[[bin]]
diff --git a/packages/coln-store/README.md b/packages/coln-store/README.md
index 9e8cf50a..0c31e476 100644
--- a/packages/coln-store/README.md
+++ b/packages/coln-store/README.md
@@ -10,9 +10,9 @@ Coln Store is the storage engine for Coln. Roughly it ought to support:
This repo lets you give the store a compiled Coln theory, load it, and then add
data to its tables.
-Example of loading a schema and inserting several related rows. Type `/help`
-for the full command list (for example `load-schema`, `add`, `dump-table`,
-`dump-store`, and the batch form below).
+Example of loading a schema and inserting several related rows. Type `.help`
+for the full command list (for example `.load`, `.schema`, `.rules`,
+`.dump`, `.tables`, `.open`, `.save`, `add`, and the batch form below).
## Transaction
@@ -27,12 +27,12 @@ The snippet below loads the `paths` theory, creates two graphs (`g0`, `g1`),
records `g1` as the designated graph for the `Path.G0` and `Path.G1` indices,
adds two vertices on `g0`, and adds one edge between them on `g0`.
-After `commit`, inspect what landed with `dump-table
;`. For example,
-`dump-table Path.Graphs;`, `dump-table Path.G.V;`, or `dump-table Path.G.E;`,
-or print the entire store with `dump-store;`.
+After `commit`, inspect what landed with `.dump `. For example,
+`.dump Path.Graphs`, `.dump Path.G.V`, or `.dump Path.G.E`, print every
+table with `.tables`, or print all compiled rules with `.rules`.
```text
-coln-store> load-schema tests/data/Path.json;
+coln-store> .load tests/data/Path.json
begin transact;
g0 = add Path.Graphs values ();
g1 = add Path.Graphs values ();
@@ -47,8 +47,24 @@ begin transact;
commit;
-persist paths.bin;
-load-store paths.bin;
+.save paths.bin
+.open paths.bin
+
+.load tests/data/ExprRealm.json
+begin transact;
+ t0 = add ExprRealm.exprs.t values ();
+ t1 = add ExprRealm.exprs.t values ();
+ t2 = add ExprRealm.exprs.t values ();
+ va = add ExprRealm.v0 values ();
+ vb = add ExprRealm.v0 values ();
+
+ ta = add ExprRealm.exprs.vars values (va t0);
+ tb = add ExprRealm.exprs.vars values (vb t1);
+
+ apb = add ExprRealm.exprs.plus values (t0 t1 t2);
+
+commit;
+
```
To get a violation of the law, say (`Path.Hom.V.total`), change the line
diff --git a/packages/coln-store/src/commit/mod.rs b/packages/coln-store/src/commit/mod.rs
index 7504c1f3..55271723 100644
--- a/packages/coln-store/src/commit/mod.rs
+++ b/packages/coln-store/src/commit/mod.rs
@@ -203,6 +203,8 @@ fn collect_op_hashes(pending: &[PendingOp], hash_mapper: &mut HashMapper) {
#[cfg(test)]
mod tests {
+ use std::sync::LazyLock;
+
use super::*;
use crate::commit::chunk::{Chunk, hash};
use crate::commit::hash::HASH_SIZE;
@@ -210,7 +212,6 @@ mod tests {
use crate::ir::{BuiltinTy, ColType, ColumnEntry, EntityVariant, Path};
use crate::table::RowId;
use crate::txn::ops::{RowRef, TempRowId};
- use std::sync::LazyLock;
fn zero_hash() -> CommitHash {
CommitHash([0u8; HASH_SIZE])
diff --git a/packages/coln-store/src/commit/pst.rs b/packages/coln-store/src/commit/pst.rs
index ecb83a1e..bf3fe209 100644
--- a/packages/coln-store/src/commit/pst.rs
+++ b/packages/coln-store/src/commit/pst.rs
@@ -125,6 +125,8 @@ fn decode_store_chunks(next_oid: TableOid, chunks: Vec) -> Result Schema {
Schema {
diff --git a/packages/coln-store/src/commit/utils.rs b/packages/coln-store/src/commit/utils.rs
index 0f08f019..906b7a10 100644
--- a/packages/coln-store/src/commit/utils.rs
+++ b/packages/coln-store/src/commit/utils.rs
@@ -2,9 +2,10 @@
//
// SPDX-License-Identifier: Apache-2.0 OR MIT
-use crate::commit::error::CodecError;
use std::io::Write;
+use crate::commit::error::CodecError;
+
// ── Write helpers ───────────────────────────────────────────────────────────
pub(crate) fn write_u8(buf: &mut Vec, value: u8) -> Result<(), CodecError> {
diff --git a/packages/coln-store/src/commit/wire/data.rs b/packages/coln-store/src/commit/wire/data.rs
index 60a7f815..56dadb09 100644
--- a/packages/coln-store/src/commit/wire/data.rs
+++ b/packages/coln-store/src/commit/wire/data.rs
@@ -2,12 +2,15 @@
//
// SPDX-License-Identifier: Apache-2.0 OR MIT
-use hexane::v1::{Column, DeltaColumn};
use std::io::Write;
+use hexane::v1::{Column, DeltaColumn};
+
use crate::commit::author::Author;
use crate::commit::leb128 as commit_leb128;
-use crate::commit::wire::prim::{ValueMeta, decode_prim_value, encode_prim_value};
+use crate::commit::wire::prim::{
+ self, ValueMeta, decode_prim_value, encode_path, encode_prim_value,
+};
use crate::{
commit::{
error::CodecError,
@@ -307,9 +310,8 @@ fn encode_op_group(
}
let mut buf = Vec::new();
- let path = table.to_string();
- commit_leb128::write_len(&mut buf, path.len());
- buf.write_all(path.as_bytes())?;
+ let path = encode_path(table);
+ buf.write_all(&path)?;
commit_leb128::write_len(&mut buf, rows.len());
@@ -470,12 +472,7 @@ where
{
let mut pos = 0usize;
- let path_len = commit_leb128::read_len(data, &mut pos, "op group table path length")?;
- let path_bytes = read_slice(data, &mut pos, path_len, "op group table path")?;
- let table =
- Path::from(std::str::from_utf8(path_bytes).map_err(|_| {
- CodecError::DataFormatError("op group table path: invalid utf-8".into())
- })?);
+ let table = prim::decode_path(data, &mut pos)?;
let row_count = commit_leb128::read_len(data, &mut pos, "op group row count")?;
let column_count = commit_leb128::read_len(data, &mut pos, "op group column count")?;
@@ -862,9 +859,8 @@ mod tests {
let encoded = encode_op_group(&table, &schema, &ops, &hash_mapper).expect("encode group");
let mut pos = 0usize;
- let path_len = commit_leb128::read_len(&encoded, &mut pos, "path length").unwrap();
- let path_bytes = read_slice(&encoded, &mut pos, path_len, "path").unwrap();
- assert_eq!(std::str::from_utf8(path_bytes).unwrap(), "T");
+ let path = prim::decode_path(&encoded, &mut pos).unwrap();
+ assert_eq!(path.to_string(), "T");
assert_eq!(
commit_leb128::read_len(&encoded, &mut pos, "row count").unwrap(),
2
@@ -1016,10 +1012,8 @@ mod tests {
let group_a =
commit_leb128::read_len_prefixed_bytes(&encoded, &mut pos, "group a").unwrap();
let mut group_pos = 0usize;
- let path_len =
- commit_leb128::read_len(group_a, &mut group_pos, "group a path length").unwrap();
- let path = read_slice(group_a, &mut group_pos, path_len, "group a path").unwrap();
- assert_eq!(std::str::from_utf8(path).unwrap(), "A");
+ let path = prim::decode_path(group_a, &mut group_pos).unwrap();
+ assert_eq!(path.to_string(), "A");
assert_eq!(
commit_leb128::read_len(group_a, &mut group_pos, "group a rows").unwrap(),
2
@@ -1032,10 +1026,8 @@ mod tests {
let group_b =
commit_leb128::read_len_prefixed_bytes(&encoded, &mut pos, "group b").unwrap();
let mut group_pos = 0usize;
- let path_len =
- commit_leb128::read_len(group_b, &mut group_pos, "group b path length").unwrap();
- let path = read_slice(group_b, &mut group_pos, path_len, "group b path").unwrap();
- assert_eq!(std::str::from_utf8(path).unwrap(), "B");
+ let path = prim::decode_path(group_b, &mut group_pos).unwrap();
+ assert_eq!(path.to_string(), "B");
assert_eq!(
commit_leb128::read_len(group_b, &mut group_pos, "group b rows").unwrap(),
1
diff --git a/packages/coln-store/src/commit/wire/prim.rs b/packages/coln-store/src/commit/wire/prim.rs
index 28170ab9..3db17298 100644
--- a/packages/coln-store/src/commit/wire/prim.rs
+++ b/packages/coln-store/src/commit/wire/prim.rs
@@ -4,9 +4,10 @@
use std::io::Write;
-use coln_flir_rs::ir::BuiltinTy;
+use coln_flir_rs::ir::{self, BuiltinTy};
use hexane::{PackError, lebsize};
+use crate::commit::leb128 as commit_leb128;
use crate::{commit::error::CodecError, txn::ops::TxnCellValue};
/// Number of low bits reserved for the [`ValueType`] code in a [`ValueMeta`].
@@ -141,6 +142,8 @@ impl hexane::v1::PrefixValue for ValueMeta {
}
}
+/// Writes to the buffer `out` the encoded data, and returns the corresponding
+/// `ValueMeta` representation
pub(crate) fn encode_prim_value(
value: &TxnCellValue,
prim: &BuiltinTy,
@@ -208,6 +211,37 @@ pub(crate) fn decode_prim_value(
}
}
+pub(crate) fn encode_path(path: &ir::Path) -> Vec {
+ let mut out = Vec::new();
+ commit_leb128::write_len(&mut out, path.0.len());
+ for qname in &path.0 {
+ commit_leb128::write_len(&mut out, qname.len());
+ for part in qname {
+ commit_leb128::write_len_prefixed_bytes(&mut out, part.as_bytes());
+ }
+ }
+ out
+}
+
+pub(crate) fn decode_path(data: &[u8], pos: &mut usize) -> Result {
+ let qname_count = commit_leb128::read_len(data, pos, "path name count")?;
+ let mut path = Vec::with_capacity(qname_count);
+
+ for _ in 0..qname_count {
+ let part_count = commit_leb128::read_len(data, pos, "part count")?;
+ let mut qname = Vec::with_capacity(part_count);
+ for _ in 0..part_count {
+ let part_bytes = commit_leb128::read_len_prefixed_bytes(data, pos, "qname part")?;
+ let part = std::str::from_utf8(part_bytes)
+ .map_err(|_| CodecError::DataFormatError("path part invalid utf-8".into()))?;
+ qname.push(part.to_owned())
+ }
+ path.push(qname);
+ }
+
+ Ok(ir::Path(path))
+}
+
#[allow(dead_code)]
fn encode_bigint(value: &[u8], out: &mut Vec) -> Result {
out.write_all(value)?;
diff --git a/packages/coln-store/src/lib.rs b/packages/coln-store/src/lib.rs
index 340d672f..f72c692d 100644
--- a/packages/coln-store/src/lib.rs
+++ b/packages/coln-store/src/lib.rs
@@ -2,10 +2,11 @@
//
// SPDX-License-Identifier: Apache-2.0 OR MIT
-pub use coln_flir_rs::ir;
+use coln_flir_rs::ir;
pub mod commit;
#[cfg(feature = "native")]
pub mod repl;
+mod roweq;
pub mod solver;
pub mod store;
pub mod table;
diff --git a/packages/coln-store/src/main.rs b/packages/coln-store/src/main.rs
index 6ac4d01c..b2a9effe 100644
--- a/packages/coln-store/src/main.rs
+++ b/packages/coln-store/src/main.rs
@@ -2,15 +2,29 @@
//
// SPDX-License-Identifier: Apache-2.0 OR MIT
+use clap::Parser;
use coln_store::repl;
use tracing_subscriber::EnvFilter;
-fn main() -> Result<(), Box> {
+#[derive(Parser, Debug)]
+struct Args {
+ #[arg(long, default_value_t = false, hide = true)]
+ enable_sql_mode: bool,
+}
+
+fn main() -> anyhow::Result<()> {
+ const DEFAULT_FILTER: &str = if cfg!(debug_assertions) {
+ "coln_store=debug"
+ } else {
+ "coln_store=info"
+ };
+
tracing_subscriber::fmt()
.with_env_filter(
- EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("coln_store=info")),
+ EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(DEFAULT_FILTER)),
)
.init();
- repl::run()
+ let args = Args::parse();
+ repl::run(args.enable_sql_mode)
}
diff --git a/packages/coln-store/src/repl/cli.rs b/packages/coln-store/src/repl/cli.rs
new file mode 100644
index 00000000..8b350691
--- /dev/null
+++ b/packages/coln-store/src/repl/cli.rs
@@ -0,0 +1,228 @@
+// SPDX-FileCopyrightText: 2026 Coln contributors
+//
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+use std::path::PathBuf;
+
+use anyhow::Result;
+use rustyline::completion::{Completer, FilenameCompleter, Pair};
+use rustyline::error::ReadlineError;
+use rustyline::highlight::Highlighter;
+use rustyline::hint::Hinter;
+use rustyline::validate::Validator;
+use rustyline::{Context, Helper};
+
+use crate::repl::ShellMode;
+
+const COMMANDS: &[&str] = &[
+ ".help", ".exit", ".quit", ".load", ".open", ".save", ".tables", ".rules", ".schema", ".dump",
+ "add", "begin",
+];
+
+const PATH_COMMANDS: &[&str] = &[".load", ".open", ".save"];
+
+const HISTORY_FILE: &str = ".coln-store_history";
+
+pub(super) fn history_path() -> PathBuf {
+ std::env::var_os("HOME")
+ .map(PathBuf::from)
+ .unwrap_or_else(|| PathBuf::from("."))
+ .join(HISTORY_FILE)
+}
+
+pub(super) struct CommandHelper {
+ filename_completer: FilenameCompleter,
+}
+
+impl CommandHelper {
+ pub(super) fn new() -> Self {
+ Self {
+ filename_completer: FilenameCompleter::new(),
+ }
+ }
+}
+impl Helper for CommandHelper {}
+
+impl Hinter for CommandHelper {
+ type Hint = String;
+}
+
+impl Highlighter for CommandHelper {}
+
+impl Validator for CommandHelper {}
+
+impl Completer for CommandHelper {
+ type Candidate = Pair;
+
+ fn complete(
+ &self,
+ line: &str,
+ pos: usize,
+ ctx: &Context<'_>,
+ ) -> Result<(usize, Vec), ReadlineError> {
+ if wants_path_completion(line, pos) {
+ return self.filename_completer.complete(line, pos, ctx);
+ }
+ let (start, candidates) = complete_command(line, pos);
+ Ok((start, candidates))
+ }
+}
+
+fn wants_path_completion(line: &str, pos: usize) -> bool {
+ let prefix = &line[..pos];
+ let Some(command) = prefix.split_whitespace().next() else {
+ return false;
+ };
+ PATH_COMMANDS.contains(&command) && prefix.len() > command.len()
+}
+
+fn complete_command(line: &str, pos: usize) -> (usize, Vec) {
+ let prefix = &line[..pos];
+ if prefix.split_whitespace().count() > 1 || prefix.contains(' ') {
+ return (0, Vec::new());
+ }
+
+ let matches = COMMANDS
+ .iter()
+ .filter(|command| command.starts_with(prefix))
+ .map(|command| Pair {
+ display: (*command).to_string(),
+ replacement: (*command).to_string(),
+ })
+ .collect();
+
+ (0, matches)
+}
+
+pub(super) fn prompt(shell_mode: ShellMode, pending_statement: bool) -> &'static str {
+ if pending_statement {
+ "....> "
+ } else if shell_mode == ShellMode::Sql {
+ "coln-sql> "
+ } else {
+ "coln-store> "
+ }
+}
+
+pub(super) fn is_statement_start(input: &str) -> bool {
+ !input.trim_start().starts_with('.')
+}
+
+pub(super) fn push_statement_line(pending: &mut Option, line: &str) -> Option {
+ let pending_line = pending.get_or_insert_with(String::new);
+ if !pending_line.is_empty() {
+ pending_line.push(' ');
+ }
+ pending_line.push_str(line.trim());
+
+ let buf = pending_line.trim_start();
+ if buf.starts_with("begin transact") {
+ if pending_line.trim_end().ends_with("commit;") {
+ return pending.take();
+ }
+ return None;
+ }
+
+ if pending_line.trim_end().ends_with(';') {
+ pending.take()
+ } else {
+ None
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn completes_command_prefix() {
+ let (start, matches) = complete_command(".q", 2);
+ assert_eq!(start, 0);
+ assert_eq!(matches.len(), 1);
+ assert_eq!(matches[0].replacement, ".quit");
+ }
+
+ #[test]
+ fn does_not_complete_after_first_argument() {
+ let (_, matches) = complete_command(".load te", ".load te".len());
+ assert!(matches.is_empty());
+ }
+
+ #[test]
+ fn path_completion_applies_after_path_command() {
+ assert!(wants_path_completion(".load ", ".load ".len()));
+ assert!(wants_path_completion(
+ ".open tests/da",
+ ".open tests/da".len()
+ ));
+ assert!(wants_path_completion(
+ ".save out.bin",
+ ".save out.bin".len()
+ ));
+ }
+
+ #[test]
+ fn path_completion_does_not_apply_elsewhere() {
+ assert!(!wants_path_completion(".load", ".load".len()));
+ assert!(!wants_path_completion(".dump te", ".dump te".len()));
+ assert!(!wants_path_completion("add T 7", "add T 7".len()));
+ assert!(!wants_path_completion("", 0));
+ }
+
+ #[test]
+ fn path_completion_lists_matching_files() {
+ let helper = CommandHelper::new();
+ let history = rustyline::history::DefaultHistory::new();
+ let ctx = Context::new(&history);
+ let line = ".load tests/data/people.c";
+ let (start, matches) = helper.complete(line, line.len(), &ctx).unwrap();
+ assert_eq!(start, ".load ".len());
+ assert!(
+ matches
+ .iter()
+ .any(|pair| pair.replacement == "tests/data/people.csv"),
+ "expected people.csv in completions, got: {:?}",
+ matches.iter().map(|p| &p.replacement).collect::>()
+ );
+ }
+
+ #[test]
+ fn statement_buffer_waits_for_semicolon() {
+ let mut pending = None;
+ assert_eq!(push_statement_line(&mut pending, "add T 7"), None);
+ assert_eq!(pending.as_deref(), Some("add T 7"));
+ assert_eq!(
+ push_statement_line(&mut pending, "\"alice\";"),
+ Some("add T 7 \"alice\";".to_string())
+ );
+ assert_eq!(pending, None);
+ }
+
+ #[test]
+ fn batch_buffer_waits_for_commit_semicolon() {
+ let mut pending = None;
+ assert_eq!(push_statement_line(&mut pending, "begin transact;"), None);
+ assert_eq!(
+ push_statement_line(&mut pending, "x = add T values (1);"),
+ None
+ );
+ assert_eq!(
+ push_statement_line(&mut pending, "commit;"),
+ Some("begin transact; x = add T values (1); commit;".to_string())
+ );
+ assert_eq!(pending, None);
+ }
+
+ #[test]
+ fn meta_command_is_not_statement_start() {
+ assert!(!is_statement_start(".help"));
+ assert!(is_statement_start("add T 7"));
+ }
+
+ #[test]
+ fn prompt_reflects_mode_and_pending_statement() {
+ assert_eq!(prompt(ShellMode::Coln, false), "coln-store> ");
+ assert_eq!(prompt(ShellMode::Sql, false), "coln-sql> ");
+ assert_eq!(prompt(ShellMode::Sql, true), "....> ");
+ }
+}
diff --git a/packages/coln-store/src/repl/error.rs b/packages/coln-store/src/repl/error.rs
deleted file mode 100644
index da520b04..00000000
--- a/packages/coln-store/src/repl/error.rs
+++ /dev/null
@@ -1,90 +0,0 @@
-// SPDX-FileCopyrightText: 2026 Coln contributors
-//
-// SPDX-License-Identifier: Apache-2.0 OR MIT
-
-use std::fmt;
-
-use crate::commit::error::CodecError;
-use crate::store::error::StoreIntError;
-use crate::table::ValidationError;
-
-#[derive(Debug)]
-pub enum ReplError {
- NoSchemaLoaded,
- UnknownTable(String),
- UnknownBinding(String),
- DuplicateBinding(String),
- ColumnCountMismatch { expected: usize, got: usize },
- BadValue { column: usize, message: String },
- Io(std::io::Error),
- Json(serde_json::Error),
- Store(StoreIntError),
- Persist(CodecError),
-}
-
-/// Parse failure for a single cell inside a `begin batch` block (before column index is known).
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum BatchCellParseError {
- UnknownBinding(String),
- InvalidValue(String),
-}
-
-impl fmt::Display for ReplError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- ReplError::NoSchemaLoaded => write!(f, "no schema loaded"),
- ReplError::UnknownTable(table) => write!(f, "unknown table: {table}"),
- ReplError::UnknownBinding(name) => write!(f, "unknown binding: {name}"),
- ReplError::DuplicateBinding(name) => write!(f, "duplicate binding: {name}"),
- ReplError::ColumnCountMismatch { expected, got } => {
- write!(f, "column count mismatch: expected {expected}, got {got}")
- }
- ReplError::BadValue { column, message } => write!(f, "column {column}: {message}"),
- ReplError::Io(err) => write!(f, "{err}"),
- ReplError::Json(err) => write!(f, "{err}"),
- ReplError::Store(err) => write!(f, "{err}"),
- ReplError::Persist(err) => write!(f, "{err}"),
- }
- }
-}
-
-impl std::error::Error for ReplError {}
-
-impl From for ReplError {
- fn from(value: std::io::Error) -> Self {
- Self::Io(value)
- }
-}
-
-impl From for ReplError {
- fn from(value: serde_json::Error) -> Self {
- Self::Json(value)
- }
-}
-
-impl From for ReplError {
- fn from(value: StoreIntError) -> Self {
- Self::Store(value)
- }
-}
-
-impl From for ReplError {
- fn from(value: CodecError) -> Self {
- Self::Persist(value)
- }
-}
-
-impl From for ReplError {
- fn from(value: ValidationError) -> Self {
- Self::Store(value.into())
- }
-}
-
-impl From for ReplError {
- fn from(value: BatchCellParseError) -> Self {
- match value {
- BatchCellParseError::UnknownBinding(name) => Self::UnknownBinding(name),
- BatchCellParseError::InvalidValue(message) => Self::BadValue { column: 0, message },
- }
- }
-}
diff --git a/packages/coln-store/src/repl/exe.rs b/packages/coln-store/src/repl/exe.rs
deleted file mode 100644
index 1749996c..00000000
--- a/packages/coln-store/src/repl/exe.rs
+++ /dev/null
@@ -1,308 +0,0 @@
-// SPDX-FileCopyrightText: 2026 Coln contributors
-//
-// SPDX-License-Identifier: Apache-2.0 OR MIT
-
-use std::{
- collections::HashMap,
- fs,
- path::{Path, PathBuf},
-};
-
-use crate::{
- commit::pst::decode_store,
- ir::{BuiltinTy, ColType, ColumnEntry, FlatRealm},
- repl::{
- error::ReplError,
- parse::{BatchAssignment, parse_cell_value, parse_cell_value_batch},
- },
- store::Store,
- table::{RowId, Table},
- txn::ops::{TempRowId, TxnCellValue},
-};
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct SchemaSummary {
- pub(crate) source: PathBuf,
- pub(crate) table_count: usize,
- pub(crate) law_count: usize,
- pub(crate) tables: Vec,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct TableSummary {
- pub(crate) path: String,
- pub(crate) column_count: usize,
- pub(crate) primary_key: PrimaryKeySummary,
- pub(crate) columns: Vec,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum PrimaryKeySummary {
- None,
- Singleton,
- Columns(Vec),
-}
-
-pub struct LoadedState {
- pub(crate) store: Store,
- pub(crate) schema: SchemaSummary,
-}
-
-impl LoadedState {
- pub fn store(&self) -> &Store {
- &self.store
- }
-}
-
-fn format_primary_key(primary_key: &PrimaryKeySummary) -> String {
- match primary_key {
- PrimaryKeySummary::None => "none".to_string(),
- PrimaryKeySummary::Singleton => "singleton".to_string(),
- PrimaryKeySummary::Columns(columns) => format!("{columns:?}"),
- }
-}
-
-fn format_col_type(col_type: &ColType) -> String {
- match col_type {
- ColType::RowId { path } => format!("entity({path})"),
- ColType::BuiltinTy { builtin_ty } => match builtin_ty {
- BuiltinTy::BuiltinInt => "int".to_string(),
- BuiltinTy::BuiltinStr => "string".to_string(),
- },
- }
-}
-
-fn format_column(column: &ColumnEntry) -> String {
- format!("{}: {}", column.path, format_col_type(&column.col_type))
-}
-
-impl SchemaSummary {
- fn from_store(source: PathBuf, store: &Store) -> Self {
- let mut tables: Vec = store
- .tables()
- .map(|(_, table)| TableSummary {
- path: table.path().to_string(),
- column_count: table.schema().columns.len(),
- primary_key: match &table.schema().primary_key {
- None => PrimaryKeySummary::None,
- Some(columns) if columns.is_empty() => PrimaryKeySummary::Singleton,
- Some(columns) => PrimaryKeySummary::Columns(columns.clone()),
- },
- columns: table.schema().columns.iter().map(format_column).collect(),
- })
- .collect();
- tables.sort_by(|a, b| a.path.cmp(&b.path));
- let law_count = store.rule_entries().len();
- let table_count = tables.len();
- Self {
- source,
- table_count,
- law_count,
- tables,
- }
- }
-
- fn from_theory(source: PathBuf, theory: &FlatRealm) -> Self {
- let tables = theory
- .tables
- .iter()
- .map(|entry| TableSummary {
- path: entry.path.to_string(),
- column_count: entry.table.columns.len(),
- primary_key: match &entry.table.primary_key {
- None => PrimaryKeySummary::None,
- Some(columns) if columns.is_empty() => PrimaryKeySummary::Singleton,
- Some(columns) => PrimaryKeySummary::Columns(columns.clone()),
- },
- columns: entry.table.columns.iter().map(format_column).collect(),
- })
- .collect();
-
- Self {
- source,
- table_count: theory.tables.len(),
- law_count: theory.rules.len(),
- tables,
- }
- }
-}
-
-pub fn load_store(path: &Path) -> Result {
- let bytes = fs::read(path)?;
- let store = decode_store(&bytes)?;
- let schema = SchemaSummary::from_store(path.to_path_buf(), &store);
- Ok(LoadedState { store, schema })
-}
-
-pub fn load_schema(path: &Path) -> Result {
- let input = fs::read_to_string(path)?;
- let theory: FlatRealm = serde_json::from_str(&input)?;
- let summary = SchemaSummary::from_theory(path.to_path_buf(), &theory);
- let store = Store::try_from_theory(theory)?;
- Ok(LoadedState {
- store,
- schema: summary,
- })
-}
-
-pub fn render_schema_summary(schema: Option<&SchemaSummary>) -> String {
- let Some(schema) = schema else {
- return "no schema loaded".to_string();
- };
-
- let mut lines = vec![
- format!("source: {}", schema.source.display()),
- format!("tables: {}", schema.table_count),
- format!("laws: {}", schema.law_count),
- ];
-
- for table in &schema.tables {
- lines.push(format!(
- "- {} | cols={} | pk={} | [{}]",
- table.path,
- table.column_count,
- format_primary_key(&table.primary_key),
- table.columns.join(", ")
- ));
- }
-
- lines.join("\n")
-}
-
-pub fn add_rows(
- store: &mut Store,
- table_name: &str,
- raw_rows: &[Vec],
-) -> Result, ReplError> {
- let table_path = crate::ir::Path::from(table_name);
- let oid = store
- .resolve_table(&table_path)
- .ok_or_else(|| ReplError::UnknownTable(table_name.to_string()))?;
-
- let table = store
- .table(oid)
- .ok_or_else(|| ReplError::UnknownTable(table_name.to_string()))?;
- let mut rows = Vec::new();
- for raw_values in raw_rows {
- rows.push(parse_txn_values(table, raw_values)?);
- }
-
- let mut tx = store.transaction();
- let mut temp_ids = Vec::new();
- for values in rows {
- temp_ids.push(tx.add_internal(&table_path, values)?);
- }
- let commit = tx.commit()?;
- let row_ids = temp_ids
- .into_iter()
- .map(|temp_id| temp_id.resolve(commit))
- .collect();
- Ok(row_ids)
-}
-
-/// Parse and commit a batch transaction, allowing later rows to refer to earlier bindings.
-pub fn run_transact(
- store: &mut Store,
- assignments: &[BatchAssignment],
-) -> Result {
- let mut bindings: HashMap = HashMap::new();
- let mut pending = Vec::new();
-
- for (index, a) in assignments.iter().enumerate() {
- if bindings.contains_key(&a.name) {
- return Err(ReplError::DuplicateBinding(a.name.clone()));
- }
- let table_path = crate::ir::Path::from(a.table.as_str());
- let table = store
- .table_at(&table_path)
- .ok_or_else(|| ReplError::UnknownTable(a.table.clone()))?;
-
- let expected = table.schema().columns.len();
- if a.row.len() != expected {
- return Err(ReplError::ColumnCountMismatch {
- expected,
- got: a.row.len(),
- });
- }
-
- let mut values = Vec::with_capacity(expected);
- for (idx, column) in table.schema().columns.iter().enumerate() {
- let v =
- parse_cell_value_batch(&column.col_type, &a.row[idx], &bindings).map_err(|e| {
- let err: ReplError = e.into();
- match err {
- ReplError::BadValue { message, .. } => ReplError::BadValue {
- column: idx,
- message,
- },
- other => other,
- }
- })?;
- values.push(v);
- }
-
- let temp_id = TempRowId::from(index as u32);
- bindings.insert(a.name.clone(), temp_id);
- pending.push((a.name.clone(), table_path, values, temp_id));
- }
-
- let mut tx = store.transaction();
- for (_, table_path, values, _) in pending.iter() {
- tx.add_internal(table_path, values.clone())?;
- }
- let commit = tx.commit()?;
-
- let parts = pending
- .into_iter()
- .map(|(name, _, _, temp_id)| format!("{}=#{}", name, temp_id.resolve(commit)))
- .collect::>();
- let message = format!("committed batch: {}", parts.join(", "));
- Ok(message)
-}
-
-fn parse_txn_values(table: &Table, raw_values: &[String]) -> Result, ReplError> {
- let expected = table.schema().columns.len();
- if raw_values.len() != expected {
- return Err(ReplError::ColumnCountMismatch {
- expected,
- got: raw_values.len(),
- });
- }
-
- table
- .schema()
- .columns
- .iter()
- .enumerate()
- .map(|(idx, column)| -> Result {
- let raw = &raw_values[idx];
- parse_cell_value(&column.col_type, raw)
- .map(Into::into)
- .map_err(|message| ReplError::BadValue {
- column: idx,
- message,
- })
- })
- .collect()
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- static PATHS_IR: &str = "Path.json";
-
- #[test]
- fn lists_no_schema_message() {
- assert_eq!(render_schema_summary(None), "no schema loaded");
- }
-
- #[test]
- fn loads_schema_summary_from_fixture() {
- let loaded = load_schema(&Path::new("tests/data/").join(PATHS_IR)).expect("load schema");
- assert_eq!(loaded.store.table_count(), 16);
- assert_eq!(loaded.schema.table_count, 16);
- assert_eq!(loaded.schema.law_count, 27);
- assert_eq!(loaded.schema.tables[0].path, "Path.G0");
- }
-}
diff --git a/packages/coln-store/src/repl/exe/mod.rs b/packages/coln-store/src/repl/exe/mod.rs
new file mode 100644
index 00000000..4ee20aa3
--- /dev/null
+++ b/packages/coln-store/src/repl/exe/mod.rs
@@ -0,0 +1,560 @@
+// SPDX-FileCopyrightText: 2026 Coln contributors
+//
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+use std::{
+ collections::HashMap,
+ fs,
+ path::{Path, PathBuf},
+};
+
+use anyhow::{Context, Result, anyhow, bail};
+
+use crate::repl::{
+ Session, ShellMode, Step,
+ parse::coln::{self, BatchAssignment, parse_cell_value, parse_cell_value_batch},
+ parse::{ColnCommand, MetaCommand, SqlCommand},
+};
+use crate::{
+ commit::pst::{decode_store, encode_store},
+ ir::{BuiltinTy, ColType, ColumnEntry, FlatRealm},
+ store::Store,
+ table::{RowId, Table},
+ txn::ops::{TempRowId, TxnCellValue},
+};
+
+mod sql;
+
+fn help_text(mode: ShellMode) -> String {
+ let mut lines = vec![
+ "Commands:",
+ " .help",
+ " .exit",
+ " .quit",
+ " .load ",
+ " .open ",
+ " .save ",
+ " .tables",
+ " .rules",
+ " .schema [table]",
+ " .dump ",
+ ];
+
+ match mode {
+ ShellMode::Coln => lines.extend([
+ " add values (...), (...);",
+ " begin transact; name = add values (...); ... commit;",
+ "",
+ "Examples:",
+ " .help",
+ " .load tests/data/paths.json",
+ " .schema",
+ " .rules",
+ " .dump T",
+ " add T values (7 \"alice\"), (8 \"bob\");",
+ " begin transact; g = add Graphs values (); e = add G0 values (g); commit;",
+ ]),
+ ShellMode::Sql => lines.extend([
+ " create table ( , ...);",
+ " copy from '' with (format csv, header true);",
+ " copy from '' with (format csv, header true, delimiter E'\\t');",
+ "",
+ "Examples:",
+ " .help",
+ " .schema",
+ " .dump Person",
+ " create table Person (name text, age integer);",
+ " copy Person from 'tests/data/people.csv' with (format csv, header true);",
+ " copy Decl from 'decl.csv' with (format csv, header true, delimiter E'\\t');",
+ ]),
+ }
+
+ lines.join("\n")
+}
+
+pub(super) fn execute_sql(session: &mut Session, command: SqlCommand) -> Result {
+ sql::execute_sql(session, command)
+}
+
+pub(super) fn execute_meta(session: &mut Session, command: MetaCommand) -> Result {
+ match command {
+ MetaCommand::Help => Ok(Step::Continue(help_text(session.shell_mode))),
+ MetaCommand::Load { path } => {
+ let loaded = load_schema(std::path::Path::new(&path))?;
+ tracing::info!(
+ source = %loaded.schema.source.display(),
+ table_count = loaded.store.table_count(),
+ law_count = loaded.schema.law_count,
+ "loaded schema"
+ );
+ let message = format!(
+ "loaded schema from {} (tables: {}, laws: {})",
+ loaded.schema.source.display(),
+ loaded.store.table_count(),
+ loaded.schema.law_count
+ );
+ session.loaded = Some(loaded);
+ Ok(Step::Continue(message))
+ }
+ MetaCommand::Open { path } => {
+ let loaded = load_store(std::path::Path::new(&path))?;
+ tracing::info!(
+ source = %loaded.schema.source.display(),
+ table_count = loaded.store.table_count(),
+ law_count = loaded.schema.law_count,
+ "loaded store"
+ );
+ let message = format!(
+ "loaded store from {} (tables: {}, laws: {})",
+ loaded.schema.source.display(),
+ loaded.store.table_count(),
+ loaded.schema.law_count
+ );
+ session.loaded = Some(loaded);
+ Ok(Step::Continue(message))
+ }
+ MetaCommand::Schema { table } => {
+ let schema = session.loaded.as_ref().map(|loaded| &loaded.schema);
+ let message = match table {
+ Some(table) => render_table_schema(schema, &table)?,
+ None => render_schema_summary(schema),
+ };
+ Ok(Step::Continue(message))
+ }
+ MetaCommand::Rules => {
+ let store = session.loaded.as_ref().map(|loaded| &loaded.store);
+ Ok(Step::Continue(render_rules(store)?))
+ }
+ MetaCommand::Dump { table } => {
+ let loaded = session
+ .loaded
+ .as_mut()
+ .ok_or_else(|| anyhow!("no schema loaded"))?;
+ let tbl = loaded
+ .store
+ .table_at(&crate::ir::Path::from(table.clone()))
+ .ok_or_else(|| anyhow!("unknown table: {table}"))?;
+ Ok(Step::Continue(tbl.dump()))
+ }
+ MetaCommand::Tables => {
+ let loaded = session
+ .loaded
+ .as_mut()
+ .ok_or_else(|| anyhow!("no schema loaded"))?;
+ Ok(Step::Continue(loaded.store.dump()))
+ }
+ MetaCommand::Save { path } => {
+ let loaded = session
+ .loaded
+ .as_mut()
+ .ok_or_else(|| anyhow!("no schema loaded"))?;
+ let bytes = encode_store(&loaded.store)?;
+ let mut path = PathBuf::from(&path);
+ if path.extension().is_none() {
+ path.add_extension("colnstore");
+ }
+
+ fs::write(&path, &bytes).with_context(|| format!("cannot find path {path:?}"))?;
+ Ok(Step::Continue(format!(
+ "saved store to {}",
+ path.as_os_str().display()
+ )))
+ }
+ MetaCommand::Exit => Ok(Step::Exit),
+ }
+}
+
+pub(super) fn execute_coln(session: &mut Session, command: ColnCommand) -> Result {
+ match command {
+ ColnCommand::Add { table, rows } => {
+ let loaded = session
+ .loaded
+ .as_mut()
+ .ok_or_else(|| anyhow!("no schema loaded"))?;
+ let row_ids = add_rows(&mut loaded.store, &table, &rows)?;
+ let row_ids = row_ids
+ .into_iter()
+ .map(|row_id| format!("#{row_id}"))
+ .collect::>()
+ .join(", ");
+ Ok(format!("inserted into {table} rows [{row_ids}]"))
+ }
+ ColnCommand::Batch { assignments } => {
+ let loaded = session
+ .loaded
+ .as_mut()
+ .ok_or_else(|| anyhow!("no schema loaded"))?;
+ run_transact(&mut loaded.store, &assignments)
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct SchemaSummary {
+ pub(crate) source: PathBuf,
+ pub(crate) table_count: usize,
+ pub(crate) law_count: usize,
+ pub(crate) tables: Vec,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct TableSummary {
+ pub(crate) path: String,
+ pub(crate) column_count: usize,
+ pub(crate) primary_key: PrimaryKeySummary,
+ pub(crate) columns: Vec,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum PrimaryKeySummary {
+ None,
+ Singleton,
+ Columns(Vec),
+}
+
+pub struct LoadedState {
+ pub(crate) store: Store,
+ pub(crate) schema: SchemaSummary,
+}
+
+impl LoadedState {
+ pub fn store(&self) -> &Store {
+ &self.store
+ }
+}
+
+fn format_primary_key(primary_key: &PrimaryKeySummary) -> String {
+ match primary_key {
+ PrimaryKeySummary::None => "none".to_string(),
+ PrimaryKeySummary::Singleton => "singleton".to_string(),
+ PrimaryKeySummary::Columns(columns) => format!("{columns:?}"),
+ }
+}
+
+fn format_col_type(col_type: &ColType) -> String {
+ match col_type {
+ ColType::RowId { path } => format!("entity({path})"),
+ ColType::BuiltinTy { builtin_ty } => match builtin_ty {
+ BuiltinTy::BuiltinInt => "int".to_string(),
+ BuiltinTy::BuiltinStr => "string".to_string(),
+ },
+ }
+}
+
+fn format_column(column: &ColumnEntry) -> String {
+ format!("{}: {}", column.path, format_col_type(&column.col_type))
+}
+
+impl SchemaSummary {
+ pub(crate) fn from_store(source: PathBuf, store: &Store) -> Self {
+ let mut tables: Vec = store
+ .tables()
+ .map(|(_, table)| TableSummary {
+ path: table.path().to_string(),
+ column_count: table.schema().columns.len(),
+ primary_key: match &table.schema().primary_key {
+ None => PrimaryKeySummary::None,
+ Some(columns) if columns.is_empty() => PrimaryKeySummary::Singleton,
+ Some(columns) => PrimaryKeySummary::Columns(columns.clone()),
+ },
+ columns: table.schema().columns.iter().map(format_column).collect(),
+ })
+ .collect();
+ tables.sort_by(|a, b| a.path.cmp(&b.path));
+ let law_count = store.rule_entries().len();
+ let table_count = tables.len();
+ Self {
+ source,
+ table_count,
+ law_count,
+ tables,
+ }
+ }
+
+ fn from_theory(source: PathBuf, theory: &FlatRealm) -> Self {
+ let tables = theory
+ .tables
+ .iter()
+ .map(|entry| TableSummary {
+ path: entry.path.to_string(),
+ column_count: entry.table.columns.len(),
+ primary_key: match &entry.table.primary_key {
+ None => PrimaryKeySummary::None,
+ Some(columns) if columns.is_empty() => PrimaryKeySummary::Singleton,
+ Some(columns) => PrimaryKeySummary::Columns(columns.clone()),
+ },
+ columns: entry.table.columns.iter().map(format_column).collect(),
+ })
+ .collect();
+
+ Self {
+ source,
+ table_count: theory.tables.len(),
+ law_count: theory.rules.len(),
+ tables,
+ }
+ }
+}
+
+pub fn load_store(path: &Path) -> Result {
+ let bytes =
+ fs::read(path).with_context(|| format!("failed to read store {}", path.display()))?;
+ let store = decode_store(&bytes)
+ .with_context(|| format!("failed to decode store {}", path.display()))?;
+ let schema = SchemaSummary::from_store(path.to_path_buf(), &store);
+ Ok(LoadedState { store, schema })
+}
+
+pub fn load_schema(path: &Path) -> Result {
+ let input = fs::read_to_string(path)
+ .with_context(|| format!("failed to read schema {}", path.display()))?;
+ let theory: FlatRealm = serde_json::from_str(&input)
+ .with_context(|| format!("failed to parse schema {}", path.display()))?;
+ let summary = SchemaSummary::from_theory(path.to_path_buf(), &theory);
+ let store = Store::try_from_theory(theory)?;
+ Ok(LoadedState {
+ store,
+ schema: summary,
+ })
+}
+
+pub fn render_schema_summary(schema: Option<&SchemaSummary>) -> String {
+ let Some(schema) = schema else {
+ return "no schema loaded".to_string();
+ };
+
+ let mut lines = vec![
+ format!("source: {}", schema.source.display()),
+ format!("tables: {}", schema.table_count),
+ format!("laws: {}", schema.law_count),
+ ];
+
+ for table in &schema.tables {
+ lines.push(String::new());
+ lines.push(render_table_schema_summary(table));
+ }
+
+ lines.join("\n")
+}
+
+fn render_table_schema_summary(table: &TableSummary) -> String {
+ let mut lines = vec![
+ format!("table: {}", table.path),
+ format!("columns: {}", table.column_count),
+ format!("primary key: {}", format_primary_key(&table.primary_key)),
+ ];
+ lines.extend(table.columns.iter().map(|column| format!("- {column}")));
+
+ lines.join("\n")
+}
+
+pub fn render_table_schema(schema: Option<&SchemaSummary>, table_name: &str) -> Result {
+ let schema = schema.ok_or_else(|| anyhow!("no schema loaded"))?;
+ let table = schema
+ .tables
+ .iter()
+ .find(|table| table.path == table_name)
+ .ok_or_else(|| anyhow!("unknown table: {table_name}"))?;
+
+ Ok(render_table_schema_summary(table))
+}
+
+pub fn render_rules(store: Option<&Store>) -> Result {
+ let store = store.ok_or_else(|| anyhow!("no schema loaded"))?;
+ if store.rules().is_empty() {
+ return Ok("no rules".to_string());
+ }
+
+ Ok(store
+ .rules()
+ .iter()
+ .map(ToString::to_string)
+ .collect::>()
+ .join("\n"))
+}
+
+pub fn add_rows(
+ store: &mut Store,
+ table_name: &str,
+ raw_rows: &[Vec],
+) -> Result> {
+ let table_path = crate::ir::Path::from(table_name);
+ let oid = store
+ .resolve_table(&table_path)
+ .ok_or_else(|| anyhow!("unknown table: {table_name}"))?;
+
+ let table = store
+ .table(oid)
+ .ok_or_else(|| anyhow!("unknown table: {table_name}"))?;
+ let mut rows = Vec::new();
+ for raw_values in raw_rows {
+ rows.push(parse_txn_values(table, raw_values)?);
+ }
+
+ let mut tx = store.transaction();
+ let mut temp_ids = Vec::new();
+ for values in rows {
+ temp_ids.push(tx.add_internal(&table_path, values)?);
+ }
+ let commit = tx.commit()?;
+ let row_ids = temp_ids
+ .into_iter()
+ .map(|temp_id| temp_id.resolve(commit))
+ .collect();
+ Ok(row_ids)
+}
+
+/// Parse and commit a batch transaction, allowing later rows to refer to earlier bindings.
+pub fn run_transact(store: &mut Store, assignments: &[BatchAssignment]) -> Result {
+ let mut bindings: HashMap = HashMap::new();
+ let mut pending = Vec::new();
+
+ for (index, a) in assignments.iter().enumerate() {
+ if bindings.contains_key(&a.name) {
+ bail!("duplicate binding: {}", a.name);
+ }
+ let table_path = crate::ir::Path::from(a.table.as_str());
+ let table = store
+ .table_at(&table_path)
+ .ok_or_else(|| anyhow!("unknown table: {}", a.table))?;
+
+ let expected = table.schema().columns.len();
+ if a.row.len() != expected {
+ bail!(
+ "column count mismatch: expected {expected}, got {}",
+ a.row.len()
+ );
+ }
+
+ let mut values = Vec::with_capacity(expected);
+ for (idx, column) in table.schema().columns.iter().enumerate() {
+ let v =
+ parse_cell_value_batch(&column.col_type, &a.row[idx], &bindings).map_err(|e| {
+ match e {
+ coln::ParserError::UnknownBinding(name) => {
+ anyhow!("unknown binding: {name}")
+ }
+ coln::ParserError::InvalidValue(message) => {
+ anyhow!("column {idx}: {message}")
+ }
+ coln::ParserError::InvalidInput(message) => {
+ anyhow!("invalid intput: {message}")
+ }
+ }
+ })?;
+ values.push(v);
+ }
+
+ let temp_id = TempRowId::from(index as u32);
+ bindings.insert(a.name.clone(), temp_id);
+ pending.push((a.name.clone(), table_path, values, temp_id));
+ }
+
+ let mut tx = store.transaction();
+ for (_, table_path, values, _) in pending.iter() {
+ tx.add_internal(table_path, values.clone())?;
+ }
+ let commit = tx.commit()?;
+
+ let parts = pending
+ .into_iter()
+ .map(|(name, _, _, temp_id)| format!("{}=#{}", name, temp_id.resolve(commit)))
+ .collect::>();
+ let message = format!("committed batch: {}", parts.join(", "));
+ Ok(message)
+}
+
+fn parse_txn_values(table: &Table, raw_values: &[String]) -> Result> {
+ let expected = table.schema().columns.len();
+ if raw_values.len() != expected {
+ bail!(
+ "column count mismatch: expected {expected}, got {}",
+ raw_values.len()
+ );
+ }
+
+ table
+ .schema()
+ .columns
+ .iter()
+ .enumerate()
+ .map(|(idx, column)| -> Result {
+ let raw = &raw_values[idx];
+ parse_cell_value(&column.col_type, raw)
+ .map(Into::into)
+ .map_err(|message| anyhow!("column {idx}: {message}"))
+ })
+ .collect()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ static PATHS_IR: &str = "Path.json";
+
+ #[test]
+ fn lists_no_schema_message() {
+ assert_eq!(render_schema_summary(None), "no schema loaded");
+ }
+
+ #[test]
+ fn coln_help_lists_only_coln_statements() {
+ let help = help_text(ShellMode::Coln);
+ assert!(help.contains(".load "));
+ assert!(help.contains("add values"));
+ assert!(help.contains("begin transact;"));
+ assert!(!help.contains("create table"));
+ assert!(!help.contains("copy from"));
+ }
+
+ #[test]
+ fn sql_help_lists_only_sql_statements() {
+ let help = help_text(ShellMode::Sql);
+ assert!(help.contains(".load "));
+ assert!(help.contains("create table "));
+ assert!(help.contains("copy from '' with (format csv, header true);"));
+ assert!(!help.contains("add values"));
+ assert!(!help.contains("begin transact;"));
+ }
+
+ #[test]
+ fn renders_all_table_schemas() {
+ let loaded = load_schema(&Path::new("tests/data/").join(PATHS_IR)).expect("load schema");
+ let rendered = render_schema_summary(Some(&loaded.schema));
+ assert!(rendered.contains("source: tests/data/Path.json"));
+ assert!(rendered.contains("table: Path.G.V"));
+ assert!(rendered.contains("- a: entity(Path.Graphs)"));
+ assert!(rendered.contains("table: Path.G.E"));
+ assert!(rendered.contains("- b: entity(Path.G.V)"));
+ }
+
+ #[test]
+ fn loads_schema_summary_from_fixture() {
+ let loaded = load_schema(&Path::new("tests/data/").join(PATHS_IR)).expect("load schema");
+ assert_eq!(loaded.store.table_count(), 16);
+ assert_eq!(loaded.schema.table_count, 16);
+ assert_eq!(loaded.schema.law_count, 27);
+ assert_eq!(loaded.schema.tables[0].path, "Path.G0");
+ }
+
+ #[test]
+ fn renders_single_table_schema() {
+ let loaded = load_schema(&Path::new("tests/data/").join(PATHS_IR)).expect("load schema");
+ let rendered =
+ render_table_schema(Some(&loaded.schema), "Path.G.V").expect("render table schema");
+ assert!(rendered.contains("table: Path.G.V"));
+ assert!(rendered.contains("primary key:"));
+ assert!(rendered.contains("- a: entity(Path.Graphs)"));
+ }
+
+ #[test]
+ fn renders_rules_one_per_line() {
+ let loaded = load_schema(&Path::new("tests/data/").join(PATHS_IR)).expect("load schema");
+ let rendered = render_rules(Some(&loaded.store)).expect("render rules");
+ let lines = rendered.lines().collect::>();
+ assert_eq!(lines.len(), loaded.store.rules().len());
+ assert!(lines[0].contains(" := forall"));
+ assert!(lines[0].contains(" |- "));
+ }
+}
diff --git a/packages/coln-store/src/repl/exe/sql.rs b/packages/coln-store/src/repl/exe/sql.rs
new file mode 100644
index 00000000..e76760be
--- /dev/null
+++ b/packages/coln-store/src/repl/exe/sql.rs
@@ -0,0 +1,285 @@
+// SPDX-FileCopyrightText: 2026 Coln contributors
+//
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+use std::path::PathBuf;
+
+use anyhow::{Context, Result, anyhow, bail};
+
+use crate::ir::{ColumnEntry, EntityVariant, Path, Schema};
+use crate::repl::Session;
+use crate::repl::exe::{LoadedState, SchemaSummary, add_rows};
+use crate::repl::parse::{SqlCol as Col, SqlCommand as Command};
+use crate::store::Store;
+
+#[derive(Debug, thiserror::Error)]
+enum SQLModeError {
+ #[error("table already exists: {table}")]
+ TableExists { table: String },
+ #[error("cannot create table after data commits have been recorded")]
+ SchemaChangeAfterData,
+}
+
+pub(super) fn execute_sql(session: &mut Session, command: Command) -> Result {
+ match command {
+ Command::CreateTable {
+ table_name,
+ columns,
+ } => create_sql_table(session, table_name, columns),
+ Command::Insert { .. } => Ok("SQL INSERT is not implemented yet".to_string()),
+ Command::CopyFromCsv {
+ table_name,
+ path,
+ delimiter,
+ } => copy_from_csv(session, &table_name, &path, delimiter),
+ }
+}
+
+/// Load a delimited file with a header row into an existing table, mapping
+/// file columns to schema columns by name.
+fn copy_from_csv(
+ session: &mut Session,
+ table_name: &str,
+ path: &str,
+ delimiter: u8,
+) -> Result {
+ let loaded = session
+ .loaded
+ .as_mut()
+ .ok_or_else(|| anyhow!("no schema loaded"))?;
+ let column_names: Vec = loaded
+ .store
+ .table_at(&Path::from(table_name))
+ .ok_or_else(|| anyhow!("unknown table: {table_name}"))?
+ .schema()
+ .columns
+ .iter()
+ .map(|column| column.path.to_string())
+ .collect();
+
+ let mut reader = csv::ReaderBuilder::new()
+ .has_headers(true)
+ .delimiter(delimiter)
+ .from_path(path)
+ .with_context(|| format!("failed to open csv {path}"))?;
+ let headers = reader
+ .headers()
+ .with_context(|| format!("failed to read csv header from {path}"))?
+ .clone();
+
+ let mut indices = Vec::with_capacity(column_names.len());
+ for name in &column_names {
+ let mut matches = headers
+ .iter()
+ .enumerate()
+ .filter(|(_, header)| header == name);
+ let Some((index, _)) = matches.next() else {
+ bail!("csv {path} is missing column: {name}");
+ };
+ if matches.next().is_some() {
+ bail!("csv {path} has duplicate column: {name}");
+ }
+ indices.push(index);
+ }
+
+ let mut rows = Vec::new();
+ for (row_index, record) in reader.records().enumerate() {
+ let record = record
+ .with_context(|| format!("failed to read csv row {} from {path}", row_index + 1))?;
+ let mut row = Vec::with_capacity(indices.len());
+ for (&index, name) in indices.iter().zip(&column_names) {
+ let field = record.get(index).ok_or_else(|| {
+ anyhow!("csv row {}: missing field for column {name}", row_index + 1)
+ })?;
+ row.push(field.to_string());
+ }
+ rows.push(row);
+ }
+
+ if rows.is_empty() {
+ return Ok(format!("copied 0 rows into {table_name}"));
+ }
+ tracing::debug!("ready to copy {} rows into the store", rows.len());
+ let row_ids = add_rows(&mut loaded.store, table_name, &rows)?;
+ Ok(format!("copied {} rows into {table_name}", row_ids.len()))
+}
+
+fn create_sql_table(
+ session: &mut Session,
+ table_name: String,
+ columns: Vec,
+) -> Result {
+ let loaded = session.loaded.get_or_insert_with(empty_sql_state);
+ let path = Path::from(table_name.as_str());
+ let schema = sql_schema_from_cols(columns);
+
+ if loaded.store.resolve_table(&path).is_some() {
+ return Err(SQLModeError::TableExists { table: table_name }.into());
+ }
+ if loaded
+ .store
+ .commits()
+ .iter_topological()
+ .any(|commit| !commit.is_root())
+ {
+ return Err(SQLModeError::SchemaChangeAfterData.into());
+ }
+
+ loaded.store.create_table(path, schema)?;
+ loaded.schema = SchemaSummary::from_store(loaded.schema.source.clone(), &loaded.store);
+
+ Ok(format!("created table {table_name}"))
+}
+
+fn empty_sql_state() -> LoadedState {
+ let store = Store::new();
+ let source = PathBuf::from("");
+ let schema = SchemaSummary::from_store(source, &store);
+ LoadedState { store, schema }
+}
+
+fn sql_schema_from_cols(columns: Vec) -> Schema {
+ Schema {
+ entity_variant: EntityVariant::Table,
+ columns: columns
+ .into_iter()
+ .map(|column| ColumnEntry {
+ path: Path::from(column.col_name.as_str()),
+ col_type: column.col_typ,
+ })
+ .collect(),
+ primary_key: None,
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use coln_flir_rs::ir::ColType;
+
+ use super::*;
+ use crate::repl::ShellMode;
+
+ fn sql_session_with_person() -> Session {
+ let mut session = Session {
+ loaded: None,
+ shell_mode: ShellMode::Sql,
+ };
+ create_sql_table(
+ &mut session,
+ "Person".to_string(),
+ vec![
+ Col {
+ col_name: "name".to_string(),
+ col_typ: ColType::BuiltinTy {
+ builtin_ty: coln_flir_rs::ir::BuiltinTy::BuiltinStr,
+ },
+ },
+ Col {
+ col_name: "age".to_string(),
+ col_typ: ColType::BuiltinTy {
+ builtin_ty: coln_flir_rs::ir::BuiltinTy::BuiltinInt,
+ },
+ },
+ ],
+ )
+ .expect("create table");
+ session
+ }
+
+ #[test]
+ fn copy_requires_loaded_schema() {
+ let err = copy_from_csv(
+ &mut Session::default(),
+ "Person",
+ "tests/data/people.csv",
+ b',',
+ )
+ .unwrap_err();
+ assert_eq!(err.to_string(), "no schema loaded");
+ }
+
+ #[test]
+ fn copy_rejects_unknown_table() {
+ let mut session = sql_session_with_person();
+ let err =
+ copy_from_csv(&mut session, "Missing", "tests/data/people.csv", b',').unwrap_err();
+ assert_eq!(err.to_string(), "unknown table: Missing");
+ }
+
+ #[test]
+ fn copy_rejects_missing_file() {
+ let mut session = sql_session_with_person();
+ let err =
+ copy_from_csv(&mut session, "Person", "tests/data/missing.csv", b',').unwrap_err();
+ assert!(
+ err.to_string()
+ .contains("failed to open csv tests/data/missing.csv")
+ );
+ }
+
+ #[test]
+ fn copy_imports_tab_delimited_file() {
+ let mut session = sql_session_with_person();
+ let message = copy_from_csv(&mut session, "Person", "tests/data/people.tsv", b'\t')
+ .expect("copy tsv");
+ assert_eq!(message, "copied 2 rows into Person");
+
+ let loaded = session.loaded.as_ref().expect("loaded session");
+ let dump = loaded
+ .store
+ .table_at(&"Person".parse().unwrap())
+ .expect("Person table")
+ .dump();
+ assert!(dump.contains("alice"));
+ assert!(dump.contains("41"));
+ }
+
+ #[test]
+ fn copy_rejects_missing_csv_column() {
+ let mut session = Session {
+ loaded: None,
+ shell_mode: ShellMode::Sql,
+ };
+ create_sql_table(
+ &mut session,
+ "Person".to_string(),
+ vec![Col {
+ col_name: "email".to_string(),
+ col_typ: ColType::BuiltinTy {
+ builtin_ty: coln_flir_rs::ir::BuiltinTy::BuiltinStr,
+ },
+ }],
+ )
+ .expect("create table");
+
+ let err = copy_from_csv(&mut session, "Person", "tests/data/people.csv", b',').unwrap_err();
+ assert_eq!(
+ err.to_string(),
+ "csv tests/data/people.csv is missing column: email"
+ );
+ }
+
+ #[test]
+ fn copy_rejects_duplicate_csv_column() {
+ let mut session = sql_session_with_person();
+ let err =
+ copy_from_csv(&mut session, "Person", "tests/data/people_dup.csv", b',').unwrap_err();
+ assert_eq!(
+ err.to_string(),
+ "csv tests/data/people_dup.csv has duplicate column: name"
+ );
+ }
+
+ #[test]
+ fn copy_rejects_invalid_int_value() {
+ let mut session = sql_session_with_person();
+ let err = copy_from_csv(
+ &mut session,
+ "Person",
+ "tests/data/people_bad_int.csv",
+ b',',
+ )
+ .unwrap_err();
+ assert_eq!(err.to_string(), "column 1: invalid int: not-a-number");
+ }
+}
diff --git a/packages/coln-store/src/repl/mod.rs b/packages/coln-store/src/repl/mod.rs
index 95c413b2..8b65fd8d 100644
--- a/packages/coln-store/src/repl/mod.rs
+++ b/packages/coln-store/src/repl/mod.rs
@@ -2,44 +2,45 @@
//
// SPDX-License-Identifier: Apache-2.0 OR MIT
-use rustyline::completion::{Completer, Pair};
+use anyhow::Result;
+use rustyline::Editor;
use rustyline::error::ReadlineError;
-use rustyline::highlight::Highlighter;
-use rustyline::hint::Hinter;
use rustyline::history::DefaultHistory;
-use rustyline::validate::Validator;
-use rustyline::{Context, Editor, Helper};
-use tracing::{info, warn};
-
-use crate::commit::pst::encode_store;
-use crate::ir::Path;
-use crate::repl::error::ReplError;
-use crate::repl::exe::{
- LoadedState, add_rows, load_schema, load_store, render_schema_summary, run_transact,
+use tracing::warn;
+
+use crate::repl::cli::{
+ CommandHelper, history_path, is_statement_start, prompt, push_statement_line,
};
+use crate::repl::exe::{LoadedState, execute_coln, execute_meta, execute_sql};
use crate::repl::parse::Command;
use crate::repl::parse::parse_command;
-pub mod error;
+mod cli;
pub mod exe;
pub mod parse;
-const COMMANDS: &[&str] = &[
- "/help",
- "/exit",
- "/quit",
- "load-schema",
- "load-store",
- "list-schema",
- "dump-table",
- "dump-store",
- "persist",
- "add",
- "begin",
-];
+const SECRET_MODE: &str = "ILOVESQL";
+
+#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)]
+pub enum ShellMode {
+ #[default]
+ Coln,
+ Sql,
+}
+
#[derive(Default)]
struct Session {
loaded: Option,
+ shell_mode: ShellMode,
+}
+
+impl Session {
+ fn new(mode: ShellMode) -> Self {
+ Self {
+ loaded: None,
+ shell_mode: mode,
+ }
+ }
}
#[derive(Debug)]
@@ -48,27 +49,24 @@ enum Step {
Exit,
}
-struct CommandHelper;
-
-fn is_statement_start(input: &str) -> bool {
- !input.trim_start().starts_with('/')
-}
-
-pub fn run() -> Result<(), Box> {
+pub fn run(enable_sql: bool) -> Result<()> {
let mut editor = Editor::::new()?;
- editor.set_helper(Some(CommandHelper));
- let mut session = Session::default();
+ editor.set_helper(Some(CommandHelper::new()));
+ let _ = editor.load_history(&history_path());
+
+ let mut session = if enable_sql {
+ Session::new(ShellMode::Sql)
+ } else {
+ Session::new(ShellMode::Coln)
+ };
let mut pending_statement: Option = None;
println!("coln-store repl");
- println!("Type /help for commands.");
+ println!("Type .help for commands.");
loop {
- let prompt = if pending_statement.is_some() {
- "....> "
- } else {
- "coln-store> "
- };
+ let prompt = prompt(session.shell_mode, pending_statement.is_some());
+
let line = match editor.readline(prompt) {
Ok(line) => line,
Err(ReadlineError::Interrupted) => {
@@ -76,7 +74,7 @@ pub fn run() -> Result<(), Box> {
pending_statement = None;
println!("cancelled pending statement");
} else {
- println!("Use `/exit` to quit.");
+ println!("Use `.exit` to quit.");
}
continue;
}
@@ -89,6 +87,15 @@ pub fn run() -> Result<(), Box> {
continue;
}
+ if pending_statement.is_none()
+ && trimmed == SECRET_MODE
+ && session.shell_mode == ShellMode::Coln
+ {
+ session.shell_mode = ShellMode::Sql;
+ println!("Welcome to SQL mode! EXPERIMENTAL ONLY. ");
+ continue;
+ }
+
let maybe_command = if pending_statement.is_some() || is_statement_start(trimmed) {
let command = push_statement_line(&mut pending_statement, trimmed);
if let Some(command) = command {
@@ -102,210 +109,47 @@ pub fn run() -> Result<(), Box> {
Some(trimmed.to_string())
};
- match parse_command(maybe_command.as_deref().expect("command")) {
+ match parse_command(session.shell_mode, &maybe_command.expect("command")) {
Ok(command) => match execute(&mut session, command) {
Ok(Step::Continue(message)) => println!("{message}"),
Ok(Step::Exit) => break,
Err(err) => {
warn!(error = %err, "repl command failed");
- eprintln!("error: {err}");
+ eprintln!("error: {err:#}");
}
},
Err(err) => eprintln!("error: {err}"),
}
}
+ let _ = editor.append_history(&history_path());
Ok(())
}
-fn push_statement_line(pending: &mut Option, line: &str) -> Option {
- let pending_line = pending.get_or_insert_with(String::new);
- if !pending_line.is_empty() {
- pending_line.push(' ');
- }
- pending_line.push_str(line.trim());
-
- let buf = pending_line.trim_start();
- if buf.starts_with("begin transact") {
- if pending_line.trim_end().ends_with("commit;") {
- return pending.take();
- }
- return None;
- }
-
- if pending_line.trim_end().ends_with(';') {
- pending.take()
- } else {
- None
- }
-}
-
-impl Helper for CommandHelper {}
-
-impl Hinter for CommandHelper {
- type Hint = String;
-}
-
-impl Highlighter for CommandHelper {}
-
-impl Validator for CommandHelper {}
-
-impl Completer for CommandHelper {
- type Candidate = Pair;
-
- fn complete(
- &self,
- line: &str,
- pos: usize,
- _ctx: &Context<'_>,
- ) -> Result<(usize, Vec), ReadlineError> {
- let (start, candidates) = complete_command(line, pos);
- Ok((start, candidates))
- }
-}
-
-fn execute(session: &mut Session, command: Command) -> Result {
+fn execute(session: &mut Session, command: Command) -> Result {
match command {
- Command::Help => Ok(Step::Continue(help_text())),
- Command::LoadSchema { path } => {
- let loaded = load_schema(std::path::Path::new(&path))?;
- info!(
- source = %loaded.schema.source.display(),
- table_count = loaded.store.table_count(),
- law_count = loaded.schema.law_count,
- "loaded schema"
- );
- let message = format!(
- "loaded schema from {} (tables: {}, laws: {})",
- loaded.schema.source.display(),
- loaded.store.table_count(),
- loaded.schema.law_count
- );
- session.loaded = Some(loaded);
- Ok(Step::Continue(message))
- }
- Command::LoadStore { path } => {
- let loaded = load_store(std::path::Path::new(&path))?;
- info!(
- source = %loaded.schema.source.display(),
- table_count = loaded.store.table_count(),
- law_count = loaded.schema.law_count,
- "loaded store"
- );
- let message = format!(
- "loaded store from {} (tables: {}, laws: {})",
- loaded.schema.source.display(),
- loaded.store.table_count(),
- loaded.schema.law_count
- );
- session.loaded = Some(loaded);
- Ok(Step::Continue(message))
- }
- Command::ListSchema => Ok(Step::Continue(render_schema_summary(
- session.loaded.as_ref().map(|loaded| &loaded.schema),
- ))),
- Command::Add { table, rows } => {
- let loaded = session.loaded.as_mut().ok_or(ReplError::NoSchemaLoaded)?;
- let row_ids = add_rows(&mut loaded.store, &table, &rows)?;
- let row_ids = row_ids
- .into_iter()
- .map(|row_id| format!("#{row_id}"))
- .collect::>()
- .join(", ");
- Ok(Step::Continue(format!(
- "inserted into {table} rows [{row_ids}]"
- )))
- }
- Command::Batch { assignments } => {
- let loaded = session.loaded.as_mut().ok_or(ReplError::NoSchemaLoaded)?;
- let message = run_transact(&mut loaded.store, &assignments)?;
- Ok(Step::Continue(message))
- }
- Command::DumpTbl { name } => {
- let loaded = session.loaded.as_mut().ok_or(ReplError::NoSchemaLoaded)?;
- let tbl = loaded
- .store
- .table_at(&Path::from(name.clone()))
- .ok_or(ReplError::UnknownTable(name))?;
- let message = tbl.dump();
- Ok(Step::Continue(message))
- }
- Command::DumpStore => {
- let loaded = session.loaded.as_mut().ok_or(ReplError::NoSchemaLoaded)?;
- let message = loaded.store.dump();
- Ok(Step::Continue(message))
- }
- Command::Persist { path } => {
- let loaded = session.loaded.as_mut().ok_or(ReplError::NoSchemaLoaded)?;
- let bytes = encode_store(&loaded.store)?;
- std::fs::write(&path, &bytes)?;
- Ok(Step::Continue(format!("persisted store to {path}")))
- }
- Command::Exit => Ok(Step::Exit),
+ Command::Meta(command) => execute_meta(session, command),
+ Command::Coln(command) => Ok(Step::Continue(execute_coln(session, command)?)),
+ Command::Sql(command) => Ok(Step::Continue(execute_sql(session, command)?)),
}
}
-
-fn help_text() -> String {
- [
- "Commands:",
- " /help",
- " /exit",
- " /quit",
- " load-schema ;",
- " load-store ;",
- " list-schema;",
- " add values (...), (...);",
- " dump-table ;",
- " dump-store;",
- " persist ;",
- " begin transact; name = add values (...); ... commit;",
- "",
- "Examples:",
- " /help",
- " load-schema tests/data/paths.json;",
- " list-schema;",
- " add T values (7 \"alice\"), (8 \"bob\");",
- " begin transact; g = add Graphs values (); e = add G0 values (g); commit;",
- ]
- .join("\n")
-}
-
-fn complete_command(line: &str, pos: usize) -> (usize, Vec) {
- let prefix = &line[..pos];
- if prefix.split_whitespace().count() > 1 || prefix.contains(' ') {
- return (0, Vec::new());
- }
-
- let matches = COMMANDS
- .iter()
- .filter(|command| command.starts_with(prefix))
- .map(|command| Pair {
- display: (*command).to_string(),
- replacement: (*command).to_string(),
- })
- .collect();
-
- (0, matches)
-}
-
#[cfg(test)]
mod tests {
use std::path::PathBuf;
+ use super::*;
use crate::{
ir::{BuiltinTy, ColType, ColumnEntry, EntityVariant},
repl::{
+ exe::add_rows,
exe::{PrimaryKeySummary, SchemaSummary, TableSummary},
- parse::Command,
+ parse::{ColnCommand, Command, SqlCol, SqlCommand},
},
store::Store,
};
- use super::*;
-
fn test_loaded_state() -> LoadedState {
use crate::ir::{Path as IrPath, Schema};
- use crate::table::Table;
let path = IrPath::from("T");
let schema = Schema {
@@ -328,7 +172,9 @@ mod tests {
};
let mut store = Store::new();
- store.insert_table(path.clone(), Table::new(path.clone(), schema));
+ store
+ .create_table(path.clone(), schema)
+ .expect("create test table");
LoadedState {
store,
@@ -350,17 +196,18 @@ mod tests {
fn add_inserts_rows_into_loaded_store() {
let mut session = Session {
loaded: Some(test_loaded_state()),
+ shell_mode: ShellMode::Coln,
};
let message = match execute(
&mut session,
- Command::Add {
+ Command::Coln(ColnCommand::Add {
table: "T".to_string(),
rows: vec![
vec!["7".to_string(), "alice".to_string()],
vec!["8".to_string(), "bob".to_string()],
],
- },
+ }),
)
.expect("execute add")
{
@@ -386,23 +233,139 @@ mod tests {
fn add_requires_loaded_schema() {
let err = execute(
&mut Session::default(),
- Command::Add {
+ Command::Coln(ColnCommand::Add {
table: "T".to_string(),
rows: vec![vec!["7".to_string()]],
- },
+ }),
)
.unwrap_err();
assert_eq!(err.to_string(), "no schema loaded");
}
+ fn sql_create_table_command(table_name: &str) -> Command {
+ Command::Sql(SqlCommand::CreateTable {
+ table_name: table_name.to_string(),
+ columns: vec![
+ SqlCol {
+ col_name: "name".to_string(),
+ col_typ: ColType::BuiltinTy {
+ builtin_ty: BuiltinTy::BuiltinStr,
+ },
+ },
+ SqlCol {
+ col_name: "age".to_string(),
+ col_typ: ColType::BuiltinTy {
+ builtin_ty: BuiltinTy::BuiltinInt,
+ },
+ },
+ ],
+ })
+ }
+
+ #[test]
+ fn sql_create_table_registers_schema() {
+ let mut session = Session {
+ loaded: None,
+ shell_mode: ShellMode::Sql,
+ };
+
+ let message = match execute(&mut session, sql_create_table_command("Person"))
+ .expect("execute create table")
+ {
+ Step::Continue(message) => message,
+ Step::Exit => panic!("unexpected exit"),
+ };
+
+ assert_eq!(message, "created table Person");
+ let loaded = session.loaded.as_ref().expect("sql store loaded");
+ assert!(loaded.store.table_at(&"Person".parse().unwrap()).is_some());
+ assert_eq!(loaded.schema.table_count, 1);
+ assert_eq!(loaded.schema.tables[0].path, "Person");
+ assert_eq!(
+ loaded.schema.tables[0].columns,
+ vec!["name: string".to_string(), "age: int".to_string()]
+ );
+ }
+
+ #[test]
+ fn sql_create_table_rejects_duplicate_name() {
+ let mut session = Session {
+ loaded: None,
+ shell_mode: ShellMode::Sql,
+ };
+
+ execute(&mut session, sql_create_table_command("Person")).expect("first create");
+ let err = execute(&mut session, sql_create_table_command("Person")).unwrap_err();
+
+ assert_eq!(err.to_string(), "table already exists: Person");
+ }
+
+ #[test]
+ fn sql_create_table_rejects_schema_change_after_data_commit() {
+ let mut session = Session {
+ loaded: None,
+ shell_mode: ShellMode::Sql,
+ };
+
+ execute(&mut session, sql_create_table_command("Person")).expect("create table");
+ execute(
+ &mut session,
+ Command::Coln(ColnCommand::Add {
+ table: "Person".to_string(),
+ rows: vec![vec!["alice".to_string(), "7".to_string()]],
+ }),
+ )
+ .expect("insert row");
+
+ let err = execute(&mut session, sql_create_table_command("Other")).unwrap_err();
+ assert_eq!(
+ err.to_string(),
+ "cannot create table after data commits have been recorded"
+ );
+ }
+
+ #[test]
+ fn sql_copy_from_csv_inserts_rows() {
+ let mut session = Session {
+ loaded: None,
+ shell_mode: ShellMode::Sql,
+ };
+ execute(&mut session, sql_create_table_command("Person")).expect("create table");
+
+ let message = match execute(
+ &mut session,
+ Command::Sql(SqlCommand::CopyFromCsv {
+ table_name: "Person".to_string(),
+ path: "tests/data/people.csv".to_string(),
+ delimiter: b',',
+ }),
+ )
+ .expect("copy csv")
+ {
+ Step::Continue(message) => message,
+ Step::Exit => panic!("unexpected exit"),
+ };
+
+ assert_eq!(message, "copied 2 rows into Person");
+ let loaded = session.loaded.as_ref().expect("loaded session");
+ let table = loaded
+ .store
+ .table_at(&"Person".parse().unwrap())
+ .expect("Person table");
+ assert_eq!(table.row_count(), 2);
+ // The fixture header order (age, name) differs from the schema order.
+ let dump = table.dump();
+ assert!(dump.contains("alice"));
+ assert!(dump.contains("30"));
+ }
+
#[test]
fn add_rejects_bad_entity_id() {
let mut store = Store::new();
let path: crate::ir::Path = "Ref".parse().unwrap();
- store.insert_table(
- path.clone(),
- crate::table::Table::new(
+ store
+ .create_table(
path,
crate::ir::Schema {
entity_variant: EntityVariant::Table,
@@ -414,60 +377,13 @@ mod tests {
}],
primary_key: None,
},
- ),
- );
+ )
+ .expect("create test table");
let err = add_rows(&mut store, "Ref", &[vec!["7".to_string()]]).unwrap_err();
assert_eq!(
err.to_string(),
- "column 0: expected entity id like #:"
+ "column 0: invalid input value expected entity id like #:"
);
}
-
- #[test]
- fn completes_command_prefix() {
- let (start, matches) = complete_command("/q", 2);
- assert_eq!(start, 0);
- assert_eq!(matches.len(), 1);
- assert_eq!(matches[0].replacement, "/quit");
- }
-
- #[test]
- fn does_not_complete_after_first_argument() {
- let (_, matches) = complete_command("load-schema te", "load-schema te".len());
- assert!(matches.is_empty());
- }
-
- #[test]
- fn statement_buffer_waits_for_semicolon() {
- let mut pending = None;
- assert_eq!(push_statement_line(&mut pending, "add T 7"), None);
- assert_eq!(pending.as_deref(), Some("add T 7"));
- assert_eq!(
- push_statement_line(&mut pending, "\"alice\";"),
- Some("add T 7 \"alice\";".to_string())
- );
- assert_eq!(pending, None);
- }
-
- #[test]
- fn batch_buffer_waits_for_commit_semicolon() {
- let mut pending = None;
- assert_eq!(push_statement_line(&mut pending, "begin transact;"), None);
- assert_eq!(
- push_statement_line(&mut pending, "x = add T values (1);"),
- None
- );
- assert_eq!(
- push_statement_line(&mut pending, "commit;"),
- Some("begin transact; x = add T values (1); commit;".to_string())
- );
- assert_eq!(pending, None);
- }
-
- #[test]
- fn slash_command_is_not_statement_start() {
- assert!(!is_statement_start("/help"));
- assert!(is_statement_start("add T 7"));
- }
}
diff --git a/packages/coln-store/src/repl/parse.rs b/packages/coln-store/src/repl/parse.rs
deleted file mode 100644
index f62713b6..00000000
--- a/packages/coln-store/src/repl/parse.rs
+++ /dev/null
@@ -1,519 +0,0 @@
-// SPDX-FileCopyrightText: 2026 Coln contributors
-//
-// SPDX-License-Identifier: Apache-2.0 OR MIT
-
-use std::collections::HashMap;
-
-use crate::{
- commit::hash::{CommitHash, HASH_SIZE},
- ir::{BuiltinTy, ColType},
- repl::error::BatchCellParseError,
- table::{CellValue, RowId},
- txn::ops::{TempRowId, TxnCellValue},
-};
-
-/// One `name = add values (...);` step inside a batch block (exactly one row).
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct BatchAssignment {
- pub name: String,
- pub table: String,
- pub row: Vec,
-}
-
-#[derive(Debug, PartialEq, Eq)]
-pub enum Command {
- Help,
- LoadSchema {
- path: String,
- },
- LoadStore {
- path: String,
- },
- ListSchema,
- Add {
- table: String,
- rows: Vec>,
- },
- /// `begin transact;` … `commit;` with assignments using previous bindings in entity columns.
- Batch {
- assignments: Vec,
- },
- DumpTbl {
- name: String,
- },
- DumpStore,
- Persist {
- path: String,
- },
- Exit,
-}
-
-pub fn parse_add_statement(input: &str) -> Result {
- let Some(rest) = input.strip_prefix("add ") else {
- return Err("usage: add values (...), (...);".to_string());
- };
- let Some((table, rows_src)) = rest.split_once(" values ") else {
- return Err("usage: add values (...), (...);".to_string());
- };
- let table = table.trim();
- if table.is_empty() {
- return Err("usage: add values (...), (...);".to_string());
- }
- let rows = parse_add_rows(rows_src.trim())?;
- if rows.is_empty() {
- return Err("add requires at least one row".to_string());
- }
- Ok(Command::Add {
- table: table.to_string(),
- rows,
- })
-}
-
-pub fn parse_command(input: &str) -> Result {
- let input = input.trim();
- if input.starts_with('/') {
- parse_meta_command(input)
- } else {
- let Some(input) = input.strip_suffix(';') else {
- return Err("statements must end with `;`".to_string());
- };
- let input = input.trim_end();
- parse_statement(input)
- }
-}
-
-pub fn parse_meta_command(input: &str) -> Result {
- let parts = shlex::split(input).ok_or_else(|| "could not parse input".to_string())?;
- let Some(command) = parts.first() else {
- return Err("empty command".to_string());
- };
-
- match command.as_str() {
- "/help" => {
- if parts.len() == 1 {
- Ok(Command::Help)
- } else {
- Err("usage: /help".to_string())
- }
- }
- "/exit" | "/quit" => {
- if parts.len() == 1 {
- Ok(Command::Exit)
- } else {
- Err(format!("usage: {command}"))
- }
- }
- _ => Err(format!(
- "unknown meta command: {command}. Type `/help` for commands."
- )),
- }
-}
-
-pub fn parse_statement(input: &str) -> Result {
- let input = input.trim();
- if input.starts_with("begin transact") {
- return parse_batch_block(input);
- }
-
- let parts = shlex::split(input).ok_or_else(|| "could not parse input".to_string())?;
- let Some(command) = parts.first() else {
- return Err("empty command".to_string());
- };
-
- match command.as_str() {
- "load-schema" => match parts.as_slice() {
- [_, path] => Ok(Command::LoadSchema { path: path.clone() }),
- _ => Err("usage: load-schema ;".to_string()),
- },
- "load-store" => match parts.as_slice() {
- [_, path] => Ok(Command::LoadStore { path: path.clone() }),
- _ => Err("usage: load-store ;".to_string()),
- },
- "list-schema" => {
- if parts.len() == 1 {
- Ok(Command::ListSchema)
- } else {
- Err("usage: list-schema;".to_string())
- }
- }
- "add" => parse_add_statement(input),
- "dump-table" => match parts.as_slice() {
- [_, name] => Ok(Command::DumpTbl { name: name.clone() }),
- _ => Err("usage: dump-table ;".to_string()),
- },
- "dump-store" => Ok(Command::DumpStore),
- "persist" => match parts.as_slice() {
- [_, path] => Ok(Command::Persist { path: path.clone() }),
- _ => Err("usage: persist ;".to_string()),
- },
- _ => Err(format!(
- "unknown statement: {command}. Statements must end with `;`, or use `/help` for meta commands."
- )),
- }
-}
-
-/// Resolve one cell for a batch insert: entity columns accept `#id` or a prior binding name.
-pub(crate) fn parse_cell_value_batch(
- col_type: &ColType,
- raw: &str,
- bindings: &HashMap,
-) -> Result {
- match col_type {
- ColType::RowId { .. } => {
- if raw.starts_with('#') {
- parse_cell_value(col_type, raw)
- .map(Into::into)
- .map_err(BatchCellParseError::InvalidValue)
- } else if is_binding_ident(raw) {
- let id = bindings
- .get(raw)
- .copied()
- .ok_or_else(|| BatchCellParseError::UnknownBinding(raw.to_string()))?;
- Ok(TxnCellValue::from(id))
- } else {
- Err(BatchCellParseError::InvalidValue(format!(
- "expected entity id like #: or a binding name, got {raw}"
- )))
- }
- }
- _ => parse_cell_value(col_type, raw)
- .map(Into::into)
- .map_err(BatchCellParseError::InvalidValue),
- }
-}
-
-/// Parse `begin transact;` … `commit` (outer `;` already stripped by [`parse_command`]).
-fn parse_batch_block(input: &str) -> Result {
- let input = input.trim();
- let Some(rest) = input.strip_prefix("begin transact") else {
- return Err("internal error: expected begin transact".to_string());
- };
- let mut rest = rest.trim_start();
- let Some(after_kw) = rest.strip_prefix(';') else {
- return Err("expected `begin transact;`".to_string());
- };
- rest = after_kw.trim();
- let Some(inner) = rest.strip_suffix("commit") else {
- return Err("transaction block must end with `commit`".to_string());
- };
- let inner = inner.trim().strip_suffix(';').unwrap_or(inner).trim();
- let assignments = parse_batch_assignments(inner)?;
- Ok(Command::Batch { assignments })
-}
-
-pub fn is_binding_ident(s: &str) -> bool {
- let mut chars = s.chars();
- let Some(first) = chars.next() else {
- return false;
- };
- if !(first.is_ascii_alphabetic() || first == '_') {
- return false;
- }
- chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
-}
-
-fn split_semicolon_statements(s: &str) -> Vec {
- let mut out = Vec::new();
- let mut cur = String::new();
- let mut in_quotes = false;
- for ch in s.chars() {
- if ch == '"' {
- in_quotes = !in_quotes;
- }
- if ch == ';' && !in_quotes {
- let t = cur.trim();
- if !t.is_empty() {
- out.push(t.to_string());
- }
- cur.clear();
- } else {
- cur.push(ch);
- }
- }
- let tail = cur.trim();
- if !tail.is_empty() {
- out.push(tail.to_string());
- }
- out
-}
-
-fn parse_batch_assignments(inner: &str) -> Result, String> {
- let mut v = Vec::new();
- for stmt in split_semicolon_statements(inner) {
- v.push(parse_batch_assignment(&stmt)?);
- }
- Ok(v)
-}
-
-fn parse_batch_assignment(line: &str) -> Result {
- let line = line.trim();
- if line.is_empty() {
- return Err("empty statement inside batch block".to_string());
- }
- let Some((name, rhs)) = line.split_once(" = add ") else {
- return Err(format!(
- "expected `name = add values (...)`, got: {line}"
- ));
- };
- let name = name.trim();
- if name.is_empty() || !is_binding_ident(name) {
- return Err(format!("invalid binding name: {name}"));
- }
- let rhs = rhs.trim();
- let Some((table, rows_src)) = rhs.split_once(" values ") else {
- return Err(format!(
- "expected `values (...)` after table name in: {line}"
- ));
- };
- let table = table.trim();
- if table.is_empty() {
- return Err("missing table name".to_string());
- }
- let rows = parse_add_rows(rows_src.trim())?;
- if rows.len() != 1 {
- return Err(
- "each batch assignment must insert exactly one row (one `values (...)` group)"
- .to_string(),
- );
- }
- let row = rows.into_iter().next().expect("one row");
- Ok(BatchAssignment {
- name: name.to_string(),
- table: table.to_string(),
- row,
- })
-}
-
-/// Split `values ( ... ), ( ... )` into row bodies between outer parentheses.
-///
-/// We only need to find the matching `)` for each `(`. Inside double-quoted strings, `)` and `,`
-/// are ignored so they do not end the row. We do not support backslash escapes; `"` delimits
-/// quoted strings.
-///
-/// Row *values* are split with [`split_add_row_tokens`], not `shlex::split`: Unix shell rules
-/// treat `#` as starting a comment, which would drop entity ids like `#11`.
-///
-/// Regex is a poor fit here: a pattern like `\(([^)]*)\)` breaks when a string column contains
-/// `)`.
-pub fn parse_add_rows(input: &str) -> Result>, String> {
- let mut rows = Vec::new();
- let mut chars = input.char_indices().peekable();
-
- while let Some((_, ch)) = chars.peek().copied() {
- if ch.is_whitespace() || ch == ',' {
- chars.next();
- continue;
- }
- if ch != '(' {
- return Err("expected `(` to start a row".to_string());
- }
-
- chars.next();
- let start = chars.peek().map(|(idx, _)| *idx).unwrap_or(input.len());
- let mut in_quotes = false;
- let mut end = None;
-
- for (idx, ch) in chars.by_ref() {
- if in_quotes {
- if ch == '"' {
- in_quotes = false;
- }
- continue;
- }
-
- match ch {
- '"' => in_quotes = true,
- ')' => {
- end = Some(idx);
- break;
- }
- _ => {}
- }
- }
-
- let end = end.ok_or_else(|| "unterminated row in add statement".to_string())?;
- let row_src = &input[start..end];
- let row = split_add_row_tokens(row_src);
- rows.push(row);
- }
-
- Ok(rows)
-}
-
-/// Split the inside of one `( ... )` into values: whitespace-separated, with `"..."` for
-/// tokens that contain spaces. Does not treat `#` as a comment (entity ids are `#12`-style).
-fn split_add_row_tokens(row_src: &str) -> Vec {
- let mut out = Vec::new();
- let mut chars = row_src.chars().peekable();
- while let Some(&c) = chars.peek() {
- if c.is_whitespace() {
- chars.next();
- continue;
- }
- if c == '"' {
- chars.next();
- let mut s = String::new();
- for ch in chars.by_ref() {
- if ch == '"' {
- break;
- }
- s.push(ch);
- }
- out.push(s);
- } else {
- let mut s = String::new();
- while let Some(&ch) = chars.peek() {
- if ch.is_whitespace() {
- break;
- }
- s.push(chars.next().expect("peeked"));
- }
- out.push(s);
- }
- }
- out
-}
-
-pub fn parse_cell_value(col_type: &ColType, raw: &str) -> Result {
- match col_type {
- ColType::RowId { .. } => parse_row_id(raw).map(CellValue::Id),
- ColType::BuiltinTy { builtin_ty } => match builtin_ty {
- BuiltinTy::BuiltinInt => raw
- .parse::()
- .map(CellValue::Int)
- .map_err(|_| format!("invalid int: {raw}")),
- BuiltinTy::BuiltinStr => Ok(CellValue::Str(raw.to_string())),
- },
- }
-}
-
-fn parse_row_id(raw: &str) -> Result {
- let rest = raw
- .strip_prefix('#')
- .ok_or_else(|| "expected entity id like #:".to_string())?;
- let Some((hash_hex, counter_raw)) = rest.split_once(':') else {
- return Err("expected entity id like #:".to_string());
- };
- let hash_bytes = hex::decode(hash_hex).map_err(|_| format!("invalid entity id: {raw}"))?;
- if hash_bytes.len() != HASH_SIZE {
- return Err(format!("invalid entity id: {raw}"));
- }
- let mut hash = [0; HASH_SIZE];
- hash.copy_from_slice(&hash_bytes);
- let counter = counter_raw
- .parse::()
- .map_err(|_| format!("invalid entity id: {raw}"))?;
- Ok(RowId {
- commit: CommitHash(hash),
- counter,
- })
-}
-
-#[cfg(test)]
-mod tests {
-
- use super::*;
-
- #[test]
- fn parses_help() {
- assert_eq!(parse_command("/help").unwrap(), Command::Help);
- }
-
- #[test]
- fn valid_binding_name() {
- assert!(is_binding_ident("g0"));
- }
-
- #[test]
- fn parses_load_schema_with_quotes() {
- assert_eq!(
- parse_command("load-schema \"tests/data/paths.json\";").unwrap(),
- Command::LoadSchema {
- path: "tests/data/paths.json".to_string()
- }
- );
- }
-
- #[test]
- fn parses_add_command() {
- assert_eq!(
- parse_command("add T values (7 \"alice\"), (8 \"bob\");").unwrap(),
- Command::Add {
- table: "T".to_string(),
- rows: vec![
- vec!["7".to_string(), "alice".to_string()],
- vec!["8".to_string(), "bob".to_string()],
- ],
- }
- );
- }
-
- #[test]
- fn parses_add_command_single_value() {
- assert_eq!(
- parse_command("add T values (#11);").unwrap(),
- Command::Add {
- table: "T".to_string(),
- rows: vec![vec!["#11".to_string()],],
- }
- );
- }
-
- #[test]
- fn rejects_unknown_command() {
- let err = parse_command("wat;").unwrap_err();
- assert!(err.contains("unknown statement"));
- }
-
- #[test]
- fn rejects_missing_semicolon() {
- let err = parse_command("help").unwrap_err();
- assert_eq!(err, "statements must end with `;`");
- }
-
- #[test]
- fn parses_quit_without_semicolon() {
- assert_eq!(parse_command("/quit").unwrap(), Command::Exit);
- }
-
- #[test]
- fn parses_batch_empty() {
- assert_eq!(
- parse_command("begin transact; commit;").unwrap(),
- Command::Batch {
- assignments: vec![]
- }
- );
- }
-
- #[test]
- fn parses_batch_with_bindings() {
- let cmd = parse_command(
- "begin transact; g = add Graphs values (); x = add G0 values (g); commit;",
- )
- .unwrap();
- assert_eq!(
- cmd,
- Command::Batch {
- assignments: vec![
- BatchAssignment {
- name: "g".to_string(),
- table: "Graphs".to_string(),
- row: vec![],
- },
- BatchAssignment {
- name: "x".to_string(),
- table: "G0".to_string(),
- row: vec!["g".to_string()],
- },
- ]
- }
- );
- }
-
- #[test]
- fn rejects_batch_without_commit_keyword() {
- let err = parse_command("begin transact; g = add T values (1);").unwrap_err();
- assert!(err.contains("transaction block must end with `commit`") || err.contains("commit"));
- }
-}
diff --git a/packages/coln-store/src/repl/parse/coln.rs b/packages/coln-store/src/repl/parse/coln.rs
new file mode 100644
index 00000000..1e8be7bb
--- /dev/null
+++ b/packages/coln-store/src/repl/parse/coln.rs
@@ -0,0 +1,356 @@
+// SPDX-FileCopyrightText: 2026 Coln contributors
+//
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+use std::collections::HashMap;
+
+use crate::{
+ commit::hash::{CommitHash, HASH_SIZE},
+ ir::{BuiltinTy, ColType},
+ table::{CellValue, RowId},
+ txn::ops::{TempRowId, TxnCellValue},
+};
+
+/// Parse failure for a single cell inside a `begin batch` block (before column index is known).
+#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
+pub(crate) enum ParserError {
+ #[error("unknown binding {0}")]
+ UnknownBinding(String),
+ #[error("{0}")]
+ InvalidValue(String),
+ #[error("invalid input value {0}")]
+ InvalidInput(String),
+}
+
+/// One `name = add values (...);` step inside a batch block (exactly one row).
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct BatchAssignment {
+ pub name: String,
+ pub table: String,
+ pub row: Vec,
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub(crate) enum Command {
+ Add {
+ table: String,
+ rows: Vec>,
+ },
+ /// `begin transact;` … `commit;` with assignments using previous bindings in entity columns.
+ Batch { assignments: Vec },
+}
+
+fn invalid_input_err(s: &str) -> Result {
+ Err(ParserError::InvalidInput(s.into()))
+}
+
+fn invalid_value_err(s: impl Into) -> Result {
+ Err(ParserError::InvalidValue(s.into()))
+}
+
+pub(crate) fn parse_statement(input: &str) -> anyhow::Result {
+ let input = input.trim();
+ if input.starts_with("begin transact") {
+ return Ok(parse_batch_block(input)?);
+ }
+
+ let parts = shlex::split(input)
+ .ok_or_else(|| ParserError::InvalidInput("could not parse input".into()))?;
+ let Some(command) = parts.first() else {
+ return Ok(invalid_input_err("empty command")?);
+ };
+
+ match command.as_str() {
+ "add" => Ok(parse_add_statement(input)?),
+ _ => invalid_input_err(&format!(
+ "unknown statement: {command}. Statements must end with `;`, or use `.help` for meta commands."
+ ))?,
+ }
+}
+
+fn parse_add_statement(input: &str) -> Result {
+ let Some(rest) = input.strip_prefix("add ") else {
+ return invalid_input_err("usage: add values (...), (...);");
+ };
+ let Some((table, rows_src)) = rest.split_once(" values ") else {
+ return invalid_input_err("usage: add values (...), (...);");
+ };
+ let table = table.trim();
+ if table.is_empty() {
+ return invalid_input_err("usage: add values (...), (...);");
+ }
+ let rows = parse_add_rows(rows_src.trim())?;
+ if rows.is_empty() {
+ return invalid_input_err("add requires at least one row");
+ }
+ Ok(Command::Add {
+ table: table.to_string(),
+ rows,
+ })
+}
+
+/// Resolve one cell for a batch insert: entity columns accept `#id` or a prior binding name.
+pub(crate) fn parse_cell_value_batch(
+ col_type: &ColType,
+ raw: &str,
+ bindings: &HashMap,
+) -> Result {
+ match col_type {
+ ColType::RowId { .. } => {
+ if raw.starts_with('#') {
+ parse_cell_value(col_type, raw).map(Into::into)
+ } else if is_binding_ident(raw) {
+ let id = bindings
+ .get(raw)
+ .copied()
+ .ok_or_else(|| ParserError::UnknownBinding(raw.to_string()))?;
+ Ok(TxnCellValue::from(id))
+ } else {
+ Err(ParserError::InvalidValue(format!(
+ "expected entity id like #: or a binding name, got {raw}"
+ )))
+ }
+ }
+ _ => parse_cell_value(col_type, raw).map(Into::into),
+ }
+}
+
+/// Parse `begin transact;` … `commit` (outer `;` already stripped by [`parse_command`]).
+fn parse_batch_block(input: &str) -> Result {
+ let input = input.trim();
+ let Some(rest) = input.strip_prefix("begin transact") else {
+ return invalid_input_err("internal error: expected begin transact");
+ };
+ let mut rest = rest.trim_start();
+ let Some(after_kw) = rest.strip_prefix(';') else {
+ return invalid_input_err("expected `begin transact;`");
+ };
+ rest = after_kw.trim();
+ let Some(inner) = rest.strip_suffix("commit") else {
+ return invalid_input_err("transaction block must end with `commit`");
+ };
+ let inner = inner.trim().strip_suffix(';').unwrap_or(inner).trim();
+ let assignments = parse_batch_assignments(inner)?;
+ Ok(Command::Batch { assignments })
+}
+
+fn is_binding_ident(s: &str) -> bool {
+ let mut chars = s.chars();
+ let Some(first) = chars.next() else {
+ return false;
+ };
+ if !(first.is_ascii_alphabetic() || first == '_') {
+ return false;
+ }
+ chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
+}
+
+fn split_semicolon_statements(s: &str) -> Vec {
+ let mut out = Vec::new();
+ let mut cur = String::new();
+ let mut in_quotes = false;
+ for ch in s.chars() {
+ if ch == '"' {
+ in_quotes = !in_quotes;
+ }
+ if ch == ';' && !in_quotes {
+ let t = cur.trim();
+ if !t.is_empty() {
+ out.push(t.to_string());
+ }
+ cur.clear();
+ } else {
+ cur.push(ch);
+ }
+ }
+ let tail = cur.trim();
+ if !tail.is_empty() {
+ out.push(tail.to_string());
+ }
+ out
+}
+
+fn parse_batch_assignments(inner: &str) -> Result, ParserError> {
+ let mut v = Vec::new();
+ for stmt in split_semicolon_statements(inner) {
+ v.push(parse_batch_assignment(&stmt)?);
+ }
+ Ok(v)
+}
+
+fn parse_batch_assignment(line: &str) -> Result {
+ let line = line.trim();
+ if line.is_empty() {
+ return invalid_input_err("empty statement inside batch block");
+ }
+ let Some((name, rhs)) = line.split_once(" = add ") else {
+ return invalid_input_err(&format!(
+ "expected `name = add values (...)`, got: {line}"
+ ));
+ };
+ let name = name.trim();
+ if name.is_empty() || !is_binding_ident(name) {
+ return invalid_input_err(&format!("invalid binding name: {name}"));
+ }
+ let rhs = rhs.trim();
+ let Some((table, rows_src)) = rhs.split_once(" values ") else {
+ return invalid_input_err(&format!(
+ "expected `values (...)` after table name in: {line}"
+ ));
+ };
+ let table = table.trim();
+ if table.is_empty() {
+ return invalid_input_err("missing table name");
+ }
+ let rows = parse_add_rows(rows_src.trim())?;
+ if rows.len() != 1 {
+ return invalid_input_err(
+ "each batch assignment must insert exactly one row (one `values (...)` group)",
+ );
+ }
+ let row = rows.into_iter().next().expect("one row");
+ Ok(BatchAssignment {
+ name: name.to_string(),
+ table: table.to_string(),
+ row,
+ })
+}
+
+/// Split `values ( ... ), ( ... )` into row bodies between outer parentheses.
+///
+/// We only need to find the matching `)` for each `(`. Inside double-quoted strings, `)` and `,`
+/// are ignored so they do not end the row. We do not support backslash escapes; `"` delimits
+/// quoted strings.
+///
+/// Row *values* are split with [`split_add_row_tokens`], not `shlex::split`: Unix shell rules
+/// treat `#` as starting a comment, which would drop entity ids like `#11`.
+///
+/// Regex is a poor fit here: a pattern like `\(([^)]*)\)` breaks when a string column contains
+/// `)`.
+fn parse_add_rows(input: &str) -> Result>, ParserError> {
+ let mut rows = Vec::new();
+ let mut chars = input.char_indices().peekable();
+
+ while let Some((_, ch)) = chars.peek().copied() {
+ if ch.is_whitespace() || ch == ',' {
+ chars.next();
+ continue;
+ }
+ if ch != '(' {
+ return invalid_input_err("expected `(` to start a row");
+ }
+
+ chars.next();
+ let start = chars.peek().map(|(idx, _)| *idx).unwrap_or(input.len());
+ let mut in_quotes = false;
+ let mut end = None;
+
+ for (idx, ch) in chars.by_ref() {
+ if in_quotes {
+ if ch == '"' {
+ in_quotes = false;
+ }
+ continue;
+ }
+
+ match ch {
+ '"' => in_quotes = true,
+ ')' => {
+ end = Some(idx);
+ break;
+ }
+ _ => {}
+ }
+ }
+
+ let Some(end) = end else {
+ return invalid_input_err("unterminated row in add statement");
+ };
+ let row_src = &input[start..end];
+ let row = split_add_row_tokens(row_src);
+ rows.push(row);
+ }
+
+ Ok(rows)
+}
+
+/// Split the inside of one `( ... )` into values: whitespace-separated, with `"..."` for
+/// tokens that contain spaces. Does not treat `#` as a comment (entity ids are `#12`-style).
+fn split_add_row_tokens(row_src: &str) -> Vec {
+ let mut out = Vec::new();
+ let mut chars = row_src.chars().peekable();
+ while let Some(&c) = chars.peek() {
+ if c.is_whitespace() {
+ chars.next();
+ continue;
+ }
+ if c == '"' {
+ chars.next();
+ let mut s = String::new();
+ for ch in chars.by_ref() {
+ if ch == '"' {
+ break;
+ }
+ s.push(ch);
+ }
+ out.push(s);
+ } else {
+ let mut s = String::new();
+ while let Some(&ch) = chars.peek() {
+ if ch.is_whitespace() {
+ break;
+ }
+ s.push(chars.next().expect("peeked"));
+ }
+ out.push(s);
+ }
+ }
+ out
+}
+
+pub(crate) fn parse_cell_value(col_type: &ColType, raw: &str) -> Result {
+ match col_type {
+ ColType::RowId { .. } => parse_row_id(raw).map(CellValue::Id),
+ ColType::BuiltinTy { builtin_ty } => match builtin_ty {
+ BuiltinTy::BuiltinInt => raw
+ .parse::()
+ .map(CellValue::Int)
+ .map_err(|_| ParserError::InvalidValue(format!("invalid int: {raw}"))),
+ BuiltinTy::BuiltinStr => Ok(CellValue::Str(raw.to_string())),
+ },
+ }
+}
+
+fn parse_row_id(raw: &str) -> Result {
+ let Some(rest) = raw.strip_prefix('#') else {
+ return invalid_input_err("expected entity id like #:");
+ };
+ let Some((hash_hex, counter_raw)) = rest.split_once(':') else {
+ return invalid_input_err("expected entity id like #:");
+ };
+ let hash_bytes = hex::decode(hash_hex)
+ .map_err(|_| ParserError::InvalidValue(format!("invalid entity id: {raw}")))?;
+ if hash_bytes.len() > HASH_SIZE {
+ return invalid_value_err(format!("invalid entity id: {raw}"));
+ }
+ let mut hash = [0; HASH_SIZE];
+ let ofs = HASH_SIZE - hash_bytes.len();
+ hash[ofs..].copy_from_slice(&hash_bytes);
+ let counter = counter_raw
+ .parse::()
+ .map_err(|_| ParserError::InvalidValue(format!("invalid entity id: {raw}")))?;
+ Ok(RowId {
+ commit: CommitHash(hash),
+ counter,
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn valid_binding_name() {
+ assert!(is_binding_ident("g0"));
+ }
+}
diff --git a/packages/coln-store/src/repl/parse/meta.rs b/packages/coln-store/src/repl/parse/meta.rs
new file mode 100644
index 00000000..650d588a
--- /dev/null
+++ b/packages/coln-store/src/repl/parse/meta.rs
@@ -0,0 +1,80 @@
+// SPDX-FileCopyrightText: 2026 Coln contributors
+//
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+#[derive(Debug, PartialEq, Eq)]
+pub(crate) enum Command {
+ Help,
+ Load { path: String },
+ Open { path: String },
+ Schema { table: Option },
+ Tables,
+ Rules,
+ Exit,
+ Dump { table: String },
+ Save { path: String },
+}
+
+pub(crate) fn parse_meta_command(input: &str) -> anyhow::Result {
+ let parts = shlex::split(input).ok_or_else(|| anyhow::anyhow!("could not parse input"))?;
+ let Some(command) = parts.first() else {
+ anyhow::bail!("empty command");
+ };
+
+ match command.as_str() {
+ ".help" => {
+ if parts.len() == 1 {
+ Ok(Command::Help)
+ } else {
+ anyhow::bail!("usage: .help")
+ }
+ }
+ ".exit" | ".quit" => {
+ if parts.len() == 1 {
+ Ok(Command::Exit)
+ } else {
+ anyhow::bail!("usage: {command}")
+ }
+ }
+ ".load" => match parts.as_slice() {
+ [_, path] => Ok(Command::Load { path: path.clone() }),
+ _ => anyhow::bail!("usage: .load "),
+ },
+ ".open" => match parts.as_slice() {
+ [_, path] => Ok(Command::Open { path: path.clone() }),
+ _ => anyhow::bail!("usage: .open "),
+ },
+ ".save" => match parts.as_slice() {
+ [_, path] => Ok(Command::Save { path: path.clone() }),
+ _ => anyhow::bail!("usage: .save "),
+ },
+ ".tables" => {
+ if parts.len() == 1 {
+ Ok(Command::Tables)
+ } else {
+ anyhow::bail!("usage: .tables")
+ }
+ }
+ ".rules" => {
+ if parts.len() == 1 {
+ Ok(Command::Rules)
+ } else {
+ anyhow::bail!("usage: .rules")
+ }
+ }
+ ".schema" => match parts.as_slice() {
+ [_] => Ok(Command::Schema { table: None }),
+ [_, table] => Ok(Command::Schema {
+ table: Some(table.clone()),
+ }),
+ _ => anyhow::bail!("usage: .schema [table]"),
+ },
+ ".dump" => match parts.as_slice() {
+ [_, table] => Ok(Command::Dump {
+ table: table.clone(),
+ }),
+ _ => anyhow::bail!("usage: .dump "),
+ },
+ _ => anyhow::bail!("unknown meta command: {command}. Type `.help` for commands."),
+ }
+}
diff --git a/packages/coln-store/src/repl/parse/mod.rs b/packages/coln-store/src/repl/parse/mod.rs
new file mode 100644
index 00000000..72d745f3
--- /dev/null
+++ b/packages/coln-store/src/repl/parse/mod.rs
@@ -0,0 +1,317 @@
+// SPDX-FileCopyrightText: 2026 Coln contributors
+//
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+use anyhow::{Result, bail};
+pub(crate) use coln::Command as ColnCommand;
+pub(crate) use meta::Command as MetaCommand;
+pub(crate) use sql::{Col as SqlCol, Command as SqlCommand};
+
+use crate::repl::ShellMode;
+
+pub mod coln;
+mod meta;
+mod sql;
+
+#[derive(Debug, PartialEq, Eq)]
+pub(crate) enum Command {
+ Coln(ColnCommand),
+ Sql(SqlCommand),
+ Meta(MetaCommand),
+}
+
+pub(crate) fn parse_command(mode: ShellMode, input: &str) -> Result {
+ let input = input.trim();
+ if input.starts_with('.') {
+ meta::parse_meta_command(input).map(Command::Meta)
+ } else {
+ let Some(input) = input.strip_suffix(';') else {
+ bail!("statements must end with `;`");
+ };
+ let input = input.trim_end();
+
+ match mode {
+ ShellMode::Coln => coln::parse_statement(input).map(Command::Coln),
+ ShellMode::Sql => sql::parse_sql_statement(input).map(Command::Sql),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+
+ use coln_flir_rs::ir::BuiltinTy;
+
+ use crate::repl::parse::coln::BatchAssignment;
+
+ use super::*;
+
+ #[test]
+ fn parses_help() {
+ assert_eq!(
+ parse_command(ShellMode::Coln, ".help").unwrap(),
+ Command::Meta(meta::Command::Help)
+ );
+ }
+
+ #[test]
+ fn parses_load_with_quotes() {
+ assert_eq!(
+ parse_command(ShellMode::Coln, ".load \"tests/data/paths.json\"").unwrap(),
+ Command::Meta(MetaCommand::Load {
+ path: "tests/data/paths.json".to_string()
+ })
+ );
+ }
+
+ #[test]
+ fn parses_add_command() {
+ assert_eq!(
+ parse_command(ShellMode::Coln, "add T values (7 \"alice\"), (8 \"bob\");").unwrap(),
+ Command::Coln(ColnCommand::Add {
+ table: "T".to_string(),
+ rows: vec![
+ vec!["7".to_string(), "alice".to_string()],
+ vec!["8".to_string(), "bob".to_string()],
+ ],
+ })
+ );
+ }
+
+ #[test]
+ fn parses_add_command_single_value() {
+ assert_eq!(
+ parse_command(ShellMode::Coln, "add T values (#11);").unwrap(),
+ Command::Coln(ColnCommand::Add {
+ table: "T".to_string(),
+ rows: vec![vec!["#11".to_string()],],
+ })
+ );
+ }
+
+ #[test]
+ fn rejects_unknown_command() {
+ let err = parse_command(ShellMode::Coln, "wat;").unwrap_err();
+ assert!(err.to_string().contains("unknown statement"));
+ }
+
+ #[test]
+ fn rejects_missing_semicolon() {
+ let err = parse_command(ShellMode::Coln, "help").unwrap_err();
+ assert_eq!(err.to_string(), "statements must end with `;`");
+ }
+
+ #[test]
+ fn parses_quit_without_semicolon() {
+ assert_eq!(
+ parse_command(ShellMode::Coln, ".quit").unwrap(),
+ Command::Meta(MetaCommand::Exit)
+ );
+ }
+
+ #[test]
+ fn parses_meta_commands_without_semicolons() {
+ assert_eq!(
+ parse_command(ShellMode::Coln, ".open paths.bin").unwrap(),
+ Command::Meta(MetaCommand::Open {
+ path: "paths.bin".to_string()
+ })
+ );
+ assert_eq!(
+ parse_command(ShellMode::Coln, ".save paths.bin").unwrap(),
+ Command::Meta(MetaCommand::Save {
+ path: "paths.bin".to_string()
+ })
+ );
+ assert_eq!(
+ parse_command(ShellMode::Coln, ".tables").unwrap(),
+ Command::Meta(MetaCommand::Tables)
+ );
+ assert_eq!(
+ parse_command(ShellMode::Coln, ".rules").unwrap(),
+ Command::Meta(MetaCommand::Rules)
+ );
+ assert_eq!(
+ parse_command(ShellMode::Coln, ".schema Path.G.V").unwrap(),
+ Command::Meta(MetaCommand::Schema {
+ table: Some("Path.G.V".to_string())
+ })
+ );
+ assert_eq!(
+ parse_command(ShellMode::Coln, ".dump Path.G.V").unwrap(),
+ Command::Meta(MetaCommand::Dump {
+ table: "Path.G.V".to_string()
+ })
+ );
+ }
+
+ #[test]
+ fn parses_batch_empty() {
+ assert_eq!(
+ parse_command(ShellMode::Coln, "begin transact; commit;").unwrap(),
+ Command::Coln(ColnCommand::Batch {
+ assignments: vec![]
+ })
+ );
+ }
+
+ #[test]
+ fn parses_batch_with_bindings() {
+ let cmd = parse_command(
+ ShellMode::Coln,
+ "begin transact; g = add Graphs values (); x = add G0 values (g); commit;",
+ )
+ .unwrap();
+ assert_eq!(
+ cmd,
+ Command::Coln(ColnCommand::Batch {
+ assignments: vec![
+ BatchAssignment {
+ name: "g".to_string(),
+ table: "Graphs".to_string(),
+ row: vec![],
+ },
+ BatchAssignment {
+ name: "x".to_string(),
+ table: "G0".to_string(),
+ row: vec!["g".to_string()],
+ },
+ ]
+ })
+ );
+ }
+
+ #[test]
+ fn parses_sql_create_table() {
+ let cmd = parse_command(
+ ShellMode::Sql,
+ "create table Person (name text, age integer);",
+ )
+ .unwrap();
+
+ let Command::Sql(SqlCommand::CreateTable {
+ table_name,
+ columns,
+ }) = cmd
+ else {
+ panic!("expected SQL create table");
+ };
+
+ assert_eq!(table_name, "Person");
+ assert_eq!(columns.len(), 2);
+ assert_eq!(columns[0].col_name, "name");
+ assert_eq!(
+ columns[0].col_typ,
+ crate::ir::ColType::BuiltinTy {
+ builtin_ty: BuiltinTy::BuiltinStr
+ }
+ );
+ assert_eq!(columns[1].col_name, "age");
+ assert_eq!(
+ columns[1].col_typ,
+ crate::ir::ColType::BuiltinTy {
+ builtin_ty: BuiltinTy::BuiltinInt
+ }
+ );
+ }
+
+ #[test]
+ fn parses_sql_copy_from_csv() {
+ assert_eq!(
+ parse_command(
+ ShellMode::Sql,
+ "copy Person from 'people.csv' with (format csv, header true);",
+ )
+ .unwrap(),
+ Command::Sql(SqlCommand::CopyFromCsv {
+ table_name: "Person".to_string(),
+ path: "people.csv".to_string(),
+ delimiter: b',',
+ })
+ );
+ }
+
+ #[test]
+ fn parses_sql_copy_with_tab_delimiter() {
+ assert_eq!(
+ parse_command(
+ ShellMode::Sql,
+ "copy Decl from 'decl.csv' with (format csv, header true, delimiter E'\\t');",
+ )
+ .unwrap(),
+ Command::Sql(SqlCommand::CopyFromCsv {
+ table_name: "Decl".to_string(),
+ path: "decl.csv".to_string(),
+ delimiter: b'\t',
+ })
+ );
+ }
+
+ #[test]
+ fn rejects_sql_copy_with_non_ascii_delimiter() {
+ // sqlparser rejects multi-byte char literals before our option checks.
+ let err = parse_command(
+ ShellMode::Sql,
+ "copy Decl from 'decl.csv' with (format csv, header true, delimiter '\u{00e9}');",
+ )
+ .unwrap_err();
+ assert_eq!(err.to_string(), "failed to parse sql");
+ }
+
+ #[test]
+ fn rejects_sql_copy_without_csv_format() {
+ let err = parse_command(
+ ShellMode::Sql,
+ "copy Person from 'people.csv' with (header true);",
+ )
+ .unwrap_err();
+ assert_eq!(err.to_string(), "COPY requires FORMAT csv");
+ }
+
+ #[test]
+ fn rejects_sql_copy_without_header() {
+ let err = parse_command(
+ ShellMode::Sql,
+ "copy Person from 'people.csv' with (format csv, header false);",
+ )
+ .unwrap_err();
+ assert_eq!(err.to_string(), "COPY requires HEADER true");
+ }
+
+ #[test]
+ fn rejects_sql_copy_to() {
+ let err = parse_command(
+ ShellMode::Sql,
+ "copy Person to 'people.csv' with (format csv, header true);",
+ )
+ .unwrap_err();
+ assert_eq!(err.to_string(), "COPY TO is not supported");
+ }
+
+ #[test]
+ fn rejects_sql_copy_from_stdin() {
+ let err = parse_command(ShellMode::Sql, "copy Person from stdin;").unwrap_err();
+ assert_eq!(err.to_string(), "COPY FROM only supports a file source");
+ }
+
+ #[test]
+ fn rejects_sql_copy_with_column_list() {
+ let err = parse_command(
+ ShellMode::Sql,
+ "copy Person (name) from 'people.csv' with (format csv, header true);",
+ )
+ .unwrap_err();
+ assert_eq!(err.to_string(), "COPY column lists are not supported");
+ }
+
+ #[test]
+ fn rejects_batch_without_commit_keyword() {
+ let err =
+ parse_command(ShellMode::Coln, "begin transact; g = add T values (1);").unwrap_err();
+ assert!(
+ err.to_string()
+ .contains("transaction block must end with `commit`")
+ || err.to_string().contains("commit")
+ );
+ }
+}
diff --git a/packages/coln-store/src/repl/parse/sql.rs b/packages/coln-store/src/repl/parse/sql.rs
new file mode 100644
index 00000000..9651713c
--- /dev/null
+++ b/packages/coln-store/src/repl/parse/sql.rs
@@ -0,0 +1,213 @@
+// SPDX-FileCopyrightText: 2026 Coln contributors
+//
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+use anyhow::{Context, Result, bail};
+use coln_flir_rs::ir::BuiltinTy::{BuiltinInt, BuiltinStr};
+use sqlparser::{
+ ast::{
+ ColumnDef, CopyLegacyOption, CopyOption, CopySource, CopyTarget, DataType, Expr, Insert,
+ ObjectName, SetExpr, Statement, TableObject, Value,
+ },
+ dialect::GenericDialect,
+ parser::Parser,
+};
+
+#[derive(Debug, PartialEq, Eq)]
+pub(crate) struct Col {
+ pub(crate) col_name: String,
+ pub(crate) col_typ: coln_flir_rs::ir::ColType,
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub(crate) enum Command {
+ CreateTable {
+ table_name: String,
+ columns: Vec,
+ },
+ Insert {
+ table_name: String,
+ col_names: Vec,
+ values: Vec>,
+ },
+ /// `COPY FROM '' WITH (FORMAT csv, HEADER true)`, with an
+ /// optional `DELIMITER` such as `E'\t'` for tab-separated files.
+ CopyFromCsv {
+ table_name: String,
+ path: String,
+ delimiter: u8,
+ },
+}
+
+pub(crate) fn parse_sql_statement(input: &str) -> Result {
+ let dialect = GenericDialect {};
+ let ast = Parser::parse_sql(&dialect, input).with_context(|| "failed to parse sql")?;
+
+ // TODO support multi statement
+ let [statement] = ast.as_slice() else {
+ bail!("expected exactly one SQL statement");
+ };
+
+ if let Statement::CreateTable(sqlparser::ast::CreateTable { name, columns, .. }) = statement {
+ parse_create_table(name, columns)
+ } else if let Statement::Insert(insert) = statement {
+ parse_insert(insert)
+ } else if let Statement::Copy {
+ source,
+ to,
+ target,
+ options,
+ legacy_options,
+ values,
+ } = statement
+ {
+ parse_copy_from_csv(source, *to, target, options, legacy_options, values)
+ } else {
+ bail!("Unsupported statements {}", input);
+ }
+}
+
+fn parse_create_table(name: &ObjectName, columns: &[ColumnDef]) -> Result {
+ let mut cols = Vec::new();
+ for col in columns {
+ let col_typ = match &col.data_type {
+ DataType::Int(_) | DataType::Integer(_) => coln_flir_rs::ir::ColType::BuiltinTy {
+ builtin_ty: BuiltinInt,
+ },
+ DataType::Text | DataType::String(_) | DataType::Varchar(_) => {
+ coln_flir_rs::ir::ColType::BuiltinTy {
+ builtin_ty: BuiltinStr,
+ }
+ }
+ DataType::Uuid => coln_flir_rs::ir::ColType::RowId {
+ path: "dummy".into(),
+ },
+ other => bail!("unsupported data type: {other}"),
+ };
+ cols.push(Col {
+ col_name: col.name.to_string(),
+ col_typ,
+ })
+ }
+ Ok(Command::CreateTable {
+ table_name: name.to_string(),
+ columns: cols,
+ })
+}
+
+fn parse_insert(insert: &Insert) -> Result {
+ let TableObject::TableName(table) = &insert.table else {
+ bail!("unsupported INSERT target: {}", insert.table);
+ };
+ if insert.columns.is_empty() {
+ bail!("INSERT must specify column names");
+ }
+ if !insert.assignments.is_empty() {
+ bail!("INSERT assignments are not supported");
+ }
+ if insert.returning.is_some() {
+ bail!("INSERT RETURNING is not supported");
+ }
+
+ let source = insert
+ .source
+ .as_ref()
+ .ok_or_else(|| anyhow::anyhow!("INSERT must provide VALUES"))?;
+ let SetExpr::Values(values) = source.body.as_ref() else {
+ bail!("only INSERT ... VALUES is supported");
+ };
+
+ let col_names = insert
+ .columns
+ .iter()
+ .map(ToString::to_string)
+ .collect::>();
+ let rows = values
+ .rows
+ .iter()
+ .map(|row| {
+ row.iter()
+ .map(sql_expr_to_raw_value)
+ .collect::>>()
+ })
+ .collect::>>()?;
+
+ Ok(Command::Insert {
+ table_name: table.to_string(),
+ col_names,
+ values: rows,
+ })
+}
+
+fn parse_copy_from_csv(
+ source: &CopySource,
+ to: bool,
+ target: &CopyTarget,
+ options: &[CopyOption],
+ legacy_options: &[CopyLegacyOption],
+ values: &[Option],
+) -> Result {
+ if to {
+ bail!("COPY TO is not supported");
+ }
+ let CopySource::Table {
+ table_name,
+ columns,
+ } = source
+ else {
+ bail!("COPY FROM only supports a table source");
+ };
+ if !columns.is_empty() {
+ bail!("COPY column lists are not supported");
+ }
+ let CopyTarget::File { filename } = target else {
+ bail!("COPY FROM only supports a file source");
+ };
+ if !legacy_options.is_empty() {
+ bail!("legacy COPY options are not supported; use WITH (FORMAT csv, HEADER true)");
+ }
+ if !values.is_empty() {
+ bail!("inline COPY values are not supported");
+ }
+
+ let mut format_csv = false;
+ let mut header = false;
+ let mut delimiter = b',';
+ for option in options {
+ match option {
+ CopyOption::Format(format) => {
+ if !format.value.eq_ignore_ascii_case("csv") {
+ bail!("unsupported COPY format: {format}");
+ }
+ format_csv = true;
+ }
+ CopyOption::Header(value) => header = *value,
+ CopyOption::Delimiter(value) => delimiter = *value as u8,
+ other => bail!("unsupported COPY option: {other}"),
+ }
+ }
+ if !format_csv {
+ bail!("COPY requires FORMAT csv");
+ }
+ if !header {
+ bail!("COPY requires HEADER true");
+ }
+
+ Ok(Command::CopyFromCsv {
+ table_name: table_name.to_string(),
+ path: filename.clone(),
+ delimiter,
+ })
+}
+
+fn sql_expr_to_raw_value(expr: &Expr) -> Result {
+ let Expr::Value(value) = expr else {
+ bail!("only literal INSERT values are supported");
+ };
+
+ match &value.value {
+ Value::SingleQuotedString(value) | Value::DoubleQuotedString(value) => Ok(value.clone()),
+ Value::Number(value, _) => Ok(value.clone()),
+ other => bail!("unsupported INSERT value: {other}"),
+ }
+}
diff --git a/packages/coln-store/src/repl/summary.rs b/packages/coln-store/src/repl/summary.rs
deleted file mode 100644
index c6932f3d..00000000
--- a/packages/coln-store/src/repl/summary.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-// SPDX-FileCopyrightText: 2026 Coln contributors
-//
-// SPDX-License-Identifier: Apache-2.0 OR MIT
diff --git a/packages/coln-store/src/roweq/mod.rs b/packages/coln-store/src/roweq/mod.rs
new file mode 100644
index 00000000..5f451d64
--- /dev/null
+++ b/packages/coln-store/src/roweq/mod.rs
@@ -0,0 +1,14 @@
+// SPDX-FileCopyrightText: 2026 Coln contributors
+//
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+use crate::table::RowId;
+
+pub(crate) mod rowing;
+
+#[derive(Debug)]
+pub(crate) enum ObservedOutcome {
+ Inserted(RowId),
+ KeptOld(RowId),
+ Swap { old: RowId, new: RowId },
+}
diff --git a/packages/coln-store/src/roweq/rowing.rs b/packages/coln-store/src/roweq/rowing.rs
new file mode 100644
index 00000000..ce22c383
--- /dev/null
+++ b/packages/coln-store/src/roweq/rowing.rs
@@ -0,0 +1,398 @@
+// SPDX-FileCopyrightText: 2026 Coln contributors
+//
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+// Why is it called rowing? Because it is comparing row for equivalence by looking
+// at each value of the row one by one. And it sounds cool.
+// Anyway way it will probably be renamed in the future.
+
+use std::collections::HashMap;
+
+use coln_flir_rs::ir;
+use petgraph::unionfind::UnionFind;
+
+use crate::{
+ roweq::ObservedOutcome::{self, KeptOld},
+ table::{CellValue, RowId, Table},
+};
+
+type NodeId = u32;
+
+#[derive(Debug, Hash, PartialEq, Eq, Clone)]
+struct RowKey {
+ table: ir::Path,
+ row_id: Option,
+ cells: Vec,
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum RowingError {
+ #[error("Child row missing {rid}")]
+ MissingChild { rid: RowId },
+ #[error("duplicate rowid {rid} with different row values")]
+ InconsistentRow { rid: RowId },
+}
+
+#[derive(Debug, Clone)]
+pub(crate) struct Index {
+ row_to_node: HashMap,
+ uf: UnionFind,
+ // rootnode -> canonical row_id
+ canonical_row: HashMap,
+
+ // check whether a structurally identical row exists
+ // return value not necessary canonical
+ // this is the place hashcons is happening
+ by_key: HashMap,
+
+ by_row: HashMap,
+}
+
+impl Index {
+ pub(crate) fn new() -> Self {
+ Self {
+ row_to_node: HashMap::new(),
+ uf: UnionFind::new_empty(),
+ canonical_row: HashMap::new(),
+ by_key: HashMap::new(),
+ by_row: HashMap::new(),
+ }
+ }
+
+ pub(crate) fn canonical(&self, row_id: &RowId) -> RowId {
+ *self
+ .row_to_node
+ .get(row_id)
+ .map(|&k| self.uf.find(k))
+ .and_then(|root| self.canonical_row.get(&root))
+ .unwrap_or(row_id)
+ }
+
+ fn row_key(
+ &self,
+ table: &Table,
+ rid: &RowId,
+ values: &[CellValue],
+ ) -> Result {
+ let cells = if table.hashcons() {
+ // Hashcons rows must be observed in dependency order: every
+ // referenced child row has already been observed, and its canonical
+ // id is settled before it is embedded in a parent key.
+ values
+ .iter()
+ .map(|cell| match cell {
+ CellValue::Id(child) => {
+ if !self.by_row.contains_key(child) {
+ return Err(RowingError::MissingChild { rid: *child });
+ }
+ Ok(CellValue::Id(self.canonical(child)))
+ }
+ CellValue::Int(i) => Ok(CellValue::Int(*i)),
+ CellValue::Str(s) => Ok(CellValue::Str(s.to_owned())),
+ })
+ .collect::, RowingError>>()?
+ } else {
+ values.to_vec()
+ };
+ Ok(RowKey {
+ table: table.path().clone(),
+ row_id: if table.hashcons() { None } else { Some(*rid) },
+ cells,
+ })
+ }
+
+ fn node_for(&mut self, rid: &RowId) -> NodeId {
+ if let Some(nid) = self.row_to_node.get(rid) {
+ *nid
+ } else {
+ let n = self.uf.new_set();
+ self.row_to_node.insert(*rid, n);
+ n
+ }
+ }
+
+ pub(crate) fn observe(
+ &mut self,
+ table: &Table,
+ rid: RowId,
+ values: &[CellValue],
+ ) -> Result {
+ let row_key = self.row_key(table, &rid, values)?;
+ let node = self.node_for(&rid);
+
+ if let Some(old_key) = self.by_row.get(&rid) {
+ // If we already saw this exact row id, it should have the same key.
+ if old_key != &row_key {
+ return Err(RowingError::InconsistentRow { rid });
+ }
+ } else {
+ self.by_row.insert(rid, row_key.clone());
+ }
+
+ if !table.hashcons() {
+ return Ok(ObservedOutcome::Inserted(rid));
+ }
+
+ match self.by_key.get(&row_key).copied() {
+ None => {
+ self.by_key.insert(row_key, rid);
+ Ok(ObservedOutcome::Inserted(rid))
+ }
+ Some(existing_rid) => {
+ let old_canonical = self.canonical(&existing_rid);
+ let new_canonical = std::cmp::min(old_canonical, self.canonical(&rid));
+
+ let existing_node = self.node_for(&existing_rid);
+
+ // Roots before the union; the survivor is one of these two, so
+ // the loser becomes a stale key in `canonical_row`.
+ let root_a = self.uf.find(node);
+ let root_b = self.uf.find(existing_node);
+ self.uf.union(node, existing_node);
+
+ let root = self.uf.find(node);
+ if root_a != root {
+ self.canonical_row.remove(&root_a);
+ }
+ if root_b != root {
+ self.canonical_row.remove(&root_b);
+ }
+ self.canonical_row.insert(root, new_canonical);
+
+ self.by_key.insert(row_key, new_canonical);
+
+ if new_canonical == old_canonical {
+ Ok(KeptOld(old_canonical))
+ } else {
+ Ok(ObservedOutcome::Swap {
+ old: old_canonical,
+ new: new_canonical,
+ })
+ }
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::commit::hash::CommitHash;
+ use crate::ir::{BuiltinTy, ColType, ColumnEntry, EntityVariant, Path, Schema};
+ use crate::roweq::ObservedOutcome::{Inserted, Swap};
+
+ fn row_id(commit_byte: u8, counter: u32) -> RowId {
+ RowId {
+ commit: CommitHash([commit_byte; 32]),
+ counter,
+ }
+ }
+
+ fn int_table(path: &str, hashcons: bool) -> Table {
+ let schema = Schema {
+ entity_variant: EntityVariant::Table,
+ columns: vec![ColumnEntry {
+ path: Path::from("value"),
+ col_type: ColType::BuiltinTy {
+ builtin_ty: BuiltinTy::BuiltinInt,
+ },
+ }],
+ primary_key: None,
+ };
+ let mut table = Table::new(Path::from(path), schema);
+ table.set_hashcons_for_test(hashcons);
+ table
+ }
+
+ fn id_table(path: &str, arity: usize, hashcons: bool) -> Table {
+ let schema = Schema {
+ entity_variant: EntityVariant::Table,
+ columns: (0..arity)
+ .map(|i| ColumnEntry {
+ path: Path::from(format!("c{i}").as_str()),
+ col_type: ColType::RowId {
+ path: Path::from(path),
+ },
+ })
+ .collect(),
+ primary_key: None,
+ };
+ let mut table = Table::new(Path::from(path), schema);
+ table.set_hashcons_for_test(hashcons);
+ table
+ }
+
+ #[test]
+ fn deduplicates_equal_hashcons_rows() {
+ let table = int_table("Term", true);
+ let mut index = Index::new();
+ let first = row_id(1, 0);
+ let second = row_id(2, 0);
+
+ let first_outcome = index
+ .observe(&table, first, &[CellValue::Int(7)])
+ .expect("first row should observe");
+ let second_outcome = index
+ .observe(&table, second, &[CellValue::Int(7)])
+ .expect("second row should observe");
+
+ assert!(matches!(first_outcome, Inserted(row) if row == first));
+ assert!(matches!(second_outcome, KeptOld(row) if row == first));
+ assert_eq!(index.canonical(&first), first);
+ assert_eq!(index.canonical(&second), first);
+ }
+
+ #[test]
+ fn keeps_equal_non_hashcons_rows_distinct() {
+ let table = int_table("Term", false);
+ let mut index = Index::new();
+ let first = row_id(1, 0);
+ let second = row_id(2, 0);
+
+ let first_outcome = index
+ .observe(&table, first, &[CellValue::Int(7)])
+ .expect("first row should observe");
+ let second_outcome = index
+ .observe(&table, second, &[CellValue::Int(7)])
+ .expect("second row should observe");
+
+ assert!(matches!(first_outcome, Inserted(row) if row == first));
+ assert!(matches!(second_outcome, Inserted(row) if row == second));
+ assert_eq!(index.canonical(&first), first);
+ assert_eq!(index.canonical(&second), second);
+ }
+
+ #[test]
+ fn uses_canonical_child_ids_in_parent_key() {
+ let leaf_table = int_table("Term", false);
+ let plus_table = id_table("Plus", 2, true);
+ let mut index = Index::new();
+
+ let a = row_id(1, 0);
+ let b = row_id(1, 1);
+ let c = row_id(1, 2);
+ index
+ .observe(&leaf_table, a, &[CellValue::Int(1)])
+ .expect("observe a");
+ index
+ .observe(&leaf_table, b, &[CellValue::Int(2)])
+ .expect("observe b");
+ index
+ .observe(&leaf_table, c, &[CellValue::Int(3)])
+ .expect("observe c");
+
+ let first_child = row_id(2, 0);
+ let second_child = row_id(3, 0);
+ index
+ .observe(
+ &plus_table,
+ first_child,
+ &[CellValue::Id(a), CellValue::Id(b)],
+ )
+ .expect("observe first child");
+ index
+ .observe(
+ &plus_table,
+ second_child,
+ &[CellValue::Id(a), CellValue::Id(b)],
+ )
+ .expect("observe equivalent child");
+
+ let first_parent = row_id(4, 0);
+ let second_parent = row_id(5, 0);
+ let first_outcome = index
+ .observe(
+ &plus_table,
+ first_parent,
+ &[CellValue::Id(first_child), CellValue::Id(c)],
+ )
+ .expect("observe first parent");
+ let second_outcome = index
+ .observe(
+ &plus_table,
+ second_parent,
+ &[CellValue::Id(second_child), CellValue::Id(c)],
+ )
+ .expect("observe equivalent parent");
+
+ assert!(matches!(first_outcome, Inserted(row) if row == first_parent));
+ assert!(matches!(second_outcome, KeptOld(row) if row == first_parent));
+ assert_eq!(index.canonical(&first_parent), first_parent);
+ assert_eq!(index.canonical(&second_parent), first_parent);
+ }
+
+ #[test]
+ fn returns_error_for_missing_child() {
+ let table = id_table("Plus", 2, true);
+ let mut index = Index::new();
+ let row = row_id(1, 0);
+ let missing = row_id(2, 0);
+
+ let err = index
+ .observe(&table, row, &[CellValue::Id(missing), CellValue::Int(1)])
+ .expect_err("missing child should be rejected");
+
+ assert!(matches!(err, RowingError::MissingChild { rid } if rid == missing));
+ }
+
+ #[test]
+ fn smaller_later_row_becomes_canonical() {
+ let table = int_table("Term", true);
+ let mut index = Index::new();
+ let larger = row_id(2, 0);
+ let smaller = row_id(1, 0);
+
+ index
+ .observe(&table, larger, &[CellValue::Int(7)])
+ .expect("larger row should observe");
+ let outcome = index
+ .observe(&table, smaller, &[CellValue::Int(7)])
+ .expect("smaller row should observe");
+
+ assert!(matches!(outcome, Swap { old, new } if old == larger && new == smaller));
+ assert_eq!(index.canonical(&larger), smaller);
+ assert_eq!(index.canonical(&smaller), smaller);
+ }
+
+ #[test]
+ fn reobserving_row_with_different_values_errors() {
+ let table = int_table("Term", true);
+ let mut index = Index::new();
+ let rid = row_id(1, 0);
+
+ index
+ .observe(&table, rid, &[CellValue::Int(7)])
+ .expect("first observation should succeed");
+ let err = index
+ .observe(&table, rid, &[CellValue::Int(8)])
+ .expect_err("same row id with different values should be rejected");
+
+ assert!(matches!(err, RowingError::InconsistentRow { rid: got } if got == rid));
+ }
+
+ #[test]
+ fn three_equal_rows_merge_to_smallest() {
+ let table = int_table("Term", true);
+ let mut index = Index::new();
+ let high = row_id(3, 0);
+ let mid = row_id(2, 0);
+ let low = row_id(1, 0);
+
+ let high_outcome = index
+ .observe(&table, high, &[CellValue::Int(7)])
+ .expect("observe high");
+ let mid_outcome = index
+ .observe(&table, mid, &[CellValue::Int(7)])
+ .expect("observe mid");
+ let low_outcome = index
+ .observe(&table, low, &[CellValue::Int(7)])
+ .expect("observe low");
+
+ assert!(matches!(high_outcome, Inserted(row) if row == high));
+ assert!(matches!(mid_outcome, Swap { old, new } if old == high && new == mid));
+ assert!(matches!(low_outcome, Swap { old, new } if old == mid && new == low));
+ assert_eq!(index.canonical(&high), low);
+ assert_eq!(index.canonical(&mid), low);
+ assert_eq!(index.canonical(&low), low);
+ }
+}
diff --git a/packages/coln-store/src/solver/bind.rs b/packages/coln-store/src/solver/bind.rs
index 22796d61..f79f38f7 100644
--- a/packages/coln-store/src/solver/bind.rs
+++ b/packages/coln-store/src/solver/bind.rs
@@ -135,10 +135,10 @@ fn bind_prop(store: &Store, bindings: Vec, prop: &CompProp) -> Vec Vec {
- debug!(law_name = %law.path, law=?law, "binding vars for law");
- let initial = vec![vec![None; law.vars.len()]];
- bind_prop(store, initial, &law.antecedent)
+pub fn bind_rule(store: &Store, rule: &CompRule) -> Vec {
+ debug!(rule_name = %rule.path, rule=?rule, "binding vars for rule");
+ let initial = vec![vec![None; rule.vars.len()]];
+ bind_prop(store, initial, &rule.antecedent)
}
#[cfg(test)]
@@ -149,7 +149,7 @@ mod tests {
self, BuiltinTy, ColType, ColumnEntry, EntityVariant, Path, Rule, RuleEntry,
RuleVariant, Schema,
},
- solver::compile::compile_law,
+ solver::compile::compile_rule,
table::CellValue,
};
@@ -195,13 +195,12 @@ mod tests {
}
#[test]
- fn bind_law_joins_antecedent_atoms() {
+ fn bind_rule_joins_antecedent_atoms() {
let path = Path::from("T");
let mut store = Store::new();
- store.insert_table(
- path.clone(),
- Table::new(path.clone(), int_schema(&["c0", "c1"])),
- );
+ store
+ .create_table(path.clone(), int_schema(&["c0", "c1"]))
+ .expect("create table");
let mut txn = store.transaction();
txn.add(
@@ -221,7 +220,7 @@ mod tests {
.expect("insert row");
txn.commit().expect("commit rows");
- let law = enforced_rule(
+ let rule = enforced_rule(
"T.chain",
vec![int_ty(), int_ty(), int_ty()],
vec![
@@ -270,8 +269,8 @@ mod tests {
}],
);
- let compiled = compile_law(&law).expect("compile law");
- let bindings = bind_law(&store, &compiled);
+ let compiled = compile_rule(&rule).expect("compile rule");
+ let bindings = bind_rule(&store, &compiled);
assert_eq!(bindings.len(), 1);
assert_eq!(bindings[0][0], Some(BoundValue::Cell(CellValue::Int(1))));
@@ -284,7 +283,9 @@ mod tests {
fn store_with_int_column(values: &[i64]) -> (Store, Path) {
let path = Path::from("T");
let mut store = Store::new();
- store.insert_table(path.clone(), Table::new(path.clone(), int_schema(&["c0"])));
+ store
+ .create_table(path.clone(), int_schema(&["c0"]))
+ .expect("create table");
let mut txn = store.transaction();
for value in values {
txn.add(&path, vec![CellValue::Int(*value).into()])
@@ -298,7 +299,7 @@ mod tests {
fn eq_in_antecedent_filters_to_matching_rows() {
let (store, path) = store_with_int_column(&[1, 2, 3]);
- let law = enforced_rule(
+ let rule = enforced_rule(
"T.eq_filter",
vec![int_ty(), int_ty()],
vec![
@@ -339,8 +340,8 @@ mod tests {
}],
);
- let compiled = compile_law(&law).expect("compile law");
- let bindings = bind_law(&store, &compiled);
+ let compiled = compile_rule(&rule).expect("compile rule");
+ let bindings = bind_rule(&store, &compiled);
assert_eq!(bindings.len(), 3);
for b in &bindings {
@@ -352,7 +353,7 @@ mod tests {
fn eq_with_literal_in_antecedent_pins_var_to_value() {
let (store, path) = store_with_int_column(&[1, 2, 3]);
- let law = enforced_rule(
+ let rule = enforced_rule(
"T.eq_literal",
vec![int_ty()],
vec![
@@ -385,8 +386,8 @@ mod tests {
}],
);
- let compiled = compile_law(&law).expect("compile law");
- let bindings = bind_law(&store, &compiled);
+ let compiled = compile_rule(&rule).expect("compile rule");
+ let bindings = bind_rule(&store, &compiled);
assert_eq!(bindings.len(), 1);
assert_eq!(bindings[0][0], Some(BoundValue::Cell(CellValue::Int(2))));
diff --git a/packages/coln-store/src/solver/compile.rs b/packages/coln-store/src/solver/compile.rs
index c1e0e1ab..ab0e02bb 100644
--- a/packages/coln-store/src/solver/compile.rs
+++ b/packages/coln-store/src/solver/compile.rs
@@ -19,13 +19,14 @@ pub enum CompileError {
InvalidColumnIndex { column: i64 },
}
-/// A rule lowered into a small execution-oriented law form.
+/// A rule lowered into a small execution-oriented rule form.
///
/// The IR stores antecedents and consequents as vectors of propositions; each
/// vector is interpreted as a conjunction here.
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone)]
pub struct CompRule {
pub path: ir::Path,
+ pub rule_variant: ir::RuleVariant,
pub vars: Vec,
pub antecedent: CompProp,
pub consequent: CompProp,
@@ -84,7 +85,7 @@ pub enum CompTerm {
/// - keeps table references in their source-level `ir::Path` form
/// - validates variable and column indices
/// - rejects IR forms not yet supported by the solver
-pub fn compile_law(rule_entry: &RuleEntry) -> Result {
+pub fn compile_rule(rule_entry: &RuleEntry) -> Result {
let path = rule_entry.path.clone();
let vars = rule_entry
.rule
@@ -99,6 +100,8 @@ pub fn compile_law(rule_entry: &RuleEntry) -> Result {
let antecedent = compile_props(&rule_entry.rule.antecedents, var_count)?;
let consequent = compile_props(&rule_entry.rule.consequents, var_count)?;
+ let rule_variant = rule_entry.rule.rule_variant.clone();
+
let mut seen = HashSet::new();
let mut tables = Vec::new();
collect_atom_tables(&antecedent, &mut seen, &mut tables);
@@ -106,6 +109,7 @@ pub fn compile_law(rule_entry: &RuleEntry) -> Result {
Ok(CompRule {
path,
+ rule_variant,
vars,
antecedent,
consequent,
@@ -202,7 +206,12 @@ fn compile_term(term: &Term, var_count: usize) -> Result
impl fmt::Display for CompRule {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "{} := forall", DisplayPath(&self.path))?;
+ write!(
+ f,
+ "{} ({:?}) := forall",
+ DisplayPath(&self.path),
+ self.rule_variant
+ )?;
for var in &self.vars {
write!(
f,
@@ -306,7 +315,7 @@ impl fmt::Display for DisplayPath<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (idx, qname) in self.0.iter().enumerate() {
if idx > 0 {
- write!(f, " .")?;
+ write!(f, ".")?;
}
write!(f, "{}", display_qname(qname))?;
}
@@ -363,8 +372,8 @@ mod tests {
}
#[test]
- fn compiles_single_atom_law() {
- let law = enforced_rule(
+ fn compiles_single_atom_rule() {
+ let rule = enforced_rule(
"T.total",
vec![int_ty()],
vec![Prop::Atom {
@@ -389,7 +398,7 @@ mod tests {
}],
);
- let compiled = compile_law(&law).expect("compile law");
+ let compiled = compile_rule(&rule).expect("compile rule");
assert!(matches!(compiled.antecedent, CompProp::And(_)));
assert!(matches!(compiled.consequent, CompProp::And(_)));
assert_eq!(compiled.tables.len(), 1);
@@ -407,9 +416,10 @@ mod tests {
}
#[test]
- fn displays_compiled_law_like_lowered_output() {
+ fn displays_compiled_rule_like_lowered_output() {
let compiled = CompRule {
path: Path::from("G.E.foreignKeys"),
+ rule_variant: RuleVariant::Enforced,
vars: vec![
VarSpec {
index: 0,
@@ -465,7 +475,7 @@ mod tests {
assert_eq!(
compiled.to_string(),
- "G .E .foreignKeys := forall (a : Graphs) (b : G .V) (c : G .V) => G .E [a, b, c] |- b @ G .V [a] /\\ c @ G .V [a]"
+ "G.E.foreignKeys (Enforced) := forall (a : Graphs) (b : G.V) (c : G.V) => G.E [a, b, c] |- b @ G.V [a] /\\ c @ G.V [a]"
);
}
@@ -473,6 +483,7 @@ mod tests {
fn displays_compiled_equality_consequent() {
let compiled = CompRule {
path: Path::from("PathHom.empty"),
+ rule_variant: RuleVariant::Enforced,
vars: vec![
VarSpec {
index: 0,
@@ -506,13 +517,13 @@ mod tests {
assert_eq!(
compiled.to_string(),
- "PathHom .empty := forall (a : Path0 .t) (b : Path1 .t) => PathHom .t [a, b] |- a = b"
+ "PathHom.empty (Enforced) := forall (a : Path0.t) (b : Path1.t) => PathHom.t [a, b] |- a = b"
);
}
#[test]
fn compiles_eq_in_antecedent() {
- let law = enforced_rule(
+ let rule = enforced_rule(
"T.eq_antecedent",
vec![int_ty(), int_ty()],
vec![Prop::Eq {
@@ -527,7 +538,7 @@ mod tests {
},
}],
);
- let compiled = compile_law(&law).expect("compile law");
+ let compiled = compile_rule(&rule).expect("compile rule");
assert!(matches!(
compiled.antecedent,
CompProp::And(ref children) if matches!(children.as_slice(), [CompProp::Eq(_)])
@@ -536,7 +547,7 @@ mod tests {
#[test]
fn compiles_consequent_equality() {
- let law = enforced_rule(
+ let rule = enforced_rule(
"T.eq",
vec![int_ty(), int_ty()],
vec![Prop::Atom {
@@ -561,7 +572,7 @@ mod tests {
}],
);
- let compiled = compile_law(&law).expect("compile law");
+ let compiled = compile_rule(&rule).expect("compile rule");
match compiled.consequent {
CompProp::And(children) if matches!(children.as_slice(), [CompProp::Eq(_)]) => {
let CompProp::Eq(CompEq { left, right }) = &children[0] else {
@@ -579,7 +590,7 @@ mod tests {
#[test]
fn compiles_conjunction_of_atoms_and_eq() {
let t = Path::from("T");
- let law = enforced_rule(
+ let rule = enforced_rule(
"T.mixed",
vec![int_ty(), int_ty()],
vec![Prop::Atom {
@@ -616,7 +627,7 @@ mod tests {
],
);
- let compiled = compile_law(&law).expect("compile law");
+ let compiled = compile_rule(&rule).expect("compile rule");
match &compiled.consequent {
CompProp::And(children) => {
assert_eq!(children.len(), 2);
diff --git a/packages/coln-store/src/solver/validate.rs b/packages/coln-store/src/solver/validate.rs
index d60e8370..f3c7b863 100644
--- a/packages/coln-store/src/solver/validate.rs
+++ b/packages/coln-store/src/solver/validate.rs
@@ -4,11 +4,12 @@
use std::error::Error;
use std::fmt;
+
use tracing::debug;
use crate::{
solver::{
- bind::{Binding, BoundValue, bind_law, eval_term},
+ bind::{Binding, BoundValue, bind_rule, eval_term},
compile::{CompAtom, CompEq, CompProp, CompRule, CompVal},
matcher::term_matches,
},
@@ -25,9 +26,9 @@ pub enum ViolationCause {
UnsatisfiedEq(CompEq),
}
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone)]
pub struct RuleViolation {
- pub law: CompRule,
+ pub rule: CompRule,
pub cause: ViolationCause,
pub binding: Binding,
}
@@ -38,12 +39,12 @@ impl fmt::Display for RuleViolation {
ViolationCause::MissingAtom(atom) => write!(
f,
"rule {} violated: missing consequent atom in table {}",
- self.law, atom.table
+ self.rule, atom.table
),
ViolationCause::UnsatisfiedEq(eq) => write!(
f,
"rule {} violated: equality {:?} = {:?} did not hold",
- self.law, eq.left, eq.right
+ self.rule, eq.left, eq.right
),
}
}
@@ -127,21 +128,21 @@ fn prop_holds(store: &Store, binding: &Binding, prop: &CompProp) -> Result<(), V
}
}
-pub fn check_law(store: &Store, law: &CompRule) -> Result<(), Box> {
- let bindings = bind_law(store, law);
- debug!(law = %law.path, bindings = ?bindings, "checking law with bindings");
+pub fn check_rule(store: &Store, rule: &CompRule) -> Result<(), Box> {
+ let bindings = bind_rule(store, rule);
+ debug!(rule = %rule.path, bindings = ?bindings, "checking rule with bindings");
for binding in bindings {
- if let Err(cause) = prop_holds(store, &binding, &law.consequent) {
- debug!(law = %law.path, cause = ?cause, "law violation detected");
+ if let Err(cause) = prop_holds(store, &binding, &rule.consequent) {
+ debug!(rule = %rule.path, cause = ?cause, "rule violation detected");
return Err(Box::new(RuleViolation {
- law: law.clone(),
+ rule: rule.clone(),
cause,
binding,
}));
}
}
- debug!(law = %law.path, "law satisfied");
+ debug!(rule = %rule.path, "rule satisfied");
Ok(())
}
@@ -153,8 +154,8 @@ mod tests {
self, BuiltinTy, ColType, ColumnEntry, EntityVariant, Path, Rule, RuleEntry,
RuleVariant, Schema,
},
- solver::compile::compile_law,
- table::{CellValue, Table},
+ solver::compile::compile_rule,
+ table::CellValue,
};
fn int_ty() -> ColType {
@@ -202,12 +203,11 @@ mod tests {
fn true_antecedent_still_checks_consequent() {
let g0_path = Path::from("G0");
let mut store = Store::new();
- store.insert_table(
- g0_path.clone(),
- Table::new(g0_path.clone(), int_schema(&[])),
- );
+ store
+ .create_table(g0_path.clone(), int_schema(&[]))
+ .expect("create table");
- let law = enforced_rule(
+ let rule = enforced_rule(
"G0.total",
vec![],
vec![],
@@ -220,10 +220,10 @@ mod tests {
}],
);
- let compiled = compile_law(&law).expect("compile law");
- let violation = check_law(&store, &compiled).expect_err("missing G0 row should violate");
+ let compiled = compile_rule(&rule).expect("compile rule");
+ let violation = check_rule(&store, &compiled).expect_err("missing G0 row should violate");
- assert_eq!(violation.law.path, Path::from("G0.total"));
+ assert_eq!(violation.rule.path, Path::from("G0.total"));
match &violation.cause {
ViolationCause::MissingAtom(atom) => assert_eq!(atom.table, g0_path),
other => panic!("expected MissingAtom, got {:?}", other),
@@ -236,14 +236,12 @@ mod tests {
let source = Path::from("Src");
let target = Path::from("Dst");
let mut store = Store::new();
- store.insert_table(
- source.clone(),
- Table::new(source.clone(), int_schema(&["c0"])),
- );
- store.insert_table(
- target.clone(),
- Table::new(target.clone(), int_schema(&["c0"])),
- );
+ store
+ .create_table(source.clone(), int_schema(&["c0"]))
+ .expect("create source table");
+ store
+ .create_table(target.clone(), int_schema(&["c0"]))
+ .expect("create target table");
let mut txn = store.transaction();
txn.add(&source, vec![CellValue::Int(7).into()])
@@ -252,7 +250,7 @@ mod tests {
.expect("insert target row");
txn.commit().expect("commit matching rows");
- let law = enforced_rule(
+ let rule = enforced_rule(
"Copy.total",
vec![int_ty()],
vec![ir::Prop::Atom {
@@ -277,27 +275,27 @@ mod tests {
}],
);
- let compiled = compile_law(&law).expect("compile law");
- assert!(check_law(&store, &compiled).is_ok());
+ let compiled = compile_rule(&rule).expect("compile rule");
+ assert!(check_rule(&store, &compiled).is_ok());
}
#[test]
- fn foreign_key_law_fails_when_only_referencing_row_exists() {
+ fn foreign_key_rule_fails_when_only_referencing_row_exists() {
let left = Path::from("Left");
let right = Path::from("Right");
let link = Path::from("Link");
let mut store = Store::new();
- store.insert_table(left.clone(), Table::new(left.clone(), int_schema(&["c0"])));
- store.insert_table(
- right.clone(),
- Table::new(right.clone(), int_schema(&["c0"])),
- );
- store.insert_table(
- link.clone(),
- Table::new(link.clone(), int_schema(&["c0", "c1"])),
- );
-
- let law = enforced_rule(
+ store
+ .create_table(left.clone(), int_schema(&["c0"]))
+ .expect("create left table");
+ store
+ .create_table(right.clone(), int_schema(&["c0"]))
+ .expect("create right table");
+ store
+ .create_table(link.clone(), int_schema(&["c0", "c1"]))
+ .expect("create link table");
+
+ let rule = enforced_rule(
"Link.foreignKeys",
vec![int_ty(), int_ty()],
vec![ir::Prop::Atom {
@@ -340,7 +338,7 @@ mod tests {
],
);
- let compiled = compile_law(&law).expect("compile law");
+ let compiled = compile_rule(&rule).expect("compile rule");
let mut txn = store.transaction();
txn.add(
@@ -350,8 +348,8 @@ mod tests {
.expect("insert referencing row");
txn.commit().expect("commit referencing row");
- let violation = check_law(&store, &compiled).expect_err("missing referenced rows");
- assert_eq!(violation.law.path, Path::from("Link.foreignKeys"));
+ let violation = check_rule(&store, &compiled).expect_err("missing referenced rows");
+ assert_eq!(violation.rule.path, Path::from("Link.foreignKeys"));
match &violation.cause {
ViolationCause::MissingAtom(atom) => assert_eq!(atom.table, left),
other => panic!("expected MissingAtom, got {:?}", other),
@@ -371,20 +369,22 @@ mod tests {
.expect("insert right row");
txn.commit().expect("commit referenced rows");
- assert!(check_law(&store, &compiled).is_ok());
+ assert!(check_rule(&store, &compiled).is_ok());
}
#[test]
fn consequent_equality_holds_when_bindings_match() {
let t = Path::from("T");
let mut store = Store::new();
- store.insert_table(t.clone(), Table::new(t.clone(), int_schema(&["c0", "c1"])));
+ store
+ .create_table(t.clone(), int_schema(&["c0", "c1"]))
+ .expect("create table");
let mut txn = store.transaction();
txn.add(&t, vec![CellValue::Int(5).into(), CellValue::Int(5).into()])
.expect("insert row");
txn.commit().expect("commit row");
- let law = enforced_rule(
+ let rule = enforced_rule(
"T.eq",
vec![int_ty(), int_ty()],
vec![ir::Prop::Atom {
@@ -409,21 +409,23 @@ mod tests {
}],
);
- let compiled = compile_law(&law).expect("compile law");
- assert!(check_law(&store, &compiled).is_ok());
+ let compiled = compile_rule(&rule).expect("compile rule");
+ assert!(check_rule(&store, &compiled).is_ok());
}
#[test]
fn consequent_equality_violation_surfaces_unsatisfied_eq() {
let t = Path::from("T");
let mut store = Store::new();
- store.insert_table(t.clone(), Table::new(t.clone(), int_schema(&["c0", "c1"])));
+ store
+ .create_table(t.clone(), int_schema(&["c0", "c1"]))
+ .expect("create table");
let mut txn = store.transaction();
txn.add(&t, vec![CellValue::Int(1).into(), CellValue::Int(2).into()])
.expect("insert row");
txn.commit().expect("commit row");
- let law = enforced_rule(
+ let rule = enforced_rule(
"T.eq",
vec![int_ty(), int_ty()],
vec![ir::Prop::Atom {
@@ -448,8 +450,8 @@ mod tests {
}],
);
- let compiled = compile_law(&law).expect("compile law");
- let violation = check_law(&store, &compiled).expect_err("eq should fail");
+ let compiled = compile_rule(&rule).expect("compile rule");
+ let violation = check_rule(&store, &compiled).expect_err("eq should fail");
match &violation.cause {
ViolationCause::UnsatisfiedEq(eq) => {
assert_eq!(eq.left, crate::solver::compile::CompTerm::Var(0));
diff --git a/packages/coln-store/src/store/error.rs b/packages/coln-store/src/store/error.rs
index deffa89c..7ef862f8 100644
--- a/packages/coln-store/src/store/error.rs
+++ b/packages/coln-store/src/store/error.rs
@@ -3,6 +3,7 @@
// SPDX-License-Identifier: Apache-2.0 OR MIT
use crate::commit::error::CodecError;
+use crate::roweq::rowing::RowingError;
use crate::solver::compile::CompileError;
use crate::solver::validate::RuleViolation;
use crate::table::ValidationError;
@@ -13,13 +14,15 @@ pub enum StoreIntError {
#[error(transparent)]
Validation(#[from] ValidationError),
#[error(transparent)]
- Law(#[from] Box),
+ Rule(#[from] Box),
#[error(transparent)]
Compile(#[from] CompileError),
#[error(transparent)]
Encode(#[from] CodecError),
#[error(transparent)]
Commit(#[from] CommitApplyError),
+ #[error(transparent)]
+ Rowing(#[from] RowingError),
}
#[derive(Debug, thiserror::Error)]
diff --git a/packages/coln-store/src/store/mod.rs b/packages/coln-store/src/store/mod.rs
index b4c86df7..5f86ac66 100644
--- a/packages/coln-store/src/store/mod.rs
+++ b/packages/coln-store/src/store/mod.rs
@@ -13,6 +13,7 @@ use crate::commit::graph::CommitGraph;
use crate::commit::hash::CommitHash;
use crate::commit::wire::root::{RootCommitData, RootTableEntry};
use crate::ir::{self, FlatRealm, RuleEntry};
+use crate::roweq::{self, rowing};
use crate::solver::compile::{CompRule, CompileError};
use crate::solver::validate::RuleViolation;
use crate::solver::{self};
@@ -24,7 +25,7 @@ use crate::txn::{OwnedTransaction, Transaction};
pub mod error;
// TODO this should not be cloneable for efficiency reasons. In the future we should
-// be able to teach the law validator to check the delta
+// be able to teach the rule validator to check the delta
#[derive(Debug, Clone)]
pub struct Store {
pub(crate) next_oid: TableOid,
@@ -35,6 +36,7 @@ pub struct Store {
/// Compiled rule for this instance; table schemas live only on each [`Table`].
rules: Vec,
commits: CommitGraph,
+ rowing: rowing::Index,
}
impl Store {
@@ -49,6 +51,7 @@ impl Store {
rule_entries: vec![],
rules: vec![],
commits,
+ rowing: rowing::Index::new(),
}
}
@@ -149,6 +152,7 @@ impl Store {
rule_entries: root.laws,
rules,
commits: CommitGraph::new(),
+ rowing: rowing::Index::new(),
})
}
@@ -176,18 +180,24 @@ impl Store {
Ok(graph)
}
- #[cfg(test)]
- pub(crate) fn insert_table(&mut self, path: ir::Path, table: Table) -> TableOid {
+ #[cfg(feature = "native")]
+ // used in SQL mode only
+ pub(crate) fn create_table(
+ &mut self,
+ path: ir::Path,
+ schema: ir::Schema,
+ ) -> Result {
let oid = self.next_oid;
self.next_oid = self.next_oid.saturating_add(1);
- self.path_to_oid.insert(path, oid);
- self.tables.insert(oid, table);
- oid
+ self.path_to_oid.insert(path.clone(), oid);
+ self.tables.insert(oid, Table::new(path, schema));
+ self.commits = Self::root_commit_graph(&self.tables, &self.rule_entries)?;
+ Ok(oid)
}
/// Validates and appends a batch to this store.
///
- /// This is a low level API that mutates `self` before law checking, so
+ /// This is a low level API that mutates `self` before rule checking, so
/// callers that need atomicity must call it on a preview clone and publish
/// that clone only after success.
fn apply_batch(&mut self, ops: Vec) -> Result<(), StoreIntError> {
@@ -201,12 +211,25 @@ impl Store {
row_id,
} = op;
let oid = self.resolve_table(table).expect("validated batch");
- let t = self.table_mut(oid).expect("validated batch");
- t.append_row(values.clone(), *row_id);
+ // use self.tables to make the borrow checker happy we are accessing separate fields
+ let t = self.tables.get_mut(&oid).expect("validated batch");
+ let observed = self.rowing.observe(t, *row_id, values)?;
+
+ // TODO We are invoking rowing even if tables might not be in hashcons mode
+ match observed {
+ roweq::ObservedOutcome::Inserted(rid) => t.append_row(values.clone(), rid),
+ roweq::ObservedOutcome::KeptOld(_row_id) => {
+ // equivalent row exists, do nothing
+ }
+
+ roweq::ObservedOutcome::Swap { old, new } => {
+ t.replace_row_id(&old, new)?;
+ }
+ }
}
- self.check_laws()?;
+ self.check_rules()?;
Ok(())
}
@@ -284,6 +307,7 @@ impl Store {
rule_entries: rules,
rules: comp_rules,
commits,
+ rowing: rowing::Index::new(),
})
}
}
@@ -294,17 +318,17 @@ impl Store {
debug!(rule_count = rules.len(), "compiling rules");
let comp = rules
.iter()
- .map(solver::compile::compile_law)
+ .map(solver::compile::compile_rule)
.collect::, CompileError>>()?;
debug!(compiled_rule_count = comp.len(), "compiled rules");
Ok(comp)
}
- pub fn check_laws(&self) -> Result<(), StoreIntError> {
+ pub fn check_rules(&self) -> Result<(), StoreIntError> {
debug!(rule_count = self.rules.len(), "checking rules");
self.rules()
.iter()
- .map(|law_entry| solver::validate::check_law(self, law_entry))
+ .map(|rule| solver::validate::check_rule(self, rule))
.collect::, Box>>()?;
debug!(rule_count = self.rules.len(), "all rules satisfied");
Ok(())
@@ -634,7 +658,7 @@ mod tests {
use super::test_support::link_foreign_key_theory;
use super::*;
- use crate::ir::{BuiltinTy, ColType, ColumnEntry, EntityVariant, Path, Schema};
+ use crate::ir::{BuiltinTy, ColType, ColumnEntry, EntityVariant, Path, RuleVariant, Schema};
fn single_int_store() -> Store {
let path = Path::from("T");
@@ -649,7 +673,7 @@ mod tests {
primary_key: None,
};
let mut store = Store::new();
- store.insert_table(path.clone(), Table::new(path, schema));
+ store.create_table(path, schema).expect("create test table");
store
}
@@ -671,10 +695,10 @@ mod tests {
}],
primary_key: None,
};
- let table = Table::new(path.clone(), schema);
-
let mut store = Store::new();
- let oid0 = store.insert_table(path.clone(), table);
+ let oid0 = store
+ .create_table(path.clone(), schema)
+ .expect("create table");
assert_eq!(oid0, 0);
let t = store.table(oid0).expect("table at oid 0");
@@ -692,10 +716,9 @@ mod tests {
}],
primary_key: None,
};
- let oid1 = store.insert_table(
- Path::from("Other"),
- Table::new(Path::from("table2"), schema2),
- );
+ let oid1 = store
+ .create_table(Path::from("Other"), schema2)
+ .expect("create second table");
assert_eq!(oid1, 1);
}
@@ -712,7 +735,9 @@ mod tests {
};
let mut store = Store::new();
- let oid = store.insert_table(path.clone(), Table::new(path.clone(), schema));
+ let oid = store
+ .create_table(path.clone(), schema)
+ .expect("create table");
assert_eq!(store.resolve_table(&path), Some(oid));
assert_eq!(store.resolve_table(&Path::from("missing")), None);
@@ -732,7 +757,9 @@ mod tests {
primary_key: None,
};
let mut store = Store::new();
- store.insert_table(path.clone(), Table::new(path.clone(), schema));
+ store
+ .create_table(path.clone(), schema)
+ .expect("create table");
let mut txn = store.transaction();
txn.add(&path, vec![CellValue::Int(1).into()])
@@ -761,7 +788,9 @@ mod tests {
primary_key: None,
};
let mut store = Store::new();
- store.insert_table(path.clone(), Table::new(path.clone(), schema));
+ store
+ .create_table(path.clone(), schema)
+ .expect("create table");
let err = {
let mut txn = store.transaction();
@@ -792,7 +821,9 @@ mod tests {
primary_key: Some(vec![Path::from("c0")]),
};
let mut store = Store::new();
- store.insert_table(path.clone(), Table::new(path.clone(), schema));
+ store
+ .create_table(path.clone(), schema)
+ .expect("create table");
let mut txn = store.transaction();
txn.add(&path, vec![CellValue::Int(1).into()])
@@ -822,7 +853,9 @@ mod tests {
primary_key: None,
};
let mut store = Store::new();
- store.insert_table(path.clone(), Table::new(path.clone(), schema));
+ store
+ .create_table(path.clone(), schema)
+ .expect("create table");
let mut txn = store.transaction();
txn.add(&path, vec![CellValue::Int(42).into()])
@@ -1007,7 +1040,7 @@ mod tests {
}
#[test]
- fn transaction_leaves_store_unchanged_when_laws_fail() {
+ fn transaction_leaves_store_unchanged_when_rules_fail() {
let theory = link_foreign_key_theory();
let link = Path::from("Link");
let mut store = Store::try_from_theory(theory).expect("theory");
@@ -1020,7 +1053,7 @@ mod tests {
.expect("add");
let err = txn.commit().unwrap_err();
- assert!(matches!(err, StoreIntError::Law(_)));
+ assert!(matches!(err, StoreIntError::Rule(_)));
assert_eq!(store.table_at(&link).expect("Link").row_count(), 0);
}
@@ -1040,13 +1073,14 @@ mod tests {
let compiled_rule = solver::compile::CompRule {
path: Path::from("T.total"),
+ rule_variant: RuleVariant::Enforced,
vars: vec![],
antecedent: solver::compile::CompProp::And(vec![]),
consequent: solver::compile::CompProp::And(vec![]),
tables: vec![Path::from("T")],
};
let violation = RuleViolation {
- law: compiled_rule,
+ rule: compiled_rule,
cause: solver::validate::ViolationCause::MissingAtom(solver::compile::CompAtom {
table: Path::from("T"),
row_id: None,
@@ -1054,7 +1088,7 @@ mod tests {
}),
binding: vec![],
};
- let law = StoreIntError::from(Box::new(violation));
- assert!(matches!(law, StoreIntError::Law(_)));
+ let rule = StoreIntError::from(Box::new(violation));
+ assert!(matches!(rule, StoreIntError::Rule(_)));
}
}
diff --git a/packages/coln-store/src/table.rs b/packages/coln-store/src/table.rs
index 7949ba91..f8bef1f0 100644
--- a/packages/coln-store/src/table.rs
+++ b/packages/coln-store/src/table.rs
@@ -15,7 +15,7 @@ pub type TableOid = u64;
/// The unique id that identifies each row in a table
/// It is managed by the database, read-only for the user
-#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
+#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)]
pub struct RowId {
pub commit: CommitHash,
pub counter: u32,
@@ -37,11 +37,9 @@ pub enum ValidationError {
#[error("type mismatch at column {column}: expected {expected}, got {got}")]
TypeMismatch {
column: usize,
- expected: &'static str,
- got: &'static str,
+ expected: CellKind,
+ got: CellKind,
},
- #[error("tuple columns are not supported yet (column {column})")]
- UnsupportedTuple { column: usize },
#[error("primary key references unknown column: {name}")]
InvalidPrimaryKeyName { name: ColName },
#[error("duplicate primary key")]
@@ -61,44 +59,66 @@ pub enum ValidationError {
}
/// One cell in columnar storage: entity id, or primitive.
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum CellValue {
Id(RowId),
Int(i64),
Str(String),
}
-impl CellValue {
- pub fn kind(&self) -> &'static str {
- match self {
- CellValue::Id(_) => "id",
- CellValue::Int(_) => "int",
- CellValue::Str(_) => "string",
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum CellKind {
+ RowId,
+ Int,
+ Str,
+}
+
+impl From<&ColType> for CellKind {
+ fn from(col_type: &ColType) -> Self {
+ match col_type {
+ ColType::RowId { .. } => CellKind::RowId,
+ ColType::BuiltinTy {
+ builtin_ty: BuiltinTy::BuiltinInt,
+ } => CellKind::Int,
+ ColType::BuiltinTy {
+ builtin_ty: BuiltinTy::BuiltinStr,
+ } => CellKind::Str,
+ }
+ }
+}
+
+impl From<&CellValue> for CellKind {
+ fn from(value: &CellValue) -> Self {
+ match value {
+ CellValue::Id(_) => CellKind::RowId,
+ CellValue::Int(_) => CellKind::Int,
+ CellValue::Str(_) => CellKind::Str,
}
}
+}
+impl fmt::Display for CellKind {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(match self {
+ CellKind::RowId => "entity id",
+ CellKind::Int => "int",
+ CellKind::Str => "string",
+ })
+ }
+}
+
+impl CellValue {
fn matches_schema(&self, col_type: &ColType, column: usize) -> Result<(), ValidationError> {
- match col_type {
- ColType::RowId { .. } => match self {
- CellValue::Id(_) => Ok(()),
- _ => Err(ValidationError::TypeMismatch {
- column,
- expected: "entity id",
- got: self.kind(),
- }),
- },
- ColType::BuiltinTy { builtin_ty } => match (builtin_ty, self) {
- (BuiltinTy::BuiltinInt, CellValue::Int(_)) => Ok(()),
- (BuiltinTy::BuiltinStr, CellValue::Str(_)) => Ok(()),
- _ => Err(ValidationError::TypeMismatch {
- column,
- expected: match *builtin_ty {
- BuiltinTy::BuiltinInt => "int",
- BuiltinTy::BuiltinStr => "string",
- },
- got: self.kind(),
- }),
- },
+ let expected = CellKind::from(col_type);
+ let got = CellKind::from(self);
+ if expected == got {
+ Ok(())
+ } else {
+ Err(ValidationError::TypeMismatch {
+ column,
+ expected,
+ got,
+ })
}
}
}
@@ -128,6 +148,8 @@ pub struct Table {
path: ir::Path,
schema: Schema,
col_index: HashMap,
+ row_index: HashMap,
+ hashcons: bool,
pub(crate) row_ids: Vec,
pub(crate) cols: Vec>,
}
@@ -144,7 +166,9 @@ impl Table {
Self {
path,
col_index,
+ row_index: HashMap::new(),
schema,
+ hashcons: false,
row_ids: vec![],
cols: vec![Vec::new(); n],
}
@@ -158,6 +182,15 @@ impl Table {
&self.path
}
+ pub(crate) fn hashcons(&self) -> bool {
+ self.hashcons
+ }
+
+ #[cfg(test)]
+ pub(crate) fn set_hashcons_for_test(&mut self, hashcons: bool) {
+ self.hashcons = hashcons;
+ }
+
fn column_index(&self, name: &ir::Path) -> Option {
self.col_index.get(name).copied()
}
@@ -286,11 +319,29 @@ impl Table {
pub(crate) fn append_row(&mut self, values: Vec, row_id: RowId) {
debug_assert_eq!(values.len(), self.schema.columns.len());
+ self.row_index.insert(row_id, self.row_ids.len());
self.row_ids.push(row_id);
for (i, v) in values.into_iter().enumerate() {
self.cols[i].push(v);
}
}
+
+ /// Used for canonicalising row_ids
+ pub(crate) fn replace_row_id(
+ &mut self,
+ old: &RowId,
+ new: RowId,
+ ) -> Result<(), ValidationError> {
+ let i = self
+ .row_index
+ .remove(old)
+ .ok_or(ValidationError::InvalidRowHandle {
+ reason: format!("attempting to replace non-existing row_id {old}"),
+ })?;
+ self.row_ids[i] = new;
+ self.row_index.insert(new, i);
+ Ok(())
+ }
}
#[cfg(test)]
diff --git a/packages/coln-store/src/txn/inner.rs b/packages/coln-store/src/txn/inner.rs
index c1279395..cf26fd58 100644
--- a/packages/coln-store/src/txn/inner.rs
+++ b/packages/coln-store/src/txn/inner.rs
@@ -2,8 +2,9 @@
//
// SPDX-License-Identifier: Apache-2.0 OR MIT
-use coln_flir_rs::ir;
use std::sync::atomic::{AtomicU64, Ordering};
+
+use coln_flir_rs::ir;
use tracing::info;
use crate::{
@@ -147,7 +148,7 @@ impl TxnInner {
// 3. resolve: TxnRowId(k) -> RowId { commit: hash, counter: k }
// CellValue::TxnId(k) -> CellValue::Id(RowId { commit: hash, counter: k })
// 4. apply resolved Ops to tables via table.append_row
- // 5. check_laws
+ // 5. check_rules
// 6. push CommitMeta into store.commit_graph, advance heads
// 7. return hash
}
diff --git a/packages/coln-store/src/txn/mod.rs b/packages/coln-store/src/txn/mod.rs
index 184d0014..f6d16792 100644
--- a/packages/coln-store/src/txn/mod.rs
+++ b/packages/coln-store/src/txn/mod.rs
@@ -4,15 +4,14 @@
use coln_flir_rs::ir;
+#[cfg(feature = "native")]
+use crate::txn::ops::{TempRowId, TxnCellValue};
use crate::{
commit::hash::CommitHash,
store::{Store, error::StoreIntError},
txn::ops::{RowHandle, TxnValue},
};
-#[cfg(feature = "native")]
-use crate::txn::ops::{TempRowId, TxnCellValue};
-
mod inner;
pub mod ops;
mod timestamp;
@@ -96,7 +95,7 @@ mod tests {
use super::*;
use crate::ir::{BuiltinTy, ColType, ColumnEntry, EntityVariant, Path, Schema};
use crate::store::test_support::link_foreign_key_theory;
- use crate::table::{CellValue, Table, ValidationError};
+ use crate::table::{CellValue, ValidationError};
fn table_schema(columns: Vec, primary_key: Option>) -> Schema {
Schema {
@@ -127,7 +126,9 @@ mod tests {
let path = Path::from("T");
let schema = table_schema(vec![int_col("c0")], None);
let mut store = Store::new();
- store.insert_table(path.clone(), Table::new(path.clone(), schema));
+ store
+ .create_table(path.clone(), schema)
+ .expect("create table");
let mut tx = OwnedTransaction::new(store);
tx.add(&path, vec![42_i64.into()]).expect("add");
@@ -141,7 +142,9 @@ mod tests {
let path = Path::from("T");
let schema = table_schema(vec![int_col("c0")], None);
let mut store = Store::new();
- store.insert_table(path.clone(), Table::new(path.clone(), schema));
+ store
+ .create_table(path.clone(), schema)
+ .expect("create table");
let mut tx = OwnedTransaction::new(store);
let err = tx
@@ -170,7 +173,7 @@ mod tests {
.expect("add");
let (err, recovered) = tx.commit().unwrap_err();
- assert!(matches!(err, StoreIntError::Law(_)));
+ assert!(matches!(err, StoreIntError::Rule(_)));
assert_eq!(recovered.table_at(&link).expect("Link").row_count(), 0);
}
@@ -179,17 +182,15 @@ mod tests {
let nodes = Path::from("Nodes");
let edges = Path::from("Edges");
let mut store = Store::new();
- store.insert_table(
- nodes.clone(),
- Table::new(nodes.clone(), table_schema(vec![], None)),
- );
- store.insert_table(
- edges.clone(),
- Table::new(
+ store
+ .create_table(nodes.clone(), table_schema(vec![], None))
+ .expect("create nodes table");
+ store
+ .create_table(
edges.clone(),
table_schema(vec![row_id_col("node", nodes.clone())], None),
- ),
- );
+ )
+ .expect("create edges table");
let mut tx = store.transaction();
let node_temp = tx.add(&nodes, vec![]).expect("add node");
@@ -216,17 +217,15 @@ mod tests {
let nodes = Path::from("Nodes");
let edges = Path::from("Edges");
let mut store = Store::new();
- store.insert_table(
- nodes.clone(),
- Table::new(nodes.clone(), table_schema(vec![], None)),
- );
- store.insert_table(
- edges.clone(),
- Table::new(
+ store
+ .create_table(nodes.clone(), table_schema(vec![], None))
+ .expect("create nodes table");
+ store
+ .create_table(
edges.clone(),
table_schema(vec![row_id_col("node", nodes.clone())], None),
- ),
- );
+ )
+ .expect("create edges table");
let mut tx = store.transaction();
let node = tx.add(&nodes, vec![]).expect("add node");
@@ -248,17 +247,15 @@ mod tests {
let nodes = Path::from("Nodes");
let edges = Path::from("Edges");
let mut store = Store::new();
- store.insert_table(
- nodes.clone(),
- Table::new(nodes.clone(), table_schema(vec![], Some(vec![]))),
- );
- store.insert_table(
- edges.clone(),
- Table::new(
+ store
+ .create_table(nodes.clone(), table_schema(vec![], Some(vec![])))
+ .expect("create nodes table");
+ store
+ .create_table(
edges.clone(),
table_schema(vec![row_id_col("node", nodes.clone())], None),
- ),
- );
+ )
+ .expect("create edges table");
let mut tx = store.transaction();
let node = tx.add(&nodes, vec![]).expect("add first node");
@@ -292,7 +289,9 @@ mod tests {
let path = Path::from("T");
let schema = table_schema(vec![int_col("c0")], None);
let mut store = Store::new();
- store.insert_table(path.clone(), Table::new(path.clone(), schema));
+ store
+ .create_table(path.clone(), schema)
+ .expect("create table");
let root = store.commits().root_commit().expect("root commit").hash();
let mut tx = store.transaction();
diff --git a/packages/coln-store/tests/data/people.csv b/packages/coln-store/tests/data/people.csv
new file mode 100644
index 00000000..826d5ae6
--- /dev/null
+++ b/packages/coln-store/tests/data/people.csv
@@ -0,0 +1,3 @@
+age,name
+30,alice
+41,bob
diff --git a/packages/coln-store/tests/data/people.tsv b/packages/coln-store/tests/data/people.tsv
new file mode 100644
index 00000000..7f1c2e72
--- /dev/null
+++ b/packages/coln-store/tests/data/people.tsv
@@ -0,0 +1,3 @@
+age name
+30 alice
+41 bob
diff --git a/packages/coln-store/tests/data/people_bad_int.csv b/packages/coln-store/tests/data/people_bad_int.csv
new file mode 100644
index 00000000..07a5edf7
--- /dev/null
+++ b/packages/coln-store/tests/data/people_bad_int.csv
@@ -0,0 +1,2 @@
+name,age
+carol,not-a-number
diff --git a/packages/coln-store/tests/data/people_dup.csv b/packages/coln-store/tests/data/people_dup.csv
new file mode 100644
index 00000000..efd2c918
--- /dev/null
+++ b/packages/coln-store/tests/data/people_dup.csv
@@ -0,0 +1,2 @@
+name,name,age
+alice,bob,30
diff --git a/packages/coln-store/tests/data/ssa.json b/packages/coln-store/tests/data/ssa.json
deleted file mode 100644
index 6b1dc1cf..00000000
--- a/packages/coln-store/tests/data/ssa.json
+++ /dev/null
@@ -1 +0,0 @@
-{"laws":[{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}},{"column":3,"term":{"index":2,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"left":{"index":2,"tag":"var"},"right":{"index":3,"tag":"var"},"tag":"eq"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["acyclic"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}}]},"tag":"atom"},"consequent":{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["car"],["foreignKeys"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["first"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["first"],["foreignKeys"]]},{"law":{"antecedent":{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["first"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["first"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["first-is-top"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["first"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}},{"column":3,"term":{"index":2,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["first-is-top"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["first-is-top"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["first-is-top"],["total"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["follows"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["follows"],["foreignKeys"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["last"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["last"],["foreignKeys"]]},{"law":{"antecedent":{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["last"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["last"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["last-is-bot"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["last"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["last-is-bot"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["last-is-bot"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["last-is-bot"],["total"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["next"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["next"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["next"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["next"],["total"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["prev"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["prev"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["prev"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["prev"],["total"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["follows","is-trans-closure"],["refl"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":2,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["follows","is-trans-closure"],["refl"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["follows","is-trans-closure"],["refl"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["follows","is-trans-closure"],["refl"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["follows","is-trans-closure"],["snoc"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}},{"column":4,"term":{"index":4,"tag":"var"}},{"column":5,"term":{"index":5,"tag":"var"}},{"column":6,"term":{"index":7,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["next"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}},{"column":3,"term":{"index":6,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"left":{"index":6,"tag":"var"},"right":{"index":4,"tag":"var"},"tag":"eq"},{"atom":{"rowId":{"index":7,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["follows","is-trans-closure"],["snoc"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["next"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}},{"column":3,"term":{"index":6,"tag":"var"}}]},"tag":"atom"},{"left":{"index":6,"tag":"var"},"right":{"index":4,"tag":"var"},"tag":"eq"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["follows","is-trans-closure"],["snoc"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}},{"column":4,"term":{"index":4,"tag":"var"}},{"column":5,"term":{"index":5,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["follows","is-trans-closure"],["snoc"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["prev"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["next"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"left":{"index":4,"tag":"var"},"right":{"index":2,"tag":"var"},"tag":"eq"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["next","prev"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["next"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["prev"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"left":{"index":4,"tag":"var"},"right":{"index":2,"tag":"var"},"tag":"eq"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["prev","next"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}},{"column":3,"term":{"index":2,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"left":{"index":2,"tag":"var"},"right":{"index":3,"tag":"var"},"tag":"eq"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["acyclic"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}}]},"tag":"atom"},"consequent":{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["car"],["foreignKeys"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["first"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["first"],["foreignKeys"]]},{"law":{"antecedent":{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["first"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["first"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["first-is-top"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["first"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}},{"column":3,"term":{"index":2,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["first-is-top"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["first-is-top"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["first-is-top"],["total"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["follows"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["follows"],["foreignKeys"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["last"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["last"],["foreignKeys"]]},{"law":{"antecedent":{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["last"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["last"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["last-is-bot"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["last"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["last-is-bot"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["last-is-bot"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["last-is-bot"],["total"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["next"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["next"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["next"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["next"],["total"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["prev"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["prev"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["prev"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["prev"],["total"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["follows","is-trans-closure"],["refl"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":2,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["follows","is-trans-closure"],["refl"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["follows","is-trans-closure"],["refl"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["follows","is-trans-closure"],["refl"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["follows","is-trans-closure"],["snoc"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}},{"column":4,"term":{"index":4,"tag":"var"}},{"column":5,"term":{"index":5,"tag":"var"}},{"column":6,"term":{"index":7,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["next"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}},{"column":3,"term":{"index":6,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"left":{"index":6,"tag":"var"},"right":{"index":4,"tag":"var"},"tag":"eq"},{"atom":{"rowId":{"index":7,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["follows","is-trans-closure"],["snoc"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["next"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}},{"column":3,"term":{"index":6,"tag":"var"}}]},"tag":"atom"},{"left":{"index":6,"tag":"var"},"right":{"index":4,"tag":"var"},"tag":"eq"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["follows","is-trans-closure"],["snoc"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}},{"column":4,"term":{"index":4,"tag":"var"}},{"column":5,"term":{"index":5,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["follows","is-trans-closure"],["snoc"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["prev"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["next"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"left":{"index":4,"tag":"var"},"right":{"index":2,"tag":"var"},"tag":"eq"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["next","prev"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["next"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["prev"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"left":{"index":4,"tag":"var"},"right":{"index":2,"tag":"var"},"tag":"eq"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["prev","next"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["outputs"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["outputs"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opOutputs"],["bwd"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}},{"column":4,"term":{"index":5,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opType"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["opinfo"],["outputArity"]],"values":[{"column":0,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["ssainsta"],["ops"],["outputs"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["OpType"]],"tag":"entityType"},{"path":[["opinfo"],["outputArity"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["outputs"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opOutputs"],["bwd"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opType"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["opinfo"],["outputArity"]],"values":[{"column":0,"term":{"index":3,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opOutputs"],["bwd"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["OpType"]],"tag":"entityType"},{"path":[["opinfo"],["outputArity"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opOutputs"],["bwd"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opOutputs"],["fwd"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}},{"column":4,"term":{"index":5,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opType"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["outputs"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["opinfo"],["outputArity"]],"values":[{"column":0,"term":{"index":4,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["outputs"]],"tag":"entityType"},{"path":[["opinfo"],["OpType"]],"tag":"entityType"},{"path":[["opinfo"],["outputArity"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opOutputs"],["fwd"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["outputs"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opOutputs"],["fwd"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["outputs"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opOutputs"],["fwd"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opType"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["opinfo"],["outputArity"]],"values":[{"column":0,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opOutputs"],["bwd"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}},{"column":4,"term":{"index":5,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opOutputs"],["fwd"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":5,"tag":"var"}},{"column":4,"term":{"index":6,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"left":{"index":6,"tag":"var"},"right":{"index":4,"tag":"var"},"tag":"eq"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["OpType"]],"tag":"entityType"},{"path":[["opinfo"],["outputArity"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["outputs"]],"tag":"entityType"},{"path":[["opinfo"],["outputArity"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opOutputs"],["bwd","fwd"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["outputs"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opOutputs"],["fwd"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}},{"column":4,"term":{"index":4,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opOutputs"],["bwd"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}},{"column":4,"term":{"index":5,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"left":{"index":5,"tag":"var"},"right":{"index":3,"tag":"var"},"tag":"eq"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["outputs"]],"tag":"entityType"},{"path":[["opinfo"],["outputArity"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["outputs"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opOutputs"],["fwd","bwd"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opType"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["OpType"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opType"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opType"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opType"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opInputs","X"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}},{"column":4,"term":{"index":5,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opType"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["opinfo"],["inputArity"]],"values":[{"column":0,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["OpType"]],"tag":"entityType"},{"path":[["opinfo"],["inputArity"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opInputs","X"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opType"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["opinfo"],["inputArity"]],"values":[{"column":0,"term":{"index":3,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opInputs","X"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["OpType"]],"tag":"entityType"},{"path":[["opinfo"],["inputArity"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opInputs","X"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opInputs","output"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}},{"column":4,"term":{"index":5,"tag":"var"}},{"column":5,"term":{"index":6,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opType"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["opinfo"],["inputArity"]],"values":[{"column":0,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":6,"tag":"var"},"table":[["ssainsta"],["ops"],["outputs"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":5,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["OpType"]],"tag":"entityType"},{"path":[["opinfo"],["inputArity"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["outputs"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opInputs","output"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opType"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["opinfo"],["inputArity"]],"values":[{"column":0,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opInputs","output"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}},{"column":4,"term":{"index":5,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["OpType"]],"tag":"entityType"},{"path":[["opinfo"],["inputArity"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opInputs","output"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opInputs","rel"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}},{"column":4,"term":{"index":5,"tag":"var"}},{"column":5,"term":{"index":6,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opType"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["opinfo"],["inputArity"]],"values":[{"column":0,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":6,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":5,"tag":"var"}},{"column":3,"term":{"index":2,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["OpType"]],"tag":"entityType"},{"path":[["opinfo"],["inputArity"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opInputs","rel"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opType"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["opinfo"],["inputArity"]],"values":[{"column":0,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opInputs","rel"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}},{"column":4,"term":{"index":5,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["OpType"]],"tag":"entityType"},{"path":[["opinfo"],["inputArity"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opInputs","rel"],["total"]]}],"tables":[{"path":[["opinfo"],["OpType"]],"table":{"columns":[],"primaryKey":null}},{"path":[["opinfo"],["inputArity"]],"table":{"columns":[{"path":[["opinfo"],["OpType"]],"tag":"entityType"}],"primaryKey":null}},{"path":[["opinfo"],["outputArity"]],"table":{"columns":[{"path":[["opinfo"],["OpType"]],"tag":"entityType"}],"primaryKey":null}},{"path":[["ssainsta"],["BlockId"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"}],"primaryKey":null}},{"path":[["ssainsta"],["RegionId"]],"table":{"columns":[],"primaryKey":null}},{"path":[["ssainsta"],["ops"],["args"],["car"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"}],"primaryKey":null}},{"path":[["ssainsta"],["ops"],["args"],["first"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}],"primaryKey":[0,1]}},{"path":[["ssainsta"],["ops"],["args"],["first-is-top"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"}],"primaryKey":[0,1,2]}},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}],"primaryKey":null}},{"path":[["ssainsta"],["ops"],["args"],["last"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}],"primaryKey":[0,1]}},{"path":[["ssainsta"],["ops"],["args"],["last-is-bot"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"}],"primaryKey":[0,1,2]}},{"path":[["ssainsta"],["ops"],["args"],["next"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}],"primaryKey":[0,1,2]}},{"path":[["ssainsta"],["ops"],["args"],["prev"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}],"primaryKey":[0,1,2]}},{"path":[["ssainsta"],["ops"],["args"],["follows","is-trans-closure"],["refl"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"}],"primaryKey":[0,1,2]}},{"path":[["ssainsta"],["ops"],["args"],["follows","is-trans-closure"],["snoc"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"}],"primaryKey":[0,1,2,3,4,5]}},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"}],"primaryKey":null}},{"path":[["ssainsta"],["ops"],["ops"],["first"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}],"primaryKey":[0,1]}},{"path":[["ssainsta"],["ops"],["ops"],["first-is-top"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"}],"primaryKey":[0,1,2]}},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}],"primaryKey":null}},{"path":[["ssainsta"],["ops"],["ops"],["last"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}],"primaryKey":[0,1]}},{"path":[["ssainsta"],["ops"],["ops"],["last-is-bot"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"}],"primaryKey":[0,1,2]}},{"path":[["ssainsta"],["ops"],["ops"],["next"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}],"primaryKey":[0,1,2]}},{"path":[["ssainsta"],["ops"],["ops"],["prev"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}],"primaryKey":[0,1,2]}},{"path":[["ssainsta"],["ops"],["ops"],["follows","is-trans-closure"],["refl"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"}],"primaryKey":[0,1,2]}},{"path":[["ssainsta"],["ops"],["ops"],["follows","is-trans-closure"],["snoc"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"}],"primaryKey":[0,1,2,3,4,5]}},{"path":[["ssainsta"],["ops"],["outputs"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}],"primaryKey":null}},{"path":[["ssainsta"],["ops"],["payload"],["opOutputs"],["bwd"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["outputArity"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["outputs"]],"tag":"entityType"}],"primaryKey":[0,1,2,3]}},{"path":[["ssainsta"],["ops"],["payload"],["opOutputs"],["fwd"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["outputs"]],"tag":"entityType"},{"path":[["opinfo"],["outputArity"]],"tag":"entityType"}],"primaryKey":[0,1,2,3]}},{"path":[["ssainsta"],["ops"],["payload"],["opType"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["OpType"]],"tag":"entityType"}],"primaryKey":[0,1,2]}},{"path":[["ssainsta"],["ops"],["payload"],["opInputs","X"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["inputArity"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}],"primaryKey":[0,1,2,3]}},{"path":[["ssainsta"],["ops"],["payload"],["opInputs","output"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["inputArity"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["outputs"]],"tag":"entityType"}],"primaryKey":[0,1,2,3,4]}},{"path":[["ssainsta"],["ops"],["payload"],["opInputs","rel"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["inputArity"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"}],"primaryKey":[0,1,2,3,4]}}]}
\ No newline at end of file
diff --git a/packages/coln-store/tests/test_path.rs b/packages/coln-store/tests/test_path.rs
index d8b0467d..9abc39fa 100644
--- a/packages/coln-store/tests/test_path.rs
+++ b/packages/coln-store/tests/test_path.rs
@@ -231,7 +231,7 @@ fn test_missing_graph_witness_rejects_batch_without_mutation() {
tx.add(&Path::from("Path.Graphs"), vec![])
.expect("add graph row");
let err = tx.commit().expect_err("missing g0 and g1");
- assert!(matches!(err, StoreIntError::Law(_)));
+ assert!(matches!(err, StoreIntError::Rule(_)));
assert_eq!(
store
@@ -280,7 +280,7 @@ fn test_fk() {
.expect("add edge");
let err = tx.commit().expect_err("missing v2");
- assert!(matches!(err, StoreIntError::Law(_)));
+ assert!(matches!(err, StoreIntError::Rule(_)));
}
#[test]
@@ -312,7 +312,7 @@ fn test_divergent_commits_merge_between_stores() {
let mut base = Store::try_from_theory(theory).expect("valid theory");
add_basic_data_to_path(&mut base).expect("add shared baseline data");
- // Add a second law-free graph so we can add vertices to different graphs to
+ // Add a second rule-free graph so we can add vertices to different graphs to
// make two commits different
{
let mut tx = base.transaction();
diff --git a/packages/coln-store/tests/test_repl.rs b/packages/coln-store/tests/test_repl.rs
index b064f8df..f7219e11 100644
--- a/packages/coln-store/tests/test_repl.rs
+++ b/packages/coln-store/tests/test_repl.rs
@@ -9,10 +9,7 @@ use std::path::PathBuf;
use coln_flir_rs::ir::{FlatRealm, Path};
use coln_store::{
- repl::{
- exe::run_transact,
- parse::{Command, parse_command},
- },
+ repl::{exe::run_transact, parse::coln::BatchAssignment},
store::Store,
};
@@ -31,17 +28,43 @@ fn batch_block_matches_apply_batch_for_paths_fixture() {
let theory = fixture_theory(PATHS_IR);
let mut store = Store::try_from_theory(theory).expect("valid theory");
- let cmd = parse_command(
- "begin transact; gid1 = add Path.Graphs values (); gid2 = add Path.Graphs values (); \
- g0 = add Path.G0 values (gid2); g1 = add Path.G1 values (gid2); \
- v1 = add Path.G.V values (gid1); v2 = add Path.G.V values (gid1); \
- ge = add Path.G.E values (gid1 v1 v2); commit;",
- )
- .expect("parse batch");
-
- let Command::Batch { assignments } = cmd else {
- panic!("expected Command::Batch");
- };
+ let assignments = vec![
+ BatchAssignment {
+ name: "gid1".to_string(),
+ table: "Path.Graphs".to_string(),
+ row: vec![],
+ },
+ BatchAssignment {
+ name: "gid2".to_string(),
+ table: "Path.Graphs".to_string(),
+ row: vec![],
+ },
+ BatchAssignment {
+ name: "g0".to_string(),
+ table: "Path.G0".to_string(),
+ row: vec!["gid2".to_string()],
+ },
+ BatchAssignment {
+ name: "g1".to_string(),
+ table: "Path.G1".to_string(),
+ row: vec!["gid2".to_string()],
+ },
+ BatchAssignment {
+ name: "v1".to_string(),
+ table: "Path.G.V".to_string(),
+ row: vec!["gid1".to_string()],
+ },
+ BatchAssignment {
+ name: "v2".to_string(),
+ table: "Path.G.V".to_string(),
+ row: vec!["gid1".to_string()],
+ },
+ BatchAssignment {
+ name: "ge".to_string(),
+ table: "Path.G.E".to_string(),
+ row: vec!["gid1".to_string(), "v1".to_string(), "v2".to_string()],
+ },
+ ];
let msg = run_transact(&mut store, &assignments).expect("run batch");
assert!(msg.contains("gid1=#"), "expected binding summary: {msg}");