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}");