[
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\non: [push, pull_request, workflow_dispatch]\npermissions:\n  contents: read\n\njobs:\n  test:\n    name: Test\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n\n    steps:\n    - uses: actions/checkout@v3\n    - uses: dtolnay/rust-toolchain@1.93.1\n      id: toolchain\n      with:\n        components: clippy, rustfmt\n    - uses: actions/cache@v3\n      with:\n        path: target\n        key: ${{runner.os}}-target-${{steps.toolchain.outputs.cachekey}}-${{hashFiles('Cargo.lock')}}\n    - run: cargo build --bins --tests\n    - run: cargo test\n    - run: cargo clippy --tests --no-deps -- -D warnings\n    - run: cargo fmt --check\n    - run: cargo doc --no-deps \n      env:\n        RUSTDOCFLAGS: -D warnings"
  },
  {
    "path": ".gitignore",
    "content": "/cluster/toydb*/data\n/data\n/docs/crate/target\n/target\n.DS_Store\n.vscode/\n**/*.rs.bk\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"toydb\"\nversion = \"1.0.0\"\ndescription = \"A simple distributed SQL database, built for education\"\nauthors = [\"Erik Grinaker <erik@grinaker.org>\"]\nlicense = \"Apache-2.0\"\nhomepage = \"https://github.com/erikgrinaker/toydb\"\nrepository = \"https://github.com/erikgrinaker/toydb\"\nedition = \"2024\"\ndefault-run = \"toydb\"\npublish = false\n\n[lib]\ndoctest = false\n\n[dependencies]\nbincode = { version = \"2.0\", features = [\"serde\"] }\nclap = { version = \"4.5\", features = [\"cargo\", \"derive\"] }\nconfig = \"0.15\"\ncrossbeam = { version = \"0.8\", features = [\"crossbeam-channel\"] }\ndyn-clone = \"1.0\"\nfs4 = \"0.13\"\nhdrhistogram = \"7.5\"\nitertools = \"0.14\"\nlog = \"0.4\"\npetname = \"2.0.2\"\nrand = \"0.10\"\nregex = \"1.12\"\nrustyline = \"17.0\"\nrustyline-derive = \"0.11\"\nserde = { version = \"1.0\", features = [\"derive\"] }\nserde_bytes = \"0.11\"\nsimplelog = \"0.12\"\nuuid = { version = \"1.21\", features = [\"serde\", \"v4\"] }\n\n[dev-dependencies]\nescargot = \"0.5\"\ngoldenscript = \"0.7\"\nhex = \"0.4\"\npaste = \"1.0\"\nserde_json = \"1.0\"\ntempfile = \"3.25\"\ntest-case = \"3.3\"\ntest_each_file = \"0.3\"\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "README.md",
    "content": "# <a><img src=\"./docs/architecture/images/toydb.svg\" height=\"40\" valign=\"top\" /></a> toyDB\n\nDistributed SQL database in Rust, built from scratch as an educational project. Main features:\n\n* [Raft distributed consensus][raft] for linearizable state machine replication.\n\n* [ACID transactions][txn] with MVCC-based snapshot isolation.\n\n* [Pluggable storage engine][storage] with [BitCask][bitcask] and [in-memory][memory] backends.\n\n* [Iterator-based query engine][query] with [heuristic optimization][optimizer] and time-travel \n  support.\n\n* [SQL interface][sql] including joins, aggregates, and transactions.\n\ntoyDB is intended to be simple and understandable, and also functional and correct. Other aspects\nlike performance, scalability, and availability are non-goals -- these are major sources of\ncomplexity in production-grade databases, and obscure the basic underlying concepts. Shortcuts have\nbeen taken where possible.\n\nI originally wrote toyDB in 2020 to learn more about database internals. Since then, I've spent\nseveral years building real distributed SQL databases at\n[CockroachDB](https://github.com/cockroachdb/cockroach) and\n[Neon](https://github.com/neondatabase/neon). Based on this experience, I've rewritten toyDB as a\nsimple illustration of the architecture and concepts behind distributed SQL databases.\n\n[raft]: https://github.com/erikgrinaker/toydb/blob/main/src/raft/mod.rs\n[txn]: https://github.com/erikgrinaker/toydb/blob/main/src/storage/mvcc.rs\n[storage]: https://github.com/erikgrinaker/toydb/blob/main/src/storage/engine.rs\n[bitcask]: https://github.com/erikgrinaker/toydb/blob/main/src/storage/bitcask.rs\n[memory]: https://github.com/erikgrinaker/toydb/blob/main/src/storage/memory.rs\n[query]: https://github.com/erikgrinaker/toydb/blob/main/src/sql/execution/executor.rs\n[optimizer]: https://github.com/erikgrinaker/toydb/blob/main/src/sql/planner/optimizer.rs\n[sql]: https://github.com/erikgrinaker/toydb/blob/main/src/sql/parser/parser.rs\n\n## Documentation\n\n* [Architecture guide](docs/architecture/index.md): a guided tour of toyDB's code and architecture.\n\n* [SQL examples](docs/examples.md): walkthrough of toyDB's SQL features.\n\n* [SQL reference](docs/sql.md): reference documentation for toyDB's SQL dialect.\n\n* [References](docs/references.md): research materials used while building toyDB.\n\n## Usage\n\nWith a [Rust compiler](https://www.rust-lang.org/tools/install) installed, a local five-node \ncluster can be built and started as:\n\n```\n$ ./cluster/run.sh\nStarting 5 nodes on ports 9601-9605 with data under cluster/*/data/.\nTo connect to node 1, run: cargo run --release --bin toysql\n\ntoydb4 21:03:55 [INFO] Listening on [::1]:9604 (SQL) and [::1]:9704 (Raft)\ntoydb1 21:03:55 [INFO] Listening on [::1]:9601 (SQL) and [::1]:9701 (Raft)\ntoydb2 21:03:55 [INFO] Listening on [::1]:9602 (SQL) and [::1]:9702 (Raft)\ntoydb3 21:03:55 [INFO] Listening on [::1]:9603 (SQL) and [::1]:9703 (Raft)\ntoydb5 21:03:55 [INFO] Listening on [::1]:9605 (SQL) and [::1]:9705 (Raft)\ntoydb2 21:03:56 [INFO] Starting new election for term 1\n[...]\ntoydb2 21:03:56 [INFO] Won election for term 1, becoming leader\n```\n\nA command-line client can be built and used with node 1 on `localhost:9601`:\n\n```\n$ cargo run --release --bin toysql\nConnected to toyDB node n1. Enter !help for instructions.\ntoydb> CREATE TABLE movies (id INTEGER PRIMARY KEY, title VARCHAR NOT NULL);\ntoydb> INSERT INTO movies VALUES (1, 'Sicario'), (2, 'Stalker'), (3, 'Her');\ntoydb> SELECT * FROM movies;\n1, 'Sicario'\n2, 'Stalker'\n3, 'Her'\n```\n\ntoyDB supports most common SQL features, including joins, aggregates, and transactions. Below is an\n`EXPLAIN` query plan of a more complex query (fetches all movies from studios that have released any\nmovie with an IMDb rating of 8 or more):\n\n```\ntoydb> EXPLAIN SELECT m.title, g.name AS genre, s.name AS studio, m.rating\n  FROM movies m JOIN genres g ON m.genre_id = g.id,\n    studios s JOIN movies good ON good.studio_id = s.id AND good.rating >= 8\n  WHERE m.studio_id = s.id\n  GROUP BY m.title, g.name, s.name, m.rating, m.released\n  ORDER BY m.rating DESC, m.released ASC, m.title ASC;\n\nRemap: m.title, genre, studio, m.rating (dropped: m.released)\n└─ Order: m.rating desc, m.released asc, m.title asc\n   └─ Projection: m.title, g.name as genre, s.name as studio, m.rating, m.released\n      └─ Aggregate: m.title, g.name, s.name, m.rating, m.released\n         └─ HashJoin: inner on m.studio_id = s.id\n            ├─ HashJoin: inner on m.genre_id = g.id\n            │  ├─ Scan: movies as m\n            │  └─ Scan: genres as g\n            └─ HashJoin: inner on s.id = good.studio_id\n               ├─ Scan: studios as s\n               └─ Scan: movies as good (good.rating > 8 OR good.rating = 8)\n```\n\n## Architecture\n\ntoyDB's architecture is fairly typical for a distributed SQL database: a transactional\nkey/value store managed by a Raft cluster with a SQL query engine on top. See the\n[architecture guide](./docs/architecture/index.md) for more details.\n\n[![toyDB architecture](./docs/architecture/images/architecture.svg)](./docs/architecture/index.md)\n\n## Tests\n\ntoyDB mainly uses [Goldenscripts](https://github.com/erikgrinaker/goldenscript) for tests. These \nscript various scenarios, capture events and output, and later assert that the behavior remains the \nsame. See e.g.:\n\n* [Raft cluster tests](https://github.com/erikgrinaker/toydb/tree/main/src/raft/testscripts/node)\n* [MVCC transaction tests](https://github.com/erikgrinaker/toydb/tree/main/src/storage/testscripts/mvcc)\n* [SQL execution tests](https://github.com/erikgrinaker/toydb/tree/main/src/sql/testscripts)\n* [End-to-end tests](https://github.com/erikgrinaker/toydb/tree/main/tests/scripts)\n\nRun tests with `cargo test`, or have a look at the latest \n[CI run](https://github.com/erikgrinaker/toydb/actions/workflows/ci.yml).\n\n## Benchmarks\n\ntoyDB is not optimized for performance, but comes with a `workload` benchmark tool that can run \nvarious workloads against a toyDB cluster. For example:\n\n```sh\n# Start a 5-node toyDB cluster.\n$ ./cluster/run.sh\n[...]\n\n# Run a read-only benchmark via all 5 nodes.\n$ cargo run --release --bin workload read\nPreparing initial dataset... done (0.179s)\nSpawning 16 workers... done (0.006s)\nRunning workload read (rows=1000 size=64 batch=1)...\n\nTime   Progress     Txns      Rate       p50       p90       p99      pMax\n1.0s      13.1%    13085   13020/s     1.3ms     1.5ms     1.9ms     8.4ms\n2.0s      27.2%    27183   13524/s     1.3ms     1.5ms     1.8ms     8.4ms\n3.0s      41.3%    41301   13702/s     1.2ms     1.5ms     1.8ms     8.4ms\n4.0s      55.3%    55340   13769/s     1.2ms     1.5ms     1.8ms     8.4ms\n5.0s      70.0%    70015   13936/s     1.2ms     1.5ms     1.8ms     8.4ms\n6.0s      84.7%    84663   14047/s     1.2ms     1.4ms     1.8ms     8.4ms\n7.0s      99.6%    99571   14166/s     1.2ms     1.4ms     1.7ms     8.4ms\n7.1s     100.0%   100000   14163/s     1.2ms     1.4ms     1.7ms     8.4ms\n\nVerifying dataset... done (0.002s)\n```\n\nThe available workloads are:\n\n* `read`: single-row primary key lookups.\n* `write`: single-row inserts to sequential primary keys.\n* `bank`: bank transfers between various customers and accounts. To make things interesting, this\n  includes joins, secondary indexes, sorting, and conflicts.\n\nFor more information about workloads and parameters, run `cargo run --bin workload -- --help`.\n\nExample workload results are listed below. Write performance is atrocious, due to\n[fsync](https://en.wikipedia.org/wiki/Sync_(Unix)) and a lack of write batching in the Raft layer.\nDisabling fsync, or using the in-memory engine, significantly improves write performance (at the\nexpense of durability).\n\n| Workload | BitCask     | BitCask w/o fsync | Memory      |\n|----------|-------------|-------------------|-------------|\n| `read`   | 14163 txn/s | 13941 txn/s       | 13949 txn/s |\n| `write`  | 35 txn/s    | 4719 txn/s        | 7781 txn/s  |\n| `bank`   | 21 txn/s    | 1120 txn/s        | 1346 txn/s  |\n\n## Debugging\n\n[VSCode](https://code.visualstudio.com) and the [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb)\nextension can be used to debug toyDB, with the debug configuration under `.vscode/launch.json`.\n\nUnder the \"Run and Debug\" tab, select e.g. \"Debug executable 'toydb'\" or \"Debug unit tests in\nlibrary 'toydb'\".\n\n## Credits\n\nThe toyDB logo is courtesy of [@jonasmerlin](https://github.com/jonasmerlin)."
  },
  {
    "path": "cluster/run.sh",
    "content": "#!/usr/bin/env bash\n#\n# This script builds and runs a 5-node toyDB cluster listening on ports\n# 9601-9605. Config and data is stored under the toydb* directories.\n# To connect a toysql client to node 1 on port 9601, run:\n#\n# cargo run --release --bin toysql\n\nset -euo pipefail\n\n# Change into the script directory.\ncd \"$(dirname $0)\"\n\n# Build toyDB using release optimizations.\ncargo build --release --bin toydb\n\n# Start nodes 1-5 in the background, prefixing their output with the node ID.\necho \"Starting 5 nodes on ports 9601-9605 with data under cluster/*/data/.\"\necho \"To connect to node 1, run: cargo run --release --bin toysql\"\necho \"\"\n\nfor ID in 1 2 3 4 5; do\n    (cargo run -q --release -- -c toydb$ID/toydb.yaml 2>&1 | sed -e \"s/\\\\(.*\\\\)/toydb$ID \\\\1/g\") &\ndone\n\n# Wait for the background processes to exit. Kill all toyDB processes when the\n# script exits (e.g. via Ctrl-C).\ntrap 'kill -TERM -- -$$ 2>/dev/null' INT TERM EXIT\nwait"
  },
  {
    "path": "cluster/toydb1/toydb.yaml",
    "content": "id: 1\ndata_dir: toydb1/data\nlisten_sql: localhost:9601\nlisten_raft: localhost:9701\npeers:\n  '2': localhost:9702\n  '3': localhost:9703\n  '4': localhost:9704\n  '5': localhost:9705"
  },
  {
    "path": "cluster/toydb2/toydb.yaml",
    "content": "id: 2\ndata_dir: toydb2/data\nlisten_sql: localhost:9602\nlisten_raft: localhost:9702\npeers:\n  '1': localhost:9701\n  '3': localhost:9703\n  '4': localhost:9704\n  '5': localhost:9705"
  },
  {
    "path": "cluster/toydb3/toydb.yaml",
    "content": "id: 3\ndata_dir: toydb3/data\nlisten_sql: localhost:9603\nlisten_raft: localhost:9703\npeers:\n  '1': localhost:9701\n  '2': localhost:9702\n  '4': localhost:9704\n  '5': localhost:9705"
  },
  {
    "path": "cluster/toydb4/toydb.yaml",
    "content": "id: 4\ndata_dir: toydb4/data\nlisten_sql: localhost:9604\nlisten_raft: localhost:9704\npeers:\n  '1': localhost:9701\n  '2': localhost:9702\n  '3': localhost:9703\n  '5': localhost:9705"
  },
  {
    "path": "cluster/toydb5/toydb.yaml",
    "content": "id: 5\ndata_dir: toydb5/data\nlisten_sql: localhost:9605\nlisten_raft: localhost:9705\npeers:\n  '1': localhost:9701\n  '2': localhost:9702\n  '3': localhost:9703\n  '4': localhost:9704"
  },
  {
    "path": "config/toydb.yaml",
    "content": "# The node ID (must be unique in the cluster), and map of peer IDs and Raft\n# addresses (empty for single node).\nid: 1\npeers: {}\n\n# Addresses to listen for SQL and Raft connections on.\nlisten_sql: localhost:9601\nlisten_raft: localhost:9701\n\n# The log level. Valid values are DEBUG, INFO, WARN, and ERROR.\nlog_level: INFO\n\n# Node data directory. The Raft log is stored in the file \"raft\", and the SQL\n# database in \"sql\".\ndata_dir: data\n\n# Storage engine to use for the Raft log and SQL database.\n#\n# * bitcask (default): an append-only log-structured store.\n# * memory: an in-memory store using the Rust standard library's BTreeMap.\nstorage_raft: bitcask\nstorage_sql: bitcask\n\n# Whether to fsync writes to disk. Disabling this yields much better write\n# performance, but may lose data on host crashes and violate Raft guarantees. It\n# only affects Raft log writes (the SQL state machine is never fsynced since it\n# can be reconstructed from the Raft log).\nfsync: true\n\n# The minimum garbage fraction and bytes to trigger Bitcask log compaction on\n# node startup.\ncompact_threshold: 0.2\ncompact_min_bytes: 1000000"
  },
  {
    "path": "docs/architecture/README.md",
    "content": "See [`index.md`](index.md)."
  },
  {
    "path": "docs/architecture/client.md",
    "content": "# Client\n\nThe toyDB client is in the [`client`](https://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/client.rs)\nmodule. It uses the same Bincode-based protocol that we saw in the server section, sending\n`toydb::Request` and receiving `toydb::Response`.\n\n## Client Library\n\nThe main client library `toydb::Client` is used to communicate with a toyDB server:\n\nhttps://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/client.rs#L15-L24\n\nWhen initialized, it connects to a toyDB server over TCP, which establishes a SQL session for it:\n\nhttps://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/client.rs#L27-L33\n\nIt can then send Bincode-encoded `toydb::Request` to the server, and receive `toydb::Response`\nback.\n\nhttps://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/client.rs#L35-L40\n\n\nIn particular, `Client::execute` can be used to execute arbitrary SQL statements in the client's\ncurrent session:\n\nhttps://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/client.rs#L42-L56\n\n## `toysql` Binary\n\nHowever, `toydb::Client` is a programmatic API, and we want a more convenient user interface.\nThe `toysql` client in [`src/bin/toysql.rs`](https://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/bin/toysql.rs)\nprovides a typical [REPL](https://en.wikipedia.org/wiki/Read–eval–print_loop) (read-evaluate-print loop) where users can enter SQL statements and view the results.\n\nLike `toydb`, `toysql` is a tiny [`clap`](https://docs.rs/clap/latest/clap/) command that takes a\ntoyDB server address to connect to and starts an interactive shell:\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/bin/toysql.rs#L29-L53\n\nIt first attempts to connect to the toyDB server using the `toydb::Client` client, and then starts\nan interactive shell using the [Rustyline](https://docs.rs/rustyline/latest/rustyline/) library.\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/bin/toysql.rs#L55-L81\n\nThe shell is simply a loop that prompts the user to input a SQL statement:\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/bin/toysql.rs#L216-L250\n\nEach statement is the executed against the server via `toydb::Client::execute`, and the response\nis formatted and printed as output:\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/bin/toysql.rs#L83-L92\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/bin/toysql.rs#L175-L204\n\nAnd with that, we have a fully functional SQL database system and can run queries to our heart's\ncontent. Have fun!\n\n---\n\n<p align=\"center\">\n← <a href=\"server.md\">Server</a>\n</p>"
  },
  {
    "path": "docs/architecture/encoding.md",
    "content": "# Key/Value Encoding\n\nThe key/value store uses binary `Vec<u8>` keys and values, so we need an encoding scheme to \ntranslate between in-memory Rust data structures and the on-disk binary data. This is provided by\nthe [`encoding`](https://github.com/erikgrinaker/toydb/tree/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/encoding)\nmodule, with separate schemes for key and value encoding.\n\n## `Bincode` Value Encoding\n\nValues are encoded using [Bincode](https://github.com/bincode-org/bincode), a third-party binary\nencoding scheme for Rust. Bincode is convenient because it can easily encode any arbitrary Rust\ndata type. But we could also have chosen e.g. [JSON](https://en.wikipedia.org/wiki/JSON),\n[Protobuf](https://protobuf.dev), [MessagePack](https://msgpack.org/), or any other encoding.\n\nWe won't dwell on the actual binary format here, see the [Bincode specification](https://git.sr.ht/~stygianentity/bincode/tree/trunk/item/docs/spec.md)\nfor details.\n\nTo use a consistent configuration for all encoding and decoding, we provide helper functions in\nthe [`encoding::bincode`](https://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/encoding/bincode.rs)\nmodule which use `bincode::config::standard()`.\n\nhttps://github.com/erikgrinaker/toydb/blob/0ce1fb34349fda043cb9905135f103bceb4395b4/src/encoding/bincode.rs#L15-L27\n\nBincode uses the very common [Serde](https://serde.rs) framework for its API. toyDB also provides an\n`encoding::Value` helper trait for value types which adds automatic `encode()` and `decode()`\nmethods:\n\nhttps://github.com/erikgrinaker/toydb/blob/b57ae6502e93ea06df00d94946a7304b7d60b977/src/encoding/mod.rs#L39-L68\n\nHere's an example of how this can be used to encode and decode an arbitrary `Dog` data type:\n\n```rust\n#[derive(serde::Serialize, serde::Deserialize)]\nstruct Dog {\n    name: String,\n    age: u8,\n    good_boy: bool,\n}\n\nimpl encoding::Value for Dog {}\n\nlet pluto = Dog { name: \"Pluto\".into(), age: 4, good_boy: true };\nlet bytes = pluto.encode();\nprintln!(\"{bytes:02x?}\");\n\n// Outputs [05, 50, 6c, 75, 74, 6f, 04, 01]:\n//\n// * Length of string \"Pluto\": 05.\n// * String \"Pluto\": 50 6c 75 74 6f.\n// * Age 4: 04.\n// * Good boy: 01 (true).\n\nlet pluto = Dog::decode(&bytes)?; // gives us back Pluto\n```\n\n## `Keycode` Key Encoding\n\nUnlike values, keys can't just use any binary encoding like Bincode. As mentioned in the storage\nsection, the storage engine sorts data by key to enable range scans. The key encoding must therefore\npreserve the [lexicographical order](https://en.wikipedia.org/wiki/Lexicographic_order) of the\nencoded values: the binary byte slices must sort in the same order as the original values.\n\nAs an example of why we can't just use Bincode, consider the strings \"house\" and \"key\". These should\nbe sorted in alphabetical order: \"house\" before \"key\". However, Bincode encodes strings prefixed by\ntheir length, so \"key\" would be sorted before \"house\" in binary form:\n\n```\n03 6b 65 79        ← 3 bytes: key\n05 68 6f 75 73 65  ← 5 bytes: house\n```\n\nFor similar reasons, we can't just encode numbers in their native binary form: the\n[little-endian](https://en.wikipedia.org/wiki/Endianness) representation will order very large\nnumbers before small numbers, and the [sign bit](https://en.wikipedia.org/wiki/Sign_bit) will order\npositive numbers before negative numbers. This would violate the ordering of natural numbers.\n\nWe also have to be careful with value sequences, which should be ordered element-wise. For example,\nthe pair (\"a\", \"xyz\") should be ordered before (\"ab\", \"cd\"), so we can't just encode the strings\none after the other like \"axyz\" and \"abcd\" since that would sort (\"ab\", \"cd\") first.\n\ntoyDB provides an order-preserving encoding called \"Keycode\" in the [`encoding::keycode`](https://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/encoding/keycode.rs)\nmodule. Like Bincode, the Keycode encoding is not self-describing: the binary data does not say what\nthe data type is, the caller must provide a type to decode into. It only supports a handful of\nprimitive data types, and only needs to order values of the same type.\n\nKeycode is implemented as a [Serde](https://serde.rs) (de)serializer, which requires a lot of\nboilerplate code to satisfy the trait, but we'll just focus on the actual encoding. The encoding\nscheme is as follows:\n\n* `bool`: `00` for `false` and `01` for `true`.\n\n    https://github.com/erikgrinaker/toydb/blob/2027641004989355c2162bbd9eeefcc991d6b29b/src/encoding/keycode.rs#L113-L117\n\n* `u64`: the [big-endian](https://en.wikipedia.org/wiki/Endianness) binary encoding.\n\n    https://github.com/erikgrinaker/toydb/blob/2027641004989355c2162bbd9eeefcc991d6b29b/src/encoding/keycode.rs#L157-L161\n\n* `i64`: the [big-endian](https://en.wikipedia.org/wiki/Endianness) binary encoding, but with the\n   sign bit flipped to order negative numbers before positive ones.\n\n    https://github.com/erikgrinaker/toydb/blob/2027641004989355c2162bbd9eeefcc991d6b29b/src/encoding/keycode.rs#L131-L143\n\n* `f64`: the [big-endian IEEE 754](https://en.wikipedia.org/wiki/Double-precision_floating-point_format)\n  binary encoding, but with the sign bit flipped, and all bits flipped for negative numbers, to\n  order negative numbers correctly.\n\n    https://github.com/erikgrinaker/toydb/blob/2027641004989355c2162bbd9eeefcc991d6b29b/src/encoding/keycode.rs#L167-L179\n\n* `Vec<u8>`: terminated by `00 00`, with `00` escaped as `00 ff` to disambiguate it.\n\n    https://github.com/erikgrinaker/toydb/blob/2027641004989355c2162bbd9eeefcc991d6b29b/src/encoding/keycode.rs#L190-L205\n\n* `String`: like `Vec<u8>`.\n\n    https://github.com/erikgrinaker/toydb/blob/2027641004989355c2162bbd9eeefcc991d6b29b/src/encoding/keycode.rs#L185-L188\n\n* `Vec<T>`, `[T]`, `(T,)`: the concatenation of the inner values.\n\n    https://github.com/erikgrinaker/toydb/blob/2027641004989355c2162bbd9eeefcc991d6b29b/src/encoding/keycode.rs#L295-L307\n\n* `enum`: the variant's numerical index as a `u8`, then the inner values (if any).\n\n    https://github.com/erikgrinaker/toydb/blob/2027641004989355c2162bbd9eeefcc991d6b29b/src/encoding/keycode.rs#L223-L227\n\nLike `encoding::Value`, there is also an `encoding::Key` helper trait:\n\nhttps://github.com/erikgrinaker/toydb/blob/b57ae6502e93ea06df00d94946a7304b7d60b977/src/encoding/mod.rs#L20-L37\n\nDifferent kinds of keys are usually represented as enums. For example, if we wanted to store cars\nand video games, we could use:\n\n```rust\n#[derive(serde::Serialize, serde::Deserialize)]\nenum Key {\n    Car(String, String, u64),    // make, model, year\n    Game(String, u64, Platform), // name, year, platform\n}\n\n#[derive(serde::Serialize, serde::Deserialize)]\nenum Platform {\n    PC,\n    PS5,\n    Switch,\n    Xbox,\n}\n\nimpl encoding::Key for Key {}\n\nlet returnal = Key::Game(\"Returnal\".into(), 2021, Platform::PS5);\nlet bytes = returnal.encode();\nprintln!(\"{bytes:02x?}\");\n\n// Outputs [01, 52, 65, 74, 75, 72, 6e, 61, 6c, 00, 00, 00, 00, 00, 00, 00, 00, 07, e5, 01].\n//\n// * Key::Game: 01\n// * Returnal: 52 65 74 75 72 6e 61 6c 00 00\n// * 2021: 00 00 00 00 00 00 07 e5\n// * Platform::PS5: 01\n\nlet returnal = Key::decode(&bytes)?;\n```\n\nBecause the keys are sorted in element-wise order, this would allow us to e.g. perform a prefix\nscan to fetch all platforms which Returnal (2021) was released on, or perform a range scan to fetch \nall models of Nissan Altima released between 2010 and 2015.\n\n---\n\n<p align=\"center\">\n← <a href=\"storage.md\">Storage Engine</a> &nbsp; | &nbsp; <a href=\"mvcc.md\">MVCC Transactions</a> →\n</p>"
  },
  {
    "path": "docs/architecture/index.md",
    "content": "# toyDB Architecture\n\ntoyDB is a simple distributed SQL database, intended to illustrate how such systems are built. The\noverall structure is similar to real-world distributed databases, but the design and implementation\nhas been kept as simple as possible for understandability. Performance and scalability are explicit\nnon-goals, as these are major sources of complexity in real-world systems.\n\nThis guide will walk through toyDB's architecture and code from the bottom up, with plenty of links\nto the actual source code.\n\n> ℹ️ View on GitHub with a desktop browser for inline code listings.\n\n* [Overview](overview.md)\n  * [Properties](overview.md#properties)\n  * [Components](overview.md#components)\n* [Storage Engine](storage.md)\n  * [`Memory` Storage Engine](storage.md#memory-storage-engine)\n  * [`BitCask` Storage Engine](storage.md#bitcask-storage-engine)\n* [Key/Value Encoding](encoding.md)\n  * [`Bincode` Value Encoding](encoding.md#bincode-value-encoding)\n  * [`Keycode` Key Encoding](encoding.md#keycode-key-encoding)\n* [MVCC Transactions](mvcc.md)\n* [Raft Consensus](raft.md)\n  * [Log Storage](raft.md#log-storage)\n  * [State Machine Interface](raft.md#state-machine-interface)\n  * [Node Roles](raft.md#node-roles)\n  * [Node Interface and Communication](raft.md#node-interface-and-communication)\n  * [Leader Election and Terms](raft.md#leader-election-and-terms)\n  * [Client Requests and Forwarding](raft.md#client-requests-and-forwarding)\n  * [Write Replication and Application](raft.md#write-replication-and-application)\n  * [Read Processing](raft.md#read-processing)\n* [SQL Engine](sql.md)\n  * [Data Model](sql-data.md)\n    * [Data Types](sql-data.md#data-types)\n    * [Schemas](sql-data.md#schemas)\n    * [Expressions](sql-data.md#expressions)\n  * [Storage](sql-storage.md)\n    * [Key/Value Representation](sql-storage.md#keyvalue-representation)\n    * [Schema Catalog](sql-storage.md#schema-catalog)\n    * [Row Storage and Transactions](sql-storage.md#row-storage-and-transactions)\n  * [Raft Replication](sql-raft.md)\n  * [Parsing](sql-parser.md)\n    * [Lexer](sql-parser.md#lexer)\n    * [Abstract Syntax Tree](sql-parser.md#abstract-syntax-tree)\n    * [Parser](sql-parser.md#parser)\n  * [Planning](sql-planner.md)\n    * [Execution Plan](sql-planner.md#execution-plan)\n    * [Scope and Name Resolution](sql-planner.md#scope-and-name-resolution)\n    * [Planner](sql-planner.md#planner)\n  * [Optimization](sql-optimizer.md)\n    * [Constant Folding](sql-optimizer.md#constant-folding)\n    * [Filter Pushdown](sql-optimizer.md#filter-pushdown)\n    * [Index Lookups](sql-optimizer.md#index-lookups)\n    * [Hash Join](sql-optimizer.md#hash-join)\n    * [Short Circuiting](sql-optimizer.md#short-circuiting)\n  * [Execution](sql-execution.md)\n    * [Plan Executor](sql-execution.md#plan-executor)\n    * [Session Management](sql-execution.md#session-management)\n* [Server](server.md)\n  * [Raft Routing](server.md#raft-routing)\n  * [SQL Service](server.md#sql-service)\n  * [`toydb` Binary](server.md#toydb-binary)\n* [Client](client.md)\n  * [Client Library](client.md#client-library)\n  * [`toysql` Binary](client.md#toysql-binary)\n\n---\n\n<p align=\"center\">\n<a href=\"overview.md\">Overview</a> →\n</p>"
  },
  {
    "path": "docs/architecture/mvcc.md",
    "content": "# MVCC Transactions\n\nTransactions are groups of reads and writes (e.g. to different keys) that are submitted together as\na single unit. For example, a bank transaction that transfers $100 from account A to account B might\nconsist of this group of reads and writes:\n\n```\na = get(A)\nb = get(B)\nif a < 100:\n    error(\"insufficient balance\")\nset(A, a - 100)\nset(B, b + 100)\n```\n\ntoyDB provides [ACID](https://en.wikipedia.org/wiki/ACID) transactions, a set of very strong\nguarantees:\n\n* **Atomicity:** all of the writes take effect as an single, atomic unit, at the same instant, when\n  they are _committed_. Other users will never see some of the writes without the others.\n\n* **Consistency:** database constraints are never violated (e.g. referential integrity or uniqueness\n  contraints). We'll see how this is implemented later in the SQL execution layer.\n\n* **Isolation:** users should appear to have the entire database to themselves, unaffected by other\n  simultaneous users. Two transactions may conflict, in which case one has to retry, but if a\n  transaction succeeds then the user knows with certainty that the operations were executed without\n  interference by anyone else. This eliminates the risk of [race conditions](https://en.wikipedia.org/wiki/Race_condition).\n  \n* **Durability:** committed writes are never lost (even if the system crashes).\n\nTo illustrate how transactions work, here's an example MVCC test script where two concurrent users\nmodify a set of bank accounts (there's many [other test scripts](https://github.com/erikgrinaker/toydb/tree/aa14deb71f650249ce1cab8828ed7bcae2c9206e/src/storage/testscripts/mvcc)\nthere too):\n\nhttps://github.com/erikgrinaker/toydb/blob/a73e24b7e77671b9f466e0146323cd69c3e27bdf/src/storage/testscripts/mvcc/bank#L1-L69\n\nTo provide these guarantees, toyDB uses a common technique called\n[Multi-Version Concurrency Control](https://en.wikipedia.org/wiki/Multiversion_concurrency_control)\n(MVCC). It is implemented at the key/value storage level, in the [`storage::mvcc`](https://github.com/erikgrinaker/toydb/blob/8f8eae0dcf70b1a0df2e853b1f6600e0c7075340/src/storage/mvcc.rs)\nmodule. It uses a `storage::Engine` for actual data storage.\n\nhttps://github.com/erikgrinaker/toydb/blob/8f8eae0dcf70b1a0df2e853b1f6600e0c7075340/src/storage/mvcc.rs#L220-L231\n\nMVCC provides an [isolation level](https://en.wikipedia.org/wiki/Isolation_(database_systems)#Isolation_levels)\ncalled [snapshot isolation](https://en.wikipedia.org/wiki/Snapshot_isolation): a transaction sees a\nsnapshot of the database as it was when the transaction began. Any later changes are invisible to\nit.\n\nIt does this by storing historical versions of key/value pairs. The version number is simply a\nnumber that's incremented for every new transaction:\n\nhttps://github.com/erikgrinaker/toydb/blob/8f8eae0dcf70b1a0df2e853b1f6600e0c7075340/src/storage/mvcc.rs#L155-L158\n\nEach transaction has its own unique version number. When it writes a key/value pair it appends its\nversion number to the key as `Key::Version(&[u8], Version)` (using the Keycode encoding we've seen\npreviously). If an old version of the key already exists, it will have a different version number\nsuffix and therefore be stored as a separate key in the storage engine. Deleted keys are versions\nwith a special tombstone value.\n\nhttps://github.com/erikgrinaker/toydb/blob/8f8eae0dcf70b1a0df2e853b1f6600e0c7075340/src/storage/mvcc.rs#L183-L189\n\nHere's a simple diagram of what a history of versions 1 to 5 of keys `a` to `d` might look like:\n\nhttps://github.com/erikgrinaker/toydb/blob/8f8eae0dcf70b1a0df2e853b1f6600e0c7075340/src/storage/mvcc.rs#L11-L26\n\nAdditionally, we need to keep track of the currently ongoing (uncommitted) transaction versions,\nknown as the \"active set\".\n\nWith versioning and the active set, we can summarize the MVCC protocol with a few simple rules:\n\n1. When a new transaction begins, it:\n    * Obtains the next available version number.\n    * Takes a snapshot of the active set (other uncommitted transactions).\n    * Adds its version number to the active set.\n\n2. When the transaction reads a key, it:\n    * Returns the latest version of the key at or below its own version.\n    * Ignores versions above its own version.\n    * Ignores versions in its active set snapshot.\n\n3. When the transaction writes a key, it:\n    * Looks for a key version above its own version; errors if found.\n    * Looks for a key version in its active set snapshot; errors if found.\n    * Writes a key/value pair with its own version.\n\n4. When the transaction commits, it:\n    * Flushes all writes to disk.\n    * Removes itself from the active set.\n\nThe magic happens when the transaction removes itself from the active set. This is a single, atomic\noperation, and when it completes all of its writes immediately become visible to _new_ transactions.\nHowever, ongoing transactions still won't see these writes, because the version is still in their\nactive set snapshot or at a later version (hence they are isolated from this transaction).\n\nFurthermore, the transaction could see its own uncommitted writes even though noone else could, and\nif any writes conflicted with another transaction it would error out and have to retry.\n\nNot only that, this also allows us to do time-travel queries, where we can query the database as it\nwas at any time in the past: we simply pick a version number to read at.\n\nThere are a few more details that we've left out here: transaction rollbacks need to keep track of\nthe writes and undo them, and read-only queries can avoid allocating new version numbers. We also\ndon't garbage collect old version, for simplicity. See the module documentation for more details:\n\nhttps://github.com/erikgrinaker/toydb/blob/8f8eae0dcf70b1a0df2e853b1f6600e0c7075340/src/storage/mvcc.rs#L1-L140\n\nLet's walk through a simple example with code pointers to get a feel for how this is implemented.\nNotice how we don't have to deal with any version numbers when we're using the MVCC API -- this is\nan internal MVCC implementation detail.\n\n```rust\n// Open a BitCask database in the file \"toy.db\" with MVCC support.\nlet path = PathBuf::from(\"toy.db\");\nlet db = MVCC::new(BitCask::new(path)?);\n\n// Begin a new transaction.\nlet txn = db.begin()?;\n\n// Read the key \"foo\", and decode the binary value as a u64 with bincode.\nlet bytes = txn.get(b\"foo\")?.expect(\"foo not found\");\nlet mut value: u64 = bincode::deserialize(&bytes)?;\n\n// Delete \"foo\".\ntxn.delete(b\"foo\")?;\n\n// Add 1 to the value, and write it back to the key \"bar\".\nvalue += 1;\nlet bytes = bincode::serialize(&value);\ntxn.set(b\"bar\", bytes)?;\n\n// Commit the transaction.\ntxn.commit()?;\n```\n\nFirst, we begin a new transaction with `MVCC::begin()`, which calls through to\n`Transaction::begin()`. This obtains a version number stored in `Key::NextVersion` and increments\nit, then takes a snapshot of the active set in `Key::ActiveSet` and adds itself to it:\n\nhttps://github.com/erikgrinaker/toydb/blob/8f8eae0dcf70b1a0df2e853b1f6600e0c7075340/src/storage/mvcc.rs#L368-L391\n\nThis returns a `Transaction` object which provides the main key/value API, with get/set/delete\nmethods. It keeps track of the main state of the transaction: it's version number and active set.\n\nhttps://github.com/erikgrinaker/toydb/blob/8f8eae0dcf70b1a0df2e853b1f6600e0c7075340/src/storage/mvcc.rs#L294-L327\n\nNext, we call `Transaction::get(b\"foo\")` to read the value of the key `foo`. This finds the latest\nversion that's visible to us (ignoring future versions and the active set). Recall that we store\nmultiple version of each key as `Key::Version(key, version)`. The Keycode encoding ensures that all\nversions are stored in sorted order, so we can do a reverse range scan from `Key::Version(b\"foo\",\nself.version)` to  `Key::Version(b\"foo\", 0)` and return the latest version that's visible to us:\n\nhttps://github.com/erikgrinaker/toydb/blob/8f8eae0dcf70b1a0df2e853b1f6600e0c7075340/src/storage/mvcc.rs#L564-L581\n\nWe then call `Transaction::delete(b\"foo\")` and `Transaction::set(b\"bar\", value)`. Both of these just\ncall through to the same `Transaction::write_version()` method, but use `Some(value)` for a regular\nkey/value pair and `None` as a deletion tombstone:\n\nhttps://github.com/erikgrinaker/toydb/blob/8f8eae0dcf70b1a0df2e853b1f6600e0c7075340/src/storage/mvcc.rs#L514-L522\n\nTo write a new version of a key, we first have to check for conflicts by seeing if there's a\nversion of the key that's invisible to us -- if it is, we conflicted with a concurrent transaction.\nWe use a range scan for this, like we did in `Transaction::get()`.\n\nIf there are no conflicts, we go on to write `Key::Version(b\"foo\", self.version)` and encode the\nvalue as an `Option<value>` to accomodate the `None` tombstone marker. We also write a\n`Key::TxnWrite(version, key)` to keep track of the keys we've written in case we have to roll back.\n\nhttps://github.com/erikgrinaker/toydb/blob/8f8eae0dcf70b1a0df2e853b1f6600e0c7075340/src/storage/mvcc.rs#L524-L562\n\nFinally, `Transaction::commit()` will make our transaction take effect and become visible. It does\nthis simply by removing itself from the active set in `Key::ActiveSet`, and also cleaning up its\n`Key::TxnWrite` write tracking. As the comment says, we don't actually have to flush to durable\nstorage here, because the Raft log will provide durability for us -- we'll get back to this later.\n\nhttps://github.com/erikgrinaker/toydb/blob/8f8eae0dcf70b1a0df2e853b1f6600e0c7075340/src/storage/mvcc.rs#L466-L485\n\n---\n\n<p align=\"center\">\n← <a href=\"encoding.md\">Key/Value Encoding</a> &nbsp; | &nbsp; <a href=\"raft.md\">Raft Consensus</a> →\n</p>"
  },
  {
    "path": "docs/architecture/overview.md",
    "content": "# Overview\n\ntoyDB consists of a cluster of nodes that execute [SQL](https://en.wikipedia.org/wiki/SQL)\ntransactions against a replicated state machine. Clients can connect to any node in the cluster and\nsubmit SQL statements. The cluster remains available if a minority of nodes crash or disconnect,\nbut halts if a majority of nodes fail.\n\n## Properties\n\n* **Distributed:** runs across a cluster of nodes.\n* **Highly available:** tolerates failure of a minority of nodes.\n* **SQL compliant:** correctly supports most common [SQL](https://en.wikipedia.org/wiki/SQL)\n  features.\n* **Strongly consistent:** committed writes are immediately visible to all readers ([linearizability](https://en.wikipedia.org/wiki/Linearizability)).\n* **Transactional:** provides [ACID](https://en.wikipedia.org/wiki/ACID) transactions\n  * **Atomic:** groups of writes are applied as a single, atomic unit.\n  * **Consistent:** database constraints and referential integrity are always enforced.\n  * **Isolated:** concurrent transactions don't affect each other ([snapshot isolation](https://en.wikipedia.org/wiki/Snapshot_isolation)).\n  * **Durable:** committed writes are never lost.\n\nFor simplicity, toyDB is:\n\n* **Not scalable:** every node stores the full dataset, and reads/writes execute on one node.\n* **Not reliable:** only handles crash failures, not e.g. partial network partitions or node stalls.\n* **Not performant:** data processing is slow, and not optimized at all.\n* **Not efficient:** loads entire tables into memory, no compression or garbage collection, etc.\n* **Not full-featured:** only basic SQL functionality is implemented.\n* **Not backwards compatible:** changes to data formats and protocols will break databases.\n* **Not flexible:** nodes can't be added or removed while running, and take a long time to join.\n* **Not secure:** there is no authentication, authorization, nor encryption.\n\n## Components\n\nInternally, toyDB is made up of a few main components:\n\n* **Storage engine:** stores data on disk and manages transactions.\n* **Raft consensus engine:** replicates data and coordinates cluster nodes.\n* **SQL engine:** organizes SQL data, manages SQL sessions, and executes SQL statements.\n* **Server:** manages network communication, both with SQL clients and Raft nodes.\n* **Client:** provides a SQL user interface and communicates with the server.\n\nThis diagram illustrates the internal structure of a single toyDB node:\n\n![toyDB architecture](./images/architecture.svg)\n\nWe will go through each of these components from the bottom up.\n\n---\n\n<p align=\"center\">\n← <a href=\"index.md\">toyDB Architecture</a> &nbsp; | &nbsp; <a href=\"storage.md\">Storage Engine</a> →\n</p>"
  },
  {
    "path": "docs/architecture/raft.md",
    "content": "# Raft Consensus\n\n[Raft](https://raft.github.io) is a distributed consensus protocol which replicates data across a\ncluster of nodes in a consistent and durable manner. It is described in the very readable\n[Raft paper](https://raft.github.io/raft.pdf), and in the more comprehensive\n[Raft thesis](https://web.stanford.edu/~ouster/cgi-bin/papers/OngaroPhD.pdf).\n\nThe toyDB Raft implementation is in the [`raft`](https://github.com/erikgrinaker/toydb/tree/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/raft)\nmodule, and is described in the module documentation:\n\nhttps://github.com/erikgrinaker/toydb/blob/d96c6dd5ae7c0af55ee609760dcd958c289a44f2/src/raft/mod.rs#L1-L240\n\nRaft is fundamentally the same protocol as [Paxos](https://lamport.azurewebsites.net/pubs/paxos-simple.pdf)\nand [Viewstamped Replication](https://pmg.csail.mit.edu/papers/vr-revisited.pdf), but an\nopinionated variant designed to be simple, understandable, and practical. It is widely used in the\nindustry: [CockroachDB](https://www.cockroachlabs.com), [TiDB](https://www.pingcap.com),\n[etcd](https://etcd.io), [Consul](https://developer.hashicorp.com/consul), and many others.\n\nBriefly, Raft elects a leader node which coordinates writes and replicates them to followers. Once a\nmajority (>50%) of nodes have acknowledged a write, it is considered durably committed. It is common\nfor the leader to also serve reads, since it always has the most recent data and is thus strongly\nconsistent.\n\nA cluster must have a majority of nodes (known as a [quorum](https://en.wikipedia.org/wiki/Quorum_(distributed_computing)))\nlive and connected to remain available, otherwise it will not commit writes in order to guarantee\ndata consistency and durability. Since there can only be one majority in the cluster, this prevents\na [split brain](https://en.wikipedia.org/wiki/Split-brain_(computing)) scenario where two active\nleaders can exist concurrently (e.g. during a [network partition](https://en.wikipedia.org/wiki/Network_partition))\nand store conflicting values.\n\nThe Raft leader appends writes to an ordered command log, which is then replicated to followers.\nOnce a majority has replicated the log up to a given entry, that log prefix is committed and then\napplied to a state machine. This ensures that all nodes will apply the same commands in the same\norder and eventually reach the same state (assuming the commands are deterministic). Raft itself\ndoesn't care what the state machine and commands are, but in toyDB's case it's SQL tables and rows\nstored in an MVCC key/value store.\n\nThis diagram from the Raft paper illustrates how a Raft node receives a command from a client (1),\nadds it to its log and reaches consensus with other nodes (2), then applies it to its state machine\n(3) before returning a result to the client (4):\n\n<img src=\"./images/raft.svg\" alt=\"Raft node\" width=\"400\" style=\"display: block; margin: 30px auto;\">\n\nYou may notice that Raft is not very scalable, since all reads and writes go via the leader node,\nand every node must store the entire dataset. Raft solves replication and availability, but not\nscalability. Real-world systems typically provide horizontal scalability by splitting a large\ndataset across many separate Raft clusters (i.e. sharding), but this is out of scope for toyDB.\n\nFor simplicitly, toyDB implements the bare minimum of Raft, and omits optimizations described in\nthe paper such as state snapshots, log truncation, leader leases, and more. The implementation is\nin the [`raft`](https://github.com/erikgrinaker/toydb/blob/d96c6dd5ae7c0af55ee609760dcd958c289a44f2/src/raft/mod.rs)\nmodule, and we'll walk through the main components next.\n\nThere is a comprehensive set of Raft test scripts in [`src/raft/testscripts/node`](https://github.com/erikgrinaker/toydb/blob/386153f5c00cb1a88b1ac8489ae132674d96f68a/src/raft/testscripts/node),\nwhich illustrate the protocol in a wide variety of scenarios.\n\n## Log Storage\n\nRaft replicates an ordered command log consisting of `raft::Entry`:\n\nhttps://github.com/erikgrinaker/toydb/blob/90a6cae47ac20481ac4eb2f20eea50f02e6c2b33/src/raft/log.rs#L10-L28\n\n`index` specifies the position in the log, and `command` contains the binary command to apply to the\nstate machine. The `term` identifies the leadership term in which the command was proposed: a new\nterm begins when a new leader election is held (we'll get back to this later).\n\nEntries are appended to the log by the leader and replicated to followers. Once acknowledged by a\nquorum, the log up to that index is committed and will never change. Entries that are not yet\ncommitted may be replaced or removed if the leader changes.\n\nThe Raft log enforces the following invariants:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/log.rs#L80-L91\n\n`raft::Log` implements a Raft log, and stores log entries in a `storage::Engine` key/value store:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/log.rs#L43-L116\n\nIt also stores some additional metadata that we'll need later: the current term, vote, and commit\nindex. These are stored as separate keys:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/log.rs#L30-L39\n\nIndividual entries are appended to the log via `Log::append`, typically when the leader wants to\nreplicate a new write:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/log.rs#L190-L203\n\nEntries can also be appended in bulk via `Log::splice`, typically when entries are replicated to\nfollowers. This also allows replacing existing uncommitted entries, e.g. after a leader change:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/log.rs#L269-L343\n\nCommitted entries are marked by `Log::commit`, making them immutable and eligible for state machine\napplication:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/log.rs#L205-L222\n\nThe log also has methods to read entries from the log, either individually as `Log::get` or by\niterating over a range with `Log::scan`:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/log.rs#L224-L267\n\n## State Machine Interface\n\nRaft doesn't know or care what the log commands are, nor what the state machine does with them. It\nsimply takes `raft::Entry` from the log and gives them to the state machine.\n\nThe Raft state machine is represented by the `raft::State` trait. Raft will ask about the last\napplied entry via `State::get_applied_index`, and feed it newly committed entries via\n`State::apply`. It also allows reads via `State::read`, but we'll get back to that later.\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/state.rs#L4-L51\n\nThe state machine does not have to flush its state to durable storage after each transition; on node\ncrashes, the state machine is allowed to regress, and will be caught up by replaying the unapplied\nlog entries. It is also possible to implement a purely in-memory state machine (and in fact, toyDB\nallows running the state machine with a `Memory` storage engine).\n\nThe state machine must take care to be deterministic: the same commands applied in the same order\nmust result in the same state across all nodes. This means that a command can't e.g. read the\ncurrent time or generate a random number -- these values must be included in the command. It also\nmeans that non-deterministic errors, such as an IO error, must halt command application (in toyDB's\ncase, we just panic and crash the node).\n\nIn toyDB's, the state machine is an MVCC key/value store that stores SQL tables and rows, as we'll\nsee in the SQL Raft replication section.\n\n## Node Roles\n\nIn Raft, a node can have one out of three roles:\n\n* **Leader:** replicates writes to followers and serves client requests.\n* **Follower:** replicates writes from a leader.\n* **Candidate:** campaigns for leadership.\n\nThe Raft paper summarizes these roles and transitions in the following diagram (we'll discuss\nleader election in detail below):\n\n<img src=\"./images/raft-states.svg\" alt=\"Raft states\" width=\"400\" style=\"display: block; margin: 30px auto;\">\n\nIn toyDB, a node is represented by the `raft::Node` enum, with variants for each state:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L47-L66\n\nThis wraps the `raft::RawNode<Role>` type which contains the inner node state. It is generic over\nthe role, and uses the [typestate pattern](http://cliffle.com/blog/rust-typestate/) to provide\nmethods and transitions depending on the node's current role. This enforces state transitions and\ninvariants at compile time via Rust's type system -- for example, only `RawNode<Candidate>` has an\n`into_leader()` method, since only candidates can transition to leaders (when they win an election).\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L156-L177\n\nThe `RawNode::role` field contains role-specific state as structs implementing the `Role` marker\ntrait:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L661-L680\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L242-L255\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L523-L531\n\nWe'll see what the various fields are used for in the following sections.\n\n## Node Interface and Communication\n\nThe `raft::Node` enum has two main methods that drive the node: `tick()` and `step()`. These consume\nthe current node and return a new node, possibly with a different role.\n\n`tick()` advances time by a logical tick. This is used to measure the passage of time, e.g. to\ntrigger election timeouts or periodic leader heartbeats. toyDB uses a tick interval of 100\nmilliseconds (see `raft::TICK_INTERVAL`), and will call `tick()` on the node at this rate.\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L125-L132\n\n`step()` processes an inbound message from a different node or client:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L107-L123\n\nOutbound messages to other nodes are sent via the `RawNode::tx` channel:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L171-L172\n\nNodes are identified by a unique node ID, which is given at node startup:\n\nhttps://github.com/erikgrinaker/toydb/blob/90a6cae47ac20481ac4eb2f20eea50f02e6c2b33/src/raft/node.rs#L17-L18\n\nMessages are wrapped in a `raft::Envelope` specifying the sender and recipient:\n\nhttps://github.com/erikgrinaker/toydb/blob/d96c6dd5ae7c0af55ee609760dcd958c289a44f2/src/raft/message.rs#L10-L21\n\nThe envelope contains a `raft::Message`, an enum which encodes the Raft message protocol. We won't\ndwell on the specific message types here, but discuss them invididually in the following sections.\nRaft does not require reliable message delivery, so messages may be dropped or reordered at any\ntime, although toyDB's use of TCP provides stronger delivery guarantees.\n\nhttps://github.com/erikgrinaker/toydb/blob/d96c6dd5ae7c0af55ee609760dcd958c289a44f2/src/raft/message.rs#L25-L152\n\nThis is an entirely synchronous and deterministic model -- the same sequence of calls on a given\nnode in a given initial state will always produce the same result. This is very convenient for\ntesting and understandability. We will see in the server section how toyDB drives the node on a\nseparate thread, provides a network transport for messages, and ticks it at regular intervals.\n\n## Leader Election and Terms\n\nIn the steady state, Raft simply has a leader which replicates writes to followers. But to reach\nthis steady state, we must elect a leader, which is where much of the subtle complexity lies. See\nthe Raft paper for comprehensive details and safety arguments, we'll summarize it briefly below.\n\nRaft divides time into _terms_. The term is a monotonically increasing number starting at 1. There\ncan only be one leader in a term (or none if an election fails), and the term can never regress.\nReplicated commands belong to the specific term under which they were proposed.\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L20-L21\n\nLet's walk through an election, where we bootstrap a brand new, empty toyDB cluster with 3 nodes.\n\nNodes are initialized by calling `Node::new()`. Since this is a new cluster, they are given an empty\n`raft::Log` and `raft::State`, at term 0. Nodes start with role `Follower`, but without a leader.\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L68-L87\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L266-L290\n\nNow, nothing really happens for a while, as the nodes are waiting to maybe hear from an existing\nleader (there is none). Every 100 ms we call `tick()`, until we reach `election_timeout`:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L489-L497\n\nNotice how `new()` set `election_timeout` to a random value (in the range `ELECTION_TIMEOUT_RANGE`\nof 10-20 ticks, i.e. 1-2 seconds). If all nodes had the same timeout, they would likely campaign for\nleadership simultaneously, resulting in an election tie -- Raft uses randomized election timeouts to\navoid such ties.\n\nOnce a node reaches `election_timeout` it transitions to role `Candidate`:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L292-L312\n\nWhen it becomes a candidate it campaigns for leadership by increasing its term to 1, voting for\nitself, and sending `Message::Campaign` to all peers asking for their vote:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L647-L658\n\nIn Raft, the term can't regress, and a node can only cast a single vote in each term (even across\nrestarts), so both of these are persisted to disk via `Log::set_term_vote()`.\n\nWhen the two other nodes (still in state `Follower`) receive the `Message::Campaign` asking for a\nvote, they will first increase their term to 1 (since this is a newer term than their local term 0):\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L347-L351\n\nThey then grant the vote since they haven't yet voted for anyone else in term 1. They persist the\nvote to disk via `Log::set_term_vote()` and return a `Message::CampaignResponse { vote: true }` to\nthe candidate:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L424-L449\n\nThey also check that the candidate's log is at least as long as theirs, which is trivially true in\nthis case since the log is empty. This is necessary to ensure that a leader has all committed\nentries (see section 5.4.1 in the Raft paper).\n\nWhen the candidate receives the `Message::CampaignResponse` it records the vote from each node. Once\nit has a quorum (in this case 2 out of 3 votes including its own vote) it becomes leader in term 1:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L599-L606\n\nWhen it becomes leader, it sends a `Message::Heartbeat` to all peers to tell them it is now the\nleader in term 1. It also appends an empty entry to its log and replicates it, but we will ignore\nthis for now (see section 5.4.2 in the Raft paper for why).\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L563-L583\n\nWhen the other nodes receive the heartbeat, they become followers of the new leader in its term:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L359-L384\n\nFrom now on, the leader will send periodic `Message::Heartbeat` every 4 ticks (see\n`HEARTBEAT_INTERVAL`) to assert its leadership:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L945-L953\n\nThe followers record when they last received any message from the leader (including heartbeats), and\nwill hold a new election if they haven't heard from the leader in an election timeout (e.g. due to a\nleader crash or network partition):\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L353-L356\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L489-L497\n\nThis entire process is illustrated in the test script [`election`](https://github.com/erikgrinaker/toydb/blob/cb234a0b776484608118fd9382869ee5bc30d4f0/src/raft/testscripts/node/election),\nalong with several other test scripts that show e.g. [election ties](https://github.com/erikgrinaker/toydb/blob/cb234a0b776484608118fd9382869ee5bc30d4f0/src/raft/testscripts/node/election_tie),\n[contested elections](https://github.com/erikgrinaker/toydb/blob/cb234a0b776484608118fd9382869ee5bc30d4f0/src/raft/testscripts/node/election_contested),\nand other scenarios:\n\nhttps://github.com/erikgrinaker/toydb/blob/cb234a0b776484608118fd9382869ee5bc30d4f0/src/raft/testscripts/node/election#L1-L72\n\n## Client Requests and Forwarding\n\nOnce a leader has been elected, we can submit read and write requests to it. This is done by\nstepping a `Message::ClientRequest` into the node using the local node ID, with a unique request ID\n(toyDB uses UUIDv4), and waiting for an outbound response message with the same ID:\n\nhttps://github.com/erikgrinaker/toydb/blob/d96c6dd5ae7c0af55ee609760dcd958c289a44f2/src/raft/message.rs#L134-L151\n\nhttps://github.com/erikgrinaker/toydb/blob/d96c6dd5ae7c0af55ee609760dcd958c289a44f2/src/raft/message.rs#L164-L188\n\nThe requests and responses themselves are arbitrary binary data which is interpreted by the state\nmachine. For our purposes here, let's pretend the requests are:\n\n* `Request::Write(\"key=value\")` → `Response::Write(\"ok\")`\n* `Request::Read(\"key\")` → `Response::Read(\"value\")`\n\nThe fundamental difference between read and write requests are that write requests are replicated\nthrough Raft and executed on all nodes, while read requests are only executed on the leader without\nbeing appended to the log. It would be possible to execute reads on followers too, for load\nbalancing, but these reads would be eventually consistent and thus violate linearizability, so toyDB\nonly executes reads on the leader.\n\nIf a request is submitted to a follower, it will be forwarded to the leader and the response\nforwarded back to the client (distinguished by the sender/recipient node ID -- a local client always\nuses the local node ID):\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L451-L474\n\nFor simplicity, we cancel the request with `Error::Abort` if a request is submitted to a candidate,\nand similarly if a follower changes its role to candidate or discovers a new leader. We could have\nheld on to these and redirected them to a new leader, but we keep it simple and ask the client to\nretry.\n\nWe'll look at the actual read and write request processing next.\n\n## Write Replication and Application\n\nWhen the leader receives a write request, it proposes the command for replication to followers. It\nkeeps track of the in-flight write and its log entry index in `writes`, such that it can respond to\nthe client with the command result once the entry has been committed and applied.\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L895-L904\n\nTo propose the command, the leader appends it to its log and sends a `Message::Append` to each\nfollower to replicate it to their logs:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L966-L980\n\nIn steady state, `Message::Append` just contains the single log entry we appended above:\n\nhttps://github.com/erikgrinaker/toydb/blob/d96c6dd5ae7c0af55ee609760dcd958c289a44f2/src/raft/message.rs#L87-L108\n\nHowever, sometimes followers may be lagging behind the leader (e.g. after a crash), or their log may\nhave diverged from the leader (e.g. unsuccessful proposals from a stale leader after a network\npartition). To handle these cases, the leader tracks the replication progress of each follower as\n`raft::Progress`:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L682-L698\n\nWe'll gloss over these cases here (see the Raft paper and the code in `raft::Progress` and\n`maybe_send_append()` for details). In the steady state, where each entry is successfully appended\nand replicated one at a time, `maybe_send_append()` will fall through to the bottom and send a\nsingle entry:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L1068-L1128\n\nThe `Message::Append` contains the index/term of the entry immediately before the new entry as\n`base_index` and `base_term`. If the follower's log also contains an entry with this index and term\nthen its log is guaranteed to match (be equal to) the leader's log up to this entry (see section 5.3\nin the Raft paper). The follower can then append the new log entry and return a\n`Message::AppendResponse` confirming that the entry was appended and that its log matches the\nleader's log up to `match_index`:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L386-L410\n\nWhen the leader receives the `Message::AppendResponse`, it will update its view of the follower's\n`match_index`.\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L844-L858\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L701-L710\n\nOnce a quorum of nodes (in our case 2 out of 3 including the leader) have the entry in their log,\nthe leader can commit the entry and apply it to the state machine. It also looks up the in-flight\nwrite request from `writes` and sends the command result back to the client as\n`Message::ClientResponse`:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L982-L1032\n\nThe leader will also propagate the new commit index to followers via the next heartbeat, so that\nthey can also apply any pending log entries to their state machine. This isn't strictly necessary,\nsince reads are executed on the leader and nodes have to apply pending entries before becoming\nleaders, but we do it anyway so that they don't fall too far behind on application.\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L359-L384\n\nThis process is illustrated in the test scripts [`append`](https://github.com/erikgrinaker/toydb/blob/cb234a0b776484608118fd9382869ee5bc30d4f0/src/raft/testscripts/node/append) and [`heartbeat_commits_follower`](https://github.com/erikgrinaker/toydb/blob/cb234a0b776484608118fd9382869ee5bc30d4f0/src/raft/testscripts/node/heartbeat_commits_follower)\n(along with many other scenarios):\n\nhttps://github.com/erikgrinaker/toydb/blob/cb234a0b776484608118fd9382869ee5bc30d4f0/src/raft/testscripts/node/append#L1-L43\n\nhttps://github.com/erikgrinaker/toydb/blob/cb234a0b776484608118fd9382869ee5bc30d4f0/src/raft/testscripts/node/heartbeat_commits_follower#L1-L50\n\n## Read Processing\n\nFor linearizable (aka strongly consistent) reads, we must execute read requests on the leader, as\nmentioned above. However, this is not sufficient: under e.g. a network partition, a node may think\nit's still the leader while in fact a different leader has been elected elsewhere (in a later term)\nand executed writes there.\n\nTo handle this case, the leader must confirm that it is still the leader for each read, by sending a\n`Message::Read` to its followers containing a read sequence number. Only if a quorum confirms that\nit is still the leader can the read be executed. This incurs an additional network roundtrip, which\nis clearly inefficient, so real-world systems often use leader leases instead (see section 6.4.1 of\nthe Raft _thesis_, not the paper) -- but it's fine for toyDB.\n\nhttps://github.com/erikgrinaker/toydb/blob/d96c6dd5ae7c0af55ee609760dcd958c289a44f2/src/raft/message.rs#L125-L132\n\nWhen the leader receives the read request, it increments the read sequence number, stores the\npending read request in `reads`, and sends a `Message::Read` to all followers:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L906-L917\n\nWhen the followers receive the `Message::Read`, they simply respond with a `Message::ReadResponse`\nif it's from their current leader (messages from stale terms are ignored):\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L342-L346\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L412-L422\n\nWhen the leader receives the `Message::ReadResponse` it records it in the peer's `Progress`, and\nexecutes the read once a quorum have confirmed the sequence number:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L860-L866\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/node.rs#L1034-L1066\n\nWe now have a Raft-managed state machine with replicated writes and linearizable reads.\n\n---\n\n<p align=\"center\">\n← <a href=\"mvcc.md\">MVCC Transactions</a> &nbsp; | &nbsp; <a href=\"sql.md\">SQL Engine</a> →\n</p>"
  },
  {
    "path": "docs/architecture/server.md",
    "content": "# Server\n\nNow that we've gone over the individual components, we'll tie them all together in the toyDB\nserver `toydb::Server`, located in the [`server`](https://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/server.rs) module.\n\nThe server wraps an inner Raft node `raft::Node`, which manages the SQL state machine, and is\nresponsible for routing network traffic between the Raft node, its Raft peers, and SQL clients.\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/server.rs#L27-L44\n\nFor network protocol, the server uses the Bincode encoding that we've discussed in the encoding\nsection, sent over a TCP connection. There's no need for any further framing, since Bincode knows\nhow many bytes to expect for each message depending on the type it's decoding into.\n\nThe server does not use [async Rust](https://rust-lang.github.io/async-book/) and e.g.\n[Tokio](https://tokio.rs), instead opting for regular OS threads. Async Rust can significantly\ncomplicate the code, which would obscure the main concepts, and any efficiency gains would be\nentirely irrelevant for toyDB.\n\nInternally in the server, messages are passed around between threads using\n[Crossbeam channels](https://docs.rs/crossbeam/latest/crossbeam/channel/index.html).\n\nThe main server loop `Server::serve()` listens for inbound TCP connections on port 9705 for Raft\npeers and 9605 for SQL clients, and spawns threads to process them. We'll look at Raft and SQL\nservices separately.\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/server.rs#L66-L110\n\n## Raft Routing\n\nThe heart of the server is the Raft processing thread `Server::raft_route()`. This is responsible\nfor periodically ticking the Raft node via `raft::Node::tick()`, stepping inbound messages from\nRaft peers into the node via `raft::Node::step()`, and sending outbound messages to peers.\n\nIt also takes inbound Raft client requests from the `sql::engine::Raft` SQL engine, steps them\ninto the Raft node via `raft::Node::step()`, and passes responses back to the appropriate client\nas the node emits them.\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/server.rs#L169-L249\n\nWhen the node starts up, it spawns a `Server::raft_send_peer()` thread for each Raft peer to send\noutbound messages to them.\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/server.rs#L84-L91\n\nThese threads continually attempt to connect to the peer via TCP, and then read any outbound\n`raft::Envelope(raft::Message)` messages from `Server::raft_route()` via a channel and writes the\nmessages into the TCP connection using Bincode:\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/server.rs#L146-L167\n\nThe server also continually listens for inbound Raft TCP connections from peers in\n`Server::raft_accept()`:\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/server.rs#L112-L134\n\nWhen an inbound connection is accepted, a `Server::raft_receive_peer()` thread is spawned that reads\nBincode-encoded `raft::Envelope(raft::Message)` messages from the TCP connection and sends them to\n`Server::raft_route()` via a channel.\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/server.rs#L136-L144\n\nThe Raft cluster is now fully connected, and the nodes can all talk to each other.\n\n## SQL Service\n\nNext, let's serve some SQL clients. The SQL service uses the enums `toydb::Request` and\n`toydb::Response` as a client protocol, again Bincode-encoded over TCP.\n\nThe primary request type is `Request::Execute` which executes a SQL statement against a\n`sql::execution::Session` and returns a `sql::execution::StatementResult`, as we've seen previously.\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/server.rs#L312-L337\n\nThe server sets up a `sql::engine::Raft` SQL engine, with a Crossbeam channel that's used to send\n`raft::Request` Raft client requests to `Server::raft_route()` and onwards to the local\n`raft::Node`.  It then spawns a `Server::sql_accept()` thread to listen for inbound SQL client\nconnections:\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/server.rs#L104-L106\n\nWhen a SQL client connection is accepted, a new client session `sql::execution::Session` is set up\nfor the client, and we spawn a `Server::sql_session()` thread to serve the connection:\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/server.rs#L251-L272\n\nThese session threads continually read `Request` messages from the client, execute them against the\nSQL session (and ultimately the Raft node), before sending a `Response` back to the client.\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/server.rs#L274-L309\n\n## `toydb` Binary\n\nThe `toydb` binary in `src/bin/toydb.rs` launches the server, and is a thin wrapper around\n`toydb::Server`. It is a tiny [`clap`](https://docs.rs/clap/latest/clap/) command:\n\nhttps://github.com/erikgrinaker/toydb/blob/8f8eae0dcf70b1a0df2e853b1f6600e0c7075340/src/bin/toydb.rs#L82-L89\n\nIt first parses a server configuration from the `toydb.yaml` file:\n\nhttps://github.com/erikgrinaker/toydb/blob/8f8eae0dcf70b1a0df2e853b1f6600e0c7075340/src/bin/toydb.rs#L30-L59\n\nThen it initializes the Raft log storage and SQL state machine:\n\nhttps://github.com/erikgrinaker/toydb/blob/8f8eae0dcf70b1a0df2e853b1f6600e0c7075340/src/bin/toydb.rs#L105-L133\n\nAnd finally it launches the `toydb::Server`:\n\nhttps://github.com/erikgrinaker/toydb/blob/8f8eae0dcf70b1a0df2e853b1f6600e0c7075340/src/bin/toydb.rs#L135-L137\n\ntoyDB is now up and running!\n\n---\n\n<p align=\"center\">\n← <a href=\"sql-execution.md\">SQL Execution</a> &nbsp; | &nbsp; <a href=\"client.md\">Client</a> →\n</p>"
  },
  {
    "path": "docs/architecture/sql-data.md",
    "content": "# SQL Data Model\n\nThe SQL data model represents user data in tables and rows. It is made up of data types and schemas,\nin the [`sql::types`](https://github.com/erikgrinaker/toydb/tree/686d3971a253bfc9facc2ba1b0e716cff5c109fb/src/sql/types)\nmodule.\n\n## Data Types\n\ntoyDB supports four basic scalar data types as `sql::types::DataType`: booleans, integers, floats,\nand strings.\n\nhttps://github.com/erikgrinaker/toydb/blob/b2fe7b76ee634ca6ad31616becabfddb1c03d34b/src/sql/types/value.rs#L15-L27\n\nSpecific values are represented as `sql::types::Value`, using the corresponding Rust types. toyDB\nalso supports SQL `NULL` values, i.e. unknown values, following the rules of\n[three-valued logic](https://en.wikipedia.org/wiki/Three-valued_logic).\n\nhttps://github.com/erikgrinaker/toydb/blob/b2fe7b76ee634ca6ad31616becabfddb1c03d34b/src/sql/types/value.rs#L40-L64\n\nThe `Value` type provides basic formatting, conversion, and mathematical operations.\n\nhttps://github.com/erikgrinaker/toydb/blob/686d3971a253bfc9facc2ba1b0e716cff5c109fb/src/sql/types/value.rs#L68-L79\n\nhttps://github.com/erikgrinaker/toydb/blob/686d3971a253bfc9facc2ba1b0e716cff5c109fb/src/sql/types/value.rs#L164-L370\n\nIt also specifies comparison and ordering semantics, but these are subtly different from the SQL\nsemantics. For example, in Rust code `Value::Null == Value::Null` yields `true`, while in SQL\n`NULL = NULL` yields `NULL`.  This mismatch is necessary for the Rust code to properly detect and\nprocess `Null` values, and the desired SQL semantics are implemented during expression evaluation\nwhich we'll cover below.\n\nhttps://github.com/erikgrinaker/toydb/blob/b2fe7b76ee634ca6ad31616becabfddb1c03d34b/src/sql/types/value.rs#L91-L162\n\nDuring execution, a row of values is represented as `sql::types::Row`, with multiple rows emitted\nvia `sql::types::Rows` row iterators:\n\nhttps://github.com/erikgrinaker/toydb/blob/b2fe7b76ee634ca6ad31616becabfddb1c03d34b/src/sql/types/value.rs#L378-L388\n\n## Schemas\n\ntoyDB schemas only support tables. There are no named indexes or constraints, and there's only a\nsingle unnamed database.\n\nTables are represented by `sql::types::Table`:\n\nhttps://github.com/erikgrinaker/toydb/blob/c2b0f7f1d6cbf6e2cdc09fc0aec7b050e840ec21/src/sql/types/schema.rs#L12-L25\n\nA table is made up of a set of columns, represented by `sql::types::Column`. These support the data\ntypes described above, along with unique constraints, foreign keys, and secondary indexes.\n\nhttps://github.com/erikgrinaker/toydb/blob/c2b0f7f1d6cbf6e2cdc09fc0aec7b050e840ec21/src/sql/types/schema.rs#L29-L53\n\nThe table name serves as a unique identifier, and can't be changed later. In fact, tables schemas\nare entirely static: they can only be created or dropped (there are no schema changes).\n\nTable schemas are stored in the catalog, represented by the `sql::engine::Catalog` trait. We'll\nrevisit the implementation of this trait in the SQL storage section.\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/sql/engine/engine.rs#L60-L79\n\nTable schemas are validated when created via `Table::validate()`, which enforces invariants and\ninternal consistency. It uses the catalog to look up information about other tables, e.g. that\nforeign key references point to a valid target column in a different table.\n\nhttps://github.com/erikgrinaker/toydb/blob/c2b0f7f1d6cbf6e2cdc09fc0aec7b050e840ec21/src/sql/types/schema.rs#L98-L170\n\nTable rows are validated via `Table::validate_row()`, which ensures that a `sql::types::Row`\nconforms to the schema (e.g. that value types match the column data types). It uses a\n`sql::engine::Transaction` to look up other rows in the database, e.g. to check for primary key\nconflicts (we'll get back to this later).\n\nhttps://github.com/erikgrinaker/toydb/blob/c2b0f7f1d6cbf6e2cdc09fc0aec7b050e840ec21/src/sql/types/schema.rs#L172-L236\n\n## Expressions\n\nDuring SQL execution, we also have to model _expressions_, such as `1 + 2 * 3`. These are\nrepresented as values and operations on them, and can be nested as a tree to represent compound\noperations.\n\nhttps://github.com/erikgrinaker/toydb/blob/9419bcf6aededf0e20b4e7485e2a5fa3e975d79f/src/sql/types/expression.rs#L11-L64\n\n\nFor example, the expression `1 + 2 * 3` (taking [precedence](https://en.wikipedia.org/wiki/Order_of_operations)\ninto account) is represented as:\n\n```rust\n//    +\n//   / \\\n//  1   *\n//     /  \\\n//    2    3\nExpression::Add(\n    Expression::Constant(Value::Integer(1)),\n    Expression::Multiply(\n        Expression::Constant(Value::Integer(2)),\n        Expression::Constant(Value::Integer(3)),\n    ),\n)\n```\n\nAn `Expression` can contain two kinds of values: constant values as\n`Expression::Constant(sql::types::Value)`, and dynamic values as `Expression::Column(usize)` column\nreferences. The latter will fetch a `sql::types::Value` from a `sql::types::Row` at the specified\nindex during evaluation.\n\nWe'll see later how the SQL parser and planner transforms text expression like `1 + 2 * 3` into an\n`Expression`, and how it resolves column names to row indexes like `price * 0.25` to\n`row[3] * 0.25`.\n\nExpressions are evaluated recursively via `Expression::evalute()`, given a `sql::types::Row` with\ninput values for column references, and return a final `sql::types::Value` result:\n\nhttps://github.com/erikgrinaker/toydb/blob/9419bcf6aededf0e20b4e7485e2a5fa3e975d79f/src/sql/types/expression.rs#L73-L208\n\nMany of the comparison operations like `==` are implemented explicitly here instead of using\n`sql::types::Value` comparisons. This is where we implement the SQL semantics of special values like\n`NULL`, such that `NULL = NULL` yields `NULL` instead of `TRUE`.\n\nFor mathematical operations however, we generally dispatch to these methods on `sql::types::Value`:\n\nhttps://github.com/erikgrinaker/toydb/blob/b2fe7b76ee634ca6ad31616becabfddb1c03d34b/src/sql/types/value.rs#L185-L295\n\nExpression parsing and evaluation is tested via test scripts in\n[`sql/testscripts/expression`](https://github.com/erikgrinaker/toydb/tree/9419bcf6aededf0e20b4e7485e2a5fa3e975d79f/src/sql/testscripts/expressions).\n\n---\n\n<p align=\"center\">\n← <a href=\"sql.md\">SQL Engine</a> &nbsp; | &nbsp; <a href=\"sql-storage.md\">SQL Storage</a> →\n</p>"
  },
  {
    "path": "docs/architecture/sql-execution.md",
    "content": "# SQL Execution\n\nNow that the planner and optimizer have done all the hard work of figuring out how to execute a\nquery, it's time to actually execute it.\n\n## Plan Executor\n\nPlan execution is done by `sql::execution::Executor` in the\n[`sql::execution`](https://github.com/erikgrinaker/toydb/tree/9419bcf6aededf0e20b4e7485e2a5fa3e975d79f/src/sql/execution)\nmodule, using a `sql::engine::Transaction` to access the SQL storage engine.\n\nhttps://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/sql/execution/executor.rs#L14-L49\n\nThe executor takes a `sql::planner::Plan` as input, and will return an `ExecutionResult` depending\non the statement type.\n\nhttps://github.com/erikgrinaker/toydb/blob/686d3971a253bfc9facc2ba1b0e716cff5c109fb/src/sql/execution/executor.rs#L331-L339\n\nWhen executing the plan, the executor will branch off depending on the statement type:\n\nhttps://github.com/erikgrinaker/toydb/blob/686d3971a253bfc9facc2ba1b0e716cff5c109fb/src/sql/execution/executor.rs#L57-L101\n\nWe'll focus on `SELECT` queries here, which are the most interesting.\n\ntoyDB uses the iterator model (also known as the volcano model) for query execution. In the case of\na `SELECT` query, the result is a row iterator, and pulling from this iterator by calling `next()`\nwill drive the entire execution pipeline by recursively calling `next()` on the child nodes' row\niterators. This maps very naturally onto Rust's iterators, and we leverage these to construct the\nexecution pipeline as nested iterators.\n\nExecution itself is fairly straightforward, since we're just doing exactly what the planner tells us\nto do in the plan. We call `Executor::execute_node` recursively on each `sql::planner:Node`,\nstarting with the root node. Each node returns a result row iterator that the parent node can pull\nits input rows from, process them, and output the resulting rows via its own row iterator (with the\nroot node's iterator being returned to the caller):\n\nhttps://github.com/erikgrinaker/toydb/blob/686d3971a253bfc9facc2ba1b0e716cff5c109fb/src/sql/execution/executor.rs#L103-L104\n\n`Executor::execute_node()` will simply look at the type of `Node`, recursively call\n`Executor::execute_node()` on any child nodes, and then process the rows accordingly.\n\nhttps://github.com/erikgrinaker/toydb/blob/686d3971a253bfc9facc2ba1b0e716cff5c109fb/src/sql/execution/executor.rs#L103-L212\n\nWe won't discuss every plan node in detail, but let's consider the movie plan we've looked at\npreviously:\n\n```\nSelect\n└─ Order: movies.released desc\n   └─ Projection: movies.title, movies.released, genres.name as genre\n      └─ HashJoin: inner on movies.genre_id = genres.id\n         ├─ Scan: movies (released >= 2000)\n         └─ Scan: genres\n```\n\nWe'll recursively call `execute_node()` until we end up in the two `Scan` nodes. These simply\ncall through to the SQL engine (either using Raft or local disk) via `Transaction::scan()`, passing\nin the scan predicate if any, and return the resulting row iterator:\n\nhttps://github.com/erikgrinaker/toydb/blob/686d3971a253bfc9facc2ba1b0e716cff5c109fb/src/sql/execution/executor.rs#L203-L204\n\n`HashJoin` will then join the output rows from the `movies` and `genres` iterators by using a\nhash join. This builds an in-memory table for `genres` and then iterates over `movies`, joining\nthe rows:\n\nhttps://github.com/erikgrinaker/toydb/blob/686d3971a253bfc9facc2ba1b0e716cff5c109fb/src/sql/execution/executor.rs#L128-L141\n\nhttps://github.com/erikgrinaker/toydb/blob/889aef9f24c0fa4d58e314877fa17559a9f3d5d2/src/sql/execution/join.rs#L103-L183\n\nThe `Projection` node will simply evaluate the (trivial) column expressions using each joined\nrow as input:\n\nhttps://github.com/erikgrinaker/toydb/blob/686d3971a253bfc9facc2ba1b0e716cff5c109fb/src/sql/execution/executor.rs#L179-L186\n\nAnd finally the `Order` node will sort the results (which requires buffering them all in memory):\n\nhttps://github.com/erikgrinaker/toydb/blob/686d3971a253bfc9facc2ba1b0e716cff5c109fb/src/sql/execution/executor.rs#L173-L177\n\nhttps://github.com/erikgrinaker/toydb/blob/686d3971a253bfc9facc2ba1b0e716cff5c109fb/src/sql/execution/executor.rs#L298-L328\n\nThe output row iterator of `Order` is returned via `ExecutionResult::Select`, and the caller can now\ngo ahead and pull the resulting rows from it.\n\n## Session Management\n\nThe entry point to the SQL engine is the `sql::execution::Session`, which represents a single user\nsession. It is obtained via `sql::engine::Engine::session()`.\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/sql/execution/session.rs#L14-L21\n\nThe session takes a series of raw SQL statement strings as input and parses them:\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/sql/execution/session.rs#L29-L33\n\nFor each statement, it returns a result depending on the kind of statement:\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/sql/execution/session.rs#L132-L148\n\nThe session itself performs transaction control. It handles `BEGIN`, `COMMIT`, and `ROLLBACK`\nstatements, and modifies the transaction accordingly.\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/sql/execution/session.rs#L34-L70\n\nAny other statements are processed by the SQL planner, optimizer, and executor as we've seen in\nprevious sections.\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/sql/execution/session.rs#L77-L83\n\nThese statements are always executed using the session's current transaction. If there is no active\ntransaction, the session will create a new, implicit transaction for each statement.\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/sql/execution/session.rs#L87-L112\n\nAnd with that, we have a fully functional SQL engine!\n\n---\n\n<p align=\"center\">\n← <a href=\"sql-optimizer.md\">SQL Optimization</a> &nbsp; | &nbsp; <a href=\"server.md\">Server</a> →\n</p>"
  },
  {
    "path": "docs/architecture/sql-optimizer.md",
    "content": "# SQL Optimization\n\n[Query optimization](https://en.wikipedia.org/wiki/Query_optimization) attempts to improve query\nperformance and efficiency by altering the execution plan. This is a deep and complex field, and\nwe can only scratch the surface here.\n\ntoyDB's query optimizer is very basic -- it only has a handful of rudimentary heuristic\noptimizations to illustrate how the process works. Real-world optimizers use much more sophisticated\nmethods, including statistical analysis, cost estimation, adaptive execution, etc.\n\nThe optimizers are located in the [`sql::planner::optimizer`](https://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/sql/planner/optimizer.rs) module.\nAn optimizer `sql::planner::Optimizer` just takes in a plan node `sql::planner::Node` (the root node\nin the plan), and returns an optimized node:\n\nhttps://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/sql/planner/optimizer.rs#L20-L25\n\nOptimizations are always implemented as recursive node transformations. To help with this, `Node`\nhas the helper methods `Node::transform` and `Node::transform_expressions` which recurse into a node\nor expression tree and call a given transformation closure on each node, as either\n[pre-order](https://en.wikipedia.org/wiki/Tree_traversal#Pre-order,_NLR) or\n[post-order](https://en.wikipedia.org/wiki/Tree_traversal#Post-order,_LRN) transforms:\n\nhttps://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/sql/planner/plan.rs#L269-L371\n\nA technique that's often useful during optimization is to convert expressions into\n[conjunctive normal form](https://en.wikipedia.org/wiki/Conjunctive_normal_form), i.e. \"an AND of\nORs\". For example, the two following expressions are equivalent, but the latter is in conjunctive\nnormal form (it's a chain of ANDs):\n\n```\n(a AND b) OR (c AND d)  →  (a OR c) AND (a OR d) AND (b OR c) AND (b OR d)\n```\n\nThis is useful because we can often move each AND operand independently around in the plan tree\nand still get the same result -- we'll see this in action later. Expressions are converted into\nconjunctive normal form via `Expression::into_cnf`, which is implemented using\n[De Morgan's laws](https://en.wikipedia.org/wiki/De_Morgan%27s_laws):\n\nhttps://github.com/erikgrinaker/toydb/blob/9419bcf6aededf0e20b4e7485e2a5fa3e975d79f/src/sql/types/expression.rs#L289-L351\n\nWe'll have a brief look at all of toyDB's optimizers, which are listed here in the order they're\napplied:\n\nhttps://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/sql/planner/optimizer.rs#L9-L18\n\nTest scripts for the optimizers are in [`src/sql/testscripts/optimizers`](https://github.com/erikgrinaker/toydb/tree/9419bcf6aededf0e20b4e7485e2a5fa3e975d79f/src/sql/testscripts/optimizers),\nand show how query plans evolve as each optimizer is applied.\n\n## Constant Folding\n\nThe `ConstantFolding` optimizer performs [constant folding](https://en.wikipedia.org/wiki/Constant_folding).\nThis pre-evaluates constant expressions in the plan during planning, instead of evaluating them\nfor every row during execution.\n\nhttps://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/sql/planner/optimizer.rs#L27-L30\n\nFor example, consider the query `SELECT 1 + 2 * 3 - foo FROM bar`. There is no point in\nre-evaluating `1 + 2 * 3` for every row in `bar`, because the result is always the same, so we can\njust evaluate this once during planning, transforming the expression into `7 - foo`.\n\nConcretely, this plan:\n\n```\nSelect\n└─ Projection: 1 + 2 * 3 - bar.foo\n   └─ Scan: bar\n```\n\nShould be transformed into this plan:\n\n```\nSelect\n└─ Projection: 7 - bar.foo\n   └─ Scan: bar\n```\n\nTo do this, `ConstantFolding` simply checks whether an `Expression` tree contains an\n`Expression::Column` node -- if it doesn't, then it much be a constant expression (since that's the\nonly dynamic value in an expression), and we can evaluate it with a `None` input row and replace the\noriginal expression node with an `Expression::Constant` node.\n\nThis is done recursively for each plan node, and recursively for each expression node (so it does\nthis both for `SELECT`, `WHERE`, `ORDER BY`, and all other parts of the query). Notably, it does a\npost-order expression transform, so it starts at the expression leaf nodes and attempts to transform\neach expression node as it moves back up the tree -- this allows it to iteratively evaluate constant\nparts as far as possible for each branch.\n\nhttps://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/sql/planner/optimizer.rs#L32-L56\n\nAdditionally, `ConstantFolding` also short-circuits logical expressions. For example, the expression\n`foo AND FALSE` will always be `FALSE`, regardless of what `foo` is, so we can replace it with\n`FALSE`:\n\nhttps://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/sql/planner/optimizer.rs#L58-L84\n\nAs the code comment mentions though, this doesn't fold optimally: it doesn't attempt to rearrange\nexpressions, which would require knowledge of precedence rules. For example, `(1 + foo) - 2` could\nbe folded into `foo - 1` by first rearranging it as `foo + (1 - 2)`, but we don't do this currently.\n\n## Filter Pushdown\n\nThe `FilterPushdown` optimizer attempts to push filter predicates as far down into the plan as\npossible, to reduce the number of rows each node has to process.\n\nhttps://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/sql/planner/optimizer.rs#L90-L95\n\nRecall the `movies` query plan from the planning section:\n\n```\nSelect\n└─ Order: movies.released desc\n   └─ Projection: movies.title, movies.released, genres.name as genre\n      └─ Filter: movies.released >= 2000\n         └─ NestedLoopJoin: inner on movies.genre_id = genres.id\n            ├─ Scan: movies\n            └─ Scan: genres\n```\n\nEven though we're filtering on `release >= 2000`, the `Scan` node still has to read all of them from\ndisk and send them via Raft, and the `NestedLoopJoin` node still has to join all of them. It would\nbe nice if we could push this filtering into the `NestedLoopJoin` and `Scan` nodes and avoid this\nextra work, and this is exactly what `FilterPushdown` does.\n\nThe only plan nodes that have predicates that can be pushed down are `Filter` nodes and\n`NestedLoopJoin` nodes, so we recurse through the plan tree and look for these nodes, attempting\nto push down.\n\nhttps://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/sql/planner/optimizer.rs#L97-L110\n\nWhen it encounters the `Filter` node, it will extract the predicate and attempt to push it down\ninto its `source` node:\n\nhttps://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/sql/planner/optimizer.rs#L139-L153\n\nIf the source node is a `Filter`, `NestedLoopJoin`, or `Scan` node, then we can push the predicate\ndown into it by `AND`ing it with the existing predicate (if any).\n\nhttps://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/sql/planner/optimizer.rs#L112-L137\n\nIn our case, we were able to push the `Filter` into the `NestedLoopJoin`, and our plan now looks\nlike this:\n\n```\nSelect\n└─ Order: movies.released desc\n   └─ Projection: movies.title, movies.released, genres.name as genre\n      └─ NestedLoopJoin: inner on movies.genre_id = genres.id AND movies.released >= 2000\n         ├─ Scan: movies\n         └─ Scan: genres\n```\n\nBut we're still not done, as we'd like to push `movies.released >= 2000` down into the `Scan` node.\nPushdown for join nodes is a little more tricky, because we can only push down parts of the\nexpression that reference one of the source nodes.\n\nWe first have to convert the expression into conjunctive normal form, i.e. and AND of ORs, as we've\ndiscussed previously. This allows us to examine and push down each AND part in isolation, because it\nhas the same effect regardless of whether it is evaluated in the `NestedLoopJoin` node or one of\nthe source nodes. Our expression is already in conjunctive normal form, though.\n\nWe then look at each AND part, and check which side of the join it has column references for.  If it\nonly references one of the sides, then the expression can be pushed down into it. We also make some\neffort here to move primary/foreign key constants across to both sides, but we'll gloss over that.\n\nhttps://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/sql/planner/optimizer.rs#L155-L247\n\nThis allows us to push down the `movies.released >= 2000` predicate into the corresponding `Scan`\nnode, significantly reducing the amount of data transferred across Raft:\n\n```\nSelect\n└─ Order: movies.released desc\n   └─ Projection: movies.title, movies.released, genres.name as genre\n      └─ NestedLoopJoin: inner on movies.genre_id = genres.id\n         ├─ Scan: movies (released >= 2000)\n         └─ Scan: genres\n```\n\n## Index Lookups\n\nThe `IndexLookup` optimizer uses primary key or secondary index lookups instead of full table\nscans where possible.\n\nhttps://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/sql/planner/optimizer.rs#L250-L252\n\nThe optimizer itself is fairly straightforward. It assumes that `FilterPushdown` has already pushed\npredicates down into `Scan` nodes, so it only needs to examine these. It converts the predicate into\nconjunctive normal form, and looks for any parts that are direct column lookups -- i.e.\n`column = value` (possibly a long OR chain of these).\n\nIf it finds any, and the column is either a primary key or secondary index column, then we convert\nthe `Scan` node into either a `KeyLookup` or `IndexLookup` node respectively. If there are any\nfurther AND predicates remaining, we add a parent `Filter` node to keep these predicates.\n\nFor example, the following plan:\n\n```\nSelect\n└─ Scan: movies ((id = 1 OR id = 7 OR id = 3) AND released >= 2000)\n```\n\nWill be transformed into one that does individual key lookups rather than a full table scan:\n\n```\nSelect\n└─ Filter: movies.released >= 2000\n   └─ KeyLookup: movies (1, 3, 7)\n```\n\nThe code is as outlined above:\n\nhttps://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/sql/planner/optimizer.rs#L254-L303\n\nHelped by `Expression::is_column_lookup()` and `Expression::into_column_values()`:\n\nhttps://github.com/erikgrinaker/toydb/blob/9419bcf6aededf0e20b4e7485e2a5fa3e975d79f/src/sql/types/expression.rs#L363-L421\n\n## Hash Join\n\nThe `HashJoin` optimizer will replace a `NestedLoopJoin` with a `HashJoin` where possible.\n\nhttps://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/sql/planner/optimizer.rs#L305-L307\n\nA [nested loop join](https://en.wikipedia.org/wiki/Nested_loop_join) is a very inefficient O(n²)\nalgorithm, which iterates over all rows in the right source for each row in the left source to see\nif they match. However, it is completely general, and can join on arbitraily complex predicates.\n\nIn the common case where the join predicate is an equality comparison such as\n`movies.genre_id = genres.id` (i.e. an [equijoin](https://en.wikipedia.org/wiki/Relational_algebra#θ-join_and_equijoin)),\nthen we can instead use a [hash join](https://en.wikipedia.org/wiki/Hash_join). This scans the right\ntable once, builds an in-memory hash table from it, and for each left row it looks up any right rows\nin the hash table. This is a much more efficient O(n) algorithm.\n\nIn our previous movie example, we are in fact doing an equijoin:\n\n```\nSelect\n└─ Order: movies.released desc\n   └─ Projection: movies.title, movies.released, genres.name as genre\n      └─ NestedLoopJoin: inner on movies.genre_id = genres.id\n         ├─ Scan: movies (released >= 2000)\n         └─ Scan: genres\n```\n\nAnd so our `NestedLoopJoin` can be replaced by a `HashJoin`:\n\n```\nSelect\n└─ Order: movies.released desc\n   └─ Projection: movies.title, movies.released, genres.name as genre\n      └─ HashJoin: inner on movies.genre_id = genres.id\n         ├─ Scan: movies (released >= 2000)\n         └─ Scan: genres\n```\n\nThe `HashJoin` optimizer is extremely simple: if the join predicate is an equijoin, use a hash join.\nThis isn't always a good idea (the right source can be huge and we can run out of memory for the\nhash table), but we keep it simple.\n\nhttps://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/sql/planner/optimizer.rs#L309-L348\n\nOf course there are many other join algorithms out there, and one of the harder problems in SQL\noptimization is how to efficiently perform large N-way multijoins. We don't attempt to tackle these\nproblems here -- the `HashJoin` optimizer is just a very simple example of such join optimization.\n\n## Short Circuiting\n\nThe `ShortCircuit` optimizer tries to find nodes that can't possibly do any useful work, and either\nremoves them from the plan, or replaces them with trivial nodes that don't do anything. It is kind\nof similar to the `ConstantFolding` optimizer in spirit, but works on plan nodes rather than\nexpression nodes.\n\nhttps://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/sql/planner/optimizer.rs#L350-L354\n\nFor example, `Filter` nodes with a `TRUE` predicate won't actually filter anything:\n\n```\nSelect\n└─ Filter: true\n   └─ Scan: movies\n```\n\nSo we can just remove them:\n\n```\nSelect\n└─ Scan: movies\n```\n\nSimilarly, `Filter` nodes with a `FALSE` predicate will never emit anything:\n\n```\nSelect\n└─ Filter: false\n   └─ Scan: movies\n```\n\nThere's no point doing a scan in this case, so we can just replace it with a `Nothing` node that\ndoes no work and doesn't emit anything:\n\n```\nSelect\n└─ Nothing\n```\n\nThe optimizer tries to find a bunch of such patterns. This can also tidy up query plans a fair bit\nby removing unnecessary cruft.\n\nhttps://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/sql/planner/optimizer.rs#L356-L438\n\n---\n\n<p align=\"center\">\n← <a href=\"sql-planner.md\">SQL Planning</a> &nbsp; | &nbsp; <a href=\"sql-execution.md\">SQL Execution</a> →\n</p>"
  },
  {
    "path": "docs/architecture/sql-parser.md",
    "content": "# SQL Parsing\n\nWe finally arrive at SQL. The SQL parser is the first stage in processing SQL queries and\nstatements, located in the [`sql::parser`](https://github.com/erikgrinaker/toydb/tree/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/parser)\nmodule.\n\nThe SQL parser's job is to take a raw SQL string and turn it into a structured form that's more\nconvenient to work with. In doing so, it will validate that the string is in fact valid SQL\n_syntax_. However, it doesn't know if the SQL statement actually makes sense -- it has no idea which\ntables or columns exist, what their data types are, and so on. That's the job of the planner, which\nwe'll look at later.\n\nFor example, let's say the parser is given the following SQL query:\n\n```sql\nSELECT name, price, price * 25 / 100 AS vat\nFROM products JOIN categories ON products.category_id = categories.id\nWHERE categories.code = 'BLURAY' AND stock > 0\nORDER BY price DESC\nLIMIT 10\n```\n\nIt will generate a structure that looks something like this (in simplified syntax):\n\n```rust\n// A SELECT statement.\nStatement::Select {\n    // SELECT name, price, price * 25 / 100 AS vat\n    select: [\n        (Column(\"name\"), None),\n        (Column(\"price\"), None),\n        (\n            Divide(\n                Multiply(Column(\"price\"), Integer(25)),\n                Integer(100)\n            ),\n            Some(\"vat\"),\n        ),\n    ]\n\n    // FROM products JOIN categories ON products.category_id = categories.id\n    from: [\n        Join {\n            left: Table(\"products\"),\n            right: Table(\"categories\"),\n            type: Inner,\n            predicate: Some(\n                Equal(\n                    Column(\"products.category_id)\",\n                    Column(\"categories.id\"),\n                )\n            )\n        }\n    ]\n\n    // WHERE categories.code = 'BLURAY' AND stock > 0\n    where: Some(\n        And(\n            Equal(\n                Column(\"categories.code\"),\n                String(\"BLURAY\"),\n            ),\n            GreaterThan(\n                Column(\"stock\"),\n                Integer(0),\n            )\n        )\n    )\n\n    // ORDER BY price DESC\n    order: [\n        (Column(\"price\"), Descending),\n    ]\n\n    // LIMIT 10\n    limit: Some(Integer(10))\n}\n```\n\nLet's have a look at how this happens.\n\n## Lexer\n\nWe begin with the `sql::parser::Lexer`, which takes the raw SQL string and performs\n[lexical analysis](https://en.wikipedia.org/wiki/Lexical_analysis) to convert it into a sequence of\ntokens. These tokens are things like number, string, identifier, SQL keyword, and so on.\n\nThis preprocessing is useful to deal with some of the \"noise\" of SQL text, such as whitespace,\nstring quotes, identifier normalization, and so on. It also specifies which symbols and keywords are\nvalid in our SQL queries. This makes the parser's life a lot easier.\n\nThe lexer doesn't care about SQL structure at all, only that the individual pieces (tokens) of a\nstring are well-formed. For example, the following input string:\n\n```\n'foo' ) 3.14 SELECT + x\n```\n\nWill result in these tokens:\n\n```\nString(\"foo\") CloseParen Number(\"3.14\") Keyword(Select) Plus Ident(\"x\")\n```\n\nTokens and keywords are represented by the `sql::parser::Token` and `sql::parser::Keyword` enums\nrespectively:\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/parser/lexer.rs#L8-L47\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/parser/lexer.rs#L86-L155\n\nThe lexer takes an input string and emits tokens as an iterator:\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/parser/lexer.rs#L311-L337\n\nIt does this by repeatedly attempting to scan the next token until it reaches the end of the string\n(or errors). It can determine the kind of token by looking at the first character:\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/parser/lexer.rs#L358-L373\n\nAnd then scan across the following characters as appropriate to generate a valid token. For example,\nthis is how a quoted string (e.g. `'foo'`) is lexed into a `Token::String` (including handling of\nany escaped quotes inside the string):\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/parser/lexer.rs#L435-L451\n\nThese tokens become the input to the parser.\n\n## Abstract Syntax Tree\n\nThe end result of the parsing process will be an [abstract syntax tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree)\n(AST), which is a structured representation of a SQL statement, located in the\n[`sql::parser::ast`](https://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/parser/ast.rs) module.\n\nThe root of this tree is the `sql::parser::ast::Statement` enum, which represents all the different\nkinds of SQL statements that we support, along with their contents:\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/parser/ast.rs#L6-L145\n\nThe nested tree structure is particularly apparent with expressions, which represent values and\noperations on them. For example, the expression `2 * 3 - 4 / 2`, which evaluates to the value `4`.\n\nWe've seen in the data model section how such expressions are represented as\n`sql::types::Expression`, but before we get there we have to parse them. The parser has its own\nrepresentation `sql::parser::ast::Expression` -- this is necessary e.g. because in the AST, we\nrepresent columns as names rather than numeric indexes (we don't know yet which columns exist or\nwhat their names are, we'll get to that during planning).\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/parser/ast.rs#L147-L170\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/parser/ast.rs#L204-L234\n\nFor example, `2 * 3 - 4 / 2` is represented as:\n\n```rust\nExpression::Operator(Operator::Subtract(\n    // The left-hand operand of -\n    Expression::Operator(Operator::Multiply(\n        // The left-hand operand of *\n        Expression::Literal(Literal::Integer(2)),\n        // The right-hand operand of *\n        Expression::Literal(Literal::Integer(3)),\n    )),\n    // The right-hand operand of -\n    Expression::Operator(Operator::Divide(\n        // The left-hand operand of /\n        Expression::Literal(Literal::Integer(4)),\n        // The right-hand operand of /\n        Expression::Literal(Literal::Integer(2)),\n    )),\n))\n```\n\n## Parser\n\nThe parser, `sql::parser::Parser`, takes lexer tokens as input and builds an `ast::Statement`\nfrom them:\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/parser/parser.rs#L9-L32\n\nWe can determine the kind of statement we're parsing simply by looking at the first keyword:\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/parser/parser.rs#L109-L130\n\nLet's see how a `SELECT` statement is parsed. The different clauses in a `SELECT` (e.g. `FROM`,\n`WHERE`, etc.) must always be given in a specific order, and they always begin with the appropriate\nkeyword, so we can simply try to parse each clause in the expected order:\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/parser/parser.rs#L330-L342\n\nParsing each clause is also just a matter of parsing the expected parts in order. For example, the\ninitial `SELECT` clause is just a comma-separated list of expressions with an optional alias:\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/parser/parser.rs#L344-L365\n\nThe `FROM` clause is a comma-separated list of table name, optionally joined with other tables:\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/parser/parser.rs#L367-L427\n\nAnd the `WHERE` clause is just a predicate expression to filter by:\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/parser/parser.rs#L429-L435\n\nExpression parsing is where this gets tricky, because we have to respect the rules of operator\nprecedence and associativity. For example, according to mathematical order of operations (aka\n\"PEMDAS\") the expression `2 * 3 - 4 / 2` must be parsed as `(2 * 3) - (4 / 2)` which yields 4, not\n`2 * (3 - 4) / 2` which yields -1.\n\ntoyDB does this using the [precedence climbing algorithm](https://en.wikipedia.org/wiki/Operator-precedence_parser#Precedence_climbing_method),\nwhich is a fairly simple and compact algorithm as far as these things go. In a nutshell, it will\ngreedily and recursively group operators together as long as their precedence is the same or higher\nthan that of the operators preceding them (hence \"precedence climbing\"). For example:\n\n```\n-----   ----- Precedence 2: * and /\n------------- Precedence 1: -\n2 * 3 - 4 / 2\n```\n\nThe algorithm is documented in more detail on `Parser::parse_expression()`:\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/parser/parser.rs#L501-L696\n\n---\n\n<p align=\"center\">\n← <a href=\"sql-raft.md\">SQL Raft Replication</a> &nbsp; | &nbsp; <a href=\"sql-planner.md\">SQL Planning</a> →\n</p>"
  },
  {
    "path": "docs/architecture/sql-planner.md",
    "content": "# SQL Planning\n\nThe SQL planner in the [`sql::planner`](https://github.com/erikgrinaker/toydb/tree/c64012e29c5712d6fe028d3d5375a98b8faea266/src/sql/planner)\nmodule takes a SQL statement AST from the parser and generates an execution plan for it. We won't\nactually execute it just yet though, only figure out how to execute it.\n\n## Execution Plan\n\nA plan is represented by the `sql::planner::Plan` enum. The variant specifies the operation to\nexecute (e.g. `SELECT`, `INSERT`, `UPDATE`, `DELETE`):\n\nhttps://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/sql/planner/plan.rs#L15-L73\n\nBelow the root, the plan is typically made of up of a tree of nested `sql::planner::Node`. Each node\nemits a stream of SQL rows as output, and may take streams of input rows from child nodes.\n\nhttps://github.com/erikgrinaker/toydb/blob/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/sql/planner/plan.rs#L106-L175\n\nHere is an example, taken from the `Plan` code comment above:\n\n```sql\nSELECT title, released, genres.name AS genre\nFROM movies INNER JOIN genres ON movies.genre_id = genres.id\nWHERE released >= 2000\nORDER BY released\n```\n\nWhich results in this query plan:\n\n```\nSelect\n└─ Order: movies.released desc\n   └─ Projection: movies.title, movies.released, genres.name as genre\n      └─ Filter: movies.released >= 2000\n         └─ NestedLoopJoin: inner on movies.genre_id = genres.id\n            ├─ Scan: movies\n            └─ Scan: genres\n```\n\nRows flow from the tree leaves to the root:\n\n1. `Scan` nodes read rows from the tables `movies` and `genres`.\n2. `NestedLoopJoin` joins the rows from `movies` and `genres`.\n3. `Filter` discards rows with release dates older than 2000.\n4. `Projection` picks out the requested column values from the rows.\n5. `Order` sorts the rows by release date.\n6. `Select` returns the final rows to the client.\n\n## Scope and Name Resolution\n\nOne of the main jobs of the planner is to resolve column names to column indexes in the input rows\nof each node.\n\nIn the query example above, the `WHERE released >= 2000` filter may refer to a column `released`\nfrom either the joined `movies` table or the `genres` tables. The planner needs to figure out which\ntable has a `released` column, and also figure out which column number in the `NestedLoopJoin`\noutput rows corresponds to the `released` column (for example column number 2).\n\nThis job is further complicated by the fact that many nodes can alias, reorder, or drop columns,\nand some nodes may also refer to columns that shouldn't be part of the result at all (for example,\nit's possible to `ORDER BY` a column that won't be output by a `SELECT` projection at all, but\nthe `Order` node still needs access to the column data to sort by it).\n\nThe planner uses a `sql::planner::Scope` to keep track of which column names are currently visible,\nand which column indexes they refer to. For each node the planner builds, starting from the leaves,\nit creates a new `Scope` that contains the currently visible columns, tracking how they are modified\nand rearranged by each node.\n\nhttps://github.com/erikgrinaker/toydb/blob/6f6cec4db10bc015a37ee47ff6c7dae383147dd5/src/sql/planner/planner.rs#L577-L610\n\nWhen an AST expression refers to a column name, the planner can use `Scope::lookup_column()` to find\nout which column number the expression should take its input value from.\n\nhttps://github.com/erikgrinaker/toydb/blob/6f6cec4db10bc015a37ee47ff6c7dae383147dd5/src/sql/planner/planner.rs#L660-L686\n\n## Planner\n\nThe planner itself is `sql:planner::Planner`. It uses a `sql::engine::Catalog` to look up\ninformation about tables and columns from storage.\n\nhttps://github.com/erikgrinaker/toydb/blob/6f6cec4db10bc015a37ee47ff6c7dae383147dd5/src/sql/planner/planner.rs#L12-L20\n\nTo build an execution plan, the planner first looks at the `ast::Statement` kind to determine\nwhat kind of plan to build:\n\nhttps://github.com/erikgrinaker/toydb/blob/6f6cec4db10bc015a37ee47ff6c7dae383147dd5/src/sql/planner/planner.rs#L28-L47\n\nLet's build this `SELECT` plan from above:\n\n```sql\nSELECT title, released, genres.name AS genre\nFROM movies INNER JOIN genres ON movies.genre_id = genres.id\nWHERE released >= 2000\nORDER BY released\n```\n\nWhich should result in this plan:\n\n```\nSelect\n└─ Order: movies.released desc\n   └─ Projection: movies.title, movies.released, genres.name as genre\n      └─ Filter: movies.released >= 2000\n         └─ NestedLoopJoin: inner on movies.genre_id = genres.id\n            ├─ Scan: movies\n            └─ Scan: genres\n```\n\nThe planner is given the following (simplified) AST from the parser as input:\n\n```rust\n// A SELECT statement.\nStatement::Select {\n    // SELECT title, released, genres.name AS genre\n    select: [\n        (Column(\"title\"), None),\n        (Column(\"released\"), None),\n        (Column(\"genres.name\"), \"genre\"),\n    ]\n\n    // FROM movies INNER JOIN genres ON movies.genre_id = genres.id\n    from: [\n        Join {\n            left: Table(\"movies\"),\n            right: Table(\"genres\"),\n            type: Inner,\n            predicate: Some(\n                Equal(\n                    Column(\"movies.genre_id\"),\n                    Column(\"genres.id\"),\n                )\n            )\n        }\n    ]\n\n    // WHERE released >= 2000\n    where: Some(\n        GreaterThanOrEqual(\n            Column(\"released\"),\n            Integer(2000),\n        )\n    )\n\n    // ORDER BY released\n    order: [\n        (Column(\"released\"), Ascending),\n    ]\n}\n```\n\nThe first thing `Planner::build_select` does is to create an empty scope (which will track column\nnames and indexes) and build the `FROM` clause which will generate the initial input rows:\n\nhttps://github.com/erikgrinaker/toydb/blob/6f6cec4db10bc015a37ee47ff6c7dae383147dd5/src/sql/planner/planner.rs#L170-L179\n\nhttps://github.com/erikgrinaker/toydb/blob/6f6cec4db10bc015a37ee47ff6c7dae383147dd5/src/sql/planner/planner.rs#L283-L289\n\n`Planner::build_from()` first encounters the `ast::From::Join` item, which joins `movies` and\n`genres`. This will build a `Node::NestedLoopJoin` plan node for the join, which is the simplest and\nmost straightforward join algorithm -- it simply iterates over all rows in the `genres` table for\nevery row in the `movies` table and emits the joined rows (we'll see how to optimize it with a\nbetter join algorithm later).\n\nhttps://github.com/erikgrinaker/toydb/blob/6f6cec4db10bc015a37ee47ff6c7dae383147dd5/src/sql/planner/planner.rs#L319-L344\n\nIt first recurses into `Planner::build_from()` to build each of the `ast::From::Table` nodes for\neach table.  This will look up the table schemas in the catalog, add them to the current scope, and\nbuild a `Node::Scan` node which will emit all rows from each table. The `Node::Scan` nodes are\nplaced into the `Node::NestedLoopJoin` above.\n\nhttps://github.com/erikgrinaker/toydb/blob/6f6cec4db10bc015a37ee47ff6c7dae383147dd5/src/sql/planner/planner.rs#L312-L317\n\nWhile building the `Node::NestedLoopJoin`, it also needs to convert the join expression\n`movies.genre_id = genres.id` into a proper `sql::types::Expression`. This is done by\n`Planner::build_expression()`:\n\nhttps://github.com/erikgrinaker/toydb/blob/6f6cec4db10bc015a37ee47ff6c7dae383147dd5/src/sql/planner/planner.rs#L493-L568\n\nExpression building is mostly a direct translation from an `ast::Expression` variant to a\ncorresponding `sql::types::Expression` variant (for example from\n`ast::Expression::Operator(ast::Operator::Equal)` to `sql::types::Expression::Equal`). However, as\nmentioned earlier, `ast::Expression` contains column references by name, while\n`sql::types::Expression` contains column references as row indexes. This name resolution is done\nhere, by looking up the column names in the scope:\n\nhttps://github.com/erikgrinaker/toydb/blob/6f6cec4db10bc015a37ee47ff6c7dae383147dd5/src/sql/planner/planner.rs#L521-L523\n\nThe expression we're building is the join predicate of `Node::NestedLoopJoin`, so it operates on\njoined rows containing all columns of `movies` then all columns of `genres`. It also operates on all\ncombinations of joined rows (the [Cartesian product](https://en.wikipedia.org/wiki/Cartesian_product)),\nand the purpose of the join predicate is to determine which joined rows to actually keep. For\nexample, the full set of joined rows that are evaluated might be:\n\n| movies.id | movies.title | movies.released | movies.genre_id | genres.id | genres.name |\n|-----------|--------------|-----------------|-----------------|-----------|-------------|\n| 1         | Sicario      | 2015            | 2               | 1         | Drama       |\n| 2         | Sicario      | 2015            | 2               | 2         | Action      |\n| 3         | 21 Grams     | 2003            | 1               | 1         | Drama       |\n| 4         | 21 Grams     | 2003            | 1               | 2         | Action      |\n| 5         | Heat         | 1995            | 2               | 1         | Drama       |\n| 6         | Heat         | 1995            | 2               | 2         | Action      |\n\nThe join predicate should pick out the rows where `movies.genre_id = genres.id`. The scope will\nreflect the column layout in the example above, and can resolve the column names to zero-based row\nindexes as `#3 = #4`, which will be the final built `Expression`.\n\nNow that we've built the `FROM` clause into a `Node::NestedLoopJoin` of two `Node::Scan` nodes, we\nmove on to the `WHERE` clause. This simply builds the `WHERE` expression `released >= 2000`, like\nwe've already seen with the join predicate, and creates a `Node::Filter` node which takes its input\nrows from the `Node::NestedLoopJoin` and filters them by the given expression. Again, the scope\nkeeps track of which input columns we're getting from the join node and resolves the `released`\ncolumn reference in the expression.\n\nhttps://github.com/erikgrinaker/toydb/blob/6f6cec4db10bc015a37ee47ff6c7dae383147dd5/src/sql/planner/planner.rs#L202-L206\n\nWe then build the `SELECT` clause, which emits the `title, released, genres.name AS genre` columns.\nThis is just a list of expressions that are built in the current scope and placed into a\n`Node::Projection` (the expressions could be arbitrarily complex). However, we also have to make\nsure to update the scope with the final three columns that are output to subsequent nodes, taking\ninto account the `genre` alias for the original `genres.name` column (we won't dwell on the \"hidden\ncolumns\" mentioned there -- they're not relevant for our query).\n\nhttps://github.com/erikgrinaker/toydb/blob/6f6cec4db10bc015a37ee47ff6c7dae383147dd5/src/sql/planner/planner.rs#L214-L234\n\nFinally, we build the `ORDER BY` clause. Again, this just builds a trivial expression for `released`\nand places it into an `Node::Order` node which takes input rows from the `Node::Projection` and\nsorts them by the order expression.\n\nhttps://github.com/erikgrinaker/toydb/blob/6f6cec4db10bc015a37ee47ff6c7dae383147dd5/src/sql/planner/planner.rs#L245-L252\n\nAnd that's it. The `Node::Order` is placed into the root `Plan::Select`, and we have our final plan.\n\n```\nSelect\n└─ Order: movies.released desc\n   └─ Projection: movies.title, movies.released, genres.name as genre\n      └─ Filter: movies.released >= 2000\n         └─ NestedLoopJoin: inner on movies.genre_id = genres.id\n            ├─ Scan: movies\n            └─ Scan: genres\n```\n\nWe'll see how to execute it soon, but first we should optimize it to see if we can make it run\nfaster -- in particular, to see if we can avoid reading all movies from storage, and if we can do\nbetter than the very slow nested loop join.\n\n---\n\n<p align=\"center\">\n← <a href=\"sql-parser.md\">SQL Parsing</a> &nbsp; | &nbsp; <a href=\"sql-optimizer.md\">SQL Optimization</a> →\n</p>"
  },
  {
    "path": "docs/architecture/sql-raft.md",
    "content": "# SQL Raft Replication\n\ntoyDB uses Raft to replicate SQL storage across a cluster of nodes (see the Raft section for\ndetails). All nodes will store a full copy of the SQL database, and the Raft leader will replicate\nwrites across nodes and execute reads.\n\nRecall the Raft state machine interface `raft::State`:\n\nhttps://github.com/erikgrinaker/toydb/blob/8782c2b05f11333c1586ef248f1a13dc1c8dec4a/src/raft/state.rs#L4-L51\n\nIn toyDB, the state machine is just a `sql::engine::Local` storage engine with a thin wrapper:\n\nhttps://github.com/erikgrinaker/toydb/blob/c2b0f7f1d6cbf6e2cdc09fc0aec7b050e840ec21/src/sql/engine/raft.rs#L278-L291\n\nRaft will submit read and write commands to this state machine as binary `Vec<u8>` data, so we have\nto represent the methods of `sql::engine::Engine` as binary Raft commands. We do this as two\nenums, `sql::engine::raft::Read` and `sql::engine::raft::Write`, which we'll Bincode-encode:\n\nhttps://github.com/erikgrinaker/toydb/blob/c2b0f7f1d6cbf6e2cdc09fc0aec7b050e840ec21/src/sql/engine/raft.rs#L16-L71\n\nNotice that almost all requests include a `mvcc::TransactionState`. Most of the useful methods of\n`sql::engine::Engine` are on the `sql::engine::Transaction`, but unlike the `Local` engine, below\nRaft we can't hold on to a `Transaction` object in memory between each command -- nodes may restart\nand leadership may move, and we want client transactions to keep working despite this. Instead, we\nwill use the client-supplied `mvcc::TransactionState` to reconstruct a `Transaction` for every\ncommand via `mvcc::Transaction::resume()` and call methods on it.\n\nWhen the state machine receives a write command, it decodes it as a `Write` and calls the\nappropriate `Local` method. The result is Bincode-encoded and returned to the caller, who knows what\nreturn type to expect for a given command. The state machine also keeps track of the Raft applied\nindex of each command as a separate key in the key/value store.\n\nhttps://github.com/erikgrinaker/toydb/blob/c2b0f7f1d6cbf6e2cdc09fc0aec7b050e840ec21/src/sql/engine/raft.rs#L346-L367\n\nhttps://github.com/erikgrinaker/toydb/blob/c2b0f7f1d6cbf6e2cdc09fc0aec7b050e840ec21/src/sql/engine/raft.rs#L306-L338\n\nSimilarly, read commands are decoded as a `Read` and the appropriate `Local` method is called:\n\nhttps://github.com/erikgrinaker/toydb/blob/c2b0f7f1d6cbf6e2cdc09fc0aec7b050e840ec21/src/sql/engine/raft.rs#L369-L404\n\nThat's the state machine running below Raft. But how do we actually send these commands to Raft and\nreceive results? That's handled by the `sql::engine::Raft` implementation, which uses a channel to\nsend requests to the local Raft node (we'll see how this plumbing works in the server section):\n\nhttps://github.com/erikgrinaker/toydb/blob/c2b0f7f1d6cbf6e2cdc09fc0aec7b050e840ec21/src/sql/engine/raft.rs#L80-L95\n\nThe channel takes a `raft::Request` containing binary Raft client requests and a return channel\nwhere the Raft node can send back a `raft::Response`. The Raft engine has a few convenience methods\nto send requests and receive responses, for both read and write requests:\n\nhttps://github.com/erikgrinaker/toydb/blob/c2b0f7f1d6cbf6e2cdc09fc0aec7b050e840ec21/src/sql/engine/raft.rs#L114-L135\n\nAnd the implementation of the `sql::engine::Engine` and `sql::engine::Transaction` traits simply\nsend these requests via Raft:\n\nhttps://github.com/erikgrinaker/toydb/blob/c2b0f7f1d6cbf6e2cdc09fc0aec7b050e840ec21/src/sql/engine/raft.rs#L194-L276\n\nOne thing to note here is that we don't support streaming data via Raft, so e.g. the\n`Transaction::scan` method will buffer the entire result in a `Vec`. With a full table scan, this\nwill load the entire table into memory -- that's unfortunate, but we keep it simple.\n\nTo summarize, this is what happens when `Transaction::insert()` is called to insert a row via Raft:\n\n1. `sql::engine::raft::Transaction::insert()`: called to insert a row.\n2. `sql::engine::raft::Write::Insert`: enum representation of the insert command.\n3. `raft::Request::Write`: raft request containing the Bincode-encoded `Write::Insert` command.\n4. `sql::engine::raft::Engine::tx`: sends the `Request::Write` and response channel to Raft.\n5. `raft::Node::step()`: the `Request::Write` is given to Raft in a `Message::ClientRequest`.\n6. Raft does its replication thing, and commits the command's log entry.\n7. `raft::State::apply()`: the Bincode-encoded `Write::Insert` is passed to the state machine.\n8. `sql::engine::raft::State::apply()`: decodes the command to a `Write::Insert`.\n9. `sql::engine::raft::State::local`: contains the `Local` engine on each node.\n10. `sql::engine::local::Engine::resume()`: called to obtain the SQL/MVCC transaction.\n11. `sql::engine::local::Transaction::insert()`: the row is inserted to the local engine.\n12. `raft::RawNode::tx`: the `Ok(())` result is sent as a Bincode-encoded `Message::ClientResponse`.\n13. `sql::engine::raft::Transaction::insert()`: receives the result and returns it to the caller.\n\nThe plumbing here will be covered in more details in the server section.\n\n---\n\n<p align=\"center\">\n← <a href=\"sql-storage.md\">SQL Storage</a> &nbsp; | &nbsp; <a href=\"sql-parser.md\">SQL Parsing</a> →\n</p>"
  },
  {
    "path": "docs/architecture/sql-storage.md",
    "content": "# SQL Storage\n\nThe SQL storage engine, in the [`sql::engine`](https://github.com/erikgrinaker/toydb/tree/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/sql/engine)\nmodule, stores tables and rows. toyDB has two SQL storage implementations:\n\n* `sql::engine::Local`: local storage using a `storage::Engine` key/value store.\n* `sql::engine::Raft`: Raft-replicated storage, using `Local` on each node below Raft.\n\nThese implement the `sql::engine::Engine` trait, which specifies the SQL storage API. SQL execution\ncan use either simple local storage or Raft-replicated storage -- toyDB itself always uses the\nRaft-replicated engine, but many tests use a local in-memory engine.\n\nThe `sql::engine::Engine` trait is fully transactional, based on the `storage::MVCC` transaction\nengine discussed previously. As such, the trait just has a few methods that begin transactions --\nthe storage logic itself is implemented in the transaction, which we'll cover in next. The trait\nalso has a `session()` method to start SQL sessions for query execution, which we'll revisit in the\nexecution section.\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/sql/engine/engine.rs#L9-L29\n\nHere, we'll only look at the `Local` engine, and we'll discuss Raft replication afterwards. `Local`\nitself is just a thin wrapper around a `storage::MVCC<storage::Engine>` to create transactions:\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/engine/local.rs#L50-L97\n\n## Key/Value Representation\n\n`Local` uses a `storage::Engine` key/value store to store SQL table schemas, table rows, and\nsecondary index entries. But how do we represent these as keys and values?\n\nThe keys are represented by the `sql::engine::Key` enum, and encoded using the Keycode encoding\nthat we've discussed in the encoding section:\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/engine/local.rs#L15-L31\n\nThe values are encoded using the Bincode encoding, where the value type is given by the key:\n\n* `Key::Table` → `sql::types::Table` (table schemas)\n* `Key::Index` → `BTreeSet<sql::types::Value>` (indexed primary keys)\n* `Key::Row` → `sql::types::Row` (table rows)\n\nRecall that the Keycode encoding will store keys in sorted order. This means that all `Key::Table`\nentries come first, then all `Key::Index`, then all `Key::Row`. These are further grouped and\nsorted by their fields.\n\nFor example, consider these SQL tables containing movies and genres, with a secondary index on\n`movies.genre_id` for fast lookups of movies with a given genre:\n\n```sql\nCREATE TABLE genres (\n    id INTEGER PRIMARY KEY,\n    name STRING NOT NULL\n);\n\nCREATE TABLE movies (\n    id INTEGER PRIMARY KEY,\n    title STRING NOT NULL,\n    released INTEGER NOT NULL,\n    genre_id INTEGER NOT NULL INDEX REFERENCES genres\n);\n\nINSERT INTO genres VALUES (1, 'Drama'), (2, 'Action');\n\nINSERT INTO movies VALUES\n    (1, 'Sicario', 2015, 2),\n    (2, '21 Grams', 2003, 1),\n    (3, 'Heat', 1995, 2);\n```\n\nThis would result in the following illustrated keys and values, in the given order:\n\n```\n/Table/genres → Table { name: \"genres\", primary_key: 0, columns: ... }\n/Table/movies → Table { name: \"movies\", primary_key: 0, columns: ... }\n/Index/movies/genre_id/Integer(1) → BTreeSet { Integer(2) }\n/Index/movies/genre_id/Integer(2) → BTreeSet { Integer(1), Integer(3) }\n/Row/genres/Integer(1) → Row { Integer(1), String(\"Action\") }\n/Row/genres/Integer(2) → Row { Integer(2), String(\"Drama\") }\n/Row/movies/Integer(1) → Row { Integer(1), String(\"Sicario\"), Integer(2015), Integer(2) }\n/Row/movies/Integer(2) → Row { Integer(2), String(\"21 Grams\"), Integer(2003), Integer(1) }\n/Row/movies/Integer(3) → Row { Integer(3), String(\"Heat\"), Integer(1995), Integer(2) }\n```\n\nThus, if we want to do a full table scan of the `movies` table, we just do a prefix scan of\n`/Row/movies/`. If we want to do a secondary index lookup of all movies with `genre_id = 2`, we\nfetch `/Index/movies/genre_id/Integer(2)` and find that movies with `id = {1,3}` have this genre.\n\nTo help with prefix scans, the valid key prefixes are represented as `sql::engine::KeyPrefix`:\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/engine/local.rs#L35-L48\n\nFor a look at the actual on-disk binary storage format, see the test scripts under\n[`src/sql/testscripts/writes`](https://github.com/erikgrinaker/toydb/tree/c2b0f7f1d6cbf6e2cdc09fc0aec7b050e840ec21/src/sql/testscripts/writes),\nwhich output the logical and raw binary representation of write operations.\n\n## Schema Catalog\n\nThe `sql::engine::Catalog` trait is used to store table schemas, i.e. `sql::types::Table`. It has a\nhandful of methods for creating, dropping and fetching tables (recall that toyDB does not support\nschema changes). The `Table::name` field is used as a unique table identifier throughout.\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/sql/engine/engine.rs#L60-L79\n\nThe `Catalog` trait is also fully transactional, as it must be implemented on a transaction via the\n`type Transaction: Transaction + Catalog` trait bound on `sql::engine::Engine`.\n\nCreating a table is straightforward: insert a key/value pair with a Keycode-encoded `Key::Table`\nfor the key and a Bincode-encoded `sql::types::Table` for the value. We first check that the\ntable doesn't already exist, and validate the table schema using `Table::validate()`.\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/engine/local.rs#L340-L347\n\nSimilarly, fetching and listing tables is straightforward: just key/value gets or scans using the\nappropriate keys.\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/engine/local.rs#L390-L399\n\nDropping tables is a bit more involved, since we have to perform some validation and also delete the\nactual table rows and any secondary index entries, but it's not terribly complicated:\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/engine/local.rs#L349-L388\n\n## Row Storage and Transactions\n\nThe workhorse of the SQL storage engine is the `Transaction` trait, which provides\n[CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) operations (create, read,\nupdate, delete) on table rows and secondary index entries. For performance (especially with Raft),\nit operates on row batches rather than individual rows.\n\nhttps://github.com/erikgrinaker/toydb/blob/0839215770e31f1e693d5cccf20a68210deaaa3f/src/sql/engine/engine.rs#L31-L58\n\nThe `Local::Transaction` implementation is just a wrapper around an MVCC transaction, and the\ncommit/rollback methods just call straight through to it:\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/engine/local.rs#L99-L102\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/engine/local.rs#L182-L192\n\nTo insert new rows into a table, we first have to perform some validation: check that the table\nexists and validate the rows against the table schema (including checking for e.g. primary key\nconflicts and foreign key references). We then store the rows as a key/value pairs, using a\n`Key::Row` with the table name and primary key value. And finally, we update secondary index entries\n(if any).\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/engine/local.rs#L252-L268\n\nRow updates are similar to inserts, but in the case of a primary key change we instead delete the\nold row and insert a new one, for simplicity. Secondary index updates also have to update both the\nold and new entries.\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/engine/local.rs#L296-L337\n\nRow deletions are also similar: validate that the deletion is safe (e.g. check that there are no\nforeign key references to it), then delete the `Key::Row` keys and any secondary index entries:\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/engine/local.rs#L194-L246\n\nTo fetch rows by primary key, we simply call through to key/value gets using the appropriate\n`Key::Row`:\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/engine/local.rs#L248-L250\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/engine/local.rs#L127-L133\n\nSimilarly, index lookups fetch a `Key::Index` for the indexed value, returning matching primary\nkeys:\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/engine/local.rs#L270-L273\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/engine/local.rs#L115-L125\n\nScanning table rows just performs a prefix scan with the appropriate `KeyPrefix::Row`, returning a\nrow iterator. This can optionally also do row filtering via filter pushdowns, which we'll revisit\nwhen we look at the SQL optimizer.\n\nhttps://github.com/erikgrinaker/toydb/blob/39c6b60afc4c235f19113dc98087176748fa091d/src/sql/engine/local.rs#L275-L294\n\nAnd with that, we can now store and retrieve SQL tables and rows on disk. Let's see how to replicate\nit across nodes via Raft.\n\n---\n\n<p align=\"center\">\n← <a href=\"sql-data.md\">SQL Data Model</a> &nbsp; | &nbsp; <a href=\"sql-raft.md\">SQL Raft Replication</a> →\n</p>"
  },
  {
    "path": "docs/architecture/sql.md",
    "content": "# SQL Engine\n\nThe SQL engine provides support for the SQL query language, and is the main database interface. It\nuses a key/value store for data storage, MVCC for transactions, and Raft for replication. The SQL\nengine itself consists of several distinct components that form a pipeline:\n\n> Client → Session → Lexer → Parser → Planner → Optimizer → Executor → Storage\n\nThe SQL engine is located in the [`sql`](https://github.com/erikgrinaker/toydb/tree/b2fe7b76ee634ca6ad31616becabfddb1c03d34b/src/sql)\nmodule. We'll discuss each of the components in a bottom-up manner.\n\nThe SQL engine is tested as a whole by test scripts under\n[`src/sql/testscripts`](https://github.com/erikgrinaker/toydb/tree/9419bcf6aededf0e20b4e7485e2a5fa3e975d79f/src/sql/testscripts).\nThese typically take a raw SQL string as input, execute them against an in-memory storage engine,\nand output the result along with intermediate state such as the query plan, storage operations,\nand binary key/value data.\n\n---\n\n<p align=\"center\">\n← <a href=\"raft.md\">Raft Consensus</a> &nbsp; | &nbsp; <a href=\"sql-data.md\">SQL Data Model</a> →\n</p>"
  },
  {
    "path": "docs/architecture/storage.md",
    "content": "# Storage Engine\n\ntoyDB uses an embedded [key/value store](https://en.wikipedia.org/wiki/Key–value_database) for data\nstorage, located in the [`storage`](https://github.com/erikgrinaker/toydb/tree/213e5c02b09f1a3cac6a8bbd0a81773462f367f5/src/storage)\nmodule. This stores arbitrary keys and values as binary byte strings. The storage engine doesn't\nknow or care what the keys and values contain -- we'll see later how the SQL data model, with tables\nand rows, is mapped onto this key/value structure.\n\nThe storage engine supports simple set/get/delete operations on individual keys. It does not itself\nsupport transactions -- this is built on top, and we'll get back to it shortly.\n\nKeys are stored in sorted order. This allows range scans, where we can iterate over all key/value\npairs between two specific keys, or with a specific key prefix. This will be needed by other\ncomponents in the system, e.g. to scan all rows in a specific SQL table, to scan all versions of an\nMVCC key, to scan the tail of the Raft log, etc.\n\nThe storage engine is pluggable: there are multiple implementations, and the user can choose which\none to use in the config file. These implement the `storage::Engine` trait:\n\nhttps://github.com/erikgrinaker/toydb/blob/4804df254034c51f367d1380d389d80695cd7054/src/storage/engine.rs#L8-L58\n\nLet's look at the existing storage engine implementations.\n\n## `Memory` Storage Engine\n\nThe simplest storage engine is the `storage::Memory` engine. This is a trivial implementation which\nstores data in memory using the Rust standard library's\n[`BTreeMap`](https://doc.rust-lang.org/std/collections/struct.BTreeMap.html), without persisting\nit to disk. It is primarily used for testing.\n\nSince this is just a wrapper around the `BTreeMap` we can include it in its entirety here:\n\nhttps://github.com/erikgrinaker/toydb/blob/8f8eae0dcf70b1a0df2e853b1f6600e0c7075340/src/storage/memory.rs#L8-L77\n\n## `BitCask` Storage Engine\n\nThe main storage engine is `storage::BitCask`. This is a very simple variant of\n[BitCask](https://riak.com/assets/bitcask-intro.pdf), used in the [Riak](https://riak.com/)\ndatabase. It is kind of like the [LSM-tree](https://en.wikipedia.org/wiki/Log-structured_merge-tree)'s\nbaby cousin.\n\nhttps://github.com/erikgrinaker/toydb/blob/3e467512dca55843f0b071b3e239f14724f59a41/src/storage/bitcask.rs#L15-L55\n\ntoyDB's BitCask implementation uses a single append-only log file for storage. To write a key/value\npair, we simply append it to the file. To delete a key, we append a special tombstone value. When\nreading a key, the last entry for that key in the file is used.\n\nThe file format for a key/value pair is simply:\n\n1. The key length, as a big-endian `u32` (4 bytes).\n2. The value length, as a big-endian `i32` (4 bytes). -1 if tombstone.\n3. The binary key (n bytes).\n4. The binary value (n bytes).\n\nFor example, the key/value pair `foo=bar` would be written as follows (in hexadecimal):\n\n```\nkeylen   valuelen key    value\n00000003 00000003 666f6f 626172\n```\n\nBecause the data file is a simple log, we don't need a separate [write-ahead log](https://en.wikipedia.org/wiki/Write-ahead_logging)\nfor crash recovery -- the data file _is_ the write-ahead log.\n\nTo quickly look up key/value pairs when reading, we maintain an in-memory `KeyDir` index which maps\na key to the latest value's position in the file. All keys must therefore fit in memory.\n\nhttps://github.com/erikgrinaker/toydb/blob/3e467512dca55843f0b071b3e239f14724f59a41/src/storage/bitcask.rs#L57-L65\n\nWe initially generate this index by scanning through the entire file when it is opened:\n\nhttps://github.com/erikgrinaker/toydb/blob/3e467512dca55843f0b071b3e239f14724f59a41/src/storage/bitcask.rs#L267-L332\n\nTo write a key, we append it to the file and update the `KeyDir`:\n\nhttps://github.com/erikgrinaker/toydb/blob/3e467512dca55843f0b071b3e239f14724f59a41/src/storage/bitcask.rs#L155-L159\n\nhttps://github.com/erikgrinaker/toydb/blob/3e467512dca55843f0b071b3e239f14724f59a41/src/storage/bitcask.rs#L342-L366\n\nTo delete a key, we append a tombstone value instead:\n\nhttps://github.com/erikgrinaker/toydb/blob/3e467512dca55843f0b071b3e239f14724f59a41/src/storage/bitcask.rs#L122-L126\n\nTo read a value for a key, we look up the key's file location in the `KeyDir` index (if the key\nexists), and then read it from the file:\n\nhttps://github.com/erikgrinaker/toydb/blob/3e467512dca55843f0b071b3e239f14724f59a41/src/storage/bitcask.rs#L334-L340\n\nThe `KeyDir` uses an inner stdlib `BTreeMap` to keep track of keys. This allows range scans, where\nwe iterate over a sorted set of keys between the range bounds, loading each key from the file:\n\nhttps://github.com/erikgrinaker/toydb/blob/3e467512dca55843f0b071b3e239f14724f59a41/src/storage/bitcask.rs#L144-L146\n\nhttps://github.com/erikgrinaker/toydb/blob/3e467512dca55843f0b071b3e239f14724f59a41/src/storage/bitcask.rs#L207-L225\n\nAs keys are updated and deleted, we'll keep accumulating old versions in the log file. To remove\nthese, the log file is compacted on startup. This writes out the latest value of every live\nkey/value pair to a new file, and replaces the old file. The keys are written in sorted order, to\nmake later scans faster.\n\nhttps://github.com/erikgrinaker/toydb/blob/3e467512dca55843f0b071b3e239f14724f59a41/src/storage/bitcask.rs#L172-L195\n\n---\n\n<p align=\"center\">\n← <a href=\"overview.md\">Overview</a> &nbsp; | &nbsp; <a href=\"encoding.md\">Key/Value Encoding</a> →\n</p>"
  },
  {
    "path": "docs/architecture.md",
    "content": "Moved to [`architecture/index.md`](architecture/index.md)."
  },
  {
    "path": "docs/crate/Cargo.toml",
    "content": "[package]\nname = \"toydb\"\nversion = \"1.0.1\"\ndescription = \"A simple distributed SQL database, built for education\"\nauthors = [\"Erik Grinaker <erik@grinaker.org>\"]\nlicense = \"Apache-2.0\"\nhomepage = \"https://github.com/erikgrinaker/toydb\"\nrepository = \"https://github.com/erikgrinaker/toydb\"\nedition = \"2024\"\n"
  },
  {
    "path": "docs/crate/README.md",
    "content": "# toyDB\n\ntoyDB is a distributed SQL database in Rust, built from scratch as an educational project. Main\nfeatures:\n\n* Raft distributed consensus for linearizable state machine replication.\n\n* ACID transactions with MVCC-based snapshot isolation.\n\n* Pluggable storage engine with BitCask and in-memory backends.\n\n* Iterator-based query engine with heuristic optimization and time-travel  support.\n\n* SQL interface including joins, aggregates, and transactions.\n\ntoyDB is not distributed as a crate, see <https://github.com/erikgrinaker/toydb> for more.\n\nThis crate used to contain the [joydb](https://crates.io/crates/joydb) database. Thanks to Serhii\nPotapov for donating the crate name."
  },
  {
    "path": "docs/crate/src/lib.rs",
    "content": "//! This crate is just a simple README.md placeholder. toydb is not intended to be used as a\n//! library, and is not distributed as a crate. See <https://github.com/erikgrinaker/toydb>.\n"
  },
  {
    "path": "docs/examples.md",
    "content": "# SQL Examples\n\nThe following examples demonstrate some of toyDB's SQL features. For more details, see the\n[SQL reference](sql.md).\n\n- [Setup](#setup)\n- [Creating Tables and Data](#creating-tables-and-data)\n- [Constraints and Referential Integrity](#constraints-and-referential-integrity)\n- [Basic SQL Queries](#basic-sql-queries)\n- [Expressions](#expressions)\n- [Joins](#joins)\n- [Explain](#explain)\n- [Aggregates](#aggregates)\n- [Transactions](#transactions)\n- [Time-Travel Queries](#time-travel-queries)\n\n## Setup\n\nTo start a five-node cluster on the local machine (requires a working\n[Rust compiler](https://www.rust-lang.org/tools/install)), run:\n\n```\n$ ./cluster/run.sh\ntoydb2 19:06:28 [ INFO] Listening on 0.0.0.0:9602 (SQL) and 0.0.0.0:9702 (Raft)\ntoydb2 19:06:28 [ERROR] Failed connecting to Raft peer 127.0.0.1:9705: Connection refused\ntoydb5 19:06:28 [ INFO] Listening on 0.0.0.0:9605 (SQL) and 0.0.0.0:9705 (Raft)\n[...]\ntoydb5 19:06:29 [ INFO] Voting for toydb-d in term 1 election\ntoydb3 19:06:29 [ INFO] Voting for toydb-d in term 1 election\ntoydb4 19:06:29 [ INFO] Won election for term 1, becoming leader\n```\n\nIn a separate terminal, start a `toysql` client and check the server status:\n\n```\n$ cargo run --release --bin toysql\nConnected to toyDB node \"toydb-a\". Enter !help for instructions.\ntoydb> !status\n\nServer:    5 (leader 4 in term 1 with 5 nodes)\nRaft log:  1 committed, 0 applied, 0.000 MB (hybrid storage)\nNode logs: 1:1 2:1 3:1 4:1 5:1\nSQL txns:  0 active, 0 total (bitcask storage)\n```\n\nThe cluster is shut down by pressing Ctrl-C. Data is saved under `clusters/toydb-?/data/`,\ndelete the contents to start over.\n\n## Creating Tables and Data\n\nAs a basis for later examples, we'll create a small movie database. The following SQL statements\ncan be pasted into `toysql`:\n\n```sql\nCREATE TABLE genres (\n    id INTEGER PRIMARY KEY,\n    name STRING NOT NULL\n);\nINSERT INTO genres VALUES\n    (1, 'Science Fiction'),\n    (2, 'Action'),\n    (3, 'Drama'),\n    (4, 'Comedy');\n\nCREATE TABLE studios (\n    id INTEGER PRIMARY KEY,\n    name STRING NOT NULL\n);\nINSERT INTO studios VALUES\n    (1, 'Mosfilm'),\n    (2, 'Lionsgate'),\n    (3, 'StudioCanal'),\n    (4, 'Warner Bros'),\n    (5, 'Focus Features');\n\nCREATE TABLE movies (\n    id INTEGER PRIMARY KEY,\n    title STRING NOT NULL,\n    studio_id INTEGER NOT NULL INDEX REFERENCES studios,\n    genre_id INTEGER NOT NULL INDEX REFERENCES genres,\n    released INTEGER NOT NULL,\n    rating FLOAT\n);\nINSERT INTO movies VALUES\n    (1,  'Stalker',             1, 1, 1979, 8.2),\n    (2,  'Sicario',             2, 2, 2015, 7.6),\n    (3,  'Primer',              3, 1, 2004, 6.9),\n    (4,  'Heat',                4, 2, 1995, 8.2),\n    (5,  'The Fountain',        4, 1, 2006, 7.2),\n    (6,  'Solaris',             1, 1, 1972, 8.1),\n    (7,  'Gravity',             4, 1, 2013, 7.7),\n    (8,  '21 Grams',            5, 3, 2003, 7.7),\n    (9,  'Birdman',             4, 4, 2014, 7.7),\n    (10, 'Inception',           4, 1, 2010, 8.8),\n    (11, 'Lost in Translation', 5, 4, 2003, 7.7),\n    (12, 'Eternal Sunshine of the Spotless Mind', 5, 3, 2004, 8.3);\n```\n\ntoyDB supports some basic datatypes, as well as primary keys, foreign keys, and column indexes.\nFor more information on these, see the [SQL reference](sql.md). Schema changes such as\n`ALTER TABLE` are not supported, only `CREATE TABLE` and `DROP TABLE`.\n\nThe tables can be inspected via the `!tables` and `!table` commands:\n\n```sql\ntoydb> !tables\ngenres\nmovies\nstudios\n\ntoydb> !table genres\nCREATE TABLE genres (\n  id INTEGER PRIMARY KEY,\n  name STRING NOT NULL\n)\n```\n\n## Constraints and Referential Integrity\n\nSchemas enforce referential integrity and other constraints:\n\n```sql\ntoydb> DROP TABLE studios;\nError: Table studios is referenced by table movies column studio_id\n\ntoydb> DELETE FROM studios WHERE id = 1;\nError: Primary key 1 is referenced by table movies column studio_id\n\ntoydb> UPDATE movies SET id = 1;\nError: Primary key 1 already exists for table movies\n\ntoydb> INSERT INTO movies VALUES (13, 'Nebraska', 6, 3, 2013, 7.7);\nError: Referenced primary key 6 in table studios does not exist\n\ntoydb> INSERT INTO movies VALUES (13, 'Nebraska', NULL, 3, 2013, 7.7);\nError: NULL value not allowed for column studio_id\n\ntoydb> INSERT INTO movies VALUES (13, 'Nebraska', 'Unknown', 3, 2013, 7.7);\nError: Invalid datatype STRING for INTEGER column studio_id\n```\n\n## Basic SQL Queries\n\nMost basic SQL query functionality is supported:\n\n```sql\ntoydb> SELECT * FROM studios;\n1|Mosfilm\n2|Lionsgate\n3|StudioCanal\n4|Warner Bros\n5|Focus Features\n\ntoydb> SELECT title, rating FROM movies WHERE released >= 2000 ORDER BY rating DESC LIMIT 3;\nInception|8.8\nEternal Sunshine of the Spotless Mind|8.3\nGravity|7.7\n```\n\nColumn headers can be enabled with `!headers on`:\n\n```sql\ntoydb> !headers on\nHeaders enabled\n\ntoydb> SELECT id, name AS genre FROM genres;\nid|genre\n1|Science Fiction\n2|Action\n3|Drama\n4|Comedy\n```\n\n## Expressions\n\nAll common mathematical operators are implemented:\n\n```sql\ntoydb> SELECT 1 + 2 * 3;\n7\n\ntoydb> SELECT (1 + 2) * 4 / -3;\n-4\n\nSELECT 3! + 7 % 4 - 2 ^ 3;\n1\n```\n\n64-bit floating point arithmetic is also supported, including infinity and NaN:\n\n```sql\ntoydb> SELECT 3.14 * 2.718;\n8.53452\n\ntoydb> SELECT 1.0 / 0.0;\ninf\n\ntoydb> SELECT 1e10 ^ 8;\n100000000000000000000000000000000000000000000000000000000000000000000000000000000\n\ntoydb> SELECT 1e10 ^ 8 / INFINITY, 1e10 ^ 1e10, INFINITY / INFINITY;\n0|inf|NaN\n```\n\nAnd of course three-valued logic:\n\n```sql\ntoydb> SELECT TRUE AND TRUE, TRUE AND FALSE, TRUE AND NULL, FALSE AND NULL;\nTRUE|FALSE|NULL|FALSE\n\ntoydb> SELECT TRUE OR FALSE, FALSE OR FALSE, TRUE OR NULL, FALSE OR NULL;\nTRUE|FALSE|TRUE|NULL\n\ntoydb> SELECT NOT TRUE, NOT FALSE, NOT NULL;\nFALSE|TRUE|NULL\n```\n\nWhich would be useless without comparison operators for all types:\n\n```sql\ntoydb> SELECT 3 > 1, 3 <= 1, 3 = 3.0;\nTRUE|FALSE|TRUE\n\ntoydb> SELECT 'a' = 'A', 'foo' > 'bar', '👍' != '👎';\nFALSE|TRUE|TRUE\n\ntoydb> SELECT INFINITY > -INFINITY, NULL = NULL;\nTRUE|NULL\n```\n\n## Joins\n\nNo SQL database would be complete without joins, and toyDB supports most join types such as\ninner joins (both implicit and explicit):\n\n```sql\ntoydb> SELECT m.id, m.title, g.name FROM movies m JOIN genres g ON m.genre_id = g.id LIMIT 4;\n1|Stalker|Science Fiction\n2|Sicario|Action\n3|Primer|Science Fiction\n4|Heat|Action\n\ntoydb> SELECT m.id, m.title, g.name FROM movies m, genres g WHERE m.genre_id = g.id LIMIT 4;\n1|Stalker|Science Fiction\n2|Sicario|Action\n3|Primer|Science Fiction\n4|Heat|Action\n```\n\nLeft and right outer joins:\n\n```sql\ntoydb> SELECT s.id, s.name, g.name FROM studios s LEFT JOIN genres g ON s.id = g.id;\n1|Mosfilm|Science Fiction\n2|Lionsgate|Action\n3|StudioCanal|Drama\n4|Warner Bros|Comedy\n5|Focus Features|NULL\n\ntoydb> SELECT g.id, g.name, s.name FROM genres g RIGHT JOIN studios s ON g.id = s.id;\n1|Science Fiction|Mosfilm\n2|Action|Lionsgate\n3|Drama|StudioCanal\n4|Comedy|Warner Bros\nNULL|NULL|Focus Features\n```\n\nAnd cross joins (both implicit and explicit):\n\n```sql\ntoydb> SELECT g.name, s.name FROM genres g, studios s WHERE s.name < 'S';\nScience Fiction|Mosfilm\nScience Fiction|Lionsgate\nScience Fiction|Focus Features\nAction|Mosfilm\nAction|Lionsgate\nAction|Focus Features\nDrama|Mosfilm\nDrama|Lionsgate\nDrama|Focus Features\nComedy|Mosfilm\nComedy|Lionsgate\nComedy|Focus Features\n```\n\nWe can join on arbitrary predicates, such as joining movies with any genres whose name is\nordered after the movie's title:\n\n```sql\ntoydb>  SELECT   m.title, g.name\n        FROM     movies m JOIN genres g ON g.name > m.title\n        ORDER BY m.title, g.name;\n\n21 Grams|Action\n21 Grams|Comedy\n21 Grams|Drama\n21 Grams|Science Fiction\nBirdman|Comedy\nBirdman|Drama\nBirdman|Science Fiction\nEternal Sunshine of the Spotless Mind|Science Fiction\nGravity|Science Fiction\nHeat|Science Fiction\nInception|Science Fiction\nLost in Translation|Science Fiction\nPrimer|Science Fiction\n```\n\nAnd we can join multiple tables, even using the same table multiple times - like in this example\nwhere we find all science fiction movies released since 2000 by studios that have released any \nmovie rated 8 or higher:\n\n```sql\ntoydb> SELECT   m.id, m.title, g.name AS genre, m.released, s.name AS studio\n       FROM     movies m JOIN genres g ON m.genre_id = g.id,\n                studios s JOIN movies good ON good.studio_id = s.id AND good.rating >= 8\n       WHERE    m.studio_id = s.id AND m.released >= 2000 AND g.id = 1\n       ORDER BY m.title ASC;\n\n7|Gravity|Science Fiction|2013|Warner Bros\n10|Inception|Science Fiction|2010|Warner Bros\n5|The Fountain|Science Fiction|2006|Warner Bros\n```\n\n## Explain\n\nWhen optimizing complex queries with several joins, it can often be useful to inspect the query\nplan via an `EXPLAIN` query:\n\n```sql\ntoydb> EXPLAIN\n       SELECT   m.id, m.title, g.name AS genre, m.released, s.name AS studio\n       FROM     movies m JOIN genres g ON m.genre_id = g.id,\n                studios s JOIN movies good ON good.studio_id = s.id AND good.rating >= 8\n       WHERE    m.studio_id = s.id AND m.released >= 2000 AND g.id = 1\n       ORDER BY m.title ASC;\n\nOrder: m.title asc\n└─ Projection: m.id, m.title, g.name, m.released, s.name\n   └─ HashJoin: inner on m.studio_id = s.id\n      ├─ HashJoin: inner on m.genre_id = g.id\n      │  ├─ Filter: m.released > 2000 OR m.released = 2000\n      │  │  └─ IndexLookup: movies as m column genre_id (1)\n      │  └─ KeyLookup: genres as g (1)\n      └─ HashJoin: inner on s.id = good.studio_id\n         ├─ Scan: studios as s\n         └─ Scan: movies as good (good.rating > 8 OR good.rating = 8)\n```\n\nHere, we can see that the planner does a primary key lookup on `genres` and an index lookup on\n`movies.genre_id`, filtering the resulting movies by release year and joining them. It also\ndoes full table scans of `studios` and `movies` (to find the good movies) and joins them, pusing\nthe `rating >= 8` filter down to the `movies` table scan. The results of these two joins are also\njoined to produce the final result, which is then formatted and sorted.\n\n## Aggregates\n\nMost basic aggregate functions are supported:\n\n```sql\ntoydb> SELECT COUNT(*), MIN(rating), MAX(rating), AVG(rating), SUM(rating) FROM movies;\n12|6.9|8.8|7.841666666666668|94.10000000000001\n```\n\nWe can group by values and filter the aggregate results:\n\n```sql\ntoydb> SELECT s.id, s.name, AVG(m.rating) AS average\n       FROM movies m JOIN studios s ON m.studio_id = s.id\n       GROUP BY s.id, s.name\n       HAVING average > 7.8\n       ORDER BY average DESC, s.name ASC;\n1|Mosfilm|8.149999999999999\n4|Warner Bros|7.919999999999999\n5|Focus Features|7.900000000000001\n```\n\nAnd we can combine aggregate functions with arbitrary expressions, both inside and outside:\n\n```sql\ntoydb> SELECT s.id, s.name, ((MAX(rating^2) - MIN(rating^2)) / AVG(rating^2)) ^ (0.5) AS spread\n       FROM movies m JOIN studios s ON m.studio_id = s.id\n       GROUP BY s.id, s.name\n       HAVING MAX(rating) - MIN(rating) > 0.5\n       ORDER BY spread DESC;\n4|Warner Bros|0.6373540990222496\n5|Focus Features|0.39194971607693424\n```\n\n## Transactions\n\ntoyDB supports ACID transactions via MVCC-based snapshot isolation. This provides atomic\ntransactions with good isolation, without taking out locks or blocking reads on writes. As a basic\nexample, the below transaction is rolled back without taking effect, as opposed to `COMMIT`\nwhich would make it permanent:\n\n```sql\ntoydb> BEGIN;\nBegan transaction 131\n\ntoydb:131> INSERT INTO genres VALUES (5, 'Western');\ntoydb:131> SELECT * FROM genres;\n1|Science Fiction\n2|Action\n3|Drama\n4|Comedy\n5|Western\ntoydb:131> ROLLBACK;\nRolled back transaction 131\n\ntoydb> SELECT * FROM genres;\n1|Science Fiction\n2|Action\n3|Drama\n4|Comedy\n```\n\nWe'll demonstrate transactions by covering most common transaction anomalies given two\nconcurrent sessions, and show how toyDB prevents these anomalies in all cases but one. In these\nexamples, the left half is user A and the right is user B. Time flows downwards such that\ncommands on the same line happen at the same time.\n\n**Dirty write:** an uncommitted write by A should not be affected by a concurrent B write.\n\n```sql\na> BEGIN;\na> INSERT INTO genres VALUES (5, 'Western');\n                                                   b> INSERT INTO genres VALUES (5, 'Romance');\n                                                   Error: Serialization failure, retry transaction\na> SELECT * FROM genres WHERE id = 5;\n5|Western\n```\n\nThe serialization failure here occurs because the first write always wins. This may not be an\noptimal strategy, but it is correct in terms of preventing serialization anomalies.\n\n**Dirty read:** an uncommitted write by A should not be visible to B until committed.\n\n```sql\na> BEGIN;\na> INSERT INTO genres VALUES (5, 'Western');\n                                                  b> SELECT * FROM genres WHERE id = 5;\n                                                  No rows returned\na> COMMIT;\n                                                  b> SELECT * FROM genres WHERE id = 5;\n                                                  5|Western\n```\n\n**Lost update:** when A and B both read a value, before updating it in turn, the first write should\nnot be overwritten by the second.\n\n```sql\na> BEGIN;                                         b> BEGIN;\na> SELECT title, rating FROM movies WHERE id = 2; b> SELECT title, rating FROM movies WHERE id = 2;\nSicario|7.6                                       Sicario|7.6\na> UPDATE movies SET rating = 7.8 WHERE id = 2;\n                                                  b> UPDATE movies SET rating = 7.7 WHERE id = 2;\n                                                  Error: Serialization failure, retry transaction\na> COMMIT;\n```\n\n**Fuzzy read:** B should not see a value suddenly change in its transaction, even if A commits a \nnew value.\n\n```sql\na> BEGIN;                                         b> BEGIN;\n                                                  b> SELECT * FROM genres WHERE id = 1;\n                                                  1|Science Fiction\na> UPDATE genres SET name = 'Scifi' WHERE id = 1;\na> COMMIT;\n                                                  b> SELECT * FROM genres WHERE id = 1;\n                                                  1|Science Fiction\n                                                  b> COMMIT;\n\n                                                  b> SELECT * FROM genres WHERE id = 1;\n                                                  1|Scifi\n```\n\n**Read skew:** if A reads two values, and B modifies the second value in between the reads, A \nshould see the old second value.\n\n```sql\na> BEGIN;\na> SELECT * FROM genres WHERE id = 2;\n2|Action\n                                                  b> BEGIN;\n                                                  b> UPDATE genres SET name = 'Drama' WHERE id = 2;\n                                                  b> UPDATE genres SET name = 'Action' WHERE id = 3;\n                                                  b> COMMIT;\na> SELECT * FROM genres WHERE id = 3;\n3|Drama\n```\n\n**Phantom read:** when A runs a query with a predicate, and B commits a matching write, A should\nnot see the write when rerunning it.\n\n```sql\na> BEGIN;\na> SELECT * FROM genres WHERE id > 2;\n3|Drama\n4|Comedy\n                                                  b> INSERT INTO genres VALUES (5, 'Western');\na> SELECT * FROM genres WHERE id > 2;\n3|Drama\n4|Comedy\n```\n\n**Write skew:** when A reads row X and writes it to row Y, B should not concurrently be able to\nread row Y and write it to row X.\n\n```sql\na> BEGIN;                                         b> BEGIN;\na> SELECT * FROM genres WHERE id = 2;\n2|Action\n                                                  b> SELECT * FROM genres WHERE id = 3;\n                                                  3|Drama\n                                                  b> UPDATE genres SET name = 'Drama' WHERE id = 2;\na> UPDATE genres SET name = 'Action' WHERE id = 3;\na> COMMIT;                                        b> COMMIT;\n```\n\nHere, the writes actually go through. This anomaly is not protected against by snapshot isolation, \nand thus not by toyDB either - doing so would require implementing serializable snapshot isolation. \nHowever, this is the only common serialization anomaly not handled by toyDB, and is not among the\nmost severe.\n\n## Time-Travel Queries\n\nSince toyDB uses MVCC for transactions and keeps all historical versions, the state of the database\ncan be queried at any arbitrary point in the past. toyDB uses incremental transaction IDs as\nlogical timestamps:\n\n```sql\ntoydb> SELECT * FROM genres;\n1|Science Fiction\n2|Drama\n3|Action\n4|Comedy\n\ntoydb> BEGIN;\nBegan transaction 173\ntoydb:173> UPDATE genres SET name = 'Scifi' WHERE id = 1;\ntoydb:173> INSERT INTO genres VALUES (5, 'Western');\ntoydb:173> COMMIT;\nCommitted transaction 173\n\ntoydb> SELECT * FROM genres;\n1|Scifi\n2|Drama\n3|Action\n4|Comedy\n5|Western\n\ntoydb> BEGIN READ ONLY AS OF SYSTEM TIME 172;\nBegan read-only transaction 175 in snapshot at version 172\ntoydb@172> SELECT * FROM genres;\n1|Science Fiction\n2|Drama\n3|Action\n4|Comedy\n```"
  },
  {
    "path": "docs/references.md",
    "content": "# References\n\nThis is the main research material I used while building toyDB. It is a subset of my\n[reading list](https://github.com/erikgrinaker/readings).\n\n## Introduction\n\nAndy Pavlo's CMU lectures are an absolutely fantastic introduction to database internals:\n\n- 🎥 [CMU 15-445 Intro to Database Systems](https://www.youtube.com/playlist?list=PLSE8ODhjZXjbohkNBWQs_otTrBTrjyohi) (A Pavlo 2019)\n- 🎥 [CMU 15-721 Advanced Database Systems](https://www.youtube.com/playlist?list=PLSE8ODhjZXjasmrEd2_Yi1deeE360zv5O) (A Pavlo 2020)\n\nMartin Kleppman has written an excellent overview of database technologies and concepts, while Alex\nPetrov goes in depth on implementation of storage engines and distributed systems algorithms:\n\n- 📖 [Designing Data-Intensive Applications](https://dataintensive.net/) (M Kleppmann 2017)\n- 📖 [Database Internals](https://www.databass.dev) (A Petrov 2019)\n\n## Raft\n\nThe Raft consensus algorithm is described in a very readable paper by Diego Ongaro, and in a talk\ngiven by his advisor John Ousterhout:\n\n- 📄 [In Search of an Understandable Consensus Algorithm](https://raft.github.io/raft.pdf) (D Ongaro, J Ousterhout 2014)\n- 🎥 [Designing for Understandability: The Raft Consensus Algorithm](https://www.youtube.com/watch?v=vYp4LYbnnW8) (J Ousterhout 2016)\n\nHowever, Raft has several subtle pitfalls, and Jon Gjengset's student guide was very helpful in\ndrawing attention to these:\n\n- 🔗 [Students' Guide to Raft](https://thesquareplanet.com/blog/students-guide-to-raft/) (J Gjengset 2016)\n\n## Parsing\n\nThorsten Ball has written a very enjoyable hands-on introduction to parsers where he implements\nfirst an interpreter and then a compiler for the made-up Monkey programming language (in Go):\n\n- 📖 [Writing An Interpreter In Go](https://interpreterbook.com) (T Ball 2016) \n- 📖 [Writing A Compiler In Go](https://compilerbook.com) (T Ball 2018)\n\nThe toyDB expression parser is inspired by a blog post by Eli Bendersky describing the precedence\nclimbing algorithm, which is the algorithm I found the most elegant:\n\n- 💬 [Parsing Expressions by Precedence Climbing](https://eli.thegreenplace.net/2012/08/02/parsing-expressions-by-precedence-climbing) (E Bendersky 2012)\n\n## Transactions\n\nJepsen (i.e. Kyle Kingsbury) has an excellent overview of consistency and isolation models, which \nis very helpful in making sense of the jungle of overlapping and ill-defined terms:\n\n- 🔗 [Consistency Models](https://jepsen.io/consistency) (Jepsen 2016)\n\nFor more background on this, in particular on how snapshot isolation provided by the MVCC\ntransaction engine used in toyDB does not fit into the traditional SQL isolation levels, the\nfollowing classic papers were useful:\n\n- 📄 [A Critique of ANSI SQL Isolation Levels](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-95-51.pdf) (H Berenson et al 1995)\n- 📄 [Generalized Isolation Level Definitions](http://pmg.csail.mit.edu/papers/icde00.pdf) (A Adya, B Liskov, P ONeil 2000)\n\nAs for actually implementing MVCC, I found blog posts to be the most helpful:\n\n- 💬 [Implementing Your Own Transactions with MVCC](https://levelup.gitconnected.com/implementing-your-own-transactions-with-mvcc-bba11cab8e70) (E Chance 2015)\n- 💬 [How Postgres Makes Transactions Atomic](https://brandur.org/postgres-atomicity) (B Leach 2017)\n"
  },
  {
    "path": "docs/sql.md",
    "content": "# SQL Reference\n\n## Data Types\n\nThe following data types are supported:\n\n* `BOOLEAN` (`BOOL`): logical truth values, i.e. true and false.\n* `FLOAT` (`DOUBLE`): 64-bit signed floating point numbers, using [IEEE 754 `binary64`](https://en.wikipedia.org/wiki/binary64) encoding. Supports magnitudes of 10⁻³⁰⁷ to 10³⁰⁸ with 53-bit precision (~15 significant figures), as well as the special values infinity and NaN.\n* `INTEGER` (`INT`): 64-bit signed integer numbers with a range of ±2⁶³-1.\n* `STRING` (`TEXT`, `VARCHAR`): UTF-8 encoded strings.\n\nIn addition, the special `NULL` value is used for an unknown value, following the rules of [three-valued logic](https://en.wikipedia.org/wiki/Three-valued_logic).\n\nNumeric types are not interchangable; a float value (even without a fractional part) cannot be stored in an integer column and vice-versa.\n\n## SQL Syntax\n\n### Keywords\n\nKeywords are reserved words with special meaning in SQL statements. They are case-insensitive, and must be quoted with `\"` to be used as identifiers. The complete list is:\n\n`AS`, `ASC`, `AND`, `BEGIN`, `BOOL`, `BOOLEAN`, `BY`, `COMMIT`, `CREATE`, `CROSS`, `DEFAULT`,`DELETE`, `DESC`, `DOUBLE`, `DROP`, `EXISTS`, `EXPLAIN`, `FALSE`, `FLOAT`, `FROM`, `GROUP`, `HAVING`, `IF`, `INDEX`, `INFINITY`, `INNER`, `INSERT`, `INT`, `INTEGER`, `INTO`, `IS`, `JOIN`, `KEY`, `LEFT`, `LIKE`, `LIMIT`, `NAN`, `NOT`, `NULL`, `OF`, `OFFSET`, `ON`, `ONLY`, `OR`, `ORDER`, `OUTER`, `PRIMARY`, `READ`, `REFERENCES`, `RIGHT`, `ROLLBACK`, `SELECT`, `SET`, `STRING`, `SYSTEM`, `TABLE`, `TEXT`, `TIME`, `TRANSACTION`, `TRUE`, `UNIQUE`, `UPDATE`, `VALUES`, `VARCHAR`, `WHERE`, `WRITE`\n\n### Identifiers\n\nIdentifiers are names for database objects such as tables and columns. Unless quoted with `\"`, they must begin with a Unicode letter followed by any combination of letters, numbers, and `_`, and cannot be reserved keywords. `\"\"` can be used to escape a double quote character. They are always converted to lowercase.\n\n### Constants\n\n#### Named constants\n\nThe following keywords evaluate to constants:\n\n* `FALSE`: the boolean false value.\n* `INFINITY`: the floating-point value for infinity.\n* `NAN`: the floating-point value for NaN (not a number).\n* `NULL`: an unknown value.\n* `TRUE`: the boolean true value.\n\n#### String literals\n\nString literals are surrounded by single quotes `'`, and can contain any valid UTF-8 character. Single quotes must be escaped by an additional single quote, i.e. `''`, no other escape sequences are supported. For example:\n\n```\n'A string with ''quotes'' and emojis 😀'\n```\n\n#### Numeric literals\n\nSequences of digits `0-9` are parsed as a 64-bit signed integer. Numbers with decimal points or in scientific notation are parsed as 64-bit floating point numbers. The following pattern is supported:\n\n```\n999[.[999]][e[+-]999]\n```\n\nThe `-` prefix operator can be used to take negative numbers.\n\n### Expressions\n\nExpressions can be used wherever a value is expected, e.g. as `SELECT` columns nd `INSERT` values. They are made up of constants, a column references, an operator invocations, and a function calls.\n\nColumn references can either be unqualified, e.g. `name`, or prefixed with the relation identifier separated by `.`, e.g. `person.name`. Unqualified identifiers must be unambiguous.\n\n## SQL Operators\n\n### Logical operators\n\nLogical operators apply standard logic operations on boolean operands.\n\n* `AND`: the logical conjunction, e.g. `TRUE AND TRUE` yields `TRUE`.\n* `OR`: the logical disjunction, e.g. `TRUE OR FALSE` yields `TRUE`.\n* `NOT`: the logical negation, e.g. `NOT TRUE` yields `FALSE`.\n\nThe complete truth tables are:\n\n| `AND`       | `TRUE`  | `FALSE` | `NULL`  |\n|-------------|---------|---------|---------|\n| **`TRUE`**  | `TRUE`  | `FALSE` | `NULL`  |\n| **`FALSE`** | `FALSE` | `FALSE` | `FALSE` |\n| **`NULL`**  | `NULL`  | `FALSE` | `NULL`  |\n\n| `OR`        | `TRUE` | `FALSE` | `NULL` |\n|-------------|--------|---------|--------|\n| **`TRUE`**  | `TRUE` | `TRUE`  | `TRUE` |\n| **`FALSE`** | `TRUE` | `FALSE` | `NULL` |\n| **`NULL`**  | `TRUE` | `NULL`  | `NULL` |\n\n| `NOT`       |         |\n|-------------|---------|\n| **`TRUE`**  | `FALSE` |\n| **`FALSE`** | `TRUE`  |\n| **`NULL`**  | `NULL`  |\n\n### Comparison operators\n\nComparison operators compare values of the same data type, and return `TRUE` if the comparison holds or `FALSE` otherwise. `INTEGER` and `FLOAT` values are interchangeable. `STRING` comparisons use the string's byte values, i.e. case-sensitive with `'B' < 'a'` due to their UTF-8 code points. `FALSE` is considered lesser than `TRUE`. Comparison with `NULL` always yields `NULL` (even `NULL = NULL`).\n\nBinary operators:\n\n* `=`: equality, e.g. `1 = 1` yields `TRUE`.\n* `!=`: inequality, e.g. `1 != 2` yields `TRUE`.\n* `>`: greater than, e.g. `2 > 1` yields `TRUE`.\n* `>=`: greater than or equal, e.g. `1 >= 1` yields `TRUE`.\n* `<`: lesser than, e.g. `1 < 2` yields `TRUE`.\n* `<=`: lesser than or equal, e.g. `1 <= 1` yields `TRUE`.\n\nUnary operators:\n\n* `IS NULL`: checks if the value is `NULL`, e.g. `NULL IS NULL` yields `TRUE`.\n* `IS NOT NULL`: checks if the value is not `NULL`, e.g. `TRUE IS NOT NULL` yields `TRUE`.\n* `IS NAN`: checks if the value is a float `NAN`, e.g. `NAN IS NAN` yields `TRUE`. Errors on \n  non-float datatypes, except `NULL` which yields `NULL`.\n* `IS NOT NAN`: checks if the value is not a float `NAN`, e.g. `3.14 IS NOT NAN` yields `TRUE`.\n\n### Mathematical operators\n\nMathematical operators apply standard math operations on numeric (`INTEGER` or `FLOAT`) operands. If either operand is a `FLOAT`, both operands are converted to `FLOAT` and the result is a `FLOAT`. If either operand is `NULL`, the result is `NULL`. The special values `INFINITY` and `NAN` are handled according to the IEEE 754 spec.\n\nFor `INTEGER` operands, failure conditions such as overflow and division by zero yield an error. For `FLOAT` operands, these return `INFINITY` or `NAN` as appropriate.\n\nBinary operators:\n\n* `+`: addition, e.g. `1 + 2` yields `3`.\n* `-`: subtraction, e.g. `3 - 2` yields `1`.\n* `*`: multiplication, e.g. `3 * 2` yields `6`.\n* `/`: division, e.g. `6 / 2` yields `3`.\n* `^`: exponentiation, e.g. `2 ^ 4` yields `16`.\n* `%`: remainder, e.g. `8 % 3` yields `2`. Unlike modulo, the result has the sign of the dividend.\n\nUnary operators:\n\n* `+` (prefix): identity, e.g. `+1` yields `1`.\n* `-` (prefix): negation, e.g. `- -2` yields `2`.\n* `!` (postfix): factorial, e.g. `5!` yields `15`.\n\n### String operators\n\nString operators operate on string operands.\n\n* `LIKE`: compares a string with the given pattern, using `%` as multi-character wildcard and `_` as single-character wildcard, returning `TRUE` if the string matches the pattern - e.g. `'abc' LIKE 'a%'` yields `TRUE`.\n\n### Operator precedence\n\nThe operator precedence (order of operations) is as follows:\n\n| Precedence | Operator                | Associativity |\n|------------|-------------------------|---------------|\n| 10         | `+`, `-` (prefix)       | Right         |\n| 9          | `!` (postfix)           | Left          |\n| 8          | `^`                     | Right         |\n| 7          | `*`, `/`, `%`           | Left          |\n| 6          | `+`, `-`                | Left          |\n| 5          | `>`, `>=`, `<`, `<=`    | Left          |\n| 4          | `=`, `!=`, `LIKE`, `IS` | Left          |\n| 3          | `NOT`                   | Right         |\n| 2          | `AND`                   | Left          |\n| 1          | `OR`                    | Left          |\n\nPrecedence can be overridden by wrapping an expression in parentheses, e.g. `(1 + 2) * 3`.\n\n### Functions\n\n* `sqrt(expr)`: returns the square root of a numerical argument.\n\n### Aggregate functions\n\nAggregate function aggregate an expression across all rows, optionally grouped into buckets given by `GROUP BY`, and results can be filtered via `HAVING`.\n\n* `AVG(expr)`: returns the average of numerical values.\n\n* `COUNT(expr)`: returns the number of rows for which ***`expr`*** evaluates to a non-`NULL` value. `COUNT(*)` can be used to count all rows.\n\n* `MAX(expr)`: returns the maximum value, according to the datatype's ordering.\n\n* `MIN(expr)`: returns the minimum value, according to the datatype's ordering.\n\n* `SUM(expr)`: returns the sum of numerical values.\n\n## SQL Statements\n\n### `BEGIN`\n\nStarts a new [transaction](#transactions).\n\n<pre>\nBEGIN [ TRANSACTION ] [ READ ONLY | READ WRITE ] [ AS OF SYSTEM TIME <b><i>txn_id</i></b> ]\n</pre>\n\n* ***`txn_id`***: A past transaction ID to run a read-only transaction for, for time-travel queries.\n\n### `COMMIT`\n\nCommits an active [transaction](#transactions).\n\n### `CREATE TABLE`\n\nCreates a new table.\n\n<pre>\nCREATE TABLE <b><i>table_name</i></b> (\n    [ <b><i>column_name</i></b> <b><i>data_type</i></b> [ <b><i>column_constraint</i></b> [ ... ] ]  [ INDEX ] [, ... ] ]\n)\n\nwhere <b><i>column_constraint</i></b> is:\n\n{ NOT NULL | NULL | PRIMARY KEY | DEFAULT <b><i>expr</i></b> | REFERENCES <b><i>ref_table</i></b> | UNIQUE }\n</pre>\n\n* ***`table_name`***: The name of the table. Must be a [valid identifier](#identifiers). Errors if a table with this name already exists.\n\n* ***`column_name`***: The name of the column. Must be a [valid identifier](#identifiers), and unique within the table.\n\n* ***`data_type`***: The data type of the column, see [data types](#data-types) for valid types.\n\n* `NOT NULL`: The column may not contain `NULL` values.\n\n* `NULL`: The column may contain `NULL` values. This is the default.\n\n* `PRIMARY KEY`: The column should act as a primary key, i.e. the main row identifier. A table must have exactly one primary key column, and it must be unique and non-nullable.\n\n* `DEFAULT`***`expr`***: Specifies a default value for the column when `INSERT` statements do not give a value. ***`expr`*** can be any constant expression of an appropriate data type, e.g. `'abc'` or `1 + 2 * 3`. For nullable columns, the default value is `NULL` unless specified otherwise.\n\n* `REFERENCES`***`ref_table`***: The column is a foreign key to ***`ref_table`***'s primary key, enforcing referential integrity.\n\n* `UNIQUE`: The column may only contain unique (distinct) values. `NULL` values are not considered equal, thus a `UNIQUE` column which allows `NULL` may contain multiple `NULL` values. `PRIMARY KEY` columns are implicitly `UNIQUE`.\n\n* `INDEX`: Create an index for the column.\n\n#### Example\n\n```sql\nCREATE TABLE movie (\n    id INTEGER PRIMARY KEY,\n    title STRING NOT NULL,\n    release_year INTEGER INDEX,\n    imdb_id STRING INDEX UNIQUE,\n    bluray BOOLEAN NOT NULL DEFAULT TRUE\n)\n```\n\n### `DELETE`\n\nDeletes rows in a table.\n\n<pre>\nDELETE FROM <b><i>table_name</i></b>\n    [ WHERE <b><i>predicate</i></b> ]\n</pre>\n\nDeletes rows where ***`predicate`*** evaluates to `TRUE`, or all rows if no `WHERE` clause is given.\n\n* ***`table_name`***: the table to delete from. Errors if it does not exist.\n\n* ***`predicate`***: an expression which determines which rows to delete by evaluting to `TRUE`. Must evaluate to a `BOOLEAN` or `NULL`, otherwise an error is returned.\n\n#### Example\n\n```sql\nDELETE FROM movie\nWHERE release_year < 2000 AND bluray = FALSE\n```\n\n### `DROP TABLE`\n\nDeletes a table and all contained data. Errors if the table does not\nexist, unless `IF EXISTS` is given.\n\n<pre>\nDROP TABLE [ IF EXISTS ] <b><i>table_name</i></b>\n</pre>\n\n* ***`table_name`***: the table to delete.\n\n### `EXPLAIN`\n\nOutputs the execution plan for the given statement.\n\n<pre>\nEXPLAIN [ <b><i>statement</i></b> ]\n</pre>\n\n### `INSERT`\n\nInserts rows into a table.\n\n<pre>\nINSERT INTO <b><i>table_name</i></b>\n    [ ( <b><i>column_name</i></b> [, ... ] ) ]\n    VALUES ( <b><i>expression</i></b> [, ... ] ) [, ... ]\n</pre>\n\nIf column names are given, an identical number of values must be given. If no column names are given, values must be given in the table's column order. Omitted columns will get a default value if specified, otherwise an error will be returned.\n\n* ***`table_name`***: the table to insert into. Errors if it does not exist.\n\n* ***`column_name`***: a column to insert into in the given table. Errors if it does not exist.\n\n* ***`expression`***: an expression to insert into the corresponding column. Must be a constant expression, i.e. it cannot refer to table columns.\n\n#### Example\n\n```sql\nINSERT INTO movie\n    (id, title, release_year)\nVALUES\n    (1, 'Sicario', 2015),\n    (2, 'Stalker', 1979),\n    (3, 'Her', 2013)\n```\n\n### `ROLLBACK`\n\nRolls back an active [transaction](#transactions).\n\n### `SELECT`\n\nSelects rows from a table.\n\n<pre>\nSELECT [ * | <b><i>expression</i></b> [ [ AS ] <b><i>output_name</i></b> [, ...] ] ]\n    [ FROM <b><i>from_item</i></b> [, ...] ]\n    [ WHERE <b><i>predicate</i></b> ]\n    [ GROUP BY <b><i>group_expr</i></b> [, ...] ]\n    [ HAVING <b><i>having_expr</i></b> ]\n    [ ORDER BY <b><i>order_expr</i></b> [ ASC | DESC ] [, ...] ]\n    [ LIMIT <b><i>count</i></b> ]\n    [ OFFSET <b><i>start</i></b> ]\n\nwhere <b><i>from_item</i></b> is one of:\n\n<b><i>table_name</i></b> [ [ AS ] <b><i>alias</i></b> ]\n<b><i>from_item</i></b> <b><i>join_type</i></b> <b><i>from_item</i></b> [ ON <b><i>join_predicate</i></b> ]\n\nwhere <b><i>join_type</i></b> is one of:\n\nCROSS JOIN\n[ INNER ] JOIN\nLEFT [ OUTER ] JOIN\nRIGHT [ OUTER ] JOIN\n\n</pre>\n\nFetches rows or expressions, either from table ***`table_name`*** (if given) or generated.\n\n* ***`expression`***: [expression](#expressions) to fetch (can be a simple column name).\n\n* ***`output_name`***: output column [identifier](#identifier), defaults to column name (if single column) otherwise nothing (displayed as `?`).\n\n* ***`table_name`***: table to fetch rows from.\n\n* ***`alias`***: table alias.\n\n* ***`predicate`***: only return rows for which this [expression](#expressions) evaluates to `TRUE`.\n\n* ***`group_expr`***: an expression to group aggregates by. Non-aggregate `SELECT` expressions must either reference a column given in `group_expr`, be idential with a `group_expr`, or have an `output_name` that is referenced by a `group_expr` column.\n\n* ***`having_expr`***: only return aggregate results for which this [expression](#expressions) evaluates to `TRUE`.\n\n* ***`order_expr`***: order rows by this expression (can be a simple column name).\n\n* ***`count`***: maximum number of rows to return. Must be a constant integer expression.\n\n* ***`start`***: number of rows to skip. Must be a constant integer expression.\n\n* ***`join_predicate`***: only return rows for which this [expression](#expressions) evaluates to `TRUE`.\n\nJoin types:\n\n* `CROSS JOIN`: returns the Carthesian product of the joined tables. Does not accept a join predicate (`ON` clause).\n\n* `INNER JOIN`: returns the rows of the tables' Carthesian product for which  ***`join_predicate`*** evaluates to `TRUE`.\n\n* `LEFT OUTER JOIN`: returns the rows joined on the ***`join_predicate`***, or for any rows in the left table that does not have a match in the right table a single row is returned with the right table's columns set to `NULL`.\n\n* `RIGHT OUTER JOIN`: the same as a `LEFT OUTER JOIN` but with the left and right tables switched.\n\n#### Example\n\n```sql\nSELECT id, title, 2020 - released AS age\nFROM movies\nWHERE released >= 2000 AND ultrahd\nORDER BY released DESC, title ASC\nLIMIT 10\nOFFSET 10\n```\n\n### `UPDATE`\n\nUpdates rows in a table.\n\n<pre>\nUPDATE <b><i>table_name</i></b>\n    SET <b><i>column_name</i></b> = <b><i>expression</i></b> | DEFAULT [, ... ]\n    [ WHERE <b><i>predicate</i></b> ]\n</pre>\n\nUpdates columns given by ***`column_name`*** to the corresponding ***`expression`*** for all rows where ***`predicate`*** evaluates to `TRUE`. If no `WHERE` clause is given, all rows are updated.\n\n* ***`table_name`***: the table to update. Errors if it does not exist.\n\n* ***`column_name`***: a column to update. Errors if it does not exist.\n\n* ***`expression`***: an expression whose evaluated value will be set for the corresponding column and row. Expressions can refer to column values, and must evaluate to the same datatype as the updated column. Using `DEFAULT` will set the column's default value, if any.\n\n* ***`predicate`***: an expression which determines which rows to update by evaluting to `TRUE`. Must evaluate to a `BOOLEAN` or `NULL`, otherwise an error is returned.\n\n#### Example\n\n```sql\nUPDATE movie\nSET bluray = TRUE\nWHERE release_year >= 2000 AND bluray = FALSE\n```\n\n## Transactions\n\ntoyDB supports ACID transactions using MVCC-based snapshot isolation, protecting from the following anomalies: dirty writes, dirty reads, lost updates, fuzzy reads, read skew, and phantom reads. However, write skew anomalies are possible since serializable snapshot isolation is not implemented.\n\nA new transaction is started with `BEGIN`, and ended with either `COMMIT` (atomically writing all changes) or `ROLLBACK` (discarding all changes). If any conflicts occur between concurrent transactions, the lowest transaction ID wins and the others will fail with a serialization error and must retry.\n\nAll past data is versioned and retained, and can be queried as of a given transaction ID via `BEGIN TRANSACTION READ ONLY AS OF SYSTEM TIME <txn_id>`.\n\nA transaction is still valid for use if a contained statement returns an error. It is up to the client to take appropriate action.\n"
  },
  {
    "path": "docs/tools/update-links.py",
    "content": "#!/usr/bin/env python3\n#\n# Updates GitHub code links to the latest commit SHA.\n\nimport os, re, sys, argparse\nimport requests\n\nGITHUB_API = \"https://api.github.com\"\n\ndef get_latest_sha(owner, repo, path, token):\n    url = f\"{GITHUB_API}/repos/{owner}/{repo}/commits\"\n    headers = {}\n    if token:\n        headers[\"Authorization\"] = f\"token {token}\"\n    params = {\"path\": path, \"sha\": \"main\", \"per_page\": 1}\n    resp = requests.get(url, headers=headers, params=params)\n    resp.raise_for_status()\n    data = resp.json()\n    return data[0][\"sha\"] if data else None\n\ndef process_markdown(text, token):\n    pattern = re.compile(\n        r\"https://github\\.com/(?P<owner>[^/]+)/(?P<repo>[^/]+)/blob/\"\n        r\"(?P<oldsha>[0-9a-f]{7,40})/(?P<path>[^#)\\s]+)\"\n    )\n    cache = {}\n    def replacer(m):\n        print(f\"Checking {m.group(0)}\")\n        owner, repo, oldsha, path = m.group(\"owner\",\"repo\",\"oldsha\",\"path\")\n        key = (owner, repo, path)\n        print(f\"Key: {key}\")\n        if key not in cache:\n            cache[key] = get_latest_sha(owner, repo, path, token)\n        newsha = cache[key]\n        if newsha and newsha != oldsha:\n            print(f\"Updating {m.group(0)} to {newsha}\")\n            return m.group(0).replace(oldsha, newsha)\n        return m.group(0)\n    return pattern.sub(replacer, text)\n\ndef main():\n    p = argparse.ArgumentParser(description=\"Update GitHub blob links to latest SHAs\")\n    p.add_argument(\"file\", nargs=\"?\", help=\"Markdown file to update (defaults to stdin/stdout)\")\n    args = p.parse_args()\n    token = os.getenv(\"GITHUB_TOKEN\")\n    if args.file:\n        text = open(args.file, encoding=\"utf-8\").read()\n        updated = process_markdown(text, token)\n        with open(args.file, \"w\", encoding=\"utf-8\") as f:\n            f.write(updated)\n    else:\n        text = sys.stdin.read()\n        sys.stdout.write(process_markdown(text, token))\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "rust-toolchain",
    "content": "1.93.1"
  },
  {
    "path": "rustfmt.toml",
    "content": "use_small_heuristics = \"Max\"\n"
  },
  {
    "path": "src/bin/toydb.rs",
    "content": "//! The toyDB server. Takes configuration from a config file (default\n//! config/toydb.yaml) or corresponding TOYDB_ environment variables. Listens\n//! for SQL clients (default port 9601) and Raft connections from other toyDB\n//! peers (default port 9701). The Raft log and SQL database are stored at\n//! data/raft and data/sql by default.\n//!\n//! Use the toysql command-line client to connect to the server.\n\n#![warn(clippy::all)]\n\nuse std::collections::HashMap;\nuse std::path::Path;\n\nuse clap::Parser as _;\nuse serde::Deserialize;\n\nuse toydb::Server;\nuse toydb::errinput;\nuse toydb::error::Result;\nuse toydb::raft;\nuse toydb::sql;\nuse toydb::storage;\n\nfn main() {\n    if let Err(error) = Command::parse().run() {\n        eprintln!(\"Error: {error}\")\n    }\n}\n\n/// The toyDB server configuration. Can be provided via config file (default\n/// config/toydb.yaml) or TOYDB_ environment variables.\n#[derive(Debug, Deserialize)]\nstruct Config {\n    /// The node ID. Must be unique in the cluster.\n    id: raft::NodeID,\n    /// The other nodes in the cluster, and their Raft TCP addresses.\n    peers: HashMap<raft::NodeID, String>,\n    /// The Raft listen address.\n    listen_raft: String,\n    /// The SQL listen address.\n    listen_sql: String,\n    /// The log level.\n    log_level: String,\n    /// The path to this node's data directory. The Raft log is stored in\n    /// the file \"raft\", and the SQL state machine in \"sql\".\n    data_dir: String,\n    /// The Raft storage engine: bitcask or memory.\n    storage_raft: String,\n    /// The SQL storage engine: bitcask or memory.\n    storage_sql: String,\n    /// If false, don't fsync Raft log writes to disk. Disabling this\n    /// will yield much better write performance, but may lose data on\n    /// host crashes which compromises Raft safety guarantees.\n    fsync: bool,\n    /// The garbage fraction threshold at which to trigger compaction.\n    compact_threshold: f64,\n    /// The minimum bytes of garbage before triggering compaction.\n    compact_min_bytes: u64,\n}\n\nimpl Config {\n    /// Loads the configuration from the given file.\n    fn load(file: &str) -> Result<Self> {\n        Ok(config::Config::builder()\n            .set_default(\"id\", \"1\")?\n            .set_default(\"listen_sql\", \"localhost:9601\")?\n            .set_default(\"listen_raft\", \"localhost:9701\")?\n            .set_default(\"log_level\", \"info\")?\n            .set_default(\"data_dir\", \"data\")?\n            .set_default(\"storage_raft\", \"bitcask\")?\n            .set_default(\"storage_sql\", \"bitcask\")?\n            .set_default(\"fsync\", true)?\n            .set_default(\"compact_threshold\", 0.2)?\n            .set_default(\"compact_min_bytes\", 1_000_000)?\n            .add_source(config::File::with_name(file))\n            .add_source(config::Environment::with_prefix(\"TOYDB\"))\n            .build()?\n            .try_deserialize()?)\n    }\n}\n\n/// The toyDB server command.\n#[derive(clap::Parser)]\n#[command(about = \"Starts a toyDB server.\", version, propagate_version = true)]\nstruct Command {\n    /// The configuration file path.\n    #[arg(short = 'c', long, default_value = \"config/toydb.yaml\")]\n    config: String,\n}\n\nimpl Command {\n    /// Runs the toyDB server.\n    fn run(self) -> Result<()> {\n        // Load the configuration.\n        let cfg = Config::load(&self.config)?;\n\n        // Initialize logging.\n        let loglevel = cfg.log_level.parse()?;\n        let mut logconfig = simplelog::ConfigBuilder::new();\n        if loglevel != simplelog::LevelFilter::Debug {\n            logconfig.add_filter_allow_str(\"toydb\");\n        }\n        simplelog::SimpleLogger::init(loglevel, logconfig.build())?;\n\n        // Initialize the Raft log storage engine.\n        let datadir = Path::new(&cfg.data_dir);\n        let mut raft_log = match cfg.storage_raft.as_str() {\n            \"bitcask\" | \"\" => {\n                let engine = storage::BitCask::new_maybe_compact(\n                    datadir.join(\"raft\"),\n                    cfg.compact_threshold,\n                    cfg.compact_min_bytes,\n                )?;\n                raft::Log::new(Box::new(engine))?\n            }\n            \"memory\" => raft::Log::new(Box::new(storage::Memory::new()))?,\n            name => return errinput!(\"invalid Raft storage engine {name}\"),\n        };\n        raft_log.enable_fsync(cfg.fsync);\n\n        // Initialize the SQL storage engine.\n        let raft_state: Box<dyn raft::State> = match cfg.storage_sql.as_str() {\n            \"bitcask\" | \"\" => {\n                let engine = storage::BitCask::new_maybe_compact(\n                    datadir.join(\"sql\"),\n                    cfg.compact_threshold,\n                    cfg.compact_min_bytes,\n                )?;\n                Box::new(sql::engine::Raft::new_state(engine)?)\n            }\n            \"memory\" => Box::new(sql::engine::Raft::new_state(storage::Memory::new())?),\n            name => return errinput!(\"invalid SQL storage engine {name}\"),\n        };\n\n        // Start the server.\n        Server::new(cfg.id, cfg.peers, raft_log, raft_state)?\n            .serve(&cfg.listen_raft, &cfg.listen_sql)\n    }\n}\n"
  },
  {
    "path": "src/bin/toydump.rs",
    "content": "//! toydump is a debug tool that prints a toyDB BitCask database in\n//! human-readable form. It can print both the SQL database and the Raft log\n//! (via --raft). It only outputs live BitCask data, not garbage entries.\n\n#![warn(clippy::all)]\n\nuse clap::Parser as _;\n\nuse toydb::encoding::format::{self, Formatter as _};\nuse toydb::error::Result;\nuse toydb::storage::{BitCask, Engine as _};\n\nfn main() {\n    if let Err(error) = Command::parse().run() {\n        eprintln!(\"Error: {error}\")\n    }\n}\n\n/// The toydump command.\n#[derive(clap::Parser)]\n#[command(about = \"Prints toyDB file contents.\", version, propagate_version = true)]\nstruct Command {\n    /// The BitCask file to dump (SQL database unless --raft).\n    file: String,\n    /// The file is a Raft log, not SQL database.\n    #[arg(long)]\n    raft: bool,\n    /// Also show raw key and value.\n    #[arg(long)]\n    raw: bool,\n}\n\nimpl Command {\n    /// Runs the command.\n    fn run(self) -> Result<()> {\n        let mut engine = BitCask::new(self.file.into())?;\n        let mut scan = engine.scan(..);\n        while let Some((key, value)) = scan.next().transpose()? {\n            let mut string = match self.raft {\n                true => format::Raft::<format::SQLCommand>::key_value(&key, &value),\n                false => format::MVCC::<format::SQL>::key_value(&key, &value),\n            };\n            if self.raw {\n                string = format!(\"{string} [{}]\", format::Raw::key_value(&key, &value))\n            }\n            println!(\"{string}\");\n        }\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "src/bin/toysql.rs",
    "content": "//! toySQL is a command-line client for toyDB. It connects to a toyDB node\n//! (default localhost:9601) and executes SQL statements against it via an\n//! interactive shell interface. Command history is stored in .toysql.history.\n\n#![warn(clippy::all)]\n\nuse std::path::PathBuf;\n\nuse clap::Parser as _;\nuse itertools::Itertools as _;\nuse rustyline::error::ReadlineError;\nuse rustyline::history::DefaultHistory;\nuse rustyline::validate::{ValidationContext, ValidationResult, Validator};\nuse rustyline::{Editor, Modifiers};\nuse rustyline_derive::{Completer, Helper, Highlighter, Hinter};\n\nuse toydb::Client;\nuse toydb::errinput;\nuse toydb::error::Result;\nuse toydb::sql::execution::StatementResult;\nuse toydb::sql::parser::{Lexer, Token};\n\nfn main() {\n    if let Err(error) = Command::parse().run() {\n        eprintln!(\"Error: {error}\");\n    }\n}\n\n/// The toySQL command.\n#[derive(clap::Parser)]\n#[command(about = \"A toyDB client.\", version, propagate_version = true)]\nstruct Command {\n    /// A SQL statement to execute, then exit.\n    #[arg()]\n    statement: Option<String>,\n    /// Host to connect to.\n    #[arg(short = 'H', long, default_value = \"localhost\")]\n    host: String,\n    /// Port number to connect to.\n    #[arg(short = 'p', long, default_value = \"9601\")]\n    port: u16,\n}\n\nimpl Command {\n    /// Runs the command.\n    fn run(self) -> Result<()> {\n        let mut shell = Shell::new(&self.host, self.port)?;\n        match self.statement {\n            Some(statement) => shell.execute(&statement),\n            None => shell.run(),\n        }\n    }\n}\n\n/// An interactive toySQL shell.\nstruct Shell {\n    /// The toyDB client.\n    client: Client,\n    /// The Rustyline command editor.\n    editor: Editor<InputValidator, DefaultHistory>,\n    /// The path to the history file, if any.\n    history_path: Option<PathBuf>,\n    /// If true, SELECT column headers will be displayed.\n    show_headers: bool,\n}\n\nimpl Shell {\n    /// Creates a new shell connected to the given server.\n    fn new(host: &str, port: u16) -> Result<Self> {\n        let client = Client::connect((host, port))?;\n        // Set up Rustyline. Make sure multiline pastes are handled normally.\n        let mut editor = Editor::new()?;\n        editor.set_helper(Some(InputValidator));\n        editor.bind_sequence(\n            rustyline::KeyEvent(rustyline::KeyCode::BracketedPasteStart, Modifiers::NONE),\n            rustyline::Cmd::Noop,\n        );\n        let history_path =\n            std::env::var_os(\"HOME\").map(|home| PathBuf::from(home).join(\".toysql.history\"));\n        Ok(Self { client, editor, history_path, show_headers: false })\n    }\n\n    /// Executes a SQL statement or ! command.\n    fn execute(&mut self, input: &str) -> Result<()> {\n        if input.starts_with('!') {\n            self.execute_command(input)\n        } else if !input.is_empty() {\n            self.execute_sql(input)\n        } else {\n            Ok(())\n        }\n    }\n\n    /// Executes a toySQL ! command (e.g. !help)\n    fn execute_command(&mut self, input: &str) -> Result<()> {\n        let mut input = input.split_ascii_whitespace();\n        let Some(command) = input.next() else {\n            return errinput!(\"expected command\");\n        };\n        let args = input.collect_vec();\n\n        match (command, args.as_slice()) {\n            // Toggles column headers.\n            (\"!headers\", []) => {\n                self.show_headers = !self.show_headers;\n                match self.show_headers {\n                    true => println!(\"Headers enabled\"),\n                    false => println!(\"Headers disabled\"),\n                }\n            }\n            (\"!headers\", _) => return errinput!(\"!headers takes no arguments\"),\n\n            // Displays help.\n            (\"!help\", []) => println!(\n                r#\"\nEnter a SQL statement terminated by a semicolon (;) to execute it, or Ctrl-D to\nexit. The following commands are also available:\n\n    !headers           Toggles column headers\n    !help              This help message\n    !status            Display server status\n    !table NAME        Display a table schema\n    !tables            List tables\n\"#\n            ),\n            (\"!help\", _) => return errinput!(\"!help takes no arguments\"),\n\n            // Displays server status.\n            (\"!status\", []) => {\n                let status = self.client.status()?;\n                println!(\n                    r#\"\nServer:       n{server} with Raft leader n{leader} in term {term} for {nodes} nodes\nRaft log:     {committed} committed, {applied} applied, {raft_size} MB, {raft_garbage}% garbage ({raft_storage} engine)\nReplication:  {raft_match}\nSQL storage:  {sql_keys} keys, {sql_size} MB logical, {nodes}x {sql_disk_size} MB disk, {sql_garbage}% garbage ({sql_storage} engine)\nTransactions: {active_txns} active, {versions} total\n\"#,\n                    server = status.server,\n                    leader = status.raft.leader,\n                    term = status.raft.term,\n                    nodes = status.raft.match_index.len(),\n                    committed = status.raft.commit_index,\n                    applied = status.raft.applied_index,\n                    raft_size =\n                        format_args!(\"{:.3}\", status.raft.storage.size as f64 / 1_000_000.0),\n                    raft_garbage =\n                        format_args!(\"{:.0}\", status.raft.storage.garbage_disk_percent()),\n                    raft_storage = status.raft.storage.name,\n                    raft_match =\n                        status.raft.match_index.iter().map(|(n, m)| format!(\"n{n}:{m}\")).join(\" \"),\n                    sql_keys = status.mvcc.storage.keys,\n                    sql_size = format_args!(\"{:.3}\", status.mvcc.storage.size as f64 / 1_000_000.0),\n                    sql_disk_size =\n                        format_args!(\"{:.3}\", status.mvcc.storage.disk_size as f64 / 1_000_000.0),\n                    sql_garbage = format_args!(\"{:.0}\", status.mvcc.storage.garbage_disk_percent()),\n                    sql_storage = status.mvcc.storage.name,\n                    active_txns = status.mvcc.active_txns,\n                    versions = status.mvcc.versions,\n                )\n            }\n            (\"!status\", _) => return errinput!(\"!status takes no arguments\"),\n\n            (\"!table\", [name]) => println!(\"{}\", self.client.get_table(name)?),\n            (\"!table\", _) => return errinput!(\"!table takes 1 argument\"),\n\n            (\"!tables\", []) => self.client.list_tables()?.iter().for_each(|t| println!(\"{t}\")),\n            (\"!tables\", _) => return errinput!(\"!tables takes no arguments\"),\n\n            (command, _) => return errinput!(\"unknown command {command}\"),\n        }\n        Ok(())\n    }\n\n    /// Executes a SQL statement and displays the results.\n    fn execute_sql(&mut self, statement: &str) -> Result<()> {\n        use StatementResult::*;\n        match self.client.execute(statement)? {\n            Begin(state) => match state.read_only {\n                true => println!(\"Began read-only transaction at version {}\", state.version),\n                false => println!(\"Began transaction {}\", state.version),\n            },\n            Commit { version } => println!(\"Committed transaction {version}\"),\n            Rollback { version } => println!(\"Rolled back transaction {version}\"),\n            Insert { count } => println!(\"Inserted {count} rows\"),\n            Delete { count } => println!(\"Deleted {count} rows\"),\n            Update { count } => println!(\"Updated {count} rows\"),\n            CreateTable { name } => println!(\"Created table {name}\"),\n            DropTable { name, existed } => match existed {\n                true => println!(\"Dropped table {name}\"),\n                false => println!(\"Table {name} does not exist\"),\n            },\n            Explain(plan) => println!(\"{plan}\"),\n            Select { columns, rows } => {\n                if self.show_headers {\n                    println!(\"{}\", columns.iter().map(|c| c.as_header()).join(\", \"));\n                }\n                for row in rows {\n                    println!(\"{}\", row.iter().join(\", \"));\n                }\n            }\n        }\n        Ok(())\n    }\n\n    /// Prompts the user for input. Returns None if the shell should close.\n    fn prompt(&mut self) -> rustyline::Result<String> {\n        let prompt = match self.client.txn() {\n            Some(txn) if txn.read_only => format!(\"toydb@{}> \", txn.version),\n            Some(txn) => format!(\"toydb:{}> \", txn.version),\n            None => \"toydb> \".to_string(),\n        };\n        self.editor.readline(&prompt)\n    }\n\n    /// Runs the interactive shell.\n    fn run(&mut self) -> Result<()> {\n        // Load the history file, if any.\n        if let Some(history_path) = &self.history_path {\n            match self.editor.load_history(history_path) {\n                Ok(()) => {}\n                Err(ReadlineError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => {}\n                Err(error) => return Err(error.into()),\n            }\n        }\n\n        // Print welcome message.\n        let server = self.client.status()?.server;\n        println!(\"Connected to toyDB node n{server}. Enter !help for instructions.\");\n\n        // Prompt for commands and execute them.\n        loop {\n            let input = match self.prompt() {\n                Ok(input) => input.trim().to_string(),\n                Err(ReadlineError::Interrupted) => continue,\n                Err(ReadlineError::Eof) => break,\n                Err(error) => return Err(error.into()),\n            };\n            self.editor.add_history_entry(&input)?;\n            if let Err(error) = self.execute(&input) {\n                eprintln!(\"Error: {error}\");\n            };\n        }\n\n        // Save the history file.\n        if let Some(history_path) = &self.history_path {\n            self.editor.save_history(history_path)?;\n        }\n        Ok(())\n    }\n}\n\n/// A Rustyline helper for multiline editing. After a new line is entered, it\n/// determines whether the input makes up a complete SQL statement that should\n/// be submitted to the server (i.e. it's terminated by ;), or wait for further\n/// input.\n#[derive(Completer, Helper, Highlighter, Hinter)]\nstruct InputValidator;\n\nimpl Validator for InputValidator {\n    fn validate(&self, ctx: &mut ValidationContext) -> rustyline::Result<ValidationResult> {\n        let input = ctx.input();\n        // Empty lines and ! commands are ready.\n        if input.is_empty() || input.starts_with('!') || input == \";\" {\n            return Ok(ValidationResult::Valid(None));\n        }\n        // For SQL statements, just look for any semicolon or lexer error, and\n        // rely on the server for further validation and error handling.\n        if Lexer::new(input).any(|r| matches!(r, Ok(Token::Semicolon) | Err(_))) {\n            return Ok(ValidationResult::Valid(None));\n        }\n        // Otherwise, wait for more input.\n        Ok(ValidationResult::Incomplete)\n    }\n\n    fn validate_while_typing(&self) -> bool {\n        false // only check after completed lines\n    }\n}\n"
  },
  {
    "path": "src/bin/workload.rs",
    "content": "//! Runs toyDB workload benchmarks. By default, it assumes a running 5-node\n//! cluster as launched via cluster/run.sh, but this can be modified via -H.\n//! For example, a read-only workload can be run as:\n//!\n//! cargo run --release --bin workload -- read\n//!\n//! See --help for a list of available workloads and arguments.\n\n#![warn(clippy::all)]\n\nuse std::cmp::min;\nuse std::collections::HashSet;\nuse std::io::Write as _;\nuse std::time::{Duration, Instant};\n\nuse clap::Parser;\nuse hdrhistogram::Histogram;\nuse itertools::Itertools as _;\nuse rand::SeedableRng as _;\nuse rand::distr::Distribution as _;\nuse rand::rngs::StdRng;\nuse rand::seq::IndexedRandom as _;\n\nuse toydb::error::Result;\nuse toydb::sql::types::{Row, Rows};\nuse toydb::{Client, StatementResult};\n\nfn main() {\n    let Command { runner, subcommand } = Command::parse();\n    let result = match subcommand {\n        Subcommand::Read(read) => runner.run(read),\n        Subcommand::Write(write) => runner.run(write),\n        Subcommand::Bank(bank) => runner.run(bank),\n    };\n    if let Err(error) = result {\n        eprintln!(\"Error: {error}\")\n    }\n}\n\n/// Handles command-line parsing.\n#[derive(clap::Parser)]\n#[command(about = \"Runs toyDB workload benchmarks.\", version, propagate_version = true)]\nstruct Command {\n    #[command(flatten)]\n    runner: Runner,\n\n    #[command(subcommand)]\n    subcommand: Subcommand,\n}\n\n#[derive(clap::Subcommand)]\nenum Subcommand {\n    Read(Read),\n    Write(Write),\n    Bank(Bank),\n}\n\n/// Runs a workload benchmark.\n#[derive(clap::Args)]\nstruct Runner {\n    /// Hosts to connect to (optionally with port number).\n    #[arg(\n        short = 'H',\n        long,\n        value_delimiter = ',',\n        default_value = \"localhost:9601,localhost:9602,localhost:9603,localhost:9604,localhost:9605\"\n    )]\n    hosts: Vec<String>,\n\n    /// Number of concurrent workers to spawn.\n    #[arg(short, long, default_value = \"16\")]\n    concurrency: usize,\n\n    /// Number of transactions to execute.\n    #[arg(short = 'n', long, default_value = \"100000\")]\n    count: usize,\n\n    /// Seed to use for random number generation.\n    #[arg(short, long, default_value = \"16791084677885396490\")]\n    seed: u64,\n}\n\nimpl Runner {\n    /// Runs the specified workload.\n    fn run<W: Workload>(self, workload: W) -> Result<()> {\n        let mut rng = StdRng::seed_from_u64(self.seed);\n        let mut client = Client::connect(&self.hosts[0])?;\n\n        // Set up a histogram recording txn latencies as nanoseconds. The\n        // buckets range from 0.001s to 10s.\n        let mut hist = Histogram::<u32>::new_with_bounds(1_000, 10_000_000_000, 3)?.into_sync();\n\n        // Prepare the dataset.\n        print!(\"Preparing initial dataset... \");\n        std::io::stdout().flush()?;\n        let start = Instant::now();\n        workload.prepare(&mut client, &mut rng)?;\n        println!(\"done ({:.3}s)\", start.elapsed().as_secs_f64());\n\n        // Spawn workers, round robin across hosts.\n        std::thread::scope(|s| -> Result<()> {\n            print!(\"Spawning {} workers... \", self.concurrency);\n            std::io::stdout().flush()?;\n            let start = Instant::now();\n\n            let (work_tx, work_rx) = crossbeam::channel::bounded(self.concurrency);\n            let (done_tx, done_rx) = crossbeam::channel::bounded::<()>(0);\n\n            for addr in self.hosts.iter().cycle().take(self.concurrency) {\n                let mut client = Client::connect(addr)?;\n                let mut recorder = hist.recorder();\n                let work_rx = work_rx.clone();\n                let done_tx = done_tx.clone();\n                s.spawn(move || -> Result<()> {\n                    while let Ok(item) = work_rx.recv() {\n                        let start = Instant::now();\n                        client.with_retry(|client| W::execute(client, &item))?;\n                        recorder.record(start.elapsed().as_nanos() as u64)?;\n                    }\n                    drop(done_tx); // disconnects done_rx once all workers exit\n                    Ok(())\n                });\n            }\n            drop(done_tx); // drop local copy\n\n            println!(\"done ({:.3}s)\", start.elapsed().as_secs_f64());\n\n            // Spawn work generator.\n            {\n                println!(\"Running workload {}...\", workload);\n                let generator = workload.generate(rng)?.take(self.count);\n                s.spawn(move || -> Result<()> {\n                    for item in generator {\n                        work_tx.send(item)?;\n                    }\n                    Ok(())\n                });\n            }\n\n            // Periodically print stats until all workers are done.\n            let start = Instant::now();\n            let ticker = crossbeam::channel::tick(Duration::from_secs(1));\n\n            println!();\n            println!(\"Time   Progress     Txns      Rate       p50       p90       p99      pMax\");\n\n            while let Err(crossbeam::channel::TryRecvError::Empty) = done_rx.try_recv() {\n                crossbeam::select! {\n                    recv(ticker) -> _ => {},\n                    recv(done_rx) -> _ => {},\n                }\n\n                let duration = start.elapsed().as_secs_f64();\n                hist.refresh_timeout(Duration::from_secs(1));\n\n                println!(\n                    \"{:<8} {:>5.1}%  {:>7}  {:>6.0}/s  {:>6.1}ms  {:>6.1}ms  {:>6.1}ms  {:>6.1}ms\",\n                    format!(\"{:.1}s\", duration),\n                    hist.len() as f64 / self.count as f64 * 100.0,\n                    hist.len(),\n                    hist.len() as f64 / duration,\n                    Duration::from_nanos(hist.value_at_quantile(0.5)).as_secs_f64() * 1000.0,\n                    Duration::from_nanos(hist.value_at_quantile(0.9)).as_secs_f64() * 1000.0,\n                    Duration::from_nanos(hist.value_at_quantile(0.99)).as_secs_f64() * 1000.0,\n                    Duration::from_nanos(hist.max()).as_secs_f64() * 1000.0,\n                );\n            }\n            Ok(())\n        })?;\n\n        // Verify the final dataset.\n        println!();\n        print!(\"Verifying dataset... \");\n        std::io::stdout().flush()?;\n        let start = Instant::now();\n        workload.verify(&mut client, self.count)?;\n        println!(\"done ({:.3}s)\", start.elapsed().as_secs_f64());\n\n        Ok(())\n    }\n}\n\n/// A workload.\ntrait Workload: std::fmt::Display {\n    /// A work item.\n    type Item: Send;\n\n    /// Prepares the workload by creating initial tables and data.\n    fn prepare(&self, client: &mut Client, rng: &mut StdRng) -> Result<()>;\n\n    /// Generates work items as an iterator.\n    fn generate(&self, rng: StdRng) -> Result<impl Iterator<Item = Self::Item> + Send + 'static>;\n\n    /// Executes a single work item. This will automatically be retried on\n    /// certain errors, and must use a transaction where appropriate.\n    fn execute(client: &mut Client, item: &Self::Item) -> Result<()>;\n\n    /// Verifies the dataset after the workload has completed.\n    fn verify(&self, _client: &mut Client, _txns: usize) -> Result<()> {\n        Ok(())\n    }\n}\n\n/// A read-only workload. Creates an id,value table and populates it with the\n/// given row count and value size. Then runs batches of random primary key\n/// lookups (SELECT * FROM read WHERE id = 1 OR id = 2 ...).\n#[derive(clap::Args, Clone)]\n#[command(about = \"A read-only workload using primary key lookups\")]\nstruct Read {\n    /// Total number of rows in data set.\n    #[arg(short, long, default_value = \"1000\")]\n    rows: u64,\n\n    /// Row value size (excluding primary key).\n    #[arg(short, long, default_value = \"64\")]\n    size: usize,\n\n    /// Number of rows to fetch in a single select.\n    #[arg(short, long, default_value = \"1\")]\n    batch: usize,\n}\n\nimpl std::fmt::Display for Read {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"read (rows={} size={} batch={})\", self.rows, self.size, self.batch)\n    }\n}\n\nimpl Workload for Read {\n    type Item = HashSet<u64>;\n\n    fn prepare(&self, client: &mut Client, rng: &mut StdRng) -> Result<()> {\n        client.execute(\"BEGIN\")?;\n        client.execute(r#\"DROP TABLE IF EXISTS \"read\"\"#)?;\n        client.execute(r#\"CREATE TABLE \"read\" (id INT PRIMARY KEY, value STRING NOT NULL)\"#)?;\n\n        let chars = &mut rand::distr::Alphanumeric.sample_iter(rng).map(|b| b as char);\n        let rows = (1..=self.rows).map(|id| (id, chars.take(self.size).collect::<String>()));\n        let chunks = rows.chunks(100);\n        let queries = chunks.into_iter().map(|chunk| {\n            format!(\n                r#\"INSERT INTO \"read\" (id, value) VALUES ({})\"#,\n                chunk.map(|(id, value)| format!(\"{}, '{}'\", id, value)).join(\"), (\")\n            )\n        });\n        for query in queries {\n            client.execute(&query)?;\n        }\n        client.execute(\"COMMIT\")?;\n        Ok(())\n    }\n\n    fn generate(&self, rng: StdRng) -> Result<impl Iterator<Item = Self::Item> + 'static> {\n        Ok(ReadGenerator {\n            batch: self.batch,\n            dist: rand::distr::Uniform::new(1, self.rows + 1)?,\n            rng,\n        })\n    }\n\n    fn execute(client: &mut Client, item: &Self::Item) -> Result<()> {\n        let batch_size = item.len();\n        let query = format!(\n            r#\"SELECT * FROM \"read\" WHERE {}\"#,\n            item.iter().map(|id| format!(\"id = {}\", id)).join(\" OR \")\n        );\n        let rows: Rows = client.execute(&query)?.try_into()?;\n        assert_eq!(rows.count(), batch_size, \"Unexpected row count\");\n        Ok(())\n    }\n\n    fn verify(&self, client: &mut Client, _: usize) -> Result<()> {\n        let count: i64 = client.execute(r#\"SELECT COUNT(*) FROM \"read\"\"#)?.try_into()?;\n        assert_eq!(count, self.rows as i64, \"Unexpected row count\");\n        Ok(())\n    }\n}\n\n/// A Read workload generator, yielding batches of random, unique primary keys.\nstruct ReadGenerator {\n    batch: usize,\n    rng: StdRng,\n    dist: rand::distr::Uniform<u64>,\n}\n\nimpl Iterator for ReadGenerator {\n    type Item = <Read as Workload>::Item;\n\n    fn next(&mut self) -> Option<Self::Item> {\n        let mut ids = HashSet::new();\n        for id in self.dist.sample_iter(&mut self.rng) {\n            ids.insert(id);\n            if ids.len() >= self.batch {\n                break;\n            }\n        }\n        Some(ids)\n    }\n}\n\n/// A write-only workload. Creates an id,value table, and writes rows with\n/// sequential primary keys and the given value size, in the given batch size\n/// (INSERT INTO write (id, value) VALUES ...). The number of rows written\n/// is given by Runner.count * Write.batch.\n#[derive(clap::Args, Clone)]\n#[command(about = \"A write-only workload writing sequential rows\")]\nstruct Write {\n    /// Row value size (excluding primary key).\n    #[arg(short, long, default_value = \"64\")]\n    size: usize,\n\n    /// Number of rows to write in a single insert query.\n    #[arg(short, long, default_value = \"1\")]\n    batch: usize,\n}\n\nimpl std::fmt::Display for Write {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"write (size={} batch={})\", self.size, self.batch)\n    }\n}\n\nimpl Workload for Write {\n    type Item = Vec<(u64, String)>;\n\n    fn prepare(&self, client: &mut Client, _: &mut StdRng) -> Result<()> {\n        client.execute(\"BEGIN\")?;\n        client.execute(r#\"DROP TABLE IF EXISTS \"write\"\"#)?;\n        client.execute(r#\"CREATE TABLE \"write\" (id INT PRIMARY KEY, value STRING NOT NULL)\"#)?;\n        client.execute(\"COMMIT\")?;\n        Ok(())\n    }\n\n    fn generate(&self, rng: StdRng) -> Result<impl Iterator<Item = Self::Item> + 'static> {\n        Ok(WriteGenerator { next_id: 1, size: self.size, batch: self.batch, rng })\n    }\n\n    fn execute(client: &mut Client, item: &Self::Item) -> Result<()> {\n        let batch_size = item.len();\n        let query = format!(\n            r#\"INSERT INTO \"write\" (id, value) VALUES {}\"#,\n            item.iter().map(|(id, value)| format!(\"({}, '{}')\", id, value)).join(\", \")\n        );\n        if let StatementResult::Insert { count } = client.execute(&query)? {\n            assert_eq!(count as usize, batch_size, \"Unexpected row count\");\n        } else {\n            panic!(\"Unexpected result\")\n        }\n        Ok(())\n    }\n\n    fn verify(&self, client: &mut Client, txns: usize) -> Result<()> {\n        let count: i64 = client.execute(r#\"SELECT COUNT(*) FROM \"write\"\"#)?.try_into()?;\n        assert_eq!(count as usize, txns * self.batch, \"Unexpected row count\");\n        Ok(())\n    }\n}\n\n/// A Write workload generator, yielding batches of sequential primary keys and\n/// random rows.\nstruct WriteGenerator {\n    next_id: u64,\n    size: usize,\n    batch: usize,\n    rng: StdRng,\n}\n\nimpl Iterator for WriteGenerator {\n    type Item = <Write as Workload>::Item;\n\n    fn next(&mut self) -> Option<Self::Item> {\n        let chars = &mut rand::distr::Alphanumeric.sample_iter(&mut self.rng).map(|b| b as char);\n        let mut rows = Vec::with_capacity(self.batch);\n        while rows.len() < self.batch {\n            rows.push((self.next_id, chars.take(self.size).collect()));\n            self.next_id += 1;\n        }\n        Some(rows)\n    }\n}\n\n/// A bank workload. Creates a set of customers and accounts, and makes random\n/// transfers between them. Specifically, it picks two random customers A and B,\n/// and then finds A's highest-balance account and B's lowest-balance account,\n/// and transfers a random amount without overdrawing the account. This\n/// somewhat convoluted scheme is used to make the workload slightly less\n/// trivial, including joins, ordering, and secondary indexes.\n#[derive(clap::Args, Clone)]\n#[command(about = \"A bank workload, making transfers between customer accounts\")]\nstruct Bank {\n    /// Number of customers.\n    #[arg(short, long, default_value = \"100\")]\n    customers: u64,\n\n    /// Number of accounts per customer.\n    #[arg(short, long, default_value = \"10\")]\n    accounts: u64,\n\n    /// Initial account balance.\n    #[arg(short, long, default_value = \"100\")]\n    balance: u64,\n\n    /// Max amount to transfer.\n    #[arg(short, long, default_value = \"50\")]\n    max_transfer: u64,\n}\n\nimpl std::fmt::Display for Bank {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"bank (customers={} accounts={})\", self.customers, self.accounts)\n    }\n}\n\nimpl Workload for Bank {\n    type Item = (u64, u64, u64); // from,to,amount\n\n    fn prepare(&self, client: &mut Client, rng: &mut StdRng) -> Result<()> {\n        let petnames = petname::Petnames::default();\n        client.execute(\"BEGIN\")?;\n        client.execute(\"DROP TABLE IF EXISTS account\")?;\n        client.execute(\"DROP TABLE IF EXISTS customer\")?;\n        client.execute(\n            \"CREATE TABLE customer (\n                    id INTEGER PRIMARY KEY,\n                    name STRING NOT NULL\n                )\",\n        )?;\n        client.execute(\n            \"CREATE TABLE account (\n                    id INTEGER PRIMARY KEY,\n                    customer_id INTEGER NOT NULL INDEX REFERENCES customer,\n                    balance INTEGER NOT NULL\n                )\",\n        )?;\n        client.execute(&format!(\n            \"INSERT INTO customer VALUES {}\",\n            (1..=self.customers)\n                .map(|id| {\n                    let name = [\n                        *petnames.adverbs.choose(rng).expect(\"no adverb\"),\n                        *petnames.adjectives.choose(rng).expect(\"no adjective\"),\n                        *petnames.nouns.choose(rng).expect(\"no noun\"),\n                    ]\n                    .join(\" \");\n                    (id, name)\n                })\n                .map(|(id, name)| format!(\"({}, '{}')\", id, name))\n                .join(\", \")\n        ))?;\n        client.execute(&format!(\n            \"INSERT INTO account VALUES {}\",\n            (1..=self.customers)\n                .flat_map(|c| (1..=self.accounts).map(move |a| (c, (c - 1) * self.accounts + a)))\n                .map(|(c, a)| format!(\"({}, {}, {})\", a, c, self.balance))\n                .join(\", \")\n        ))?;\n        client.execute(\"COMMIT\")?;\n        Ok(())\n    }\n\n    fn generate(&self, rng: StdRng) -> Result<impl Iterator<Item = Self::Item> + 'static> {\n        let customers = self.customers;\n        let max_transfer = self.max_transfer;\n        // Generate random u64s, then pick random from,to,amount as the\n        // remainder of the max customer and amount.\n        Ok(rand::distr::Uniform::new_inclusive(0, u64::MAX)?\n            .sample_iter(rng)\n            .tuples()\n            .map(move |(a, b, c)| (a % customers + 1, b % customers + 1, c % max_transfer + 1))\n            .filter(|(from, to, _)| from != to))\n    }\n\n    fn execute(client: &mut Client, item: &Self::Item) -> Result<()> {\n        let &(from, to, mut amount) = item;\n\n        client.execute(\"BEGIN\")?;\n\n        let row: Row = client\n            .execute(&format!(\n                \"SELECT a.id, a.balance\n                        FROM account a JOIN customer c ON a.customer_id = c.id\n                        WHERE c.id = {}\n                        ORDER BY a.balance DESC\n                        LIMIT 1\",\n                from\n            ))?\n            .try_into()?;\n        let mut row = row.into_iter();\n        let from_account: i64 = row.next().unwrap().try_into()?;\n        let from_balance: i64 = row.next().unwrap().try_into()?;\n        amount = min(amount, from_balance as u64);\n\n        let to_account: i64 = client\n            .execute(&format!(\n                \"SELECT a.id, a.balance\n                        FROM account a JOIN customer c ON a.customer_id = c.id\n                        WHERE c.id = {}\n                        ORDER BY a.balance ASC\n                        LIMIT 1\",\n                to\n            ))?\n            .try_into()?;\n\n        client.execute(&format!(\n            \"UPDATE account SET balance = balance - {} WHERE id = {}\",\n            amount, from_account,\n        ))?;\n        client.execute(&format!(\n            \"UPDATE account SET balance = balance + {} WHERE id = {}\",\n            amount, to_account,\n        ))?;\n\n        client.execute(\"COMMIT\")?;\n\n        Ok(())\n    }\n\n    fn verify(&self, client: &mut Client, _: usize) -> Result<()> {\n        let balance: i64 = client.execute(\"SELECT SUM(balance) FROM account\")?.try_into()?;\n        assert_eq!(balance as u64, self.customers * self.accounts * self.balance);\n        let negative: i64 =\n            client.execute(\"SELECT COUNT(*) FROM account WHERE balance < 0\")?.try_into()?;\n        assert_eq!(negative, 0);\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "src/client.rs",
    "content": "use std::io::{BufReader, BufWriter, Write as _};\nuse std::net::{TcpStream, ToSocketAddrs};\nuse std::time::Duration;\n\nuse rand::RngExt as _;\n\nuse crate::encoding::Value as _;\nuse crate::errdata;\nuse crate::error::{Error, Result};\nuse crate::server::{Request, Response, Status};\nuse crate::sql::execution::StatementResult;\nuse crate::sql::types::Table;\nuse crate::storage::mvcc;\n\n/// A toyDB client. Connects to a server via TCP and submits SQL statements and\n/// other requests.\npub struct Client {\n    /// Inbound response stream.\n    reader: BufReader<TcpStream>,\n    /// Outbound request stream.\n    writer: BufWriter<TcpStream>,\n    /// The current transaction, if any.\n    txn: Option<mvcc::TransactionState>,\n}\n\nimpl Client {\n    /// Connects to a toyDB server, creating a new client.\n    pub fn connect(addr: impl ToSocketAddrs) -> Result<Self> {\n        let socket = TcpStream::connect(addr)?;\n        let reader = BufReader::new(socket.try_clone()?);\n        let writer = BufWriter::new(socket);\n        Ok(Self { reader, writer, txn: None })\n    }\n\n    /// Sends a request to the server, returning the response.\n    fn request(&mut self, request: Request) -> Result<Response> {\n        request.encode_into(&mut self.writer)?;\n        self.writer.flush()?;\n        Result::decode_from(&mut self.reader)?\n    }\n\n    /// Executes a SQL statement.\n    pub fn execute(&mut self, statement: &str) -> Result<StatementResult> {\n        let result = match self.request(Request::Execute(statement.to_string()))? {\n            Response::Execute(result) => result,\n            response => return errdata!(\"unexpected response {response:?}\"),\n        };\n        // Update the transaction state.\n        match &result {\n            StatementResult::Begin(state) => self.txn = Some(state.clone()),\n            StatementResult::Commit { .. } => self.txn = None,\n            StatementResult::Rollback { .. } => self.txn = None,\n            _ => {}\n        }\n        Ok(result)\n    }\n\n    /// Fetches a table schema.\n    pub fn get_table(&mut self, table: &str) -> Result<Table> {\n        match self.request(Request::GetTable(table.to_string()))? {\n            Response::GetTable(table) => Ok(table),\n            response => errdata!(\"unexpected response: {response:?}\"),\n        }\n    }\n\n    /// Lists database tables.\n    pub fn list_tables(&mut self) -> Result<Vec<String>> {\n        match self.request(Request::ListTables)? {\n            Response::ListTables(tables) => Ok(tables),\n            response => errdata!(\"unexpected response: {response:?}\"),\n        }\n    }\n\n    /// Returns server status.\n    pub fn status(&mut self) -> Result<Status> {\n        match self.request(Request::Status)? {\n            Response::Status(status) => Ok(status),\n            response => errdata!(\"unexpected response: {response:?}\"),\n        }\n    }\n\n    /// Returns the transaction state.\n    pub fn txn(&self) -> Option<&mvcc::TransactionState> {\n        self.txn.as_ref()\n    }\n\n    /// Runs the given closure, automatically retrying serialization and abort\n    /// errors. If a transaction is open following an error, it is automatically\n    /// rolled back. It is the caller's responsibility to use a transaction in\n    /// the closure where appropriate (i.e. when it is not idempotent).\n    pub fn with_retry<T>(&mut self, f: impl Fn(&mut Client) -> Result<T>) -> Result<T> {\n        const MAX_RETRIES: u32 = 10;\n        const MIN_WAIT: u64 = 10;\n        const MAX_WAIT: u64 = 2_000;\n        let mut retries: u32 = 0;\n        loop {\n            match f(self) {\n                Ok(result) => return Ok(result),\n                Err(Error::Serialization | Error::Abort) if retries < MAX_RETRIES => {\n                    if self.txn().is_some() {\n                        self.execute(\"ROLLBACK\")?;\n                    }\n                    // Use exponential backoff starting at MIN_WAIT doubling up\n                    // to MAX_WAIT, but randomize the wait time in this interval\n                    // to reduce the chance of collisions.\n                    let mut wait = MAX_WAIT.min(MIN_WAIT * 2_u64.pow(retries));\n                    wait = rand::rng().random_range(MIN_WAIT..=wait);\n                    std::thread::sleep(Duration::from_millis(wait));\n                    retries += 1;\n                }\n                Err(error) => {\n                    if self.txn().is_some() {\n                        self.execute(\"ROLLBACK\").ok(); // ignore rollback error\n                    }\n                    return Err(error);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/encoding/bincode.rs",
    "content": "//! Bincode is used to encode values, both in key/value stores and the toyDB\n//! network protocol. It is a Rust-specific encoding that depends on the\n//! internal data structures being stable, but it's sufficient for toyDB. See:\n//! <https://github.com/bincode-org/bincode>\n//!\n//! This module wraps the [`bincode`] crate and uses the standard config.\n\nuse std::io::{Read, Write};\n\nuse serde::de::DeserializeOwned;\nuse serde::{Deserialize, Serialize};\n\nuse crate::error::{Error, Result};\n\n/// Use the standard Bincode configuration.\nconst CONFIG: bincode::config::Configuration = bincode::config::standard();\n\n/// Serializes a value using Bincode.\npub fn serialize<T: Serialize>(value: &T) -> Vec<u8> {\n    // Panic on failure, as this is a problem with the data structure.\n    bincode::serde::encode_to_vec(value, CONFIG).expect(\"value must be serializable\")\n}\n\n/// Deserializes a value using Bincode.\npub fn deserialize<'de, T: Deserialize<'de>>(bytes: &'de [u8]) -> Result<T> {\n    Ok(bincode::serde::borrow_decode_from_slice(bytes, CONFIG)?.0)\n}\n\n/// Serializes a value to a writer using Bincode.\npub fn serialize_into<W: Write, T: Serialize>(mut writer: W, value: &T) -> Result<()> {\n    bincode::serde::encode_into_std_write(value, &mut writer, CONFIG)?;\n    Ok(())\n}\n\n/// Deserializes a value from a reader using Bincode.\npub fn deserialize_from<R: Read, T: DeserializeOwned>(mut reader: R) -> Result<T> {\n    Ok(bincode::serde::decode_from_std_read(&mut reader, CONFIG)?)\n}\n\n/// Deserializes a value from a reader using Bincode, or returns None if the\n/// reader is closed.\npub fn maybe_deserialize_from<R: Read, T: DeserializeOwned>(mut reader: R) -> Result<Option<T>> {\n    match bincode::serde::decode_from_std_read(&mut reader, CONFIG) {\n        Ok(t) => Ok(Some(t)),\n        Err(bincode::error::DecodeError::Io { inner, .. })\n            if inner.kind() == std::io::ErrorKind::UnexpectedEof\n                || inner.kind() == std::io::ErrorKind::ConnectionReset =>\n        {\n            Ok(None)\n        }\n        Err(err) => Err(Error::from(err)),\n    }\n}\n"
  },
  {
    "path": "src/encoding/format.rs",
    "content": "//! Formats raw keys and values, recursively where necessary. Handles both both\n//! Raft, MVCC, SQL, and raw binary data.\n\nuse std::collections::BTreeSet;\nuse std::marker::PhantomData;\n\nuse itertools::Itertools as _;\nuse regex::Regex;\n\nuse super::{Key as _, Value as _, bincode};\nuse crate::raft;\nuse crate::sql;\nuse crate::storage::mvcc;\n\n/// Formats encoded keys and values.\npub trait Formatter {\n    /// Formats a key.\n    fn key(key: &[u8]) -> String;\n\n    /// Formats a value. Also takes the key to determine the kind of value.\n    fn value(key: &[u8], value: &[u8]) -> String;\n\n    /// Formats a key/value pair.\n    fn key_value(key: &[u8], value: &[u8]) -> String {\n        Self::key_maybe_value(key, Some(value))\n    }\n\n    /// Formats a key/value pair, where the value may not exist.\n    fn key_maybe_value(key: &[u8], value: Option<&[u8]>) -> String {\n        let fmtkey = Self::key(key);\n        let fmtvalue = value.map_or(\"None\".to_string(), |v| Self::value(key, v));\n        format!(\"{fmtkey} → {fmtvalue}\")\n    }\n}\n\n/// Formats raw byte slices without any decoding.\npub struct Raw;\n\nimpl Raw {\n    /// Formats raw bytes as escaped ASCII strings.\n    pub fn bytes(bytes: &[u8]) -> String {\n        let escaped = bytes.iter().copied().flat_map(std::ascii::escape_default).collect_vec();\n        format!(\"\\\"{}\\\"\", String::from_utf8_lossy(&escaped))\n    }\n}\n\nimpl Formatter for Raw {\n    fn key(key: &[u8]) -> String {\n        Self::bytes(key)\n    }\n\n    fn value(_key: &[u8], value: &[u8]) -> String {\n        Self::bytes(value)\n    }\n}\n\n/// Formats Raft log entries. Dispatches to F to format each Raft command.\npub struct Raft<F: Formatter>(PhantomData<F>);\n\nimpl<F: Formatter> Raft<F> {\n    /// Formats a Raft entry.\n    pub fn entry(entry: &raft::Entry) -> String {\n        let fmtcommand = entry.command.as_deref().map_or(\"None\".to_string(), |c| F::value(&[], c));\n        format!(\"{}@{} {fmtcommand}\", entry.index, entry.term)\n    }\n}\n\nimpl<F: Formatter> Formatter for Raft<F> {\n    fn key(key: &[u8]) -> String {\n        let Ok(key) = raft::Key::decode(key) else {\n            return Raw::key(key); // invalid key\n        };\n        format!(\"raft:{key:?}\")\n    }\n\n    fn value(key: &[u8], value: &[u8]) -> String {\n        let Ok(key) = raft::Key::decode(key) else {\n            return Raw::value(key, value); // invalid key\n        };\n        match key {\n            raft::Key::CommitIndex => {\n                match bincode::deserialize::<(raft::Index, raft::Term)>(value) {\n                    Ok((index, term)) => format!(\"{index}@{term}\"),\n                    Err(_) => Raw::bytes(value),\n                }\n            }\n            raft::Key::TermVote => {\n                match bincode::deserialize::<(raft::Term, Option<raft::NodeID>)>(value) {\n                    Ok((term, vote)) => format!(\n                        \"term={term} vote={}\",\n                        vote.map_or(\"None\".to_string(), |v| v.to_string()),\n                    ),\n                    Err(_) => Raw::bytes(value),\n                }\n            }\n            raft::Key::Entry(_) => match bincode::deserialize::<raft::Entry>(value) {\n                Ok(entry) => Self::entry(&entry),\n                Err(_) => Raw::bytes(value),\n            },\n        }\n    }\n}\n\n/// Formats MVCC keys/values. Dispatches to F to format the inner key/value.\npub struct MVCC<F: Formatter>(PhantomData<F>);\n\nimpl<F: Formatter> Formatter for MVCC<F> {\n    fn key(key: &[u8]) -> String {\n        let Ok(key) = mvcc::Key::decode(key) else {\n            return Raw::key(key); // invalid key\n        };\n        match key {\n            mvcc::Key::TxnWrite(version, innerkey) => {\n                format!(\"mvcc:TxnWrite({version}, {})\", F::key(&innerkey))\n            }\n            mvcc::Key::Version(innerkey, version) => {\n                format!(\"mvcc:Version({}, {version})\", F::key(&innerkey))\n            }\n            mvcc::Key::Unversioned(innerkey) => {\n                format!(\"mvcc:Unversioned({})\", F::key(&innerkey))\n            }\n            mvcc::Key::NextVersion | mvcc::Key::TxnActive(_) | mvcc::Key::TxnActiveSnapshot(_) => {\n                format!(\"mvcc:{key:?}\")\n            }\n        }\n    }\n\n    fn value(key: &[u8], value: &[u8]) -> String {\n        let Ok(key) = mvcc::Key::decode(key) else {\n            return Raw::bytes(value); // invalid key\n        };\n        match key {\n            mvcc::Key::NextVersion => {\n                let Ok(version) = bincode::deserialize::<mvcc::Version>(value) else {\n                    return Raw::bytes(value);\n                };\n                version.to_string()\n            }\n            mvcc::Key::TxnActiveSnapshot(_) => {\n                let Ok(active) = bincode::deserialize::<BTreeSet<u64>>(value) else {\n                    return Raw::bytes(value);\n                };\n                format!(\"{{{}}}\", active.iter().join(\",\"))\n            }\n            mvcc::Key::TxnActive(_) | mvcc::Key::TxnWrite(_, _) => Raw::bytes(value),\n            mvcc::Key::Version(userkey, _) => match bincode::deserialize(value) {\n                Ok(Some(value)) => F::value(&userkey, value),\n                Ok(None) => \"None\".to_string(),\n                Err(_) => Raw::bytes(value),\n            },\n            mvcc::Key::Unversioned(userkey) => F::value(&userkey, value),\n        }\n    }\n}\n\n/// Formats SQL keys/values.\npub struct SQL;\n\nimpl SQL {\n    /// Formats a list of SQL values.\n    fn values(values: impl IntoIterator<Item = sql::types::Value>) -> String {\n        values.into_iter().join(\",\")\n    }\n\n    /// Formats a table schema.\n    fn schema(table: sql::types::Table) -> String {\n        // Put it all on a single line.\n        let re = Regex::new(r#\"\\n\\s*\"#).expect(\"invalid regex\");\n        re.replace_all(&table.to_string(), \" \").into_owned()\n    }\n}\n\nimpl Formatter for SQL {\n    fn key(key: &[u8]) -> String {\n        // Special-case the Raft applied index key.\n        if key == sql::engine::Raft::APPLIED_INDEX_KEY {\n            return String::from_utf8_lossy(key).into_owned();\n        }\n        let Ok(key) = sql::engine::Key::decode(key) else {\n            return Raw::key(key); // invalid key\n        };\n        match key {\n            sql::engine::Key::Table(name) => format!(\"sql:Table({name})\"),\n            sql::engine::Key::Index(table, column, value) => {\n                format!(\"sql:Index({table}.{column}, {value})\")\n            }\n            sql::engine::Key::Row(table, id) => {\n                format!(\"sql:Row({table}, {id})\")\n            }\n        }\n    }\n\n    fn value(key: &[u8], value: &[u8]) -> String {\n        // Special-case the applied_index key.\n        if key == sql::engine::Raft::APPLIED_INDEX_KEY\n            && let Ok(applied_index) = bincode::deserialize::<raft::Index>(value)\n        {\n            return applied_index.to_string();\n        }\n\n        let Ok(key) = sql::engine::Key::decode(key) else {\n            return Raw::key(value);\n        };\n        match key {\n            sql::engine::Key::Table(_) => {\n                let Ok(table) = bincode::deserialize(value) else {\n                    return Raw::bytes(value);\n                };\n                Self::schema(table)\n            }\n            sql::engine::Key::Row(_, _) => {\n                let Ok(row) = bincode::deserialize::<sql::types::Row>(value) else {\n                    return Raw::bytes(value);\n                };\n                Self::values(row)\n            }\n            sql::engine::Key::Index(_, _, _) => {\n                let Ok(index) = bincode::deserialize::<BTreeSet<sql::types::Value>>(value) else {\n                    return Raw::bytes(value);\n                };\n                Self::values(index)\n            }\n        }\n    }\n}\n\n/// Formats SQL Raft write commands, from the Raft log.\npub struct SQLCommand;\n\nimpl Formatter for SQLCommand {\n    fn key(_key: &[u8]) -> String {\n        // There is no key, since these are wrapped in a Raft log entry.\n        panic!(\"SQL commands don't have a key\");\n    }\n\n    fn value(_key: &[u8], value: &[u8]) -> String {\n        let Ok(write) = sql::engine::Write::decode(value) else {\n            return Raw::bytes(value);\n        };\n\n        let txn = match &write {\n            sql::engine::Write::Begin => None,\n            sql::engine::Write::Commit(txn)\n            | sql::engine::Write::Rollback(txn)\n            | sql::engine::Write::Delete { txn, .. }\n            | sql::engine::Write::Insert { txn, .. }\n            | sql::engine::Write::Update { txn, .. }\n            | sql::engine::Write::CreateTable { txn, .. }\n            | sql::engine::Write::DropTable { txn, .. } => Some(txn),\n        };\n        let fmttxn =\n            txn.filter(|t| !t.read_only).map_or(\"\".to_string(), |t| format!(\"t{} \", t.version));\n\n        let fmtcommand = match write {\n            sql::engine::Write::Begin => \"BEGIN\".to_string(),\n            sql::engine::Write::Commit(_) => \"COMMIT\".to_string(),\n            sql::engine::Write::Rollback(_) => \"ROLLBACK\".to_string(),\n            sql::engine::Write::Delete { table, ids, .. } => {\n                format!(\"DELETE {table} {}\", ids.iter().map(|id| id.to_string()).join(\",\"))\n            }\n            sql::engine::Write::Insert { table, rows, .. } => {\n                format!(\n                    \"INSERT {table} {}\",\n                    rows.into_iter().map(|row| format!(\"({})\", SQL::values(row))).join(\" \")\n                )\n            }\n            sql::engine::Write::Update { table, rows, .. } => format!(\n                \"UPDATE {table} {}\",\n                rows.into_iter().map(|(id, row)| format!(\"{id}→({})\", SQL::values(row))).join(\" \")\n            ),\n            sql::engine::Write::CreateTable { schema, .. } => SQL::schema(schema),\n            sql::engine::Write::DropTable { table, .. } => format!(\"DROP TABLE {table}\"),\n        };\n        format!(\"{fmttxn}{fmtcommand}\")\n    }\n}\n"
  },
  {
    "path": "src/encoding/keycode.rs",
    "content": "//! Keycode is a lexicographical order-preserving binary encoding for use with\n//! keys in key/value stores. It is designed for simplicity, not efficiency\n//! (i.e. it does not use varints or other compression methods).\n//!\n//! Ordering is important because it allows limited scans across specific parts\n//! of the keyspace, e.g. scanning an individual table or using an index range\n//! predicate like `WHERE id < 100`. It also avoids sorting in some cases where\n//! the keys are already in the desired order, e.g. in the Raft log.\n//!\n//! The encoding is not self-describing: the caller must provide a concrete type\n//! to decode into, and the binary key must conform to its structure.\n//!\n//! Keycode supports a subset of primitive data types, encoded as follows:\n//!\n//! * [`bool`]: `0x00` for `false`, `0x01` for `true`.\n//! * [`u64`]: big-endian binary representation.\n//! * [`i64`]: big-endian binary, sign bit flipped.\n//! * [`f64`]: big-endian binary, sign bit flipped, all flipped if negative.\n//! * [`Vec<u8>`]: `0x00` escaped as `0x00ff`, terminated with `0x0000`.\n//! * [`String`]: like [`Vec<u8>`].\n//! * Sequences: concatenation of contained elements, with no other structure.\n//! * Enum: the variant's index as [`u8`], then the content sequence.\n//! * [`crate::sql::types::Value`]: like any other enum.\n//!\n//! The canonical key representation is an enum. For example:\n//!\n//! ```\n//! #[derive(Debug, Deserialize, Serialize)]\n//! enum Key {\n//!     Foo,\n//!     Bar(String),\n//!     Baz(bool, u64, #[serde(with = \"serde_bytes\")] Vec<u8>),\n//! }\n//! ```\n//!\n//! Unfortunately, byte strings such as `Vec<u8>` must be wrapped with\n//! [`serde_bytes::ByteBuf`] or use the `#[serde(with=\"serde_bytes\")]`\n//! attribute. See <https://github.com/serde-rs/bytes>.\n\nuse std::ops::Bound;\n\nuse itertools::Either;\nuse serde::de::{\n    Deserialize, DeserializeSeed, EnumAccess, IntoDeserializer as _, SeqAccess, VariantAccess,\n    Visitor,\n};\nuse serde::ser::{Impossible, Serialize, SerializeSeq, SerializeTuple, SerializeTupleVariant};\n\nuse crate::errdata;\nuse crate::error::{Error, Result};\n\n/// Serializes a key to a binary Keycode representation.\n///\n/// In the common case, the encoded key is borrowed for a storage engine call\n/// and then thrown away. We could avoid a bunch of allocations by taking a\n/// reusable byte vector to encode into and return a reference to it, but we\n/// keep it simple.\npub fn serialize<T: Serialize>(key: &T) -> Vec<u8> {\n    let mut serializer = Serializer { output: Vec::new() };\n    // Panic on failure, as this is a problem with the data structure.\n    key.serialize(&mut serializer).expect(\"key must be serializable\");\n    serializer.output\n}\n\n/// Deserializes a key from a binary Keycode representation.\npub fn deserialize<'a, T: Deserialize<'a>>(input: &'a [u8]) -> Result<T> {\n    let mut deserializer = Deserializer::from_bytes(input);\n    let t = T::deserialize(&mut deserializer)?;\n    if !deserializer.input.is_empty() {\n        return errdata!(\n            \"unexpected trailing bytes {:x?} at end of key {input:x?}\",\n            deserializer.input,\n        );\n    }\n    Ok(t)\n}\n\n/// Generates a key range for a key prefix, used e.g. for prefix scans.\n///\n/// The exclusive end bound is generated by adding 1 to the value of the last\n/// byte. If the last byte(s) is 0xff (so adding 1 would overflow), we instead\n/// find the latest non-0xff byte, increment that, and truncate the rest. If all\n/// bytes are 0xff, we scan to the end of the range, since there can't be other\n/// prefixes after it.\npub fn prefix_range(prefix: &[u8]) -> (Bound<Vec<u8>>, Bound<Vec<u8>>) {\n    let start = Bound::Included(prefix.to_vec());\n    let end = match prefix.iter().rposition(|&b| b != 0xff) {\n        Some(i) => Bound::Excluded(\n            prefix.iter().take(i).copied().chain(std::iter::once(prefix[i] + 1)).collect(),\n        ),\n        None => Bound::Unbounded,\n    };\n    (start, end)\n}\n\n/// Serializes keys as binary byte vectors.\nstruct Serializer {\n    output: Vec<u8>,\n}\n\nimpl serde::ser::Serializer for &mut Serializer {\n    type Ok = ();\n    type Error = Error;\n\n    type SerializeSeq = Self;\n    type SerializeTuple = Self;\n    type SerializeTupleVariant = Self;\n    type SerializeTupleStruct = Impossible<(), Error>;\n    type SerializeMap = Impossible<(), Error>;\n    type SerializeStruct = Impossible<(), Error>;\n    type SerializeStructVariant = Impossible<(), Error>;\n\n    /// bool simply uses 1 for true and 0 for false.\n    fn serialize_bool(self, v: bool) -> Result<()> {\n        self.output.push(if v { 1 } else { 0 });\n        Ok(())\n    }\n\n    fn serialize_i8(self, _: i8) -> Result<()> {\n        unimplemented!()\n    }\n\n    fn serialize_i16(self, _: i16) -> Result<()> {\n        unimplemented!()\n    }\n\n    fn serialize_i32(self, _: i32) -> Result<()> {\n        unimplemented!()\n    }\n\n    /// i64 uses the big-endian two's complement encoding, but flips the\n    /// left-most sign bit such that negative numbers are ordered before\n    /// positive numbers.\n    ///\n    /// The relative ordering of the remaining bits is already correct: -1, the\n    /// largest negative integer, is encoded as 01111111...11111111, ordered\n    /// after all other negative integers but before positive integers.\n    fn serialize_i64(self, v: i64) -> Result<()> {\n        let mut bytes = v.to_be_bytes();\n        bytes[0] ^= 1 << 7; // flip sign bit\n        self.output.extend(bytes);\n        Ok(())\n    }\n\n    fn serialize_u8(self, _: u8) -> Result<()> {\n        unimplemented!()\n    }\n\n    fn serialize_u16(self, _: u16) -> Result<()> {\n        unimplemented!()\n    }\n\n    fn serialize_u32(self, _: u32) -> Result<()> {\n        unimplemented!()\n    }\n\n    /// u64 simply uses the big-endian encoding.\n    fn serialize_u64(self, v: u64) -> Result<()> {\n        self.output.extend(v.to_be_bytes());\n        Ok(())\n    }\n\n    fn serialize_f32(self, _: f32) -> Result<()> {\n        unimplemented!()\n    }\n\n    /// f64 is encoded in big-endian IEEE 754 form, but it flips the sign bit to\n    /// order positive numbers after negative numbers, and also flips all other\n    /// bits for negative numbers to order them from smallest to largest. NaN is\n    /// ordered at the end.\n    fn serialize_f64(self, v: f64) -> Result<()> {\n        let mut bytes = v.to_be_bytes();\n        match v.is_sign_negative() {\n            false => bytes[0] ^= 1 << 7, // positive, flip sign bit\n            true => bytes.iter_mut().for_each(|b| *b = !*b), // negative, flip all bits\n        }\n        self.output.extend(bytes);\n        Ok(())\n    }\n\n    fn serialize_char(self, _: char) -> Result<()> {\n        unimplemented!()\n    }\n\n    // Strings are encoded like bytes.\n    fn serialize_str(self, v: &str) -> Result<()> {\n        self.serialize_bytes(v.as_bytes())\n    }\n\n    // Byte slices are terminated by 0x0000, escaping 0x00 as 0x00ff. This\n    // ensures that we can detect the end, and that for two overlapping slices,\n    // the shorter one orders before the longer one.\n    //\n    // We can't use e.g. length prefix encoding, since it doesn't sort correctly.\n    fn serialize_bytes(self, v: &[u8]) -> Result<()> {\n        let bytes = v\n            .iter()\n            .flat_map(|&byte| match byte {\n                0x00 => Either::Left([0x00, 0xff].into_iter()),\n                byte => Either::Right([byte].into_iter()),\n            })\n            .chain([0x00, 0x00]);\n        self.output.extend(bytes);\n        Ok(())\n    }\n\n    fn serialize_none(self) -> Result<()> {\n        unimplemented!()\n    }\n\n    fn serialize_some<T: Serialize + ?Sized>(self, _: &T) -> Result<()> {\n        unimplemented!()\n    }\n\n    fn serialize_unit(self) -> Result<()> {\n        unimplemented!()\n    }\n\n    fn serialize_unit_struct(self, _: &'static str) -> Result<()> {\n        unimplemented!()\n    }\n\n    /// Enum variants are serialized using their index, as a single byte.\n    fn serialize_unit_variant(self, _: &'static str, index: u32, _: &'static str) -> Result<()> {\n        self.output.push(index.try_into()?);\n        Ok(())\n    }\n\n    fn serialize_newtype_struct<T: Serialize + ?Sized>(self, _: &'static str, _: &T) -> Result<()> {\n        unimplemented!()\n    }\n\n    /// Newtype variants are serialized using the variant index and inner type.\n    fn serialize_newtype_variant<T: Serialize + ?Sized>(\n        self,\n        name: &'static str,\n        index: u32,\n        variant: &'static str,\n        value: &T,\n    ) -> Result<()> {\n        self.serialize_unit_variant(name, index, variant)?;\n        value.serialize(self)\n    }\n\n    /// Sequences are serialized as the concatenation of the serialized elements.\n    fn serialize_seq(self, _: Option<usize>) -> Result<Self::SerializeSeq> {\n        Ok(self)\n    }\n\n    /// Tuples are serialized as the concatenation of the serialized elements.\n    fn serialize_tuple(self, _: usize) -> Result<Self::SerializeTuple> {\n        Ok(self)\n    }\n\n    fn serialize_tuple_struct(\n        self,\n        _: &'static str,\n        _: usize,\n    ) -> Result<Self::SerializeTupleStruct> {\n        unimplemented!()\n    }\n\n    /// Tuple variants are serialized using the variant index and the\n    /// concatenation of the serialized elements.\n    fn serialize_tuple_variant(\n        self,\n        name: &'static str,\n        index: u32,\n        variant: &'static str,\n        _: usize,\n    ) -> Result<Self::SerializeTupleVariant> {\n        self.serialize_unit_variant(name, index, variant)?;\n        Ok(self)\n    }\n\n    fn serialize_map(self, _: Option<usize>) -> Result<Self::SerializeMap> {\n        unimplemented!()\n    }\n\n    fn serialize_struct(self, _: &'static str, _: usize) -> Result<Self::SerializeStruct> {\n        unimplemented!()\n    }\n\n    fn serialize_struct_variant(\n        self,\n        _: &'static str,\n        _: u32,\n        _: &'static str,\n        _: usize,\n    ) -> Result<Self::SerializeStructVariant> {\n        unimplemented!()\n    }\n}\n\n/// Sequences simply concatenate the serialized elements, with no external structure.\nimpl SerializeSeq for &mut Serializer {\n    type Ok = ();\n    type Error = Error;\n\n    fn serialize_element<T: Serialize + ?Sized>(&mut self, value: &T) -> Result<()> {\n        value.serialize(&mut **self)\n    }\n\n    fn end(self) -> Result<()> {\n        Ok(())\n    }\n}\n\n/// Tuples, like sequences, simply concatenate the serialized elements.\nimpl SerializeTuple for &mut Serializer {\n    type Ok = ();\n    type Error = Error;\n\n    fn serialize_element<T: Serialize + ?Sized>(&mut self, value: &T) -> Result<()> {\n        value.serialize(&mut **self)\n    }\n\n    fn end(self) -> Result<()> {\n        Ok(())\n    }\n}\n\n/// Tuples, like sequences, simply concatenate the serialized elements.\nimpl SerializeTupleVariant for &mut Serializer {\n    type Ok = ();\n    type Error = Error;\n\n    fn serialize_field<T: Serialize + ?Sized>(&mut self, value: &T) -> Result<()> {\n        value.serialize(&mut **self)\n    }\n\n    fn end(self) -> Result<()> {\n        Ok(())\n    }\n}\n\n/// Deserializes keys from byte slices into a given type. The format is not\n/// self-describing, so the caller must provide a concrete type to deserialize\n/// into.\npub struct Deserializer<'de> {\n    input: &'de [u8],\n}\n\nimpl<'de> Deserializer<'de> {\n    /// Creates a deserializer for a byte slice.\n    pub fn from_bytes(input: &'de [u8]) -> Self {\n        Deserializer { input }\n    }\n\n    /// Chops off and returns the next len bytes of the byte slice, or errors if\n    /// there aren't enough bytes left.\n    fn take_bytes(&mut self, len: usize) -> Result<&[u8]> {\n        if self.input.len() < len {\n            return errdata!(\"insufficient bytes, expected {len} bytes for {:x?}\", self.input);\n        }\n        let bytes = &self.input[..len];\n        self.input = &self.input[len..];\n        Ok(bytes)\n    }\n\n    /// Decodes and chops off the next encoded byte slice.\n    fn decode_next_bytes(&mut self) -> Result<Vec<u8>> {\n        let mut decoded = Vec::new();\n        let mut iter = self.input.iter().enumerate();\n        let taken = loop {\n            match iter.next() {\n                Some((_, 0x00)) => match iter.next() {\n                    Some((i, 0x00)) => break i + 1,        // terminator\n                    Some((_, 0xff)) => decoded.push(0x00), // escaped 0x00\n                    _ => return errdata!(\"invalid escape sequence\"),\n                },\n                Some((_, b)) => decoded.push(*b),\n                None => return errdata!(\"unexpected end of input\"),\n            }\n        };\n        self.input = &self.input[taken..];\n        Ok(decoded)\n    }\n}\n\n/// For details on serialization formats, see Serializer.\nimpl<'de> serde::de::Deserializer<'de> for &mut Deserializer<'de> {\n    type Error = Error;\n\n    fn deserialize_any<V: Visitor<'de>>(self, _: V) -> Result<V::Value> {\n        panic!(\"must provide type, Keycode is not self-describing\")\n    }\n\n    fn deserialize_bool<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {\n        visitor.visit_bool(match self.take_bytes(1)?[0] {\n            0x00 => false,\n            0x01 => true,\n            b => return errdata!(\"invalid boolean value {b}\"),\n        })\n    }\n\n    fn deserialize_i8<V: Visitor<'de>>(self, _: V) -> Result<V::Value> {\n        unimplemented!()\n    }\n\n    fn deserialize_i16<V: Visitor<'de>>(self, _: V) -> Result<V::Value> {\n        unimplemented!()\n    }\n\n    fn deserialize_i32<V: Visitor<'de>>(self, _: V) -> Result<V::Value> {\n        unimplemented!()\n    }\n\n    fn deserialize_i64<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {\n        let mut bytes = self.take_bytes(8)?.to_vec();\n        bytes[0] ^= 1 << 7; // flip sign bit\n        visitor.visit_i64(i64::from_be_bytes(bytes.as_slice().try_into()?))\n    }\n\n    fn deserialize_u8<V: Visitor<'de>>(self, _: V) -> Result<V::Value> {\n        unimplemented!()\n    }\n\n    fn deserialize_u16<V: Visitor<'de>>(self, _: V) -> Result<V::Value> {\n        unimplemented!()\n    }\n\n    fn deserialize_u32<V: Visitor<'de>>(self, _: V) -> Result<V::Value> {\n        unimplemented!()\n    }\n\n    fn deserialize_u64<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {\n        visitor.visit_u64(u64::from_be_bytes(self.take_bytes(8)?.try_into()?))\n    }\n\n    fn deserialize_f32<V: Visitor<'de>>(self, _: V) -> Result<V::Value> {\n        unimplemented!()\n    }\n\n    fn deserialize_f64<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {\n        let mut bytes = self.take_bytes(8)?.to_vec();\n        match bytes[0] >> 7 {\n            0 => bytes.iter_mut().for_each(|b| *b = !*b), // negative, flip all bits\n            1 => bytes[0] ^= 1 << 7,                      // positive, flip sign bit\n            _ => panic!(\"bits can only be 0 or 1\"),\n        }\n        visitor.visit_f64(f64::from_be_bytes(bytes.as_slice().try_into()?))\n    }\n\n    fn deserialize_char<V: Visitor<'de>>(self, _: V) -> Result<V::Value> {\n        unimplemented!()\n    }\n\n    fn deserialize_str<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {\n        let bytes = self.decode_next_bytes()?;\n        visitor.visit_str(&String::from_utf8(bytes)?)\n    }\n\n    fn deserialize_string<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {\n        let bytes = self.decode_next_bytes()?;\n        visitor.visit_string(String::from_utf8(bytes)?)\n    }\n\n    fn deserialize_bytes<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {\n        let bytes = self.decode_next_bytes()?;\n        visitor.visit_bytes(&bytes)\n    }\n\n    fn deserialize_byte_buf<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {\n        let bytes = self.decode_next_bytes()?;\n        visitor.visit_byte_buf(bytes)\n    }\n\n    fn deserialize_option<V: Visitor<'de>>(self, _: V) -> Result<V::Value> {\n        unimplemented!()\n    }\n\n    fn deserialize_unit<V: Visitor<'de>>(self, _: V) -> Result<V::Value> {\n        unimplemented!()\n    }\n\n    fn deserialize_unit_struct<V: Visitor<'de>>(self, _: &'static str, _: V) -> Result<V::Value> {\n        unimplemented!()\n    }\n\n    fn deserialize_newtype_struct<V: Visitor<'de>>(\n        self,\n        _: &'static str,\n        _: V,\n    ) -> Result<V::Value> {\n        unimplemented!()\n    }\n\n    fn deserialize_seq<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {\n        visitor.visit_seq(self)\n    }\n\n    fn deserialize_tuple<V: Visitor<'de>>(self, _: usize, visitor: V) -> Result<V::Value> {\n        visitor.visit_seq(self)\n    }\n\n    fn deserialize_tuple_struct<V: Visitor<'de>>(\n        self,\n        _: &'static str,\n        _: usize,\n        _: V,\n    ) -> Result<V::Value> {\n        unimplemented!()\n    }\n\n    fn deserialize_map<V: Visitor<'de>>(self, _: V) -> Result<V::Value> {\n        unimplemented!()\n    }\n\n    fn deserialize_struct<V: Visitor<'de>>(\n        self,\n        _: &'static str,\n        _: &'static [&'static str],\n        _: V,\n    ) -> Result<V::Value> {\n        unimplemented!()\n    }\n\n    fn deserialize_enum<V: Visitor<'de>>(\n        self,\n        _: &'static str,\n        _: &'static [&'static str],\n        visitor: V,\n    ) -> Result<V::Value> {\n        visitor.visit_enum(self)\n    }\n\n    fn deserialize_identifier<V: Visitor<'de>>(self, _: V) -> Result<V::Value> {\n        unimplemented!()\n    }\n\n    fn deserialize_ignored_any<V: Visitor<'de>>(self, _: V) -> Result<V::Value> {\n        unimplemented!()\n    }\n}\n\n/// Sequences are simply deserialized until the byte slice is exhausted.\nimpl<'de> SeqAccess<'de> for Deserializer<'de> {\n    type Error = Error;\n\n    fn next_element_seed<T: DeserializeSeed<'de>>(&mut self, seed: T) -> Result<Option<T::Value>> {\n        if self.input.is_empty() {\n            return Ok(None);\n        }\n        seed.deserialize(self).map(Some)\n    }\n}\n\n/// Enum variants are deserialized by their index.\nimpl<'de> EnumAccess<'de> for &mut Deserializer<'de> {\n    type Error = Error;\n    type Variant = Self;\n\n    fn variant_seed<V: DeserializeSeed<'de>>(self, seed: V) -> Result<(V::Value, Self::Variant)> {\n        let index = self.take_bytes(1)?[0] as u32;\n        let value: Result<_> = seed.deserialize(index.into_deserializer());\n        Ok((value?, self))\n    }\n}\n\n/// Enum variant contents are deserialized as sequences.\nimpl<'de> VariantAccess<'de> for &mut Deserializer<'de> {\n    type Error = Error;\n\n    fn unit_variant(self) -> Result<()> {\n        Ok(())\n    }\n\n    fn newtype_variant_seed<T: DeserializeSeed<'de>>(self, seed: T) -> Result<T::Value> {\n        seed.deserialize(&mut *self)\n    }\n\n    fn tuple_variant<V: Visitor<'de>>(self, _: usize, visitor: V) -> Result<V::Value> {\n        visitor.visit_seq(self)\n    }\n\n    fn struct_variant<V: Visitor<'de>>(self, _: &'static [&'static str], _: V) -> Result<V::Value> {\n        unimplemented!()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use std::borrow::Cow;\n    use std::f64::consts::PI;\n\n    use paste::paste;\n    use serde::{Deserialize, Serialize};\n    use serde_bytes::ByteBuf;\n\n    use super::*;\n    use crate::sql::types::Value;\n\n    #[derive(Debug, Deserialize, Serialize, PartialEq)]\n    enum Key<'a> {\n        Unit,\n        NewType(String),\n        Tuple(bool, #[serde(with = \"serde_bytes\")] Vec<u8>, u64),\n        Cow(\n            #[serde(with = \"serde_bytes\")]\n            #[serde(borrow)]\n            Cow<'a, [u8]>,\n            bool,\n            #[serde(borrow)] Cow<'a, str>,\n        ),\n    }\n\n    /// Assert that serializing a value yields the expected byte sequence (as a\n    /// hex-encoded string), and that deserializing it yields the original value.\n    macro_rules! test_serialize_deserialize {\n        ( $( $name:ident: $input:expr => $expect:literal, )* ) => {\n        $(\n            #[test]\n            fn $name() -> Result<()> {\n                let mut input = $input;\n                let expect = $expect;\n                let output = serialize(&input);\n                assert_eq!(hex::encode(&output), expect, \"encode failed\");\n\n                let expect = input;\n                input = deserialize(&output)?; // reuse input variable for proper type\n                assert_eq!(input, expect, \"decode failed\");\n                Ok(())\n            }\n        )*\n        };\n    }\n\n    /// Assert that deserializing invalid inputs results in errors. Takes byte\n    /// slices (as hex-encoded strings) and the type to deserialize into.\n    macro_rules! test_deserialize_error {\n        ( $( $name:ident: $input:literal as $type:ty, )* ) => {\n        paste! {\n        $(\n            #[test]\n            #[should_panic]\n            fn [< $name _deserialize_error >]() {\n                let bytes = hex::decode($input).unwrap();\n                deserialize::<$type>(&bytes).unwrap();\n            }\n        )*\n        }\n        };\n    }\n\n    // Assert that serializing a value results in an error.\n    macro_rules! test_serialize_error {\n        ( $( $name:ident: $input:expr, )* ) => {\n        paste! {\n        $(\n            #[test]\n            #[should_panic]\n            fn [< $name _serialize_error >]() {\n                let input = $input;\n                serialize(&input);\n            }\n        )*\n        }\n        };\n    }\n\n    test_serialize_deserialize! {\n        bool_false: false => \"00\",\n        bool_true: true => \"01\",\n\n        f64_min: f64::MIN => \"0010000000000000\",\n        f64_neg_inf: f64::NEG_INFINITY => \"000fffffffffffff\",\n        f64_neg_pi: -PI => \"3ff6de04abbbd2e7\",\n        f64_neg_zero: -0f64 => \"7fffffffffffffff\",\n        f64_zero: 0f64 => \"8000000000000000\",\n        f64_pi: PI => \"c00921fb54442d18\",\n        f64_max: f64::MAX => \"ffefffffffffffff\",\n        f64_inf: f64::INFINITY => \"fff0000000000000\",\n        // We don't test NAN here, since NAN != NAN.\n\n        i64_min: i64::MIN => \"0000000000000000\",\n        i64_neg_65535: -65535i64 => \"7fffffffffff0001\",\n        i64_neg_1: -1i64 => \"7fffffffffffffff\",\n        i64_0: 0i64 => \"8000000000000000\",\n        i64_1: 1i64 => \"8000000000000001\",\n        i64_65535: 65535i64 => \"800000000000ffff\",\n        i64_max: i64::MAX => \"ffffffffffffffff\",\n\n        u64_min: u64::MIN => \"0000000000000000\",\n        u64_1: 1_u64 => \"0000000000000001\",\n        u64_65535: 65535_u64 => \"000000000000ffff\",\n        u64_max: u64::MAX => \"ffffffffffffffff\",\n\n        bytes: ByteBuf::from(vec![0x01, 0xff]) => \"01ff0000\",\n        bytes_empty: ByteBuf::new() => \"0000\",\n        bytes_escape: ByteBuf::from(vec![0x00, 0x01, 0x02]) => \"00ff01020000\",\n\n        string: \"foo\".to_string() => \"666f6f0000\",\n        string_empty: \"\".to_string() => \"0000\",\n        string_escape: \"foo\\x00bar\".to_string() => \"666f6f00ff6261720000\",\n        string_utf8: \"👋\".to_string() => \"f09f918b0000\",\n\n        tuple: (true, u64::MAX, ByteBuf::from(vec![0x00, 0x01])) => \"01ffffffffffffffff00ff010000\",\n        array_bool: [false, true, false] => \"000100\",\n        vec_bool: vec![false, true, false] => \"000100\",\n        vec_u64: vec![u64::MIN, u64::MAX, 65535_u64] => \"0000000000000000ffffffffffffffff000000000000ffff\",\n\n        enum_unit: Key::Unit => \"00\",\n        enum_newtype: Key::NewType(\"foo\".to_string()) => \"01666f6f0000\",\n        enum_tuple: Key::Tuple(false, vec![0x00, 0x01], u64::MAX) => \"020000ff010000ffffffffffffffff\",\n        enum_cow: Key::Cow(vec![0x00, 0x01].into(), false, String::from(\"foo\").into()) => \"0300ff01000000666f6f0000\",\n        enum_cow_borrow: Key::Cow([0x00, 0x01].as_slice().into(), false, \"foo\".into()) => \"0300ff01000000666f6f0000\",\n\n        value_null: Value::Null => \"00\",\n        value_bool: Value::Boolean(true) => \"0101\",\n        value_int: Value::Integer(-1) => \"027fffffffffffffff\",\n        value_float: Value::Float(PI) => \"03c00921fb54442d18\",\n        value_string: Value::String(\"foo\".to_string()) => \"04666f6f0000\",\n    }\n\n    test_serialize_error! {\n        char: 'a',\n        f32: 0f32,\n        i8: 0i8,\n        i16: 0i16,\n        i32: 0i32,\n        i128: 0i128,\n        u8: 0u8,\n        u16: 0u16,\n        u32: 0u32,\n        u128: 0u128,\n        some: Some(true),\n        none: Option::<bool>::None,\n        vec_u8: vec![0u8],\n    }\n\n    test_deserialize_error! {\n        bool_empty: \"\" as bool,\n        bool_2: \"02\" as bool,\n        char: \"61\" as char,\n        f32: \"00000000\" as f32,\n        i8: \"00\" as i8,\n        i16: \"0000\" as i16,\n        i32: \"00000000\" as i32,\n        i128: \"00000000000000000000000000000000\" as i128,\n        u16: \"0000\" as u16,\n        u32: \"00000000\" as u32,\n        u64_partial: \"0000\" as u64,\n        u128: \"00000000000000000000000000000000\" as u128,\n        option: \"00\" as Option::<bool>,\n        string_utf8_invalid: \"c0\" as String,\n        tuple_partial: \"0001\" as (bool, bool, bool),\n        vec_u8: \"0000\" as Vec<u8>,\n    }\n}\n"
  },
  {
    "path": "src/encoding/mod.rs",
    "content": "//! Binary data encodings.\n//!\n//! * keycode: used for keys in the key/value store.\n//! * bincode: used for values in the key/value store and network protocols.\n\npub mod bincode;\npub mod format;\npub mod keycode;\n\nuse std::cmp::{Eq, Ord};\nuse std::collections::{BTreeSet, HashSet};\nuse std::hash::Hash;\nuse std::io::{Read, Write};\n\nuse serde::de::DeserializeOwned;\nuse serde::{Deserialize, Serialize};\n\nuse crate::error::Result;\n\n/// Adds automatic Keycode encode/decode methods to key enums. These are used\n/// as keys in the key/value store.\npub trait Key<'de>: Serialize + Deserialize<'de> {\n    /// Decodes a key from a byte slice using Keycode.\n    fn decode(bytes: &'de [u8]) -> Result<Self> {\n        keycode::deserialize(bytes)\n    }\n\n    /// Encodes a key to a byte vector using Keycode.\n    ///\n    /// In the common case, the encoded key is borrowed for a storage engine\n    /// call and then thrown away. We could avoid a bunch of allocations by\n    /// taking a reusable byte vector to encode into and return a reference to\n    /// it, but we keep it simple.\n    fn encode(&self) -> Vec<u8> {\n        keycode::serialize(self)\n    }\n}\n\n/// Adds automatic Bincode encode/decode methods to value types. These are used\n/// for values in key/value storage engines, and also for e.g. network protocol\n/// messages and other values.\npub trait Value: Serialize + DeserializeOwned {\n    /// Decodes a value from a byte slice using Bincode.\n    fn decode(bytes: &[u8]) -> Result<Self> {\n        bincode::deserialize(bytes)\n    }\n\n    /// Decodes a value from a reader using Bincode.\n    fn decode_from<R: Read>(reader: R) -> Result<Self> {\n        bincode::deserialize_from(reader)\n    }\n\n    /// Decodes a value from a reader using Bincode, or returns None if the\n    /// reader is closed.\n    fn maybe_decode_from<R: Read>(reader: R) -> Result<Option<Self>> {\n        bincode::maybe_deserialize_from(reader)\n    }\n\n    /// Encodes a value to a byte vector using Bincode.\n    fn encode(&self) -> Vec<u8> {\n        bincode::serialize(self)\n    }\n\n    /// Encodes a value into a writer using Bincode.\n    fn encode_into<W: Write>(&self, writer: W) -> Result<()> {\n        bincode::serialize_into(writer, self)\n    }\n}\n\n/// Blanket implementations for various types wrapping a value type.\nimpl<V: Value> Value for Option<V> {}\nimpl<V: Value> Value for Result<V> {}\nimpl<V: Value> Value for Vec<V> {}\nimpl<V1: Value, V2: Value> Value for (V1, V2) {}\nimpl<V: Value + Eq + Hash> Value for HashSet<V> {}\nimpl<V: Value + Eq + Ord + Hash> Value for BTreeSet<V> {}\n"
  },
  {
    "path": "src/error.rs",
    "content": "use std::fmt::Display;\n\nuse serde::{Deserialize, Serialize};\n\n/// toyDB errors.\n#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]\npub enum Error {\n    /// The operation was aborted and must be retried. This typically happens\n    /// with e.g. Raft leader changes. This is used instead of implementing\n    /// complex retry logic and replay protection in Raft.\n    Abort,\n    /// Invalid data, typically decoding errors or unexpected internal values.\n    InvalidData(String),\n    /// Invalid user input, typically parser or query errors.\n    InvalidInput(String),\n    /// An IO error.\n    IO(String),\n    /// A write was attempted in a read-only transaction.\n    ReadOnly,\n    /// A write transaction conflicted with a different writer and lost. The\n    /// transaction must be retried.\n    Serialization,\n}\n\nimpl std::error::Error for Error {}\n\nimpl Display for Error {\n    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {\n        match self {\n            Error::Abort => write!(f, \"operation aborted\"),\n            Error::InvalidData(msg) => write!(f, \"invalid data: {msg}\"),\n            Error::InvalidInput(msg) => write!(f, \"invalid input: {msg}\"),\n            Error::IO(msg) => write!(f, \"io error: {msg}\"),\n            Error::ReadOnly => write!(f, \"read-only transaction\"),\n            Error::Serialization => write!(f, \"serialization failure, retry transaction\"),\n        }\n    }\n}\n\nimpl Error {\n    /// Returns whether the error is considered deterministic. Raft state\n    /// machine application needs to know whether a command failure is\n    /// deterministic on the input command -- if it is, the command can be\n    /// considered applied and the error returned to the client, but otherwise\n    /// the state machine must panic to prevent node divergence.\n    pub fn is_deterministic(&self) -> bool {\n        match self {\n            // Aborts don't happen during application, only leader changes. But\n            // we consider them non-deterministic in case an abort should happen\n            // unexpectedly below Raft.\n            Error::Abort => false,\n            // Possible data corruption local to this node.\n            Error::InvalidData(_) => false,\n            // Input errors are (likely) deterministic. They might not be in\n            // case data was corrupted in flight, but we ignore this case.\n            Error::InvalidInput(_) => true,\n            // IO errors are typically local to the node (e.g. faulty disk).\n            Error::IO(_) => false,\n            // Write commands in read-only transactions are deterministic.\n            Error::ReadOnly => true,\n            // Write conflicts are determinstic.\n            Error::Serialization => true,\n        }\n    }\n}\n\n/// Constructs an Error::InvalidData for the given format string.\n#[macro_export]\nmacro_rules! errdata {\n    ($($args:tt)*) => { $crate::error::Error::InvalidData(format!($($args)*)).into() };\n}\n\n/// Constructs an Error::InvalidInput for the given format string.\n#[macro_export]\nmacro_rules! errinput {\n    ($($args:tt)*) => { $crate::error::Error::InvalidInput(format!($($args)*)).into() };\n}\n\n/// A toyDB Result returning Error.\npub type Result<T> = std::result::Result<T, Error>;\n\nimpl<T> From<Error> for Result<T> {\n    fn from(error: Error) -> Self {\n        Err(error)\n    }\n}\n\nimpl serde::de::Error for Error {\n    fn custom<T: Display>(msg: T) -> Self {\n        Error::InvalidData(msg.to_string())\n    }\n}\n\nimpl serde::ser::Error for Error {\n    fn custom<T: Display>(msg: T) -> Self {\n        Error::InvalidData(msg.to_string())\n    }\n}\n\nimpl From<bincode::error::DecodeError> for Error {\n    fn from(err: bincode::error::DecodeError) -> Self {\n        Error::InvalidData(err.to_string())\n    }\n}\n\nimpl From<bincode::error::EncodeError> for Error {\n    fn from(err: bincode::error::EncodeError) -> Self {\n        Error::InvalidData(err.to_string())\n    }\n}\n\nimpl From<config::ConfigError> for Error {\n    fn from(err: config::ConfigError) -> Self {\n        Error::InvalidInput(err.to_string())\n    }\n}\n\nimpl From<crossbeam::channel::RecvError> for Error {\n    fn from(err: crossbeam::channel::RecvError) -> Self {\n        Error::IO(err.to_string())\n    }\n}\n\nimpl<T> From<crossbeam::channel::SendError<T>> for Error {\n    fn from(err: crossbeam::channel::SendError<T>) -> Self {\n        Error::IO(err.to_string())\n    }\n}\n\nimpl From<crossbeam::channel::TryRecvError> for Error {\n    fn from(err: crossbeam::channel::TryRecvError) -> Self {\n        Error::IO(err.to_string())\n    }\n}\n\nimpl<T> From<crossbeam::channel::TrySendError<T>> for Error {\n    fn from(err: crossbeam::channel::TrySendError<T>) -> Self {\n        Error::IO(err.to_string())\n    }\n}\n\nimpl From<hdrhistogram::CreationError> for Error {\n    fn from(err: hdrhistogram::CreationError) -> Self {\n        panic!(\"{err}\") // faulty code\n    }\n}\n\nimpl From<hdrhistogram::RecordError> for Error {\n    fn from(err: hdrhistogram::RecordError) -> Self {\n        Error::InvalidInput(err.to_string())\n    }\n}\n\nimpl From<log::ParseLevelError> for Error {\n    fn from(err: log::ParseLevelError) -> Self {\n        Error::InvalidInput(err.to_string())\n    }\n}\n\nimpl From<log::SetLoggerError> for Error {\n    fn from(err: log::SetLoggerError) -> Self {\n        panic!(\"{err}\") // faulty code\n    }\n}\n\nimpl From<rand::distr::uniform::Error> for Error {\n    fn from(err: rand::distr::uniform::Error) -> Self {\n        Error::InvalidInput(err.to_string())\n    }\n}\n\nimpl From<regex::Error> for Error {\n    fn from(err: regex::Error) -> Self {\n        panic!(\"{err}\") // faulty code\n    }\n}\n\nimpl From<rustyline::error::ReadlineError> for Error {\n    fn from(err: rustyline::error::ReadlineError) -> Self {\n        Error::IO(err.to_string())\n    }\n}\n\nimpl From<std::array::TryFromSliceError> for Error {\n    fn from(err: std::array::TryFromSliceError) -> Self {\n        Error::InvalidData(err.to_string())\n    }\n}\n\nimpl From<std::io::Error> for Error {\n    fn from(err: std::io::Error) -> Self {\n        Error::IO(err.to_string())\n    }\n}\n\nimpl From<std::num::ParseFloatError> for Error {\n    fn from(err: std::num::ParseFloatError) -> Self {\n        Error::InvalidInput(err.to_string())\n    }\n}\n\nimpl From<std::num::ParseIntError> for Error {\n    fn from(err: std::num::ParseIntError) -> Self {\n        Error::InvalidInput(err.to_string())\n    }\n}\n\nimpl From<std::num::TryFromIntError> for Error {\n    fn from(err: std::num::TryFromIntError) -> Self {\n        Error::InvalidData(err.to_string())\n    }\n}\n\nimpl From<std::string::FromUtf8Error> for Error {\n    fn from(err: std::string::FromUtf8Error) -> Self {\n        Error::InvalidData(err.to_string())\n    }\n}\n\nimpl<T> From<std::sync::PoisonError<T>> for Error {\n    fn from(err: std::sync::PoisonError<T>) -> Self {\n        // This only happens when a different thread panics while holding a\n        // mutex. This should be fatal, so we panic here too.\n        panic!(\"{err}\")\n    }\n}\n"
  },
  {
    "path": "src/lib.rs",
    "content": "#![warn(clippy::all)]\n#![allow(clippy::large_enum_variant)]\n#![allow(clippy::module_inception)]\n#![allow(clippy::type_complexity)]\n\npub mod client;\npub mod encoding;\npub mod error;\npub mod raft;\npub mod server;\npub mod sql;\npub mod storage;\n\npub use client::Client;\npub use server::Server;\npub use sql::execution::StatementResult;\n"
  },
  {
    "path": "src/raft/log.rs",
    "content": "use std::ops::{Bound, RangeBounds};\n\nuse serde::{Deserialize, Serialize};\n\nuse super::{NodeID, Term};\nuse crate::encoding::{self, Key as _, Value as _, bincode};\nuse crate::error::Result;\nuse crate::storage;\n\n/// A log index (entry position). Starts at 1. 0 indicates no index.\npub type Index = u64;\n\n/// A log entry containing a state machine command.\n#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]\npub struct Entry {\n    /// The entry index.\n    ///\n    /// We could omit the index in the encoded value, since it's also stored in\n    /// the key, but we keep it simple.\n    pub index: Index,\n    /// The term in which the entry was added.\n    pub term: Term,\n    /// The state machine command. None (noop) commands are used during leader\n    /// election to commit old entries, see section 5.4.2 in the Raft paper.\n    pub command: Option<Vec<u8>>,\n}\n\nimpl encoding::Value for Entry {}\n\n/// A log storage key.\n#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]\npub enum Key {\n    /// A log entry, storing the term and command.\n    Entry(Index),\n    /// Stores the current term and vote (if any).\n    TermVote,\n    /// Stores the current commit index (if any).\n    CommitIndex,\n}\n\nimpl encoding::Key<'_> for Key {}\n\n/// The Raft log stores a sequence of arbitrary commands (typically writes) that\n/// are replicated across nodes and applied sequentially to the local state\n/// machine. Each entry contains an index, command, and the term in which the\n/// leader proposed it. Commands may be noops (None), which are added when a\n/// leader is elected (see section 5.4.2 in the Raft paper). For example:\n///\n/// Index | Term | Command\n/// ------|------|------------------------------------------------------\n///   1   |   1  | None\n///   2   |   1  | CREATE TABLE table (id INT PRIMARY KEY, value STRING)\n///   3   |   1  | INSERT INTO table VALUES (1, 'foo')\n///   4   |   2  | None\n///   5   |   2  | UPDATE table SET value = 'bar' WHERE id = 1\n///   6   |   2  | DELETE FROM table WHERE id = 1\n///\n/// Note that this is for illustration only, and the actual toyDB Raft commands\n/// are not SQL statements but lower-level write operations.\n///\n/// A key/value store is used to store the log entries on disk, keyed by index,\n/// along with a few other metadata keys (e.g. who we voted for in this term).\n///\n/// In the steady state, the log is append-only: when a client submits a\n/// command, the leader appends it to its own log (via [`Log::append`]) and\n/// replicates it to followers who append it to their logs (via\n/// [`Log::splice`]). When an index has been replicated to a majority of nodes\n/// it becomes committed, making the log immutable up to that index and\n/// guaranteeing that all nodes will eventually contain it. Nodes keep track of\n/// the commit index via [`Log::commit`] and apply committed commands to the\n/// state machine.\n///\n/// However, uncommitted entries can be replaced or removed. A leader may append\n/// entries to its log, but then be unable to reach consensus on them (e.g.\n/// because it is unable to communicate with a majority of nodes). If a\n/// different leader is elected and writes different commands to those same\n/// indexes, then the uncommitted entries will be replaced with entries from the\n/// new leader once the old leader (or a follower) discovers it.\n///\n/// The Raft log has the following invariants:\n///\n/// * Entry indexes are contiguous starting at 1 (no index gaps).\n/// * Entry terms never decrease from the previous entry.\n/// * Entry terms are at or below the current term.\n/// * Appended entries are durable (flushed to disk).\n/// * Appended entries use the current term.\n/// * Committed entries are never changed or removed (no log truncation).\n/// * Committed entries will eventually be replicated to all nodes.\n/// * Entries with the same index/term contain the same command.\n/// * If two logs contain a matching index/term, all previous entries\n///   are identical (see section 5.3 in the Raft paper).\npub struct Log {\n    /// The underlying storage engine. Uses a trait object instead of generics,\n    /// to allow runtime selection of the engine and avoid propagating the\n    /// generic type parameters throughout Raft.\n    pub engine: Box<dyn storage::Engine>,\n    /// The current term.\n    term: Term,\n    /// Our leader vote in the current term, if any.\n    vote: Option<NodeID>,\n    /// The index of the last stored entry.\n    last_index: Index,\n    /// The term of the last stored entry.\n    last_term: Term,\n    /// The index of the last committed entry.\n    commit_index: Index,\n    /// The term of the last committed entry.\n    commit_term: Term,\n    /// If true, fsync entries to disk when appended. This is mandated by Raft,\n    /// but comes with a hefty performance penalty (especially since we don't\n    /// optimize for it by batching entries before fsyncing). Disabling it will\n    /// yield much better write performance, but may lose data on crashes, which\n    /// in some scenarios can cause log entries to become \"uncommitted\" and\n    /// state machines diverging.\n    fsync: bool,\n}\n\nimpl Log {\n    /// Initializes a log using the given storage engine.\n    pub fn new(mut engine: Box<dyn storage::Engine>) -> Result<Self> {\n        // Load some initial in-memory state from disk.\n        let (term, vote) = engine\n            .get(&Key::TermVote.encode())?\n            .map(|v| bincode::deserialize(&v))\n            .transpose()?\n            .unwrap_or((0, None));\n        let (last_index, last_term) = engine\n            .scan_dyn((\n                Bound::Included(Key::Entry(0).encode()),\n                Bound::Included(Key::Entry(u64::MAX).encode()),\n            ))\n            .last()\n            .transpose()?\n            .map(|(_, v)| Entry::decode(&v))\n            .transpose()?\n            .map(|e| (e.index, e.term))\n            .unwrap_or((0, 0));\n        let (commit_index, commit_term) = engine\n            .get(&Key::CommitIndex.encode())?\n            .map(|v| bincode::deserialize(&v))\n            .transpose()?\n            .unwrap_or((0, 0));\n\n        let fsync = true; // fsync by default\n        Ok(Self { engine, term, vote, last_index, last_term, commit_index, commit_term, fsync })\n    }\n\n    /// Controls whether to fsync writes. Disabling this may violate Raft\n    /// guarantees, see comment on fsync attribute.\n    pub fn enable_fsync(&mut self, fsync: bool) {\n        self.fsync = fsync\n    }\n\n    /// Returns the commit index and term.\n    pub fn get_commit_index(&self) -> (Index, Term) {\n        (self.commit_index, self.commit_term)\n    }\n\n    /// Returns the last log index and term.\n    pub fn get_last_index(&self) -> (Index, Term) {\n        (self.last_index, self.last_term)\n    }\n\n    /// Returns the current term (0 if none) and vote.\n    pub fn get_term_vote(&self) -> (Term, Option<NodeID>) {\n        (self.term, self.vote)\n    }\n\n    /// Stores the current term and cast vote (if any). Enforces that the term\n    /// does not regress, and that we only vote for one node in a term. append()\n    /// will use this term, and splice() can't write entries beyond it.\n    pub fn set_term_vote(&mut self, term: Term, vote: Option<NodeID>) -> Result<()> {\n        assert!(term > 0, \"can't set term 0\");\n        assert!(term >= self.term, \"term regression {} → {}\", self.term, term);\n        assert!(term > self.term || self.vote.is_none() || vote == self.vote, \"can't change vote\");\n\n        if term == self.term && vote == self.vote {\n            return Ok(());\n        }\n        self.engine.set(&Key::TermVote.encode(), bincode::serialize(&(term, vote)))?;\n        // Always fsync, even with Log::fsync = false. Term changes are rare, so\n        // this doesn't materially affect performance, and double voting could\n        // lead to multiple leaders and split brain which is really bad.\n        self.engine.flush()?;\n        self.term = term;\n        self.vote = vote;\n        Ok(())\n    }\n\n    /// Appends a command to the log at the current term, and flushes it to\n    /// disk, returning its index. None implies a noop command, typically after\n    /// Raft leader changes.\n    pub fn append(&mut self, command: Option<Vec<u8>>) -> Result<Index> {\n        assert!(self.term > 0, \"can't append entry in term 0\");\n        let entry = Entry { index: self.last_index + 1, term: self.term, command };\n        self.engine.set(&Key::Entry(entry.index).encode(), entry.encode())?;\n        if self.fsync {\n            self.engine.flush()?;\n        }\n        self.last_index = entry.index;\n        self.last_term = entry.term;\n        Ok(entry.index)\n    }\n\n    /// Commits entries up to and including the given index. The index must\n    /// exist and be at or after the current commit index.\n    pub fn commit(&mut self, index: Index) -> Result<Index> {\n        let term = match self.get(index)? {\n            Some(entry) if entry.index < self.commit_index => {\n                panic!(\"commit index regression {} → {}\", self.commit_index, entry.index);\n            }\n            Some(entry) if entry.index == self.commit_index => return Ok(index),\n            Some(entry) => entry.term,\n            None => panic!(\"commit index {index} does not exist\"),\n        };\n        self.engine.set(&Key::CommitIndex.encode(), bincode::serialize(&(index, term)))?;\n        // NB: the commit index doesn't need to be fsynced, since the entries\n        // are fsynced and the commit index can be recovered from the quorum.\n        self.commit_index = index;\n        self.commit_term = term;\n        Ok(index)\n    }\n\n    /// Fetches an entry at an index, or None if it does not exist.\n    pub fn get(&mut self, index: Index) -> Result<Option<Entry>> {\n        self.engine.get(&Key::Entry(index).encode())?.map(|v| Entry::decode(&v)).transpose()\n    }\n\n    /// Checks if the log contains an entry with the given index and term.\n    pub fn has(&mut self, index: Index, term: Term) -> Result<bool> {\n        // Fast path: check against last_index. This is the common case when\n        // followers process appends or heartbeats.\n        if index == 0 || index > self.last_index {\n            return Ok(false);\n        }\n        if (index, term) == (self.last_index, self.last_term) {\n            return Ok(true);\n        }\n        Ok(self.get(index)?.map(|e| e.term == term).unwrap_or(false))\n    }\n\n    /// Returns an iterator over log entries in the given index range.\n    pub fn scan(&mut self, range: impl RangeBounds<Index>) -> Iterator<'_> {\n        let from = match range.start_bound() {\n            Bound::Excluded(&index) => Bound::Excluded(Key::Entry(index).encode()),\n            Bound::Included(&index) => Bound::Included(Key::Entry(index).encode()),\n            Bound::Unbounded => Bound::Included(Key::Entry(0).encode()),\n        };\n        let to = match range.end_bound() {\n            Bound::Excluded(&index) => Bound::Excluded(Key::Entry(index).encode()),\n            Bound::Included(&index) => Bound::Included(Key::Entry(index).encode()),\n            Bound::Unbounded => Bound::Included(Key::Entry(Index::MAX).encode()),\n        };\n        Iterator::new(self.engine.scan_dyn((from, to)))\n    }\n\n    /// Returns an iterator over entries that are ready to apply, starting after\n    /// the current applied index up to the commit index.\n    pub fn scan_apply(&mut self, applied_index: Index) -> Iterator<'_> {\n        // NB: we don't assert that commit_index >= applied_index, because the\n        // local commit index is not flushed to durable storage -- if lost on\n        // restart, it can be recovered from the logs of a quorum.\n        if applied_index >= self.commit_index {\n            return Iterator::new(Box::new(std::iter::empty()));\n        }\n        self.scan(applied_index + 1..=self.commit_index)\n    }\n\n    /// Splices a set of entries into the log and flushes it to disk. New\n    /// indexes will be appended. Overlapping indexes with the same term must be\n    /// equal and will be ignored. Overlapping indexes with different terms will\n    /// truncate the existing log at the first conflict and then splice the new\n    /// entries.\n    ///\n    /// The entries must have contiguous indexes and equal/increasing terms, and\n    /// the first entry must be in the range [1,last_index+1] with a term at or\n    /// above the previous (base) entry's term and at or below the current term.\n    pub fn splice(&mut self, entries: Vec<Entry>) -> Result<Index> {\n        let (Some(first), Some(last)) = (entries.first(), entries.last()) else {\n            return Ok(self.last_index); // empty input is noop\n        };\n\n        // Check that the entries are well-formed.\n        assert!(first.index > 0 && first.term > 0, \"spliced entry has index or term 0\",);\n        assert!(\n            entries.windows(2).all(|w| w[0].index + 1 == w[1].index),\n            \"spliced entries are not contiguous\"\n        );\n        assert!(\n            entries.windows(2).all(|w| w[0].term <= w[1].term),\n            \"spliced entries have term regression\",\n        );\n\n        // Check that the entries connect to the existing log (if any), and that the\n        // term doesn't regress.\n        assert!(last.term <= self.term, \"splice term {} beyond current {}\", last.term, self.term);\n        match self.get(first.index - 1)? {\n            Some(base) if first.term < base.term => {\n                panic!(\"splice term regression {} → {}\", base.term, first.term)\n            }\n            Some(_) => {}\n            None if first.index == 1 => {}\n            None => panic!(\"first index {} must touch existing log\", first.index),\n        }\n\n        // Skip entries that are already in the log.\n        let mut entries = entries.as_slice();\n        let mut scan = self.scan(first.index..=last.index);\n        while let Some(entry) = scan.next().transpose()? {\n            // [0] is ok, because the scan has the same size as entries.\n            assert!(entry.index == entries[0].index, \"index mismatch at {entry:?}\");\n            if entry.term != entries[0].term {\n                break;\n            }\n            assert!(entry.command == entries[0].command, \"command mismatch at {entry:?}\");\n            entries = &entries[1..];\n        }\n        drop(scan);\n\n        // If all entries already exist then we're done.\n        let Some(first) = entries.first() else {\n            return Ok(self.last_index);\n        };\n\n        // Write the entries that weren't already in the log, and remove the\n        // tail of the old log if any. We can't write below the commit index,\n        // since these entries must be immutable.\n        assert!(first.index > self.commit_index, \"spliced entries below commit index\");\n\n        for entry in entries {\n            self.engine.set(&Key::Entry(entry.index).encode(), entry.encode())?;\n        }\n        for index in last.index + 1..=self.last_index {\n            self.engine.delete(&Key::Entry(index).encode())?;\n        }\n        if self.fsync {\n            self.engine.flush()?;\n        }\n\n        self.last_index = last.index;\n        self.last_term = last.term;\n        Ok(self.last_index)\n    }\n\n    /// Returns log engine status.\n    pub fn status(&mut self) -> Result<storage::Status> {\n        self.engine.status()\n    }\n}\n\n/// A log entry iterator.\npub struct Iterator<'a> {\n    inner: Box<dyn storage::ScanIterator + 'a>,\n}\n\nimpl<'a> Iterator<'a> {\n    fn new(inner: Box<dyn storage::ScanIterator + 'a>) -> Self {\n        Self { inner }\n    }\n}\n\nimpl std::iter::Iterator for Iterator<'_> {\n    type Item = Result<Entry>;\n\n    fn next(&mut self) -> Option<Self::Item> {\n        self.inner.next().map(|r| r.and_then(|(_, v)| Entry::decode(&v)))\n    }\n}\n\n/// Most Raft tests are Goldenscripts under src/raft/testscripts.\n#[cfg(test)]\nmod tests {\n    use std::error::Error;\n    use std::fmt::Write as _;\n    use std::result::Result;\n\n    use crossbeam::channel::Receiver;\n    use itertools::Itertools as _;\n    use regex::Regex;\n    use tempfile::TempDir;\n    use test_each_file::test_each_path;\n\n    use super::*;\n    use crate::encoding::format::{self, Formatter as _};\n    use crate::storage::engine::test as testengine;\n\n    // Run goldenscript tests in src/raft/testscripts/log.\n    test_each_path! { in \"src/raft/testscripts/log\" as scripts => test_goldenscript }\n\n    fn test_goldenscript(path: &std::path::Path) {\n        goldenscript::run(&mut TestRunner::new(), path).expect(\"goldenscript failed\")\n    }\n\n    /// Runs Raft log goldenscript tests. For available commands, see run().\n    struct TestRunner {\n        log: Log,\n        op_rx: Receiver<testengine::Operation>,\n        #[allow(dead_code)]\n        tempdir: TempDir,\n    }\n\n    impl TestRunner {\n        fn new() -> Self {\n            // Use both a BitCask and a Memory engine, and mirror operations\n            // across them. Emit write events to op_tx.\n            let (op_tx, op_rx) = crossbeam::channel::unbounded();\n            let tempdir = TempDir::with_prefix(\"toydb\").expect(\"tempdir failed\");\n            let bitcask =\n                storage::BitCask::new(tempdir.path().join(\"bitcask\")).expect(\"bitcask failed\");\n            let memory = storage::Memory::new();\n            let engine = testengine::Emit::new(testengine::Mirror::new(bitcask, memory), op_tx);\n            let log = Log::new(Box::new(engine)).expect(\"log failed\");\n            Self { log, op_rx, tempdir }\n        }\n\n        /// Parses an index@term pair.\n        fn parse_index_term(s: &str) -> Result<(Index, Term), Box<dyn Error>> {\n            let re = Regex::new(r\"^(\\d+)@(\\d+)$\").expect(\"invalid regex\");\n            let groups = re.captures(s).ok_or_else(|| format!(\"invalid index/term {s}\"))?;\n            let index = groups.get(1).unwrap().as_str().parse()?;\n            let term = groups.get(2).unwrap().as_str().parse()?;\n            Ok((index, term))\n        }\n\n        /// Parses an index range, in Rust range syntax.\n        fn parse_index_range(s: &str) -> Result<impl RangeBounds<Index>, Box<dyn Error>> {\n            use std::ops::Bound;\n            let mut bound = (Bound::<Index>::Unbounded, Bound::<Index>::Unbounded);\n            let re = Regex::new(r\"^(\\d+)?\\.\\.(=)?(\\d+)?\").expect(\"invalid regex\");\n            let groups = re.captures(s).ok_or_else(|| format!(\"invalid range {s}\"))?;\n            if let Some(start) = groups.get(1) {\n                bound.0 = Bound::Included(start.as_str().parse()?);\n            }\n            if let Some(end) = groups.get(3) {\n                let end = end.as_str().parse()?;\n                if groups.get(2).is_some() {\n                    bound.1 = Bound::Included(end)\n                } else {\n                    bound.1 = Bound::Excluded(end)\n                }\n            }\n            Ok(bound)\n        }\n    }\n\n    impl goldenscript::Runner for TestRunner {\n        fn run(&mut self, command: &goldenscript::Command) -> Result<String, Box<dyn Error>> {\n            let mut output = String::new();\n            let mut tags = command.tags.clone();\n\n            match command.name.as_str() {\n                // append [COMMAND]\n                \"append\" => {\n                    let mut args = command.consume_args();\n                    let command = args.next_pos().map(|a| a.value.as_bytes().to_vec());\n                    args.reject_rest()?;\n                    let index = self.log.append(command)?;\n                    let entry = self.log.get(index)?.expect(\"entry not found\");\n                    let fmtentry = format::Raft::<format::Raw>::entry(&entry);\n                    writeln!(output, \"append → {fmtentry}\")?;\n                }\n\n                // commit INDEX\n                \"commit\" => {\n                    let mut args = command.consume_args();\n                    let index = args.next_pos().ok_or(\"index not given\")?.parse()?;\n                    args.reject_rest()?;\n                    let index = self.log.commit(index)?;\n                    let entry = self.log.get(index)?.expect(\"entry not found\");\n                    let fmtentry = format::Raft::<format::Raw>::entry(&entry);\n                    writeln!(output, \"commit → {fmtentry}\")?;\n                }\n\n                // dump\n                \"dump\" => {\n                    command.consume_args().reject_rest()?;\n                    let range = (std::ops::Bound::Unbounded, std::ops::Bound::Unbounded);\n                    let mut scan = self.log.engine.scan_dyn(range);\n                    while let Some((key, value)) = scan.next().transpose()? {\n                        let fmtkv = format::Raft::<format::Raw>::key_value(&key, &value);\n                        let rawkv = format::Raw::key_value(&key, &value);\n                        writeln!(output, \"{fmtkv} [{rawkv}]\")?;\n                    }\n                }\n\n                // get INDEX...\n                \"get\" => {\n                    let mut args = command.consume_args();\n                    let indexes: Vec<Index> =\n                        args.rest_pos().iter().map(|a| a.parse()).try_collect()?;\n                    args.reject_rest()?;\n                    for index in indexes {\n                        let entry = self.log.get(index)?;\n                        let fmtentry = entry\n                            .as_ref()\n                            .map(format::Raft::<format::Raw>::entry)\n                            .unwrap_or(\"None\".to_string());\n                        writeln!(output, \"{fmtentry}\")?;\n                    }\n                }\n\n                // get_term\n                \"get_term\" => {\n                    command.consume_args().reject_rest()?;\n                    let (term, vote) = self.log.get_term_vote();\n                    let vote = vote.map(|v| v.to_string()).unwrap_or(\"None\".to_string());\n                    writeln!(output, \"term={term} vote={vote}\")?;\n                }\n\n                // has INDEX@TERM...\n                \"has\" => {\n                    let mut args = command.consume_args();\n                    let indexes: Vec<(Index, Term)> = args\n                        .rest_pos()\n                        .iter()\n                        .map(|a| Self::parse_index_term(&a.value))\n                        .try_collect()?;\n                    args.reject_rest()?;\n                    for (index, term) in indexes {\n                        let has = self.log.has(index, term)?;\n                        writeln!(output, \"{has}\")?;\n                    }\n                }\n\n                // reload\n                \"reload\" => {\n                    command.consume_args().reject_rest()?;\n                    // To get owned access to the inner engine, temporarily\n                    // replace it with an empty memory engine.\n                    let engine =\n                        std::mem::replace(&mut self.log.engine, Box::new(storage::Memory::new()));\n                    self.log = Log::new(engine)?;\n                }\n\n                // scan [RANGE]\n                \"scan\" => {\n                    let mut args = command.consume_args();\n                    let range = Self::parse_index_range(\n                        args.next_pos().map_or(\"..\", |a| a.value.as_str()),\n                    )?;\n                    args.reject_rest()?;\n                    let mut scan = self.log.scan(range);\n                    while let Some(entry) = scan.next().transpose()? {\n                        let fmtentry = format::Raft::<format::Raw>::entry(&entry);\n                        writeln!(output, \"{fmtentry}\")?;\n                    }\n                }\n\n                // scan_apply APPLIED_INDEX\n                \"scan_apply\" => {\n                    let mut args = command.consume_args();\n                    let applied_index =\n                        args.next_pos().ok_or(\"applied index not given\")?.parse()?;\n                    args.reject_rest()?;\n                    let mut scan = self.log.scan_apply(applied_index);\n                    while let Some(entry) = scan.next().transpose()? {\n                        let fmtentry = format::Raft::<format::Raw>::entry(&entry);\n                        writeln!(output, \"{fmtentry}\")?;\n                    }\n                }\n\n                // set_term TERM [VOTE]\n                \"set_term\" => {\n                    let mut args = command.consume_args();\n                    let term = args.next_pos().ok_or(\"term not given\")?.parse()?;\n                    let vote = args.next_pos().map(|a| a.parse()).transpose()?;\n                    args.reject_rest()?;\n                    self.log.set_term_vote(term, vote)?;\n                }\n\n                // splice [INDEX@TERM=COMMAND...]\n                \"splice\" => {\n                    let mut args = command.consume_args();\n                    let mut entries = Vec::new();\n                    for arg in args.rest_key() {\n                        let (index, term) = Self::parse_index_term(arg.key.as_deref().unwrap())?;\n                        let command = match arg.value.as_str() {\n                            \"\" => None,\n                            value => Some(value.as_bytes().to_vec()),\n                        };\n                        entries.push(Entry { index, term, command });\n                    }\n                    args.reject_rest()?;\n                    let index = self.log.splice(entries)?;\n                    let entry = self.log.get(index)?.expect(\"entry not found\");\n                    let fmtentry = format::Raft::<format::Raw>::entry(&entry);\n                    writeln!(output, \"splice → {fmtentry}\")?;\n                }\n\n                // status [engine=BOOL]\n                \"status\" => {\n                    let mut args = command.consume_args();\n                    let engine = args.lookup_parse(\"engine\")?.unwrap_or(false);\n                    args.reject_rest()?;\n                    let (term, vote) = self.log.get_term_vote();\n                    let (last_index, last_term) = self.log.get_last_index();\n                    let (commit_index, commit_term) = self.log.get_commit_index();\n                    let vote = vote.map(|id| id.to_string()).unwrap_or(\"None\".to_string());\n                    write!(\n                        output,\n                        \"term={term} last={last_index}@{last_term} commit={commit_index}@{commit_term} vote={vote}\",\n                    )?;\n                    if engine {\n                        write!(output, \" engine={:#?}\", self.log.status()?)?;\n                    }\n                    writeln!(output)?;\n                }\n\n                name => return Err(format!(\"unknown command {name}\").into()),\n            }\n\n            // If requested, output engine operations.\n            if tags.remove(\"ops\") {\n                while let Ok(op) = self.op_rx.try_recv() {\n                    match op {\n                        testengine::Operation::Delete { key } => {\n                            let fmtkey = format::Raft::<format::Raw>::key(&key);\n                            let rawkey = format::Raw::key(&key);\n                            writeln!(output, \"engine delete {fmtkey} [{rawkey}]\")?\n                        }\n                        testengine::Operation::Flush => writeln!(output, \"engine flush\")?,\n                        testengine::Operation::Set { key, value } => {\n                            let fmtkv = format::Raft::<format::Raw>::key_value(&key, &value);\n                            let rawkv = format::Raw::key_value(&key, &value);\n                            writeln!(output, \"engine set {fmtkv} [{rawkv}]\")?\n                        }\n                    }\n                }\n            }\n\n            if let Some(tag) = tags.iter().next() {\n                return Err(format!(\"unknown tag {tag}\").into());\n            }\n\n            Ok(output)\n        }\n\n        /// If requested via [ops] tag, output engine operations for the command.\n        fn end_command(&mut self, _: &goldenscript::Command) -> Result<String, Box<dyn Error>> {\n            // Drain any remaining engine operations.\n            while self.op_rx.try_recv().is_ok() {}\n            Ok(String::new())\n        }\n    }\n}\n"
  },
  {
    "path": "src/raft/message.rs",
    "content": "use std::collections::BTreeMap;\n\nuse serde::{Deserialize, Serialize};\n\nuse super::{Entry, Index, NodeID, Term};\nuse crate::encoding;\nuse crate::error::Result;\nuse crate::storage;\n\n/// A message envelope specifying the sender and receiver.\n#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]\npub struct Envelope {\n    /// The sender.\n    pub from: NodeID,\n    /// The sender's current term.\n    pub term: Term,\n    /// The recipient.\n    pub to: NodeID,\n    /// The message.\n    pub message: Message,\n}\n\nimpl encoding::Value for Envelope {}\n\n/// A message sent between Raft nodes. Messages are sent asynchronously (i.e.\n/// they are not request/response) and may be dropped or reordered.\n///\n/// In practice, they are sent across a TCP connection and crossbeam channel\n/// which ensures messages are not dropped or reordered as long as the\n/// connection remains intact. A message and its response are sent across\n/// separate TCP connections (outbound from the respective sender).\n#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]\npub enum Message {\n    /// Candidates campaign for leadership by soliciting votes from peers.\n    /// Votes will only be granted if the candidate's log is at least as\n    /// up-to-date as the voter.\n    Campaign {\n        /// The index of the candidate's last log entry.\n        last_index: Index,\n        /// The term of the candidate's last log entry.\n        last_term: Term,\n    },\n\n    /// Followers may vote for a single candidate per term, but only if the\n    /// candidate's log is at least as up-to-date as the follower. Candidates\n    /// implicitly vote for themselves.\n    CampaignResponse {\n        /// If true, the follower granted the candidate a vote. A false response\n        /// isn't necessary, but is emitted for clarity.\n        vote: bool,\n    },\n\n    /// Leaders send periodic heartbeats. This serves several purposes:\n    ///\n    /// * Inform nodes about the leader, and prevent elections.\n    /// * Detect lost appends and reads, as a retry mechanism.\n    /// * Advance followers' commit indexes, so they can apply entries.\n    ///\n    /// The Raft paper does not have a distinct heartbeat message, and instead\n    /// uses an empty AppendEntries RPC, but we choose to add one for better\n    /// separation of concerns.\n    Heartbeat {\n        /// The index of the leader's last log entry. The term is the leader's\n        /// current term, since it appends a noop entry on election win. The\n        /// follower compares this to its own log to determine if it's\n        /// up-to-date.\n        last_index: Index,\n        /// The index of the leader's last committed log entry. Followers use\n        /// this to advance their commit index and apply entries. It's only safe\n        /// to commit this if the local log matches last_index, such that the\n        /// follower's log is identical to the leader at the commit index.\n        commit_index: Index,\n        /// The leader's latest read sequence number in this term.\n        read_seq: ReadSequence,\n    },\n\n    /// Followers respond to leader heartbeats if they still consider it leader.\n    HeartbeatResponse {\n        /// If non-zero, the heartbeat's last_index which was matched in the\n        /// follower's log. Otherwise, the follower is either divergent or\n        /// lagging behind the leader.\n        match_index: Index,\n        /// The heartbeat's read sequence number.\n        read_seq: ReadSequence,\n    },\n\n    /// Leaders replicate log entries to followers by appending to their logs\n    /// after the given base entry.\n    ///\n    /// If the base entry matches the follower's log then their logs are\n    /// identical up to it (see section 5.3 in the Raft paper), and the entries\n    /// can be appended -- possibly replacing conflicting entries. Otherwise,\n    /// the append is rejected and the leader must retry an earlier base index\n    /// until a common base is found.\n    ///\n    /// Empty appends messages (no entries) are used to probe follower logs for\n    /// a common match index in the case of divergent logs, restarted nodes, or\n    /// dropped messages. This is typically done by sending probes with a\n    /// decrementing base index until a match is found, at which point the\n    /// subsequent entries can be sent.\n    Append {\n        /// The index of the log entry to append after.\n        base_index: Index,\n        /// The  term of the base entry.\n        base_term: Term,\n        /// Log entries to append. Must start at base_index + 1.\n        entries: Vec<Entry>,\n    },\n\n    /// Followers accept or reject appends from the leader depending on whether\n    /// the base entry matches their log.\n    AppendResponse {\n        /// If non-zero, the follower appended entries up to this index. The\n        /// entire log up to this index is consistent with the leader. If no\n        /// entries were sent (a probe), this will be the matching base index.\n        match_index: Index,\n        /// If non-zero, the follower rejected an append at this base index\n        /// because the base index/term did not match its log. If the follower's\n        /// log is shorter than the base index, the reject index will be lowered\n        /// to the index after its last local index, to avoid probing each\n        /// missing index.\n        reject_index: Index,\n    },\n\n    /// Leaders need to confirm they are still the leader before serving reads,\n    /// to guarantee linearizability in case a different leader has been\n    /// estalished elsewhere. Read requests are served once the sequence number\n    /// has been confirmed by a quorum.\n    Read { seq: ReadSequence },\n\n    /// Followers confirm leadership at the read sequence numbers.\n    ReadResponse { seq: ReadSequence },\n\n    /// A client request. This can be submitted to the leader, or to a follower\n    /// which will forward it to its leader. If there is no leader, or the\n    /// leader or term changes, the request is aborted with an Error::Abort\n    /// ClientResponse and the client must retry.\n    ClientRequest {\n        /// The request ID. Must be globally unique for the request duration.\n        id: RequestID,\n        /// The request itself.\n        request: Request,\n    },\n\n    /// A client response.\n    ClientResponse {\n        /// The ID of the original ClientRequest.\n        id: RequestID,\n        /// The response, or an error.\n        response: Result<Response>,\n    },\n}\n\n/// A client request ID. Must be globally unique while in flight.\n///\n/// For simplicity, a random UUIDv4 is used. We could incorporate the\n/// node/process/MAC ID and timestamp for better collision avoidance (e.g. via\n/// UUIDv6) but it doesn't matter at this scale.\npub type RequestID = uuid::Uuid;\n\n/// A read sequence number, used to confirm leadership for linearizable reads.\npub type ReadSequence = u64;\n\n/// A client request, typically passed through to the state machine.\n#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]\npub enum Request {\n    /// A state machine read command, executed via `State::read`. This is not\n    /// replicated, and only evaluated on the leader.\n    Read(Vec<u8>),\n    /// A state machine write command, executed via `State::apply`. This is\n    /// replicated across all nodes, and must produce a deterministic result.\n    Write(Vec<u8>),\n    /// Requests Raft cluster status from the leader.\n    Status,\n}\n\nimpl encoding::Value for Request {}\n\n/// A client response. This will be wrapped in a Result for error handling.\n#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]\npub enum Response {\n    /// A state machine read result.\n    Read(Vec<u8>),\n    /// A state machine write result.\n    Write(Vec<u8>),\n    /// The current Raft leader status.\n    Status(Status),\n}\n\nimpl encoding::Value for Response {}\n\n/// Raft cluster status. Generated by the leader.\n#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]\npub struct Status {\n    /// The current Raft leader, which generated this status.\n    pub leader: NodeID,\n    /// The current Raft term.\n    pub term: Term,\n    /// The match indexes of all nodes, indicating replication progress. Uses a\n    /// BTreeMap for test determinism.\n    pub match_index: BTreeMap<NodeID, Index>,\n    /// The current commit index.\n    pub commit_index: Index,\n    /// The current applied index.\n    pub applied_index: Index,\n    /// The log storage engine status.\n    pub storage: storage::Status,\n}\n"
  },
  {
    "path": "src/raft/mod.rs",
    "content": "//! Implements the Raft distributed consensus protocol.\n//!\n//! For details, see Diego Ongaro's original writings:\n//!\n//! * Raft paper: <https://raft.github.io/raft.pdf>\n//! * Raft thesis: <https://web.stanford.edu/~ouster/cgi-bin/papers/OngaroPhD.pdf>\n//! * Raft website: <https://raft.github.io>\n//!\n//! Raft is a protocol for a group of computers to agree on some data -- or more\n//! simply, to replicate the data. It is broadly equivalent to [Paxos] and\n//! [Viewstamped Replication], but more prescriptive and simpler to understand.\n//!\n//! Raft has three main properties:\n//!\n//! * Fault tolerance: the system tolerates node failures as long as a majority\n//!   of nodes (>50%) remain operational.\n//!\n//! * Linearizability (aka strong consistency): once a client write has been\n//!   accepted, it is visible to all clients -- they never see outdated data.\n//!\n//! * Durability: a write is never lost as long as a majority of nodes remain.\n//!\n//! It does this by electing a single leader node which serves client requests\n//! and replicates writes to other nodes. Requests are executed once they have\n//! been confirmed by a strict majority of nodes (a quorum). If a leader fails,\n//! a new leader is elected. Clusters have 3 or more nodes, since a two-node\n//! cluster can't tolerate failures (1/2 is not a majority and would lead to\n//! split brain).\n//!\n//! Notably, Raft does not provide horizontal scalability. Client requests are\n//! processed by a single leader node which can quickly become a bottleneck, and\n//! each node stores a complete copy of the entire dataset. Systems often handle\n//! this by sharding the data into multiple Raft clusters and using a\n//! distributed transaction protocol across them, but this is out of scope here.\n//!\n//! toyDB follows the Raft paper fairly closely, but, like most implementations,\n//! takes some minor artistic liberties.\n//!\n//! [Paxos]: https://www.microsoft.com/en-us/research/uploads/prod/2016/12/paxos-simple-Copy.pdf\n//! [Viewstamped Replication]: https://pmg.csail.mit.edu/papers/vr-revisited.pdf\n//!\n//! RAFT LOG AND STATE MACHINE\n//! ==========================\n//!\n//! Raft maintains an ordered command log containing arbitrary write commands\n//! submitted by clients. It attempts to reach consensus on this log by\n//! replicating it to a majority of nodes. If successful, the log is considered\n//! committed and immutable up to that point.\n//!\n//! Once committed, the commands in the log are applied sequentially to a local\n//! state machine on each node. Raft itself doesn't care what the state machine\n//! and commands are -- in toyDB's case it's a SQL database, but it could be\n//! anything. Raft simply passes opaque commands to an opaque state machine.\n//!\n//! Each log entry contains an index, the leader's term (see next section), and\n//! the command. For example, a naïve illustration of a toyDB Raft log might be:\n//!\n//! Index | Term | Command\n//! ------|------|------------------------------------------------------\n//!   1   |   1  | CREATE TABLE table (id INT PRIMARY KEY, value STRING)\n//!   2   |   1  | INSERT INTO table VALUES (1, 'foo')\n//!   3   |   2  | UPDATE table SET value = 'bar' WHERE id = 1\n//!   4   |   2  | DELETE FROM table WHERE id = 1\n//!\n//! The state machine must be deterministic, such that all nodes will reach the\n//! same identical state. Raft will apply the same commands in the same order\n//! independently on all nodes, but if the commands have non-deterministic\n//! behavior such as random number generation or communication with external\n//! systems it can lead to state divergence causing different results.\n//!\n//! In toyDB, the Raft log is managed by `Log` and stored locally in a\n//! `storage::Engine`. The state machine interface is the `State` trait. See\n//! their documentation for more details.\n//!\n//! LEADER ELECTION\n//! ===============\n//!\n//! Raft nodes can be in one of three states (or roles): follower, candidate,\n//! and leader. toyDB models these as `Node::Follower`, `Node::Candidate`, and\n//! `Node::Leader`.\n//!\n//! * Follower: replicates log entries from a leader. May not know a leader yet.\n//! * Candidate: campaigns for leadership in an election.\n//! * Leader: processes client requests and replicates writes to followers.\n//!\n//! Raft fundamentally relies on a single guarantee: there can be at most one\n//! _valid_ leader at any point in time (old, since-replaced leaders may think\n//! they're still a leader, e.g. during a network partition, but they won't be\n//! able to do anything). It enforces this through the leader election protocol.\n//!\n//! Raft divides time into terms, which are monotonically increasing numbers.\n//! Higher terms always take priority over lower terms. There can be at most one\n//! leader in a term, and it can't change. Nodes keep track of their last known\n//! term and store it on disk (see `Log.set_term()`). Messages between nodes are\n//! tagged with the current term (as `Envelope.term`) -- old terms are ignored,\n//! and future terms cause the node to become a follower in that term.\n//!\n//! Nodes start out as leaderless followers. If they receive a message from a\n//! leader (in a current or future term), they follow it. Otherwise, they wait\n//! out the election timeout (a few seconds), become candidates, and hold a\n//! leader election.\n//!\n//! Candidates increase their term by 1 and send `Message::Campaign` to all\n//! nodes, requesting their vote. Nodes respond with `Message::CampaignResponse`\n//! saying whether a vote was granted. A node can only grant a single vote in a\n//! term (stored to disk via `Log.set_term()`), on a first-come first-serve\n//! basis, and candidates implicitly vote for themselves.\n//!\n//! When a candidate receives a majority of votes (>50%), it becomes leader. It\n//! sends a `Message::Heartbeat` to all nodes asserting its leadership, and all\n//! nodes become followers when they receive it (regardless of who they voted\n//! for). Leaders continue to send periodic heartbeats every second or so. The\n//! new leader also appends an empty entry to its log in order to safely commit\n//! all entries from previous terms (Raft paper section 5.4.2).\n//!\n//! The new leader must have all committed entries in its log (or the cluster\n//! would lose data). To ensure this, there is one additional condition for\n//! granting a vote: the candidate's log must be at least as up-to-date as the\n//! voter. Because an entry must be replicated to a majority before being\n//! committed, this ensures a candidate can only win a majority of votes if its\n//! log is up-to-date with all committed entries (Raft paper section 5.4.1).\n//!\n//! It's possible that no candidate wins an election, for example due to a tie\n//! or a majority of nodes being offline. After an election timeout passes,\n//! candidates will again bump their term and start a new election, until a\n//! leader can be established. To avoid frequent ties, nodes use different,\n//! randomized election timeouts (Raft paper section 5.2).\n//!\n//! Similarly, if a follower doesn't hear from a leader in an election timeout\n//! interval, it will become candidate and hold another election. The periodic\n//! leader heartbeats prevent this as long as the leader is running and\n//! connected. A node that becomes disconnected from the leader will continually\n//! hold new elections by itself until the network heals, at which point a new\n//! election will be held in its term (disrupting the current leader).\n//!\n//! REPLICATION AND CONSENSUS\n//! =========================\n//!\n//! When the leader receives a client write request, it appends the command to\n//! its local log via `Log.append()`, and sends the log entry to all peers in\n//! a `Message::Append`. Followers will attempt to durably append the entry to\n//! their local logs and respond with `Message::AppendResponse`.\n//!\n//! Once a majority have acknowledged the append, the leader commits the entry\n//! via `Log.commit()` and applies it to its local state machine, returning the\n//! result to the client. It will inform followers about the commit in the next\n//! heartbeat as `Message::Heartbeat.commit_index` so they can apply it too, but\n//! this is not necessary for correctness (they will commit and apply it if they\n//! become leader, otherwise they have no need for applying it).\n//!\n//! Followers may not be able to append the entry to their log -- they may be\n//! unreachable, lag behind the leader, or have divergent logs (see Raft paper\n//! section 5.3). The `Append` contains the index and term of the log entry\n//! immediately before the replicated entry as `base_index` and `base_term`. An\n//! index/term pair uniquely identifies a command, and if two logs have the same\n//! index/term pair then the logs are identical up to and including that entry\n//! (Raft paper section 5.3). If the base index/term matches the follower's log,\n//! it appends the entry (potentially replacing any conflicting entries),\n//! otherwise it rejects it.\n//!\n//! When a follower rejects an append, the leader must try to find a common log\n//! entry that exists in both its and the follower's log where it can resume\n//! replication. It does this by sending `Message::Append` probes only\n//! containing a base index/term but no entries -- it will continue to probe\n//! decreasing indexes one by one until the follower responds with a match, then\n//! send an `Append` with the missing entries (Raft paper section 5.3). It keeps\n//! track of each follower's `match_index` and `next_index` in a `Progress`\n//! struct to manage this.\n//!\n//! In case `Append` messages or responses are lost, leaders also send their\n//! `last_index` and term in each `Heartbeat`. If followers don't have that\n//! index/term pair in their log, they'll say so in the `HeartbeatResponse` and\n//! the leader can begin probing their logs as with append rejections.\n//!\n//! CLIENT REQUESTS\n//! ===============\n//!\n//! Client requests are submitted as `Message::ClientRequest` to the local Raft\n//! node. They are only processed on the leader, but followers will proxy them\n//! to the leader (Raft thesis section 6.2). To avoid complications with message\n//! replays (Raft thesis section 6.3), requests are not retried internally, and\n//! are explicitly aborted with `Error::Abort` on leader/term changes as well as\n//! elections.\n//!\n//! Write requests, `Request::Write`, are appended to the Raft log and\n//! replicated. The leader keeps track of the request and its log index in a\n//! `Write` struct. Once the command is committed and applied to the local state\n//! machine, the leader looks up the write request by its log index and sends\n//! the result to the client. Deterministic errors (e.g. foreign key violations)\n//! are also returned to the client, but non-deterministic errors (e.g. IO\n//! errors) must panic the node to avoid state divergence.\n//!\n//! Read requests, `Request::Read`, are only executed on the leader and don't\n//! need to be replicated via the Raft log. However, to ensure linearizability,\n//! the leader has to confirm with a quorum that it's actually still the leader.\n//! Otherwise, it's possible that a new leader has been elected elsewhere and\n//! executed writes without us knowing about it. It does this by assigning an\n//! incrementing sequence number to each read, keeping track of the request in a\n//! `Read` struct, and immediately sending a `Read` message with the latest\n//! sequence number. Followers respond with the sequence number, and once a\n//! quorum have confirmed a sequence number the read is executed and the result\n//! returned to the client.\n//!\n//! IMPLEMENTATION CAVEATS\n//! ======================\n//!\n//! For simplicity, toyDB implements the bare minimum for a functional and\n//! correct Raft protocol, and omits several advanced mechanisms that would be\n//! needed for a real production system. In particular:\n//!\n//! * No leases: for linearizability, every read request requires the leader to\n//!   confirm with followers that it's still the leader. This could be avoided\n//!   with a leader lease for a predefined time interval (Raft paper section 8,\n//!   Raft thesis section 6.3).\n//!\n//! * No cluster membership changes: to add or remove nodes, the entire cluster\n//!   must be stopped and restarted with the new configuration, otherwise it\n//!   risks multiple leaders (Raft paper section 6).\n//!\n//! * No snapshots: new or lagging nodes must be caught up by replicating and\n//!   replaying the entire log, instead of sending a state machine snapshot\n//!   (Raft paper section 7).\n//!\n//! * No log truncation: because snapshots aren't supported, the entire Raft\n//!   log must be retained forever in order to catch up new/lagging nodes,\n//!   leading to excessive storage use (Raft paper section 7).\n//!\n//! * No pre-vote or check-quorum: a node that's partially partitioned (can\n//!   reach some but not all nodes) can cause persistent unavailability with\n//!   spurious elections or heartbeats. A node rejoining after a partition can\n//!   also temporarily disrupt a leader. This requires additional pre-vote and\n//!   check-quorum protocol extensions (Raft thesis section 4.2.3 and 9.6).\n//!\n//! * No request retries: client requests will not be retried on leader changes\n//!   or message loss, and will be aggressively aborted, to ignore problems\n//!   related to message replay (Raft thesis section 6.3).\n//!\n//! * No reject hints: if a follower has a divergent log, the leader will probe\n//!   entries one by one until a match is found. The replication protocol could\n//!   instead be extended with rejection hints (Raft paper section 5.3).\n\nmod log;\nmod message;\nmod node;\nmod state;\n\nuse std::ops::Range;\nuse std::time::Duration;\n\npub use log::{Entry, Index, Key, Log};\npub use message::{Envelope, Message, ReadSequence, Request, RequestID, Response, Status};\npub use node::{Node, NodeID, Options, Term, Ticks};\npub use state::State;\n\n/// The interval between Raft ticks, the Raft unit of time.\npub const TICK_INTERVAL: Duration = Duration::from_millis(100);\n\n/// The interval between leader heartbeats in ticks.\nconst HEARTBEAT_INTERVAL: Ticks = 4;\n\n/// The default election timeout range in ticks. To avoid election ties, a node\n/// chooses a random value in this interval.\nconst ELECTION_TIMEOUT_RANGE: Range<Ticks> = 10..20;\n\n/// The maximum number of log entries to send in a single append message.\nconst MAX_APPEND_ENTRIES: usize = 100;\n"
  },
  {
    "path": "src/raft/node.rs",
    "content": "use std::cmp::{max, min};\nuse std::collections::{HashMap, HashSet, VecDeque};\nuse std::ops::Range;\n\nuse crossbeam::channel::Sender;\nuse itertools::Itertools as _;\nuse log::{debug, info};\nuse rand::RngExt as _;\n\nuse super::log::{Index, Log};\nuse super::message::{Envelope, Message, ReadSequence, Request, RequestID, Response, Status};\nuse super::state::State;\nuse super::{ELECTION_TIMEOUT_RANGE, HEARTBEAT_INTERVAL, MAX_APPEND_ENTRIES};\nuse crate::errinput;\nuse crate::error::{Error, Result};\n\n/// A node ID, unique within a cluster. Assigned manually when started.\npub type NodeID = u8;\n\n/// A leader term number. Increases monotonically on elections.\npub type Term = u64;\n\n/// A logical clock interval as number of ticks.\npub type Ticks = u8;\n\n/// Raft node options.\n#[derive(Clone, Debug, PartialEq)]\npub struct Options {\n    /// The number of ticks between leader heartbeats.\n    pub heartbeat_interval: Ticks,\n    /// The range of randomized election timeouts for followers and candidates.\n    pub election_timeout_range: Range<Ticks>,\n    /// Maximum number of entries to send in a single Append message.\n    pub max_append_entries: usize,\n}\n\nimpl Default for Options {\n    fn default() -> Self {\n        Self {\n            heartbeat_interval: HEARTBEAT_INTERVAL,\n            election_timeout_range: ELECTION_TIMEOUT_RANGE,\n            max_append_entries: MAX_APPEND_ENTRIES,\n        }\n    }\n}\n\n/// A Raft node with a dynamic role. This implements the Raft distributed\n/// consensus protocol, see the `raft` module documentation for more info.\n///\n/// The node is driven synchronously by processing inbound messages via `step()`\n/// and by advancing time via `tick()`. These methods consume the node and\n/// return a new one with a possibly different role. Outbound messages are sent\n/// via the given `tx` channel, and must be delivered to peers or clients.\n///\n/// This enum is the public interface to the node, with a closed set of roles.\n/// It wraps the `RawNode<Role>` types, which implement the actual node logic.\n/// The enum allows ergonomic use across role transitions since it can represent\n/// all roles, e.g.: `node = node.step()?`.\npub enum Node {\n    /// A candidate campaigns for leadership.\n    Candidate(RawNode<Candidate>),\n    /// A follower replicates entries from a leader.\n    Follower(RawNode<Follower>),\n    /// A leader processes client requests and replicates entries to followers.\n    Leader(RawNode<Leader>),\n}\n\nimpl Node {\n    /// Creates a new Raft node. It starts as a leaderless follower, waiting to\n    /// hear from a leader or otherwise transitioning to candidate and\n    /// campaigning for leadership. In the case of a single-node cluster (no\n    /// peers), the node immediately transitions to leader when created.\n    pub fn new(\n        id: NodeID,\n        peers: HashSet<NodeID>,\n        log: Log,\n        state: Box<dyn State>,\n        tx: Sender<Envelope>,\n        opts: Options,\n    ) -> Result<Self> {\n        let node = RawNode::new(id, peers, log, state, tx, opts)?;\n        // If this is a single-node cluster, become leader immediately.\n        if node.cluster_size() == 1 {\n            return Ok(node.into_candidate()?.into_leader()?.into());\n        }\n        Ok(node.into())\n    }\n\n    /// Returns the node's ID.\n    pub fn id(&self) -> NodeID {\n        match self {\n            Self::Candidate(node) => node.id,\n            Self::Follower(node) => node.id,\n            Self::Leader(node) => node.id,\n        }\n    }\n\n    /// Returns the node's term.\n    pub fn term(&self) -> Term {\n        match self {\n            Self::Candidate(node) => node.term(),\n            Self::Follower(node) => node.term(),\n            Self::Leader(node) => node.term(),\n        }\n    }\n\n    /// Processes an inbound message.\n    pub fn step(self, msg: Envelope) -> Result<Self> {\n        let peers = match &self {\n            Self::Candidate(node) => &node.peers,\n            Self::Follower(node) => &node.peers,\n            Self::Leader(node) => &node.peers,\n        };\n        assert_eq!(msg.to, self.id(), \"message to other node: {msg:?}\");\n        assert!(peers.contains(&msg.from) || msg.from == self.id(), \"unknown sender: {msg:?}\");\n        debug!(\"Stepping {msg:?}\");\n\n        match self {\n            Self::Candidate(node) => node.step(msg),\n            Self::Follower(node) => node.step(msg),\n            Self::Leader(node) => node.step(msg),\n        }\n    }\n\n    /// Advances time by a tick.\n    pub fn tick(self) -> Result<Self> {\n        match self {\n            Self::Candidate(node) => node.tick(),\n            Self::Follower(node) => node.tick(),\n            Self::Leader(node) => node.tick(),\n        }\n    }\n}\n\nimpl From<RawNode<Candidate>> for Node {\n    fn from(node: RawNode<Candidate>) -> Self {\n        Node::Candidate(node)\n    }\n}\n\nimpl From<RawNode<Follower>> for Node {\n    fn from(node: RawNode<Follower>) -> Self {\n        Node::Follower(node)\n    }\n}\n\nimpl From<RawNode<Leader>> for Node {\n    fn from(node: RawNode<Leader>) -> Self {\n        Node::Leader(node)\n    }\n}\n\n/// Marker trait for a Raft role: leader, follower, or candidate.\npub trait Role {}\n\n/// A Raft node with role R.\n///\n/// This implements the typestate pattern, where individual node states (roles)\n/// are encoded as RawNode<Role>. See http://cliffle.com/blog/rust-typestate/.\npub struct RawNode<R: Role> {\n    /// The node ID. Must be unique in the cluster.\n    id: NodeID,\n    /// The IDs of the other nodes in the cluster. Does not change while\n    /// running. Can change on restart, but all nodes must have the same set of\n    /// nodes, otherwise it can result in multiple leaders (split brain).\n    peers: HashSet<NodeID>,\n    /// The Raft log, which stores client commands to be executed.\n    log: Log,\n    /// The Raft state machine, which executes client commands from the log.\n    state: Box<dyn State>,\n    /// Channel for sending outbound messages to other nodes.\n    tx: Sender<Envelope>,\n    /// Node options.\n    opts: Options,\n    /// Role-specific state.\n    role: R,\n}\n\nimpl<R: Role> RawNode<R> {\n    /// Helper for role transitions.\n    fn into_role<T: Role>(self, role: T) -> RawNode<T> {\n        RawNode {\n            id: self.id,\n            peers: self.peers,\n            log: self.log,\n            state: self.state,\n            tx: self.tx,\n            opts: self.opts,\n            role,\n        }\n    }\n\n    /// Returns the node's current term.\n    fn term(&self) -> Term {\n        self.log.get_term_vote().0\n    }\n\n    /// Returns the cluster size as number of nodes.\n    fn cluster_size(&self) -> usize {\n        self.peers.len() + 1\n    }\n\n    /// Returns the cluster quorum size (strict majority).\n    fn quorum_size(&self) -> usize {\n        self.cluster_size() / 2 + 1\n    }\n\n    /// Returns the quorum value (i.e. median) of the given unsorted vector. It\n    /// must have the same length as the cluster size.\n    fn quorum_value<T: Ord + Copy>(&self, mut values: Vec<T>) -> T {\n        assert_eq!(values.len(), self.cluster_size(), \"vector size must match cluster size\");\n        *values.select_nth_unstable_by(self.quorum_size() - 1, |a, b| a.cmp(b).reverse()).1\n    }\n\n    /// Generates a random election timeout.\n    fn random_election_timeout(&self) -> Ticks {\n        rand::rng().random_range(self.opts.election_timeout_range.clone())\n    }\n\n    /// Sends a message to the given recipient.\n    fn send(&self, to: NodeID, message: Message) -> Result<()> {\n        Self::send_via(&self.tx, Envelope { from: self.id, to, term: self.term(), message })\n    }\n\n    /// Sends a message via the given channel. This avoid borrowing self, to\n    /// allow sending while holding partial borrows of self.\n    fn send_via(tx: &Sender<Envelope>, msg: Envelope) -> Result<()> {\n        debug!(\"Sending {msg:?}\");\n        Ok(tx.send(msg)?)\n    }\n\n    /// Broadcasts a message to all peers.\n    fn broadcast(&self, message: Message) -> Result<()> {\n        // Send in increasing ID order for test determinism.\n        for id in self.peers.iter().copied().sorted() {\n            self.send(id, message.clone())?;\n        }\n        Ok(())\n    }\n}\n\n/// A follower replicates log entries from a leader and forwards client requests\n/// to it. Nodes start as leaderless followers, until they either discover a\n/// leader or hold an election.\npub struct Follower {\n    /// The leader, or None if we're a leaderless follower.\n    leader: Option<NodeID>,\n    /// The number of ticks since the last message from the leader.\n    leader_seen: Ticks,\n    /// The leader_seen timeout before triggering an election.\n    election_timeout: Ticks,\n    // Local client requests that have been forwarded to the leader. These are\n    // aborted on leader/term changes.\n    forwarded: HashSet<RequestID>,\n}\n\nimpl Follower {\n    /// Creates a new follower role.\n    fn new(leader: Option<NodeID>, election_timeout: Ticks) -> Self {\n        Self { leader, leader_seen: 0, election_timeout, forwarded: HashSet::new() }\n    }\n}\n\nimpl Role for Follower {}\n\nimpl RawNode<Follower> {\n    /// Creates a new node as a leaderless follower.\n    fn new(\n        id: NodeID,\n        peers: HashSet<NodeID>,\n        log: Log,\n        state: Box<dyn State>,\n        tx: Sender<Envelope>,\n        opts: Options,\n    ) -> Result<Self> {\n        if peers.contains(&id) {\n            return errinput!(\"node ID {id} can't be in peers\");\n        }\n        let role = Follower::new(None, 0);\n        let mut node = Self { id, peers, log, state, tx, opts, role };\n        node.role.election_timeout = node.random_election_timeout();\n\n        // Apply any pending entries following restart. State machine writes are\n        // not flushed to durable storage, so a tail of writes may be lost if\n        // the host crashes or restarts. The Raft log is durable, so we can\n        // always recover the state from it. We reapply any missing entries here\n        // if that should happen.\n        node.maybe_apply()?;\n        Ok(node)\n    }\n\n    /// Transitions the follower into a candidate, by campaigning for\n    /// leadership in a new term.\n    fn into_candidate(mut self) -> Result<RawNode<Candidate>> {\n        // Abort any forwarded requests. These must be retried with new leader.\n        self.abort_forwarded()?;\n\n        // Apply any pending log entries, so that we're caught up if we win.\n        self.maybe_apply()?;\n\n        // Become candidate and campaign.\n        let election_timeout = self.random_election_timeout();\n        let mut node = self.into_role(Candidate::new(election_timeout));\n        node.campaign()?;\n\n        let (term, vote) = node.log.get_term_vote();\n        assert!(node.role.votes.contains(&node.id), \"candidate did not vote for self\");\n        assert_ne!(term, 0, \"candidate can't have term 0\");\n        assert_eq!(vote, Some(node.id), \"log vote does not match self\");\n\n        Ok(node)\n    }\n\n    /// Transitions the follower into either a leaderless follower in a new term\n    /// (e.g. if someone holds a new election) or a follower of a current leader.\n    fn into_follower(mut self, term: Term, leader: Option<NodeID>) -> Result<RawNode<Follower>> {\n        assert_ne!(term, 0, \"can't become follower in term 0\");\n\n        // Abort any forwarded requests. These must be retried with new leader.\n        self.abort_forwarded()?;\n\n        if let Some(leader) = leader {\n            // We found a leader in the current term.\n            assert!(self.peers.contains(&leader), \"leader is not a peer\");\n            assert_eq!(self.role.leader, None, \"already have leader in term\");\n            assert_eq!(term, self.term(), \"can't follow leader in different term\");\n            info!(\"Following leader {leader} in term {term}\");\n            self.role = Follower::new(Some(leader), self.role.election_timeout);\n        } else {\n            // We found a new term, but we don't know who the leader is yet.\n            // We'll find out if we step a message from it.\n            assert_ne!(term, self.term(), \"can't become leaderless follower in current term\");\n            info!(\"Discovered new term {term}\");\n            self.log.set_term_vote(term, None)?;\n            self.role = Follower::new(None, self.random_election_timeout());\n        }\n        Ok(self)\n    }\n\n    /// Processes an inbound message.\n    fn step(mut self, msg: Envelope) -> Result<Node> {\n        // Past term: outdated peer, drop the message.\n        if msg.term < self.term() {\n            debug!(\"Dropping message from past term: {msg:?}\");\n            return Ok(self.into());\n        }\n        // Future term: newer leader or candidate, become leaderless follower\n        // and step the message.\n        if msg.term > self.term() {\n            return self.into_follower(msg.term, None)?.step(msg);\n        }\n\n        // Record when we last saw a message from the leader (if any).\n        if Some(msg.from) == self.role.leader {\n            self.role.leader_seen = 0\n        }\n\n        match msg.message {\n            // The leader sends periodic heartbeats. If we don't have a leader\n            // yet, follow it. If the commit_index advances, apply commands.\n            Message::Heartbeat { last_index, commit_index, read_seq } => {\n                assert!(commit_index <= last_index, \"commit_index after last_index\");\n\n                // Make sure the heartbeat is from our leader, or follow it.\n                match self.role.leader {\n                    Some(leader) => assert_eq!(msg.from, leader, \"multiple leaders in term\"),\n                    None => self = self.into_follower(msg.term, Some(msg.from))?,\n                }\n\n                // Check if our log matches the leader's log up to last_index,\n                // and respond to the heartbeat. last_index always has the\n                // leader's term, since it only appends entries in its term.\n                let match_index = if self.log.has(last_index, msg.term)? { last_index } else { 0 };\n                self.send(msg.from, Message::HeartbeatResponse { match_index, read_seq })?;\n\n                // Advance the commit index and apply entries. We can only do\n                // this if we matched the leader's last_index, which implies\n                // that the logs are identical up to match_index. This also\n                // implies that the commit_index is present in our log.\n                if match_index != 0 && commit_index > self.log.get_commit_index().0 {\n                    self.log.commit(commit_index)?;\n                    self.maybe_apply()?;\n                }\n            }\n\n            // Append log entries from the leader to the local log.\n            Message::Append { base_index, base_term, entries } => {\n                if let Some(first) = entries.first() {\n                    assert_eq!(base_index, first.index - 1, \"base index mismatch\");\n                }\n\n                // Make sure the append is from our leader, or follow it.\n                match self.role.leader {\n                    Some(leader) => assert_eq!(msg.from, leader, \"multiple leaders in term\"),\n                    None => self = self.into_follower(msg.term, Some(msg.from))?,\n                }\n\n                // If the base entry matches our log, append the entries.\n                if base_index == 0 || self.log.has(base_index, base_term)? {\n                    let match_index = entries.last().map(|e| e.index).unwrap_or(base_index);\n                    self.log.splice(entries)?;\n                    self.send(msg.from, Message::AppendResponse { match_index, reject_index: 0 })?;\n                } else {\n                    // Otherwise, reject the base index. If the local log is\n                    // shorter than the base index, lower the reject index to\n                    // skip all missing entries.\n                    let reject_index = min(base_index, self.log.get_last_index().0 + 1);\n                    self.send(msg.from, Message::AppendResponse { reject_index, match_index: 0 })?;\n                }\n            }\n\n            // Confirm the leader's read sequence number.\n            Message::Read { seq } => {\n                // Make sure the read is from our leader, or follow it.\n                match self.role.leader {\n                    Some(leader) => assert_eq!(msg.from, leader, \"multiple leaders in term\"),\n                    None => self = self.into_follower(msg.term, Some(msg.from))?,\n                }\n\n                // Confirm the read.\n                self.send(msg.from, Message::ReadResponse { seq })?;\n            }\n\n            // A candidate is requesting our vote. We only grant one per term.\n            Message::Campaign { last_index, last_term } => {\n                // Don't vote if we already voted for someone else in this term.\n                // We can repeat our vote for the same node though.\n                if let (_, Some(vote)) = self.log.get_term_vote()\n                    && msg.from != vote\n                {\n                    self.send(msg.from, Message::CampaignResponse { vote: false })?;\n                    return Ok(self.into());\n                }\n\n                // Only vote if the candidate's log is at least as long as ours.\n                // At least one node in any quorum must have all committed\n                // entries, and this ensures we'll only elect a leader that has\n                // all committed entries. See section 5.4.1 in the Raft paper.\n                let (log_index, log_term) = self.log.get_last_index();\n                if log_term > last_term || log_term == last_term && log_index > last_index {\n                    self.send(msg.from, Message::CampaignResponse { vote: false })?;\n                    return Ok(self.into());\n                }\n\n                // Grant the vote.\n                info!(\"Voting for {} in term {} election\", msg.from, msg.term);\n                self.log.set_term_vote(msg.term, Some(msg.from))?;\n                self.send(msg.from, Message::CampaignResponse { vote: true })?;\n            }\n\n            // Forward client requests to the leader, or abort them if there is\n            // none. These will not be retried, the client should use timeouts\n            // instead.  Local client requests use our node ID as the sender.\n            Message::ClientRequest { id, request: _ } => {\n                assert_eq!(msg.from, self.id, \"client request from other node\");\n\n                if let Some(leader) = self.role.leader {\n                    debug!(\"Forwarding request to leader {leader}: {msg:?}\");\n                    self.role.forwarded.insert(id);\n                    self.send(leader, msg.message)?\n                } else {\n                    let response = Err(Error::Abort);\n                    self.send(msg.from, Message::ClientResponse { id, response })?\n                }\n            }\n\n            // Client responses from the leader are passed on to the client.\n            Message::ClientResponse { id, response } => {\n                assert_eq!(Some(msg.from), self.role.leader, \"client response from non-leader\");\n\n                if self.role.forwarded.remove(&id) {\n                    self.send(self.id, Message::ClientResponse { id, response })?;\n                }\n            }\n\n            // We may receive a vote after we lost an election, ignore it.\n            Message::CampaignResponse { .. } => {}\n\n            // We're not leader this term, so we shouldn't see these.\n            Message::HeartbeatResponse { .. }\n            | Message::AppendResponse { .. }\n            | Message::ReadResponse { .. } => {\n                panic!(\"follower received unexpected message {msg:?}\")\n            }\n        };\n        Ok(self.into())\n    }\n\n    /// Processes a logical clock tick.\n    fn tick(mut self) -> Result<Node> {\n        // Campaign if we haven't heard from the leader in a while.\n        self.role.leader_seen += 1;\n        if self.role.leader_seen >= self.role.election_timeout {\n            return Ok(self.into_candidate()?.into());\n        }\n        Ok(self.into())\n    }\n\n    /// Aborts all forwarded requests (e.g. on term/leader changes).\n    fn abort_forwarded(&mut self) -> Result<()> {\n        // Sort by ID for test determinism.\n        for id in std::mem::take(&mut self.role.forwarded).into_iter().sorted() {\n            debug!(\"Aborting forwarded request {id}\");\n            self.send(self.id, Message::ClientResponse { id, response: Err(Error::Abort) })?;\n        }\n        Ok(())\n    }\n\n    /// Applies any pending log entries.\n    fn maybe_apply(&mut self) -> Result<()> {\n        let mut iter = self.log.scan_apply(self.state.get_applied_index());\n        while let Some(entry) = iter.next().transpose()? {\n            debug!(\"Applying {entry:?}\");\n            // Throw away the result, since only the leader responds to clients.\n            // This includes errors -- any non-deterministic errors (e.g. IO\n            // errors) must panic instead to avoid node divergence.\n            _ = self.state.apply(entry);\n        }\n        Ok(())\n    }\n}\n\n/// A candidate is campaigning to become a leader.\npub struct Candidate {\n    /// Votes received (including our own).\n    votes: HashSet<NodeID>,\n    /// Ticks elapsed since election start.\n    election_duration: Ticks,\n    /// Election timeout, in ticks.\n    election_timeout: Ticks,\n}\n\nimpl Candidate {\n    /// Creates a new candidate role.\n    fn new(election_timeout: Ticks) -> Self {\n        Self { votes: HashSet::new(), election_duration: 0, election_timeout }\n    }\n}\n\nimpl Role for Candidate {}\n\nimpl RawNode<Candidate> {\n    /// Transitions the candidate to a follower. We either lost the election and\n    /// follow the winner, or we discovered a new term and step into it as a\n    /// leaderless follower.\n    fn into_follower(mut self, term: Term, leader: Option<NodeID>) -> Result<RawNode<Follower>> {\n        let election_timeout = self.random_election_timeout();\n        if let Some(leader) = leader {\n            // We lost the election, follow the winner.\n            assert_eq!(term, self.term(), \"can't follow leader in different term\");\n            info!(\"Lost election, following leader {leader} in term {term}\");\n            Ok(self.into_role(Follower::new(Some(leader), election_timeout)))\n        } else {\n            // We found a new term, but we don't necessarily know who the leader\n            // is yet. We'll find out when we step a message from it.\n            assert_ne!(term, self.term(), \"can't become leaderless follower in current term\");\n            info!(\"Discovered new term {term}\");\n            self.log.set_term_vote(term, None)?;\n            Ok(self.into_role(Follower::new(None, election_timeout)))\n        }\n    }\n\n    /// Transitions the candidate to a leader. We won the election.\n    fn into_leader(self) -> Result<RawNode<Leader>> {\n        let (term, vote) = self.log.get_term_vote();\n        assert_ne!(term, 0, \"leaders can't have term 0\");\n        assert_eq!(vote, Some(self.id), \"leader did not vote for self\");\n\n        info!(\"Won election for term {term}, becoming leader\");\n        let peers = self.peers.clone();\n        let (last_index, _) = self.log.get_last_index();\n        let mut node = self.into_role(Leader::new(peers, last_index));\n\n        // Propose an empty command when assuming leadership, to disambiguate\n        // previous entries in the log. See section 5.4.2 in the Raft paper.\n        // We do this prior to the heartbeat, to avoid a wasted replication\n        // roundtrip if the heartbeat response indicates the peer is behind.\n        node.propose(None)?;\n        node.maybe_commit_and_apply()?;\n        node.heartbeat()?;\n\n        Ok(node)\n    }\n\n    /// Processes an inbound message.\n    fn step(mut self, msg: Envelope) -> Result<Node> {\n        // Past term: outdated peer, drop the message.\n        if msg.term < self.term() {\n            debug!(\"Dropping message from past term: {msg:?}\");\n            return Ok(self.into());\n        }\n        // Future term: newer leader or candidate, become leaderless follower\n        // and step the message.\n        if msg.term > self.term() {\n            return self.into_follower(msg.term, None)?.step(msg);\n        }\n\n        match msg.message {\n            // If we received a vote, record it. If the vote gives us quorum,\n            // assume leadership.\n            Message::CampaignResponse { vote: true } => {\n                self.role.votes.insert(msg.from);\n                if self.role.votes.len() >= self.quorum_size() {\n                    return Ok(self.into_leader()?.into());\n                }\n            }\n\n            // We didn't get the vote. :(\n            Message::CampaignResponse { vote: false } => {}\n\n            // Don't grant votes for other candidates.\n            Message::Campaign { .. } => {\n                self.send(msg.from, Message::CampaignResponse { vote: false })?\n            }\n\n            // If we hear from a leader in this term, we lost the election.\n            // Follow it and step the message.\n            Message::Heartbeat { .. } | Message::Append { .. } | Message::Read { .. } => {\n                return self.into_follower(msg.term, Some(msg.from))?.step(msg);\n            }\n\n            // Abort client requests while campaigning. The client must retry.\n            Message::ClientRequest { id, request: _ } => {\n                self.send(msg.from, Message::ClientResponse { id, response: Err(Error::Abort) })?;\n            }\n\n            // We're not a leader in this term, nor are we forwarding requests,\n            // so we shouldn't see these.\n            Message::HeartbeatResponse { .. }\n            | Message::AppendResponse { .. }\n            | Message::ReadResponse { .. }\n            | Message::ClientResponse { .. } => panic!(\"unexpected message {msg:?}\"),\n        }\n        Ok(self.into())\n    }\n\n    /// Processes a logical clock tick.\n    fn tick(mut self) -> Result<Node> {\n        // If noone won this election, start a new one after a while.\n        self.role.election_duration += 1;\n        if self.role.election_duration >= self.role.election_timeout {\n            self.campaign()?;\n        }\n        Ok(self.into())\n    }\n\n    /// Hold a new election by increasing the term, voting for ourself, and\n    /// soliciting votes from all peers.\n    fn campaign(&mut self) -> Result<()> {\n        let term = self.term() + 1;\n        info!(\"Starting new election for term {term}\");\n        self.role = Candidate::new(self.random_election_timeout());\n        self.role.votes.insert(self.id); // vote for ourself\n        self.log.set_term_vote(term, Some(self.id))?;\n\n        let (last_index, last_term) = self.log.get_last_index();\n        self.broadcast(Message::Campaign { last_index, last_term })\n    }\n}\n\n/// A leader serves client requests and replicates the log to followers.\n/// If the leader loses leadership, all client requests are aborted.\npub struct Leader {\n    /// Follower replication progress.\n    progress: HashMap<NodeID, Progress>,\n    /// Tracks pending write requests by log index. Added when the write is\n    /// proposed and appended to the leader's log, and removed when the command\n    /// is applied to the state machine, returning the result to the client.\n    writes: HashMap<Index, Write>,\n    /// Tracks pending read requests. For linearizability, read requests are\n    /// assigned a sequence number and only executed once a quorum of nodes have\n    /// confirmed that we're still the leader. Otherwise, an old leader could\n    /// serve stale reads if a new leader has been elected elsewhere.\n    reads: VecDeque<Read>,\n    /// The read sequence number used for the last read. Initialized to 0 in\n    /// this term, and incremented for every read command.\n    read_seq: ReadSequence,\n    /// Number of ticks since last heartbeat.\n    since_heartbeat: Ticks,\n}\n\n/// Per-follower replication progress (in this term).\nstruct Progress {\n    /// The highest index where the follower's log is known to match the leader.\n    /// Initialized to 0, increases monotonically.\n    match_index: Index,\n    /// The next index to replicate to the follower. Initialized to\n    /// last_index+1, decreased when probing log mismatches. Always in\n    /// the range [match_index+1, last_index+1].\n    ///\n    /// Entries not yet sent are in the range [next_index, last_index].\n    /// Entries not acknowledged are in the range [match_index+1, next_index).\n    next_index: Index,\n    /// The last read sequence number confirmed by this follower. To avoid stale\n    /// reads on leader changes, a read is only served once its sequence number\n    /// is confirmed by a quorum.\n    read_seq: ReadSequence,\n}\n\nimpl Progress {\n    /// Attempts to advance a follower's match index, returning true if it did.\n    /// If next_index is below it, it is advanced to the following index.\n    fn advance(&mut self, match_index: Index) -> bool {\n        if match_index <= self.match_index {\n            return false;\n        }\n        self.match_index = match_index;\n        self.next_index = max(self.next_index, match_index + 1);\n        true\n    }\n\n    /// Attempts to advance a follower's read_seq, returning true if it did.\n    fn advance_read(&mut self, read_seq: ReadSequence) -> bool {\n        if read_seq <= self.read_seq {\n            return false;\n        }\n        self.read_seq = read_seq;\n        true\n    }\n\n    /// Attempts to regress a follower's next index to the given index, returning\n    /// true if it did. Won't regress below match_index + 1.\n    fn regress_next(&mut self, next_index: Index) -> bool {\n        if next_index >= self.next_index || self.next_index <= self.match_index + 1 {\n            return false;\n        }\n        self.next_index = max(next_index, self.match_index + 1);\n        true\n    }\n}\n\n/// A pending client write request.\nstruct Write {\n    /// The node which submitted the write.\n    from: NodeID,\n    /// The write request ID.\n    id: RequestID,\n}\n\n/// A pending client read request.\nstruct Read {\n    /// The sequence number of this read.\n    seq: ReadSequence,\n    /// The node which submitted the read.\n    from: NodeID,\n    /// The read request ID.\n    id: RequestID,\n    /// The read command.\n    command: Vec<u8>,\n}\n\nimpl Leader {\n    /// Creates a new leader role.\n    fn new(peers: HashSet<NodeID>, last_index: Index) -> Self {\n        let next_index = last_index + 1;\n        let progress = peers\n            .into_iter()\n            .map(|p| (p, Progress { next_index, match_index: 0, read_seq: 0 }))\n            .collect();\n        Self {\n            progress,\n            writes: HashMap::new(),\n            reads: VecDeque::new(),\n            read_seq: 0,\n            since_heartbeat: 0,\n        }\n    }\n}\n\nimpl Role for Leader {}\n\nimpl RawNode<Leader> {\n    /// Transitions the leader into a follower. This can only happen if we\n    /// discover a new term, so we become a leaderless follower. Stepping the\n    /// received message may then follow a new leader, if there is one.\n    fn into_follower(mut self, term: Term) -> Result<RawNode<Follower>> {\n        assert!(term > self.term(), \"leader can only become follower in later term\");\n        info!(\"Discovered new term {term}\");\n\n        // Abort in-flight requests. The client must retry. Sort the requests\n        // by ID for test determinism.\n        for write in std::mem::take(&mut self.role.writes).into_values().sorted_by_key(|w| w.id) {\n            let response = Err(Error::Abort);\n            self.send(write.from, Message::ClientResponse { id: write.id, response })?;\n        }\n        for read in std::mem::take(&mut self.role.reads).into_iter().sorted_by_key(|r| r.id) {\n            let response = Err(Error::Abort);\n            self.send(read.from, Message::ClientResponse { id: read.id, response })?;\n        }\n\n        self.log.set_term_vote(term, None)?;\n        let election_timeout = self.random_election_timeout();\n        Ok(self.into_role(Follower::new(None, election_timeout)))\n    }\n\n    /// Processes an inbound message.\n    fn step(mut self, msg: Envelope) -> Result<Node> {\n        // Past term: outdated peer, drop the message.\n        if msg.term < self.term() {\n            debug!(\"Dropping message from past term: {msg:?}\");\n            return Ok(self.into());\n        }\n        // Future term: become leaderless follower and step the message.\n        if msg.term > self.term() {\n            return self.into_follower(msg.term)?.step(msg);\n        }\n\n        match msg.message {\n            // A follower received our heartbeat and confirms our leadership.\n            // We may be able to execute new reads, and we may find that the\n            // follower's log is lagging and requires us to catch it up.\n            Message::HeartbeatResponse { match_index, read_seq } => {\n                let (last_index, _) = self.log.get_last_index();\n                assert!(match_index <= last_index, \"future match index\");\n                assert!(read_seq <= self.role.read_seq, \"future read sequence number\");\n\n                // If the read sequence number advances, try to execute reads.\n                if self.progress(msg.from).advance_read(read_seq) {\n                    self.maybe_read()?;\n                }\n\n                // If the follower didn't match our last index, an append to it\n                // must have failed (or it's catching up). Probe it to discover\n                // a matching entry and start replicating. Move next_index back\n                // to last_index since the follower just told us it doesn't have\n                // it (or a previous last_index).\n                if match_index == 0 {\n                    self.progress(msg.from).regress_next(last_index);\n                    self.maybe_send_append(msg.from, true)?;\n                }\n\n                // If the follower's match index advances, an append response\n                // got lost. Try to commit and apply.\n                //\n                // We don't need to eagerly send any pending entries, since any\n                // proposals made after this heartbeat was sent should have been\n                // eagerly replicated in steady state. If not, the next\n                // heartbeat will trigger a probe above.\n                if self.progress(msg.from).advance(match_index) {\n                    self.maybe_commit_and_apply()?;\n                }\n            }\n\n            // A follower appended our log entries (or a probe found a match).\n            // Record its progress and attempt to commit and apply.\n            Message::AppendResponse { match_index, reject_index: 0 } if match_index > 0 => {\n                let (last_index, _) = self.log.get_last_index();\n                assert!(match_index <= last_index, \"future match index\");\n\n                if self.progress(msg.from).advance(match_index) {\n                    self.maybe_commit_and_apply()?;\n                }\n\n                // Eagerly send any further pending entries. This may be a\n                // successful probe response, or the peer may be lagging and\n                // we're catching it up one MAX_APPEND_ENTRIES batch at a time.\n                self.maybe_send_append(msg.from, false)?;\n            }\n\n            // A follower confirmed our read sequence number. If it advances,\n            // try to execute reads.\n            Message::ReadResponse { seq } => {\n                if self.progress(msg.from).advance_read(seq) {\n                    self.maybe_read()?;\n                }\n            }\n\n            // A follower rejected an append because the base entry in\n            // reject_index did not match its log. Probe the previous entry by\n            // sending an empty append until we find a common base.\n            //\n            // This linear probing can be slow with long divergent logs, but we\n            // keep it simple. See also section 5.3 in the Raft paper.\n            Message::AppendResponse { reject_index, match_index: 0 } if reject_index > 0 => {\n                let (last_index, _) = self.log.get_last_index();\n                assert!(reject_index <= last_index, \"future reject index\");\n\n                // If the rejected base index is at or below the match index,\n                // the rejection is stale and can be ignored.\n                if reject_index <= self.progress(msg.from).match_index {\n                    return Ok(self.into());\n                }\n\n                // Probe below the reject index, if we haven't already moved\n                // next_index below it. This avoids sending duplicate probes\n                // (heartbeats will trigger retries if they're lost).\n                if self.progress(msg.from).regress_next(reject_index) {\n                    self.maybe_send_append(msg.from, true)?;\n                }\n            }\n\n            // AppendResponses must set either match_index or reject_index.\n            Message::AppendResponse { .. } => panic!(\"invalid message {msg:?}\"),\n\n            // A client submitted a write request. Propose it, and wait until\n            // it's replicated and applied to the state machine before returning\n            // the response to the client.\n            Message::ClientRequest { id, request: Request::Write(command) } => {\n                let index = self.propose(Some(command))?;\n                self.role.writes.insert(index, Write { from: msg.from, id });\n                if self.cluster_size() == 1 {\n                    self.maybe_commit_and_apply()?;\n                }\n            }\n\n            // A client submitted a read request. To ensure linearizability, we\n            // must confirm that we are still the leader by sending the read's\n            // sequence number and wait for quorum confirmation.\n            Message::ClientRequest { id, request: Request::Read(command) } => {\n                self.role.read_seq += 1;\n                let read = Read { seq: self.role.read_seq, from: msg.from, id, command };\n                self.role.reads.push_back(read);\n                self.broadcast(Message::Read { seq: self.role.read_seq })?;\n                if self.cluster_size() == 1 {\n                    self.maybe_read()?;\n                }\n            }\n\n            // A client submitted a status command.\n            Message::ClientRequest { id, request: Request::Status } => {\n                let response = self.status().map(Response::Status);\n                self.send(msg.from, Message::ClientResponse { id, response })?;\n            }\n\n            // Don't grant any votes (we've already voted for ourself).\n            Message::Campaign { .. } => {\n                self.send(msg.from, Message::CampaignResponse { vote: false })?\n            }\n\n            // Votes can come in after we won the election, ignore them.\n            Message::CampaignResponse { .. } => {}\n\n            // There can't be another leader in this term.\n            Message::Heartbeat { .. } | Message::Append { .. } | Message::Read { .. } => {\n                panic!(\"saw other leader {} in term {}\", msg.from, msg.term);\n            }\n\n            // Leaders don't proxy client requests.\n            Message::ClientResponse { .. } => panic!(\"unexpected message {msg:?}\"),\n        }\n\n        Ok(self.into())\n    }\n\n    /// Processes a logical clock tick.\n    fn tick(mut self) -> Result<Node> {\n        // Send periodic heartbeats.\n        self.role.since_heartbeat += 1;\n        if self.role.since_heartbeat >= self.opts.heartbeat_interval {\n            self.heartbeat()?;\n        }\n        Ok(self.into())\n    }\n\n    /// Broadcasts a heartbeat to all peers.\n    fn heartbeat(&mut self) -> Result<()> {\n        let (last_index, last_term) = self.log.get_last_index();\n        let (commit_index, _) = self.log.get_commit_index();\n        let read_seq = self.role.read_seq;\n        assert_eq!(last_term, self.term(), \"leader's last_term not in current term\");\n\n        self.role.since_heartbeat = 0;\n        self.broadcast(Message::Heartbeat { last_index, commit_index, read_seq })\n    }\n\n    /// Proposes a command for consensus by appending it to our log and\n    /// replicating it to peers. If successful, it will eventually be committed\n    /// and applied to the state machine.\n    fn propose(&mut self, command: Option<Vec<u8>>) -> Result<Index> {\n        let index = self.log.append(command)?;\n        for peer in self.peers.iter().copied().sorted() {\n            // Eagerly send the entry to the peer if it's in steady state and\n            // we've sent all previous entries. Otherwise, the peer is lagging\n            // and we're probing past entries for a match.\n            if index == self.progress(peer).next_index {\n                self.maybe_send_append(peer, false)?;\n            }\n        }\n        Ok(index)\n    }\n\n    /// Commits new entries that have been replicated to a quorum and applies\n    /// them to the state machine, returning results to clients.\n    fn maybe_commit_and_apply(&mut self) -> Result<Index> {\n        // Determine the new commit index by quorum.\n        let (last_index, _) = self.log.get_last_index();\n        let commit_index = self.quorum_value(\n            self.role.progress.values().map(|p| p.match_index).chain([last_index]).collect(),\n        );\n\n        // If the commit index doesn't advance, do nothing. We don't assert on\n        // this, since the quorum value may regress e.g. following a restart or\n        // leader change where followers are initialized with match index 0.\n        let (old_index, old_term) = self.log.get_commit_index();\n        if commit_index <= old_index {\n            return Ok(old_index);\n        }\n\n        // We can only safely commit an entry from our own term (see section\n        // 5.4.2 in Raft paper).\n        match self.log.get(commit_index)? {\n            Some(entry) if entry.term == self.term() => {}\n            Some(_) => return Ok(old_index),\n            None => panic!(\"commit index {commit_index} missing\"),\n        }\n\n        // Commit entries.\n        self.log.commit(commit_index)?;\n\n        // Apply entries and respond to clients.\n        let term = self.term();\n        let mut iter = self.log.scan_apply(self.state.get_applied_index());\n        while let Some(entry) = iter.next().transpose()? {\n            debug!(\"Applying {entry:?}\");\n            let write = self.role.writes.remove(&entry.index);\n            let result = self.state.apply(entry);\n\n            if let Some(Write { id, from: to }) = write {\n                let message = Message::ClientResponse { id, response: result.map(Response::Write) };\n                Self::send_via(&self.tx, Envelope { from: self.id, term, to, message })?;\n            }\n        }\n        drop(iter);\n\n        // If the commit term changed, there may be pending reads waiting for us\n        // to commit and apply an entry from our own term. Execute them.\n        if old_term != self.term() {\n            self.maybe_read()?;\n        }\n\n        Ok(commit_index)\n    }\n\n    /// Executes any ready read requests, where a quorum have confirmed that\n    /// we're still the leader for the read sequences.\n    fn maybe_read(&mut self) -> Result<()> {\n        if self.role.reads.is_empty() {\n            return Ok(());\n        }\n\n        // It's only safe to read if we've committed and applied an entry from\n        // our own term (the leader appends an entry when elected). Otherwise we\n        // may be behind on application and serve stale reads.\n        let (commit_index, commit_term) = self.log.get_commit_index();\n        let applied_index = self.state.get_applied_index();\n        if commit_term < self.term() || applied_index < commit_index {\n            return Ok(());\n        }\n\n        // Determine the maximum read sequence confirmed by quorum.\n        let quorum_read_seq = self.quorum_value(\n            self.role.progress.values().map(|p| p.read_seq).chain([self.role.read_seq]).collect(),\n        );\n\n        // Execute ready reads. The VecDeque is ordered by read_seq, so we\n        // can keep pulling until we hit quorum_read_seq.\n        while let Some(read) = self.role.reads.front() {\n            if read.seq > quorum_read_seq {\n                break;\n            }\n            let read = self.role.reads.pop_front().unwrap();\n            let response = self.state.read(read.command).map(Response::Read);\n            self.send(read.from, Message::ClientResponse { id: read.id, response })?;\n        }\n        Ok(())\n    }\n\n    /// Sends a batch of pending log entries to a follower, in the\n    /// [next_index,last_index] range. Limited by max_append_entries.\n    ///\n    /// If probe is true, we're trying to find a log index on the follower where\n    /// it matches our log. To do this, we send an empty append probe with\n    /// base_index of next_index-1. If the follower confirms the base_index\n    /// matches its log, the actual entries are sent next -- otherwise,\n    /// next_index is decremented and another probe is sent until a match is\n    /// found. See section 5.3 in the Raft paper.\n    ///\n    /// The probe is skipped if the follower is up-to-date (according to\n    /// match_index and last_index). If the probe's base_index has already been\n    /// confirmed via match_index, an actual append is sent instead.\n    fn maybe_send_append(&mut self, peer: NodeID, mut probe: bool) -> Result<()> {\n        let (last_index, _) = self.log.get_last_index();\n        let progress = self.role.progress.get_mut(&peer).expect(\"unknown node\");\n        assert_ne!(progress.next_index, 0, \"invalid next_index\");\n        assert!(progress.next_index > progress.match_index, \"invalid next_index <= match_index\");\n        assert!(progress.match_index <= last_index, \"invalid match_index > last_index\");\n        assert!(progress.next_index <= last_index + 1, \"invalid next_index > last_index + 1\");\n\n        // If the peer is caught up, there's no point sending an append.\n        if progress.match_index == last_index {\n            return Ok(());\n        }\n\n        // If a probe was requested, but the base_index has already been\n        // confirmed via match_index, there is no point in probing. Just send\n        // the entries instead.\n        probe = probe && progress.next_index > progress.match_index + 1;\n\n        // If there are no pending entries, and this is not a probe, there's\n        // nothing more to send until we get a response from the follower.\n        if progress.next_index > last_index && !probe {\n            return Ok(());\n        }\n\n        // Fetch the base and entries.\n        let (base_index, base_term) = match progress.next_index {\n            0 => panic!(\"next_index=0 for node {peer}\"),\n            1 => (0, 0), // first entry, there is no base\n            next => self.log.get(next - 1)?.map(|e| (e.index, e.term)).expect(\"missing base entry\"),\n        };\n        let entries = match probe {\n            false => self\n                .log\n                .scan(progress.next_index..)\n                .take(self.opts.max_append_entries)\n                .try_collect()?,\n            true => Vec::new(),\n        };\n\n        // Optimistically assume the entries will be accepted by the follower,\n        // and bump next_index to avoid resending them until a response.\n        if let Some(last) = entries.last() {\n            progress.next_index = last.index + 1;\n        }\n\n        debug!(\"Replicating {} entries with base {base_index} to {peer}\", entries.len());\n        self.send(peer, Message::Append { base_index, base_term, entries })\n    }\n\n    /// Generates cluster status.\n    fn status(&mut self) -> Result<Status> {\n        Ok(Status {\n            leader: self.id,\n            term: self.term(),\n            match_index: self\n                .role\n                .progress\n                .iter()\n                .map(|(id, p)| (*id, p.match_index))\n                .chain(std::iter::once((self.id, self.log.get_last_index().0)))\n                .collect(),\n            commit_index: self.log.get_commit_index().0,\n            applied_index: self.state.get_applied_index(),\n            storage: self.log.status()?,\n        })\n    }\n\n    /// Returns a mutable borrow of a node's progress. Convenience method.\n    fn progress(&mut self, id: NodeID) -> &mut Progress {\n        self.role.progress.get_mut(&id).expect(\"unknown node\")\n    }\n}\n\n/// Most Raft tests are Goldenscripts under src/raft/testscripts.\n#[cfg(test)]\nmod tests {\n    use std::borrow::Borrow;\n    use std::error::Error;\n    use std::fmt::Write as _;\n    use std::path::Path;\n    use std::result::Result;\n\n    use crossbeam::channel::Receiver;\n    use tempfile::TempDir;\n    use test_case::test_case;\n    use test_each_file::test_each_path;\n    use uuid::Uuid;\n\n    use super::*;\n    use crate::encoding::{Key as _, Value as _, bincode};\n    use crate::raft::Entry;\n    use crate::raft::state::test::{self as teststate, KVCommand, KVResponse};\n    use crate::storage;\n    use crate::storage::engine::test as testengine;\n\n    // Run goldenscript tests in src/raft/testscripts/node.\n    test_each_path! { in \"src/raft/testscripts/node\" as scripts => test_goldenscript }\n\n    fn test_goldenscript(path: &Path) {\n        goldenscript::run(&mut TestRunner::new(), path).expect(\"goldenscript failed\")\n    }\n\n    /// Tests RawNode.quorum_size() and cluster_size().\n    #[test_case(1 => 1)]\n    #[test_case(2 => 2)]\n    #[test_case(3 => 2)]\n    #[test_case(4 => 3)]\n    #[test_case(5 => 3)]\n    #[test_case(6 => 4)]\n    #[test_case(7 => 4)]\n    #[test_case(8 => 5)]\n    fn quorum_size(size: usize) -> usize {\n        let node = RawNode::new_noop(1, (2..=size as NodeID).collect());\n        assert_eq!(node.cluster_size(), size);\n        node.quorum_size()\n    }\n\n    /// Tests RawNode.quorum_value().\n    #[test_case(vec![1] => 1)]\n    #[test_case(vec![1,3,2] => 2)]\n    #[test_case(vec![4,1,3,2] => 2)]\n    #[test_case(vec![1,1,1,2,2] => 1)]\n    #[test_case(vec![1,1,2,2,2] => 2)]\n    fn quorum_value(values: Vec<i8>) -> i8 {\n        let size = values.len();\n        let node = RawNode::new_noop(1, (2..=size as NodeID).collect());\n        assert_eq!(node.cluster_size(), size);\n        node.quorum_value(values)\n    }\n\n    /// Test helpers for RawNode.\n    impl RawNode<Follower> {\n        /// Creates a noop node, with a noop state machine and transport.\n        fn new_noop(id: NodeID, peers: HashSet<NodeID>) -> Self {\n            let log = Log::new(Box::new(storage::Memory::new())).expect(\"log failed\");\n            let state = teststate::Noop::new();\n            let (tx, _) = crossbeam::channel::unbounded();\n            RawNode::new(id, peers, log, state, tx, Options::default()).expect(\"node failed\")\n        }\n    }\n\n    /// Helper macro which calls a closure on the inner RawNode<Role>.\n    macro_rules! with_rawnode {\n        // Node is moved.\n        ($node:expr, $closure:expr) => {{\n            fn with<R: Role, T>(node: RawNode<R>, f: impl FnOnce(RawNode<R>) -> T) -> T {\n                f(node)\n            }\n            match $node {\n                Node::Candidate(node) => with(node, $closure),\n                Node::Follower(node) => with(node, $closure),\n                Node::Leader(node) => with(node, $closure),\n            }\n        }};\n        // Node is borrowed (ref).\n        (ref $node:expr, $closure:expr) => {{\n            fn with<R: Role, T>(node: &RawNode<R>, f: impl FnOnce(&RawNode<R>) -> T) -> T {\n                f(node)\n            }\n            match $node {\n                &Node::Candidate(ref node) => with(node, $closure),\n                &Node::Follower(ref node) => with(node, $closure),\n                &Node::Leader(ref node) => with(node, $closure),\n            }\n        }};\n        // Node is mutably borrowed (ref mut).\n        (ref mut $node:expr, $closure:expr) => {{\n            fn with<R: Role, T>(node: &mut RawNode<R>, f: impl FnOnce(&mut RawNode<R>) -> T) -> T {\n                f(node)\n            }\n            match $node {\n                &mut Node::Candidate(ref mut node) => with(node, $closure),\n                &mut Node::Follower(ref mut node) => with(node, $closure),\n                &mut Node::Leader(ref mut node) => with(node, $closure),\n            }\n        }};\n    }\n\n    /// Test helpers for Node.\n    impl Node {\n        fn dismantle(self) -> (Log, Box<dyn State>) {\n            with_rawnode!(self, |n| (n.log, n.state))\n        }\n\n        fn get_applied_index(&self) -> Index {\n            with_rawnode!(ref self, |n| n.state.get_applied_index())\n        }\n\n        fn get_commit_index(&self) -> (Index, Term) {\n            with_rawnode!(ref self, |n| n.log.get_commit_index())\n        }\n\n        fn get_last_index(&self) -> (Index, Term) {\n            with_rawnode!(ref self, |n| n.log.get_last_index())\n        }\n\n        fn get_term_vote(&self) -> (Term, Option<NodeID>) {\n            with_rawnode!(ref self, |n| n.log.get_term_vote())\n        }\n\n        fn options(&self) -> Options {\n            with_rawnode!(ref self, |n| n.opts.clone())\n        }\n\n        fn peers(&self) -> HashSet<NodeID> {\n            with_rawnode!(ref self, |n| n.peers.clone())\n        }\n\n        fn read(&self, command: Vec<u8>) -> crate::error::Result<Vec<u8>> {\n            with_rawnode!(ref self, |n| n.state.read(command))\n        }\n\n        fn scan_log(&mut self) -> crate::error::Result<Vec<Entry>> {\n            with_rawnode!(ref mut self, |n| n.log.scan(..).collect())\n        }\n    }\n\n    /// Runs Raft goldenscript tests. See run() for available commands.\n    struct TestRunner {\n        /// IDs of all cluster nodes, in order.\n        ids: Vec<NodeID>,\n        /// The cluster nodes, keyed by node ID.\n        nodes: HashMap<NodeID, Node>,\n        /// Outbound send queues from each node.\n        nodes_rx: HashMap<NodeID, Receiver<Envelope>>,\n        /// Inbound receive queues to each node, to be stepped.\n        nodes_pending: HashMap<NodeID, Vec<Envelope>>,\n        /// Applied log entries for each node, after state machine application.\n        applied_rx: HashMap<NodeID, Receiver<Entry>>,\n        /// Network partitions (sender → receivers). A symmetric (bidirectional)\n        /// partition needs an entry from each side.\n        disconnected: HashMap<NodeID, HashSet<NodeID>>,\n        /// In-flight client requests.\n        requests: HashMap<RequestID, Request>,\n        /// The request ID to use for the next client request.\n        next_request_id: u64,\n        /// Temporary directory (deleted when dropped).\n        tempdir: TempDir,\n    }\n\n    impl goldenscript::Runner for TestRunner {\n        /// Runs a goldenscript command.\n        fn run(&mut self, command: &goldenscript::Command) -> Result<String, Box<dyn Error>> {\n            let mut output = String::new();\n            match command.name.as_str() {\n                // campaign [ID...]\n                // Transition the given nodes to candidates and campaign.\n                \"campaign\" => {\n                    let ids = self.parse_ids_or_all(&command.args)?;\n                    self.campaign(&ids, &mut output)?;\n                }\n\n                // cluster nodes=N [leader=ID] [heartbeat_interval=N] [election_timeout=N] [max_append_entries=N]\n                // Creates a new Raft cluster.\n                \"cluster\" => {\n                    let mut opts = Options::default();\n                    let mut args = command.consume_args();\n                    let nodes = args.lookup_parse(\"nodes\")?.unwrap_or(0);\n                    let leader = args.lookup_parse(\"leader\")?;\n                    if let Some(heartbeat_interval) = args.lookup_parse(\"heartbeat_interval\")? {\n                        opts.heartbeat_interval = heartbeat_interval;\n                    };\n                    if let Some(election_timeout) = args.lookup_parse(\"election_timeout\")? {\n                        opts.election_timeout_range = election_timeout..election_timeout + 1;\n                    }\n                    if let Some(max_append_entries) = args.lookup_parse(\"max_append_entries\")? {\n                        opts.max_append_entries = max_append_entries;\n                    }\n                    args.reject_rest()?;\n                    self.cluster(nodes, leader, opts, &mut output)?;\n                }\n\n                // deliver [from=ID] [ID...]\n                // Delivers (steps) pending messages to the given nodes. If from\n                // is given, only messages from the given node is delivered, the\n                // others are left pending.\n                \"deliver\" => {\n                    let mut args = command.consume_args();\n                    let from = args.lookup_parse(\"from\")?;\n                    let ids = self.parse_ids_or_all(&args.rest())?;\n                    self.deliver(&ids, from, &mut output)?;\n                }\n\n                // get ID KEY\n                // Sends a client request to the given node to read the given\n                // key from the state machine (key/value store).\n                \"get\" => {\n                    let mut args = command.consume_args();\n                    let id = args.next_pos().ok_or(\"must specify node ID\")?.parse()?;\n                    let key = args.next_pos().ok_or(\"must specify key\")?.value.clone();\n                    args.reject_rest()?;\n                    let request = Request::Read(KVCommand::Get { key }.encode());\n                    self.request(id, request, &mut output)?;\n                }\n\n                // heal [ID...]\n                // Heals all network partitions for the given nodes.\n                \"heal\" => {\n                    let ids = self.parse_ids_or_all(&command.args)?;\n                    self.heal(&ids, &mut output)?;\n                }\n\n                // heartbeat ID...\n                // Sends a heartbeat from the given leader nodes.\n                \"heartbeat\" => {\n                    let ids = self.parse_ids_or_all(&command.args)?;\n                    self.heartbeat(&ids, &mut output)?;\n                }\n\n                // log [ID...]\n                // Outputs the current Raft log for the given nodes.\n                \"log\" => {\n                    let ids = self.parse_ids_or_all(&command.args)?;\n                    self.log(&ids, &mut output)?;\n                }\n\n                // partition ID...\n                // Partitions the given nodes away from the rest of the cluster.\n                // They can still communicate with each other, unless they were\n                // previously partitioned.\n                \"partition\" => {\n                    let ids = self.parse_ids_or_error(&command.args)?;\n                    self.partition(&ids, &mut output)?;\n                }\n\n                // put ID KEY=VALUE\n                // Sends a client request to the given node to write a key/value\n                // pair to the state machine (key/value store).\n                \"put\" => {\n                    let mut args = command.consume_args();\n                    let id = args.next_pos().ok_or(\"must specify node ID\")?.parse()?;\n                    let kv = args.next_key().ok_or(\"must specify key/value pair\")?.clone();\n                    let (key, value) = (kv.key.unwrap(), kv.value);\n                    args.reject_rest()?;\n                    let request = Request::Write(KVCommand::Put { key, value }.encode());\n                    self.request(id, request, &mut output)?;\n                }\n\n                // restart [commit_index=INDEX] [applied_index=INDEX] [ID...]\n                // Restarts the given nodes (or all nodes). They retain their\n                // log and state, unless applied_index is given (which reverts\n                // the state machine to the given index, or 0 if empty).\n                // commit_index may be given to regress the commit index (it\n                // is not flushed to durable storage).\n                \"restart\" => {\n                    let mut args = command.consume_args();\n                    let applied_index = args.lookup_parse(\"applied_index\")?;\n                    let commit_index = args.lookup_parse(\"commit_index\")?;\n                    let ids = self.parse_ids_or_all(&args.rest())?;\n                    self.restart(&ids, commit_index, applied_index, &mut output)?;\n                }\n\n                // stabilize [heartbeat=BOOL] [ID...]\n                // Stabilizes the given nodes by repeatedly delivering messages\n                // until no more messages are pending. If heartbeat is true, also\n                // emits a heartbeat from the leader and restabilizes, e.g. to\n                // propagate the commit index.\n                \"stabilize\" => {\n                    let mut args = command.consume_args();\n                    let heartbeat = args.lookup_parse(\"heartbeat\")?.unwrap_or(false);\n                    let ids = self.parse_ids_or_all(&args.rest())?;\n                    self.stabilize(&ids, heartbeat, &mut output)?;\n                }\n\n                // state [ID...]\n                // Prints the current state machine contents on the given nodes.\n                \"state\" => {\n                    let ids = self.parse_ids_or_all(&command.args)?;\n                    self.state(&ids, &mut output)?;\n                }\n\n                // status [request=BOOL] [ID...]\n                // Prints the current node status of the given nodes. If request\n                // is true, sends a status client request to a single node,\n                // otherwise fetches status directly from each node.\n                \"status\" => {\n                    let mut args = command.consume_args();\n                    let request = args.lookup_parse(\"request\")?.unwrap_or(false);\n                    let ids = self.parse_ids_or_all(&args.rest())?;\n                    if request {\n                        let [id] = *ids.as_slice() else {\n                            return Err(\"request=true requires 1 node ID\".into());\n                        };\n                        self.request(id, Request::Status, &mut output)?;\n                    } else {\n                        self.status(&ids, &mut output)?;\n                    }\n                }\n\n                // step ID JSON\n                // Steps a manually generated JSON message on the given node.\n                \"step\" => {\n                    let mut args = command.consume_args();\n                    let id = args.next_pos().ok_or(\"node ID not given\")?.parse()?;\n                    let raw = &args.next_pos().ok_or(\"message not given\")?.value;\n                    let msg = serde_json::from_str(raw)?;\n                    args.reject_rest()?;\n                    self.transition(id, |n| n.step(msg), &mut output)?;\n                }\n\n                // tick [ID...]\n                // Ticks the given nodes.\n                \"tick\" => {\n                    let ids = self.parse_ids_or_all(&command.args)?;\n                    for id in ids {\n                        self.transition(id, |n| n.tick(), &mut output)?;\n                    }\n                }\n\n                name => return Err(format!(\"unknown command {name}\").into()),\n            }\n            Ok(output)\n        }\n    }\n\n    impl TestRunner {\n        fn new() -> Self {\n            Self {\n                ids: Vec::new(),\n                nodes: HashMap::new(),\n                nodes_rx: HashMap::new(),\n                nodes_pending: HashMap::new(),\n                applied_rx: HashMap::new(),\n                disconnected: HashMap::new(),\n                requests: HashMap::new(),\n                next_request_id: 1,\n                tempdir: TempDir::with_prefix(\"toydb\").expect(\"tempdir failed\"),\n            }\n        }\n\n        /// Creates a new empty node and inserts it.\n        fn add_node(\n            &mut self,\n            id: NodeID,\n            peers: HashSet<NodeID>,\n            opts: Options,\n        ) -> Result<(), Box<dyn Error>> {\n            // Use both a BitCask and a Memory engine, and mirror operations\n            // across them, for added engine test coverage.\n            let path = self.tempdir.path().join(format!(\"{id}.log\"));\n            let bitcask = storage::BitCask::new(path).expect(\"bitcask failed\");\n            let memory = storage::Memory::new();\n            let engine = testengine::Mirror::new(bitcask, memory);\n            let log = Log::new(Box::new(engine))?;\n            let state = teststate::KV::new();\n            self.add_node_with(id, peers, log, state, opts)\n        }\n\n        /// Creates a new node with the given log and state and inserts it.\n        fn add_node_with(\n            &mut self,\n            id: NodeID,\n            peers: HashSet<NodeID>,\n            log: Log,\n            state: Box<dyn State>,\n            opts: Options,\n        ) -> Result<(), Box<dyn Error>> {\n            let (node_tx, node_rx) = crossbeam::channel::unbounded();\n            let (applied_tx, applied_rx) = crossbeam::channel::unbounded();\n            let state = teststate::Emit::new(state, applied_tx);\n            self.nodes.insert(id, Node::new(id, peers, log, state, node_tx, opts)?);\n            self.nodes_rx.insert(id, node_rx);\n            self.nodes_pending.insert(id, Vec::new());\n            self.applied_rx.insert(id, applied_rx);\n            self.disconnected.insert(id, HashSet::new());\n            Ok(())\n        }\n\n        /// Transitions nodes to candidates and campaign in a new term.\n        fn campaign(&mut self, ids: &[NodeID], output: &mut String) -> Result<(), Box<dyn Error>> {\n            let campaign = |node| match node {\n                Node::Candidate(mut node) => {\n                    node.campaign()?;\n                    Ok(node.into())\n                }\n                Node::Follower(node) => Ok(node.into_candidate()?.into()),\n                Node::Leader(node) => {\n                    let term = node.term();\n                    Ok(node.into_follower(term + 1)?.into_candidate()?.into())\n                }\n            };\n            for id in ids.iter().copied() {\n                self.transition(id, campaign, output)?;\n            }\n            Ok(())\n        }\n\n        /// Creates a Raft cluster.\n        fn cluster(\n            &mut self,\n            nodes: u8,\n            leader: Option<NodeID>,\n            opts: Options,\n            output: &mut String,\n        ) -> Result<(), Box<dyn Error>> {\n            if !self.ids.is_empty() {\n                return Err(\"cluster already exists\".into());\n            }\n            if nodes == 0 {\n                return Err(\"cluster can't have 0 nodes\".into());\n            }\n\n            self.ids = (1..=nodes).collect();\n\n            for id in self.ids.clone() {\n                let peers = self.ids.iter().copied().filter(|i| i != &id).collect();\n                self.add_node(id, peers, opts.clone())?;\n            }\n\n            // Promote leader if requested. Suppress output.\n            if let Some(id) = leader {\n                let quiet = &mut String::new();\n                let Some(Node::Follower(node)) = self.nodes.remove(&id) else {\n                    return Err(format!(\"invalid leader {id}\").into());\n                };\n                self.nodes.insert(id, node.into_candidate()?.into_leader()?.into());\n                self.receive(id, quiet)?;\n                self.stabilize(&self.ids.clone(), true, quiet)?;\n            }\n\n            // Drain any initial applied entries.\n            for applied_rx in self.applied_rx.values_mut() {\n                while applied_rx.try_recv().is_ok() {}\n            }\n\n            // Output final cluster status.\n            self.status(&self.ids, output)\n        }\n\n        /// Delivers pending messages to the given nodes. If from is given, only\n        /// delivers messages from that node. Returns the number of delivered\n        /// messages.\n        fn deliver(\n            &mut self,\n            ids: &[NodeID],\n            from: Option<NodeID>,\n            output: &mut String,\n        ) -> Result<usize, Box<dyn Error>> {\n            // Take a snapshot of the pending queues before delivering any\n            // messages. This avoids outbound messages in response to delivery\n            // being delivered to higher node IDs in the same loop, which can\n            // give unintuitive results.\n            let mut step = Vec::new();\n            for id in ids.iter().copied() {\n                let Some(pending) = self.nodes_pending.remove(&id) else {\n                    return Err(format!(\"unknown node {id}\").into());\n                };\n                let (deliver, requeue) =\n                    pending.into_iter().partition(|msg| from.is_none() || from == Some(msg.from));\n                self.nodes_pending.insert(id, requeue);\n                step.extend(deliver);\n            }\n\n            let delivered = step.len();\n            for msg in step {\n                self.transition(msg.to, |node| node.step(msg), output)?;\n            }\n            Ok(delivered)\n        }\n\n        /// Heals the given partitioned nodes, restoring connectivity with all\n        /// other nodes.\n        fn heal(&mut self, ids: &[NodeID], output: &mut String) -> Result<(), Box<dyn Error>> {\n            for id in ids.iter().copied() {\n                self.disconnected.insert(id, HashSet::new());\n                for peers in self.disconnected.values_mut() {\n                    peers.remove(&id);\n                }\n            }\n            output.push_str(&Self::format_disconnected(&self.disconnected));\n            Ok(())\n        }\n\n        /// Emits a heartbeat from the given leader nodes.\n        fn heartbeat(&mut self, ids: &[NodeID], output: &mut String) -> Result<(), Box<dyn Error>> {\n            for id in ids.iter().copied() {\n                let Some(Node::Leader(leader)) = self.nodes.get_mut(&id) else {\n                    return Err(format!(\"{id} is not a leader\").into());\n                };\n                leader.heartbeat()?;\n                self.receive(id, output)?;\n            }\n            Ok(())\n        }\n\n        /// Outputs the current log contents for the given nodes.\n        fn log(&mut self, ids: &[NodeID], output: &mut String) -> Result<(), Box<dyn Error>> {\n            for id in ids {\n                let node = self.nodes.get_mut(id).ok_or(format!(\"unknown node {id}\"))?;\n                let nodefmt = Self::format_node(node);\n                let (last_index, last_term) = node.get_last_index();\n                let (commit_index, commit_term) = node.get_commit_index();\n                let (term, vote) = node.get_term_vote();\n                writeln!(\n                    output,\n                    \"{nodefmt} term={term} last={last_index}@{last_term} commit={commit_index}@{commit_term} vote={vote:?}\",\n                )?;\n                for entry in node.scan_log()? {\n                    writeln!(output, \"{nodefmt} entry {}\", Self::format_entry(&entry))?;\n                }\n            }\n            Ok(())\n        }\n\n        /// Partitions the given nodes from all other nodes in the cluster\n        /// (bidirectionally). The given nodes can communicate with each other\n        /// unless they were previously partitioned.\n        fn partition(&mut self, ids: &[NodeID], output: &mut String) -> Result<(), Box<dyn Error>> {\n            let ids = HashSet::<NodeID>::from_iter(ids.iter().copied());\n            for id in ids.iter().copied() {\n                for peer in self.ids.iter().copied().filter(|p| !ids.contains(p)) {\n                    self.disconnected.entry(id).or_default().insert(peer);\n                    self.disconnected.entry(peer).or_default().insert(id);\n                }\n            }\n            output.push_str(&Self::format_disconnected(&self.disconnected));\n            Ok(())\n        }\n\n        /// Receives outbound messages from a node, prints them, and queues them\n        /// for delivery. Returns the number of received messages.\n        fn receive(&mut self, id: NodeID, output: &mut String) -> Result<u32, Box<dyn Error>> {\n            let rx = self.nodes_rx.get_mut(&id).ok_or(format!(\"unknown node {id}\"))?;\n            let mut count = 0;\n            for msg in rx.try_iter() {\n                count += 1;\n                let (from, term, to) = (msg.from, msg.term, msg.to); // simplify formatting\n                let msgfmt = Self::format_message(&msg.message);\n\n                // If the peer is disconnected, drop the message and output it.\n                if self.disconnected[&msg.from].contains(&msg.to) {\n                    writeln!(\n                        output,\n                        \"n{from}@{term} ⇥ n{to} {}\",\n                        Self::format_strikethrough(&msgfmt),\n                    )?;\n                    continue;\n                }\n\n                // Intercept and output client responses.\n                if msg.from == msg.to {\n                    let Message::ClientResponse { id, response } = &msg.message else {\n                        return Err(format!(\"invalid self-addressed message: {msg:?}\").into());\n                    };\n                    writeln!(output, \"n{from}@{term} → c{to} {msgfmt}\")?;\n                    let request = &self.requests.remove(id).ok_or(\"unknown request id\")?;\n                    writeln!(\n                        output,\n                        \"c{to}@{term} {} ⇒ {}\",\n                        Self::format_request(request),\n                        Self::format_response(response),\n                    )?;\n                    continue;\n                }\n\n                // Output the message and queue it for delivery.\n                writeln!(output, \"n{from}@{term} → n{to} {msgfmt}\")?;\n                self.nodes_pending.get_mut(&msg.to).ok_or(format!(\"unknown node {to}\"))?.push(msg);\n            }\n            Ok(count)\n        }\n\n        /// Submits a client request via the given node.\n        fn request(\n            &mut self,\n            id: NodeID,\n            request: Request,\n            output: &mut String,\n        ) -> Result<(), Box<dyn Error>> {\n            let request_id = Uuid::from_u64_pair(0, self.next_request_id);\n            self.next_request_id += 1;\n            self.requests.insert(request_id, request.clone());\n\n            let term = self.nodes.get(&id).ok_or(format!(\"unknown node {id}\"))?.term();\n            let msg = Envelope {\n                from: id,\n                to: id,\n                term,\n                message: Message::ClientRequest { id: request_id, request },\n            };\n            writeln!(output, \"c{id}@{term} → n{id} {}\", Self::format_message(&msg.message))?;\n            self.transition(id, |n| n.step(msg), output)\n        }\n\n        /// Restarts the given nodes. If commit_index or applied_index are\n        /// given, the log commit index or state machine will regress.\n        fn restart(\n            &mut self,\n            ids: &[NodeID],\n            commit_index: Option<Index>,\n            applied_index: Option<Index>,\n            output: &mut String,\n        ) -> Result<(), Box<dyn Error>> {\n            for id in ids.iter().copied() {\n                let node = self.nodes.remove(&id).ok_or(format!(\"unknown node {id}\"))?;\n                let peers = node.peers();\n                let opts = node.options();\n                let (log, mut state) = node.dismantle();\n                let mut log = Log::new(log.engine)?; // reset log\n\n                // If requested, regress the commit index.\n                if let Some(commit_index) = commit_index {\n                    if commit_index > log.get_commit_index().0 {\n                        return Err(format!(\"commit_index={commit_index} beyond current\").into());\n                    }\n                    let commit_term = match log.get(commit_index)? {\n                        Some(e) => e.term,\n                        None if commit_index == 0 => 0,\n                        None => return Err(format!(\"unknown commit_index={commit_index}\").into()),\n                    };\n                    log.engine.set(\n                        &crate::raft::log::Key::CommitIndex.encode(),\n                        bincode::serialize(&(commit_index, commit_term)),\n                    )?;\n                    // Reset the log again.\n                    log = Log::new(log.engine)?;\n                }\n\n                // If requested, wipe the state machine and reapply up to the\n                // requested applied index.\n                if let Some(applied_index) = applied_index {\n                    if applied_index > log.get_commit_index().0 {\n                        return Err(format!(\"applied_index={applied_index} beyond commit\").into());\n                    }\n                    state = teststate::KV::new();\n                    let mut scan = log.scan(..=applied_index);\n                    while let Some(entry) = scan.next().transpose()? {\n                        _ = state.apply(entry); // apply errors are returned to client\n                    }\n                    assert_eq!(state.get_applied_index(), applied_index, \"wrong applied index\");\n                }\n\n                // Add node, and run a noop transition to output applied entries.\n                self.add_node_with(id, peers, log, state, opts)?;\n                self.transition(id, Ok, output)?;\n            }\n            // Output restarted node status.\n            self.status(ids, output)\n        }\n\n        /// Stabilizes the given nodes by repeatedly delivering pending messages\n        /// until no new messages are generated. If heartbeat is true, leaders\n        /// then emit a heartbeat and restabilize again, e.g. to propagate the\n        /// commit index.\n        fn stabilize(\n            &mut self,\n            ids: &[NodeID],\n            heartbeat: bool,\n            output: &mut String,\n        ) -> Result<(), Box<dyn Error>> {\n            while self.deliver(ids, None, output)? > 0 {}\n            // If requested, heartbeat the current leader (with the highest\n            // term) and re-stabilize the nodes.\n            if heartbeat {\n                let leader = self\n                    .nodes\n                    .values()\n                    .sorted_by_key(|n| n.term())\n                    .rev()\n                    .find(|n| matches!(n, Node::Leader(_)));\n                if let Some(leader) = leader {\n                    self.heartbeat(&[leader.id()], output)?;\n                    self.stabilize(ids, false, output)?;\n                }\n            }\n            Ok(())\n        }\n\n        /// Outputs the current state machine for the given nodes.\n        fn state(&mut self, ids: &[NodeID], output: &mut String) -> Result<(), Box<dyn Error>> {\n            for id in ids {\n                let node = self.nodes.get_mut(id).ok_or(format!(\"unknown node {id}\"))?;\n                let nodefmt = Self::format_node(node);\n                let applied_index = node.get_applied_index();\n                let raw = node.read(KVCommand::Scan.encode())?;\n                let KVResponse::Scan(kvs) = KVResponse::decode(&raw)? else {\n                    return Err(\"unexpected scan response\".into());\n                };\n                writeln!(output, \"{nodefmt} applied={applied_index}\")?;\n                for (key, value) in kvs {\n                    writeln!(output, \"{nodefmt} state {key}={value}\")?;\n                }\n            }\n            Ok(())\n        }\n\n        /// Outputs status for the given nodes.\n        fn status(&self, ids: &[NodeID], output: &mut String) -> Result<(), Box<dyn Error>> {\n            for id in ids {\n                let node = self.nodes.get(id).ok_or(format!(\"unknown node {id}\"))?;\n                let (last_index, last_term) = node.get_last_index();\n                let (commit_index, commit_term) = node.get_commit_index();\n                let applied_index = node.get_applied_index();\n                write!(\n                    output,\n                    \"{node} last={last_index}@{last_term} commit={commit_index}@{commit_term} applied={applied_index}\",\n                    node = Self::format_node_role(node)\n                )?;\n                if let Node::Leader(leader) = node {\n                    let progress = leader\n                        .role\n                        .progress\n                        .iter()\n                        .sorted_by_key(|(id, _)| *id)\n                        .map(|(id, pr)| format!(\"{id}:{}→{}\", pr.match_index, pr.next_index))\n                        .join(\" \");\n                    write!(output, \" progress={{{progress}}}\")?\n                }\n                output.push('\\n');\n            }\n            Ok(())\n        }\n\n        /// Applies a node transition (typically a step or tick), and outputs\n        /// relevant changes.\n        fn transition(\n            &mut self,\n            id: NodeID,\n            f: impl FnOnce(Node) -> crate::error::Result<Node>,\n            output: &mut String,\n        ) -> Result<(), Box<dyn Error>> {\n            let mut node = self.nodes.remove(&id).ok_or(format!(\"unknown node {id}\"))?;\n\n            // Fetch pre-transition info.\n            let old_noderole = Self::format_node_role(&node);\n            let (old_commit_index, _) = node.get_commit_index();\n            let mut old_entries = node.scan_log()?.into_iter();\n\n            // Apply the transition.\n            node = f(node)?;\n\n            // Fetch post-transition info.\n            let nodefmt = Self::format_node(&node);\n            let noderole = Self::format_node_role(&node);\n            let (commit_index, commit_term) = node.get_commit_index();\n\n            let entries = node.scan_log()?.into_iter();\n            let appended: Vec<Entry> = entries\n                .skip_while(|e| Some(e.term) == old_entries.next().map(|e| e.term))\n                .collect();\n\n            self.nodes.insert(id, node);\n\n            // Output relevant changes.\n            if old_noderole != noderole {\n                writeln!(output, \"{old_noderole} ⇨ {noderole}\")?\n            }\n            for entry in appended {\n                writeln!(output, \"{nodefmt} append {}\", Self::format_entry(&entry))?\n            }\n            if old_commit_index != commit_index {\n                writeln!(output, \"{nodefmt} commit {commit_index}@{commit_term}\")?;\n            }\n            for entry in self.applied_rx[&id].try_iter() {\n                writeln!(output, \"{nodefmt} apply {}\", Self::format_entry(&entry))?\n            }\n\n            // Receive any outbound messages.\n            self.receive(id, output)?;\n            Ok(())\n        }\n\n        /// Parses node IDs from the given argument values. Errors on key/value\n        /// arguments. Can take both [Argument] and [&Argument].\n        fn parse_ids<A>(&self, args: &[A]) -> Result<Vec<NodeID>, Box<dyn Error>>\n        where\n            A: Borrow<goldenscript::Argument>,\n        {\n            let mut ids = Vec::new();\n            for arg in args.iter().map(|a| a.borrow()) {\n                if let Some(key) = &arg.key {\n                    return Err(format!(\"unknown argument '{key}'\").into());\n                }\n                let id = arg.parse()?;\n                if !self.nodes.contains_key(&id) {\n                    return Err(format!(\"unknown node {id}\").into());\n                }\n                ids.push(id)\n            }\n            Ok(ids)\n        }\n\n        // Parses node IDs from the given argument values, or returns all node\n        // IDs if none were given.\n        fn parse_ids_or_all<A>(&self, args: &[A]) -> Result<Vec<NodeID>, Box<dyn Error>>\n        where\n            A: Borrow<goldenscript::Argument>,\n        {\n            let ids = self.parse_ids(args)?;\n            if ids.is_empty() {\n                return Ok(self.ids.clone());\n            }\n            Ok(ids)\n        }\n\n        // Parses node IDs from the given argument values, or errors if none.\n        fn parse_ids_or_error<A>(&self, args: &[A]) -> Result<Vec<NodeID>, Box<dyn Error>>\n        where\n            A: Borrow<goldenscript::Argument>,\n        {\n            let ids = self.parse_ids(args)?;\n            if ids.is_empty() {\n                return Err(\"node ID not given\".into());\n            }\n            Ok(ids)\n        }\n\n        /// Formats network partitions.\n        fn format_disconnected(disconnected: &HashMap<NodeID, HashSet<NodeID>>) -> String {\n            // Return early if the cluster is fully connected.\n            if disconnected.iter().all(|(_, peers)| peers.is_empty()) {\n                return format!(\n                    \"{} fully connected\\n\",\n                    disconnected.keys().sorted().map(|id| format!(\"n{id}\")).join(\" \")\n                );\n            }\n\n            let mut output = String::new();\n\n            // Separate symmetric and asymmetric partitions.\n            let mut symmetric: HashMap<NodeID, HashSet<NodeID>> = HashMap::new();\n            let mut asymmetric: HashMap<NodeID, HashSet<NodeID>> = HashMap::new();\n            for (id, peers) in disconnected {\n                for peer in peers {\n                    if disconnected[peer].contains(id) {\n                        symmetric.entry(*id).or_default().insert(*peer);\n                    } else {\n                        asymmetric.entry(*id).or_default().insert(*peer);\n                    }\n                }\n            }\n\n            // Anchor the symmetric partitions at the node with the largest number\n            // of disconnects, otherwise the smallest (first) ID.\n            for (id, peers) in &symmetric.clone() {\n                for peer in peers {\n                    // Recompute the peer set sizes for each iteration, since we\n                    // modify the peer set below.\n                    let len = symmetric.get(id).map(|p| p.len()).unwrap_or(0);\n                    let peer_len = symmetric.get(peer).map(|p| p.len()).unwrap_or(0);\n                    // If this peer set is the smallest (or we're the higher ID),\n                    // remove the entry. We may no longer be in the map.\n                    if (len < peer_len || len == peer_len && id > peer)\n                        && let Some(peers) = symmetric.get_mut(id)\n                    {\n                        peers.remove(peer);\n                        if peers.is_empty() {\n                            symmetric.remove(id);\n                        }\n                    }\n                }\n            }\n\n            // The values (HashSets) correspond to the RHS of a partition. Let's\n            // group the LHS of the partition as well, from smallest to largest,\n            // separately for symmetric and asymmetric partitions. The vector\n            // contains (LHS, RHS, symmetric) groupings for each partition.\n            let mut grouped: Vec<(HashSet<NodeID>, HashSet<NodeID>, bool)> = Vec::new();\n            for (id, peers, symm) in symmetric\n                .into_iter()\n                .map(|(i, p)| (i, p, true))\n                .chain(asymmetric.into_iter().map(|(i, p)| (i, p, false)))\n                .sorted_by_key(|(id, _, symm)| (*id, !symm))\n            {\n                // Look for an existing LHS group with the same RHS, and insert\n                // this node into it. Otherwise, create a new LHS group.\n                match grouped.iter_mut().find(|(_, rhs, s)| peers == *rhs && symm == *s) {\n                    Some((lhs, _, _)) => _ = lhs.insert(id),\n                    None => grouped.push((HashSet::from([id]), peers, symm)),\n                }\n            }\n\n            // Display the groups.\n            for (lhs, rhs, symm) in grouped {\n                let lhs = lhs.iter().sorted().map(|id| format!(\"n{id}\")).join(\" \");\n                let sep = if symm { '⇹' } else { '⇥' };\n                let rhs = rhs.iter().sorted().map(|id| format!(\"n{id}\")).join(\" \");\n                writeln!(output, \"{lhs} {sep} {rhs}\").unwrap();\n            }\n\n            output\n        }\n\n        /// Formats an entry.\n        fn format_entry(entry: &Entry) -> String {\n            let command = match entry.command.as_ref() {\n                Some(raw) => KVCommand::decode(raw).expect(\"invalid command\").to_string(),\n                None => \"None\".to_string(),\n            };\n            format!(\"{index}@{term} {command}\", index = entry.index, term = entry.term)\n        }\n\n        /// Formats a message.\n        fn format_message(msg: &Message) -> String {\n            match msg {\n                Message::Campaign { last_index, last_term } => {\n                    format!(\"Campaign last={last_index}@{last_term}\")\n                }\n                Message::CampaignResponse { vote } => {\n                    format!(\"CampaignResponse vote={vote}\")\n                }\n                Message::Heartbeat { last_index, commit_index, read_seq } => {\n                    format!(\n                        \"Heartbeat last_index={last_index} commit_index={commit_index} read_seq={read_seq}\"\n                    )\n                }\n                Message::HeartbeatResponse { match_index, read_seq } => {\n                    format!(\"HeartbeatResponse match_index={match_index} read_seq={read_seq}\")\n                }\n                Message::Append { base_index, base_term, entries } => {\n                    let ent = entries.iter().map(|e| format!(\"{}@{}\", e.index, e.term)).join(\" \");\n                    format!(\"Append base={base_index}@{base_term} [{ent}]\")\n                }\n                Message::AppendResponse { match_index, reject_index } => {\n                    match (match_index, reject_index) {\n                        (0, 0) => panic!(\"match_index and reject_index both 0\"),\n                        (match_index, 0) => format!(\"AppendResponse match_index={match_index}\"),\n                        (0, reject_index) => format!(\"AppendResponse reject_index={reject_index}\"),\n                        (_, _) => panic!(\"match_index and reject_index both set\"),\n                    }\n                }\n                Message::Read { seq } => {\n                    format!(\"Read seq={seq}\")\n                }\n                Message::ReadResponse { seq } => {\n                    format!(\"ReadResponse seq={seq}\")\n                }\n                Message::ClientRequest { id, request } => {\n                    format!(\n                        \"ClientRequest id=0x{} {}\",\n                        hex::encode(id).trim_start_matches(\"00\"),\n                        match request {\n                            Request::Read(v) => format!(\"read 0x{}\", hex::encode(v)),\n                            Request::Write(v) => format!(\"write 0x{}\", hex::encode(v)),\n                            Request::Status => \"status\".to_string(),\n                        }\n                    )\n                }\n                Message::ClientResponse { id, response } => {\n                    format!(\n                        \"ClientResponse id=0x{} {}\",\n                        hex::encode(id).trim_start_matches(\"00\"),\n                        match response {\n                            Ok(Response::Read(v)) => format!(\"read 0x{}\", hex::encode(v)),\n                            Ok(Response::Write(v)) => format!(\"write 0x{}\", hex::encode(v)),\n                            Ok(Response::Status(v)) => format!(\"status {v:?}\"),\n                            Err(error) => format!(\"Error::{error:#?}\"),\n                        }\n                    )\n                }\n            }\n        }\n\n        /// Formats a node identifier.\n        fn format_node(node: &Node) -> String {\n            format!(\"n{id}@{term}\", id = node.id(), term = node.term())\n        }\n\n        /// Formats a node identifier with role.\n        fn format_node_role(node: &Node) -> String {\n            let role = match node {\n                Node::Candidate(_) => \"candidate\".to_string(),\n                Node::Follower(node) => {\n                    let leader = node.role.leader.map(|id| format!(\"n{id}\")).unwrap_or_default();\n                    format!(\"follower({leader})\")\n                }\n                Node::Leader(_) => \"leader\".to_string(),\n            };\n            format!(\"{node} {role}\", node = Self::format_node(node))\n        }\n\n        /// Formats a request.\n        fn format_request(request: &Request) -> String {\n            match request {\n                Request::Read(c) | Request::Write(c) => KVCommand::decode(c).unwrap().to_string(),\n                Request::Status => \"status\".to_string(),\n            }\n        }\n\n        /// Formats a response.\n        fn format_response(response: &crate::error::Result<Response>) -> String {\n            match response {\n                Ok(Response::Read(r) | Response::Write(r)) => {\n                    KVResponse::decode(r).unwrap().to_string()\n                }\n                Ok(Response::Status(status)) => format!(\"{status:#?}\"),\n                Err(error) => format!(\"Error::{error:?} ({error})\"),\n            }\n        }\n\n        /// Strike-through formats the given string using a Unicode combining stroke.\n        fn format_strikethrough(s: &str) -> String {\n            s.chars().flat_map(|c| [c, '\\u{0336}']).collect()\n        }\n    }\n}\n"
  },
  {
    "path": "src/raft/state.rs",
    "content": "use super::{Entry, Index};\nuse crate::error::Result;\n\n/// A Raft-managed state machine. Raft itself does not care what the state\n/// machine is, nor what the commands and results do -- it will simply apply\n/// arbitrary binary commands sequentially from the Raft log, returning an\n/// arbitrary binary result to the client.\n///\n/// Since commands are applied identically across all nodes, they must be\n/// deterministic and yield the same state and result across all nodes too.\n/// Otherwise, the nodes will diverge, such that different nodes will produce\n/// different results.\n///\n/// Write commands (`Request::Write`) are replicated and applied on all nodes\n/// via `State::apply`. The state machine must keep track of the last applied\n/// index and return it via `State::get_applied_index`. Read commands\n/// (`Request::Read`) are only executed on a single node via `State::read` and\n/// must not make any state changes.\npub trait State: Send {\n    /// Returns the last applied log index from the state machine.\n    ///\n    /// This must correspond to the current state of the state machine, since it\n    /// determines which command to apply next. In particular, a node crash may\n    /// result in partial command application or data loss, which must be\n    /// handled appropriately.\n    fn get_applied_index(&self) -> Index;\n\n    /// Applies a log entry to the state machine, returning a client result.\n    /// Errors are considered applied and propagated back to the client.\n    ///\n    /// This is executed on all nodes, so the result must be deterministic: it\n    /// must yield the same state and result on all nodes, even if the command\n    /// is reapplied following a node crash.\n    ///\n    /// Any non-deterministic apply error (e.g. an IO error) must panic and\n    /// crash the node -- if it instead returns an error to the client, the\n    /// command is considered applied and node states will diverge. The state\n    /// machine is responsible for panicing when appropriate.\n    ///\n    /// The entry may contain a noop command, which is committed by Raft during\n    /// leader changes. This still needs to be applied to the state machine to\n    /// properly update the applied index, and should return an empty result.\n    fn apply(&mut self, entry: Entry) -> Result<Vec<u8>>;\n\n    /// Executes a read command in the state machine, returning a client result.\n    /// Errors are also propagated back to the client.\n    ///\n    /// This is only executed on a single node, so it must not result in any\n    /// state changes (i.e. it must not write).\n    fn read(&self, command: Vec<u8>) -> Result<Vec<u8>>;\n}\n\n/// Test helper state machines.\n#[cfg(test)]\npub mod test {\n    use std::collections::BTreeMap;\n    use std::fmt::Display;\n\n    use crossbeam::channel::Sender;\n    use itertools::Itertools as _;\n    use serde::{Deserialize, Serialize};\n\n    use super::*;\n    use crate::encoding::{self, Value as _};\n\n    /// Wraps a state machine and emits applied entries to the provided channel.\n    pub struct Emit {\n        inner: Box<dyn State>,\n        tx: Sender<Entry>,\n    }\n\n    impl Emit {\n        pub fn new(inner: Box<dyn State>, tx: Sender<Entry>) -> Box<Self> {\n            Box::new(Self { inner, tx })\n        }\n    }\n\n    impl State for Emit {\n        fn get_applied_index(&self) -> Index {\n            self.inner.get_applied_index()\n        }\n\n        fn apply(&mut self, entry: Entry) -> Result<Vec<u8>> {\n            let response = self.inner.apply(entry.clone())?;\n            self.tx.send(entry)?;\n            Ok(response)\n        }\n\n        fn read(&self, command: Vec<u8>) -> Result<Vec<u8>> {\n            self.inner.read(command)\n        }\n    }\n\n    /// A simple string key/value store. Takes KVCommands.\n    pub struct KV {\n        applied_index: Index,\n        data: BTreeMap<String, String>,\n    }\n\n    impl KV {\n        pub fn new() -> Box<Self> {\n            Box::new(Self { applied_index: 0, data: BTreeMap::new() })\n        }\n    }\n\n    impl State for KV {\n        fn get_applied_index(&self) -> Index {\n            self.applied_index\n        }\n\n        fn apply(&mut self, entry: Entry) -> Result<Vec<u8>> {\n            let command = entry.command.as_deref().map(KVCommand::decode).transpose()?;\n            let response = match command {\n                Some(KVCommand::Put { key, value }) => {\n                    self.data.insert(key, value);\n                    KVResponse::Put(entry.index).encode()\n                }\n                Some(c @ (KVCommand::Get { .. } | KVCommand::Scan)) => {\n                    panic!(\"{c} submitted as write command\")\n                }\n                None => Vec::new(),\n            };\n            self.applied_index = entry.index;\n            Ok(response)\n        }\n\n        fn read(&self, command: Vec<u8>) -> Result<Vec<u8>> {\n            match KVCommand::decode(&command)? {\n                KVCommand::Get { key } => {\n                    Ok(KVResponse::Get(self.data.get(&key).cloned()).encode())\n                }\n                KVCommand::Scan => Ok(KVResponse::Scan(self.data.clone()).encode()),\n                c @ KVCommand::Put { .. } => panic!(\"{c} submitted as read command\"),\n            }\n        }\n    }\n\n    /// A KV command. Returns the corresponding KVResponse.\n    #[derive(Serialize, Deserialize)]\n    pub enum KVCommand {\n        /// Fetches the value of the given key.\n        Get { key: String },\n        /// Stores the given key/value pair, returning the applied index.\n        Put { key: String, value: String },\n        /// Returns all key/value pairs.\n        Scan,\n    }\n\n    impl encoding::Value for KVCommand {}\n\n    impl Display for KVCommand {\n        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n            match self {\n                Self::Get { key } => write!(f, \"get {key}\"),\n                Self::Put { key, value } => write!(f, \"put {key}={value}\"),\n                Self::Scan => write!(f, \"scan\"),\n            }\n        }\n    }\n\n    /// A KVCommand response.\n    #[derive(Serialize, Deserialize)]\n    pub enum KVResponse {\n        /// Get returns the key's value, or None if it does not exist.\n        Get(Option<String>),\n        /// Put returns the applied index of the command.\n        Put(Index),\n        /// Scan returns the key/value pairs.\n        Scan(BTreeMap<String, String>),\n    }\n\n    impl encoding::Value for KVResponse {}\n\n    impl Display for KVResponse {\n        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n            match self {\n                Self::Get(Some(value)) => write!(f, \"{value}\"),\n                Self::Get(None) => write!(f, \"None\"),\n                Self::Put(applied_index) => write!(f, \"{applied_index}\"),\n                Self::Scan(kvs) => {\n                    write!(f, \"{}\", kvs.iter().map(|(k, v)| format!(\"{k}={v}\")).join(\",\"))\n                }\n            }\n        }\n    }\n\n    /// A state machine which does nothing. All commands are ignored.\n    pub struct Noop {\n        applied_index: Index,\n    }\n\n    impl Noop {\n        pub fn new() -> Box<Self> {\n            Box::new(Self { applied_index: 0 })\n        }\n    }\n\n    impl State for Noop {\n        fn get_applied_index(&self) -> Index {\n            self.applied_index\n        }\n\n        fn apply(&mut self, entry: Entry) -> Result<Vec<u8>> {\n            self.applied_index = entry.index;\n            Ok(Vec::new())\n        }\n\n        fn read(&self, _: Vec<u8>) -> Result<Vec<u8>> {\n            Ok(Vec::new())\n        }\n    }\n}\n"
  },
  {
    "path": "src/raft/testscripts/log/append",
    "content": "# Appending an entry with term 0 fails.\n!append foo\n---\nPanic: can't append entry in term 0\n\n# Appending to an empty log works. The term doesn't have to be 1. The entry is\n# written to the engine and flushed to durable storage.\nset_term 2\nappend foo [ops]\n---\nappend → 1@2 \"foo\"\nengine set raft:Entry(1) → 1@2 \"foo\" [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x02\\x01\\x03foo\"]\nengine flush\n\n# Appending a noop entry (no command) also works.\nappend [ops]\n---\nappend → 2@2 None\nengine set raft:Entry(2) → 2@2 None [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x02\\x02\\x00\"]\nengine flush\n\n# Check that the last index/term is updated (commit index isn't), and that\n# the engine contains the expected data, both in logical and raw form.\nstatus\nscan\ndump\n---\nterm=2 last=2@2 commit=0@0 vote=None\n1@2 \"foo\"\n2@2 None\nraft:Entry(1) → 1@2 \"foo\" [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x02\\x01\\x03foo\"]\nraft:Entry(2) → 2@2 None [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x02\\x02\\x00\"]\nraft:TermVote → term=2 vote=None [\"\\x01\" → \"\\x02\\x00\"]\n\n# Skipping a term then appending is allowed.\nset_term 3\nappend command\nset_term 5\nappend\n---\nappend → 3@3 \"command\"\nappend → 4@5 None\n\n# Dump the final status and data.\nstatus\nscan\ndump\n---\nterm=5 last=4@5 commit=0@0 vote=None\n1@2 \"foo\"\n2@2 None\n3@3 \"command\"\n4@5 None\nraft:Entry(1) → 1@2 \"foo\" [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x02\\x01\\x03foo\"]\nraft:Entry(2) → 2@2 None [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x02\\x02\\x00\"]\nraft:Entry(3) → 3@3 \"command\" [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x03\\x03\\x01\\x07command\"]\nraft:Entry(4) → 4@5 None [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x04\\x05\\x00\"]\nraft:TermVote → term=5 vote=None [\"\\x01\" → \"\\x05\\x00\"]\n"
  },
  {
    "path": "src/raft/testscripts/log/commit",
    "content": "# Committing fails on an empty engine.\n!commit 1\n---\nPanic: commit index 1 does not exist\n\n# Add some entries.\nset_term 2\nsplice 1@1= 2@1=foo 3@2=bar\n---\nsplice → 3@2 \"bar\"\n\n# Committing entry 0 fails.\n!commit 0\n---\nPanic: commit index 0 does not exist\n\n# Committing entry 1 works, and updates the commit index.\n#\n# Show the engine operations too, and notice that the commit index isn't flushed\n# to durable storage (it can be recovered from the durable quorum logs).\ncommit 1 [ops]\nstatus\n---\ncommit → 1@1 None\nengine set raft:CommitIndex → 1@1 [\"\\x02\" → \"\\x01\\x01\"]\nterm=2 last=3@2 commit=1@1 vote=None\n\n# Dump the raw engine contents.\ndump\n---\nraft:Entry(1) → 1@1 None [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x01\\x00\"]\nraft:Entry(2) → 2@1 \"foo\" [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x02\\x01\\x01\\x03foo\"]\nraft:Entry(3) → 3@2 \"bar\" [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x03\\x02\\x01\\x03bar\"]\nraft:TermVote → term=2 vote=None [\"\\x01\" → \"\\x02\\x00\"]\nraft:CommitIndex → 1@1 [\"\\x02\" → \"\\x01\\x01\"]\n\n# Commits are idempotent, which doesn't incur an engine set.\ncommit 1 [ops]\nstatus\n---\ncommit → 1@1 None\nterm=2 last=3@2 commit=1@1 vote=None\n\n# Commits can skip an entry.\ncommit 3\nstatus\n---\ncommit → 3@2 \"bar\"\nterm=2 last=3@2 commit=3@2 vote=None\n\n# Commit regressions error.\n!commit 2\nstatus\n---\nPanic: commit index regression 3 → 2\nterm=2 last=3@2 commit=3@2 vote=None\n\n# Committing non-existant indexes error.\n!commit 4\nstatus\n---\nPanic: commit index 4 does not exist\nterm=2 last=3@2 commit=3@2 vote=None\n\n# Dump the raw values.\ndump\n---\nraft:Entry(1) → 1@1 None [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x01\\x00\"]\nraft:Entry(2) → 2@1 \"foo\" [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x02\\x01\\x01\\x03foo\"]\nraft:Entry(3) → 3@2 \"bar\" [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x03\\x02\\x01\\x03bar\"]\nraft:TermVote → term=2 vote=None [\"\\x01\" → \"\\x02\\x00\"]\nraft:CommitIndex → 3@2 [\"\\x02\" → \"\\x03\\x02\"]\n"
  },
  {
    "path": "src/raft/testscripts/log/get",
    "content": "# get returns None on an empty engine.\nget 1\n---\nNone\n\n# Append a few entries.\nset_term 1\nappend\nappend foo\nset_term 2\nappend bar\n---\nappend → 1@1 None\nappend → 2@1 \"foo\"\nappend → 3@2 \"bar\"\n\n# get returns noop entries and regular entries.\nget 1 2\n---\n1@1 None\n2@1 \"foo\"\n\n# get returns None for missing entries, and for index 0.\nget 4 0\n---\nNone\nNone\n"
  },
  {
    "path": "src/raft/testscripts/log/has",
    "content": "# has returns false on an empty engine.\nhas 1@1\n---\nfalse\n\n# Append a few entries.\nset_term 1\nappend\nappend foo\nset_term 2\nappend bar\n---\nappend → 1@1 None\nappend → 2@1 \"foo\"\nappend → 3@2 \"bar\"\n\n# has returns true both for noop entries and regular entries.\nhas 1@1 2@1\n---\ntrue\ntrue\n\n# has returns false for missing entries, including index 0.\nhas 4@2 0@0\n---\nfalse\nfalse\n\n# has returns false for term mismatches.\nhas 1@2 3@1 0@1\n---\nfalse\nfalse\nfalse\n"
  },
  {
    "path": "src/raft/testscripts/log/init",
    "content": "# Tests that the log correctly initializes cached state when opened.\n\nset_term 1\n---\nok\n\nappend foo\nset_term 2 7\nappend bar\ncommit 1\n---\nappend → 1@1 \"foo\"\nappend → 2@2 \"bar\"\ncommit → 1@1 \"foo\"\n\nstatus\n---\nterm=2 last=2@2 commit=1@1 vote=7\n\nreload\n---\nok\n\nstatus\n---\nterm=2 last=2@2 commit=1@1 vote=7\n\nscan\n---\n1@1 \"foo\"\n2@2 \"bar\"\n"
  },
  {
    "path": "src/raft/testscripts/log/scan",
    "content": "# scan works on an empty engine, even when given indexes.\nscan\nscan 3..7\n---\nok\n\n# Append a few entries.\nset_term 1\nappend\nappend foo\nset_term 2\nappend bar\n---\nappend → 1@1 None\nappend → 2@1 \"foo\"\nappend → 3@2 \"bar\"\n\n# Full scan.\nscan\n---\n1@1 None\n2@1 \"foo\"\n3@2 \"bar\"\n\n# Start bound.\nscan 2..\n---\n2@1 \"foo\"\n3@2 \"bar\"\n\nscan 4..\n---\nok\n\nscan 0..\n---\n1@1 None\n2@1 \"foo\"\n3@2 \"bar\"\n\n# End bound.\nscan \"..2\"\n---\n1@1 None\n\nscan \"..=2\"\n---\n1@1 None\n2@1 \"foo\"\n\nscan \"..7\"\n---\n1@1 None\n2@1 \"foo\"\n3@2 \"bar\"\n\nscan \"..1\"\n---\nok\n\nscan \"..0\"\n---\nok\n\n# Both bounds.\nscan 1..2\n---\n1@1 None\n\nscan \"1..=2\"\n---\n1@1 None\n2@1 \"foo\"\n\nscan 0..7\n---\n1@1 None\n2@1 \"foo\"\n3@2 \"bar\"\n\nscan 1..1\n---\nok\n\n# Bounds panics.\n!scan 1..0\n---\nPanic: range start is greater than range end in BTreeMap\n\n!scan 7..3\n---\nPanic: range start is greater than range end in BTreeMap\n"
  },
  {
    "path": "src/raft/testscripts/log/scan_apply",
    "content": "# scan_apply works on an empty engine, even when given an applied index.\nscan_apply 0\nscan_apply 3\n---\nok\n\n# Append a few entries.\nset_term 1\nappend\nappend foo\nset_term 2\nappend bar\n---\nappend → 1@1 None\nappend → 2@1 \"foo\"\nappend → 3@2 \"bar\"\n\n# Nothing is committed, so scan_applied yields nothing.\nscan_apply 0\n---\nok\n\n# Commit the first two entries and apply them.\ncommit 2\nscan_apply 0\n---\ncommit → 2@1 \"foo\"\n1@1 None\n2@1 \"foo\"\n\n# Passing the commit index yields nothing.\nscan_apply 2\n---\nok\n\n# Passing an applied_index after the commit index is ok, and yields nothing.\nscan_apply 3\nscan_apply 10\n---\nok\n\n# Committing and applying the last entry works.\ncommit 3\nscan_apply 2\n---\ncommit → 3@2 \"bar\"\n3@2 \"bar\"\n\n# Scanning from a lower commit index again works.\nscan_apply 1\n---\n2@1 \"foo\"\n3@2 \"bar\"\n\nscan_apply 0\n---\n1@1 None\n2@1 \"foo\"\n3@2 \"bar\"\n"
  },
  {
    "path": "src/raft/testscripts/log/splice",
    "content": "# Splicing at index 0 should fail.\n!splice 0@1=foo\n---\nPanic: spliced entry has index or term 0\n\n# Splicing without a term should fail.\n!splice 1@1=foo\n---\nPanic: splice term 1 beyond current 0\n\n\n\n# Splicing at index 2 should fail (creates gap).\nset_term 1\n!splice 2@1=foo\n---\nPanic: first index 2 must touch existing log\n\n# Splicing entries at start should work, both with and without commands, and\n# starting at a term after 1. They should be written to the engine and flushed\n# to durable storage. It should also update the state.\nset_term 2\nsplice 1@2= 2@2=command [ops]\nstatus\nscan\n---\nsplice → 2@2 \"command\"\nengine set raft:Entry(1) → 1@2 None [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x02\\x00\"]\nengine set raft:Entry(2) → 2@2 \"command\" [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x02\\x02\\x01\\x07command\"]\nengine flush\nterm=2 last=2@2 commit=0@0 vote=None\n1@2 None\n2@2 \"command\"\n\n# Splicing an empty list should work and be a noop.\nsplice [ops]\nstatus\nscan\n---\nsplice → 2@2 \"command\"\nterm=2 last=2@2 commit=0@0 vote=None\n1@2 None\n2@2 \"command\"\n\n# Splicing multiple duplicate entries should fail.\n!splice 3@2= 3@2=\n---\nPanic: spliced entries are not contiguous\n\n# Splicing entries with a gap should fail.\n!splice 3@2= 5@2=\n---\nPanic: spliced entries are not contiguous\n\n# Splicing entries with a term regression should fail.\n!splice 3@2= 4@1=\n---\nPanic: spliced entries have term regression\n\n# Splicing entries with a gap from the base should fail.\n!splice 4@2=\n---\nPanic: first index 4 must touch existing log\n\n# Splicing with a term regression from the base should fail.\n!splice 3@1=\n---\nPanic: splice term regression 2 → 1\n\n# Splicing with a term beyond the current term should fail.\n!splice 3@3=\n!splice 3@4=\n---\nPanic: splice term 3 beyond current 2\nPanic: splice term 4 beyond current 2\n\n# Fully overlapping entries is a noop.\nsplice 1@2= 2@2=command [ops]\nscan\n---\nsplice → 2@2 \"command\"\n1@2 None\n2@2 \"command\"\n\n# An overlapping prefix is a noop.\nsplice 1@2= [ops]\nscan\n---\nsplice → 2@2 \"command\"\n1@2 None\n2@2 \"command\"\n\n# An overlapping suffix is a noop.\nsplice 2@2=command [ops]\nscan\n---\nsplice → 2@2 \"command\"\n1@2 None\n2@2 \"command\"\n\n# Changing a command with the same term/index should fail.\n!splice 2@2=foo\nscan\n---\nPanic: command mismatch at Entry { index: 2, term: 2, command: Some([99, 111, 109, 109, 97, 110, 100]) }\n1@2 None\n2@2 \"command\"\n\n# Appending a new entry in the same term should work, as should\n# appending one in a new term.\nsplice 3@2=bar\nset_term 3\nsplice 4@3=\nscan\n---\nsplice → 3@2 \"bar\"\nsplice → 4@3 None\n1@2 None\n2@2 \"command\"\n3@2 \"bar\"\n4@3 None\n\n# Splicing with suffix overlap should work, and only write the new entries.\nsplice 3@2=bar 4@3= 5@3=foo 6@3=bar [ops]\nscan\n---\nsplice → 6@3 \"bar\"\nengine set raft:Entry(5) → 5@3 \"foo\" [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x05\\x03\\x01\\x03foo\"]\nengine set raft:Entry(6) → 6@3 \"bar\" [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x06\\x03\\x01\\x03bar\"]\nengine flush\n1@2 None\n2@2 \"command\"\n3@2 \"bar\"\n4@3 None\n5@3 \"foo\"\n6@3 \"bar\"\n\n# Splicing at an existing index with a new term should replace the tail.\nset_term 4\nsplice 4@4= [ops]\nstatus\nscan\n---\nsplice → 4@4 None\nengine set raft:Entry(4) → 4@4 None [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x04\\x04\\x00\"]\nengine delete raft:Entry(5) [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\"]\nengine delete raft:Entry(6) [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\"]\nengine flush\nterm=4 last=4@4 commit=0@0 vote=None\n1@2 None\n2@2 \"command\"\n3@2 \"bar\"\n4@4 None\n\n# This also holds at the start of the log.\nset_term 5\nsplice 1@5= 2@5=foo 3@5=bar [ops]\nstatus\nscan\n---\nsplice → 3@5 \"bar\"\nengine set raft:Entry(1) → 1@5 None [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x05\\x00\"]\nengine set raft:Entry(2) → 2@5 \"foo\" [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x02\\x05\\x01\\x03foo\"]\nengine set raft:Entry(3) → 3@5 \"bar\" [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x03\\x05\\x01\\x03bar\"]\nengine delete raft:Entry(4) [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\"]\nengine flush\nterm=5 last=3@5 commit=0@0 vote=None\n1@5 None\n2@5 \"foo\"\n3@5 \"bar\"\n\n# Splicing across the commit index should work, as long as the entries match.\ncommit 2\nsplice 1@5= 2@5=foo 3@5=bar 4@5=\nstatus\nscan\n---\ncommit → 2@5 \"foo\"\nsplice → 4@5 None\nterm=5 last=4@5 commit=2@5 vote=None\n1@5 None\n2@5 \"foo\"\n3@5 \"bar\"\n4@5 None\n\n# Splicing across the commit index can replace a tail after the commit index.\nset_term 9\nsplice 3@6= 4@6=bar\nstatus\nscan\n---\nsplice → 4@6 \"bar\"\nterm=9 last=4@6 commit=2@5 vote=None\n1@5 None\n2@5 \"foo\"\n3@6 None\n4@6 \"bar\"\n\n# But replacing a tail at or before the commit index should fail.\n!splice 2@7=\n!splice 1@7=\n---\nPanic: spliced entries below commit index\nPanic: spliced entries below commit index\n\n# Dump the raw data.\ndump\n---\nraft:Entry(1) → 1@5 None [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x05\\x00\"]\nraft:Entry(2) → 2@5 \"foo\" [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x02\\x05\\x01\\x03foo\"]\nraft:Entry(3) → 3@6 None [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x03\\x06\\x00\"]\nraft:Entry(4) → 4@6 \"bar\" [\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x04\\x06\\x01\\x03bar\"]\nraft:TermVote → term=9 vote=None [\"\\x01\" → \"\\t\\x00\"]\nraft:CommitIndex → 2@5 [\"\\x02\" → \"\\x02\\x05\"]\n"
  },
  {
    "path": "src/raft/testscripts/log/status",
    "content": "# Status on empty engine works.\nstatus engine=true\n---\nterm=0 last=0@0 commit=0@0 vote=None engine=Status {\n    name: \"bitcask\",\n    keys: 0,\n    size: 0,\n    disk_size: 0,\n    live_disk_size: 0,\n}\n\n# Write some data.\nset_term 1\nappend\nappend foo\nset_term 2 1\nappend bar\ncommit 2\n---\nappend → 1@1 None\nappend → 2@1 \"foo\"\nappend → 3@2 \"bar\"\ncommit → 2@1 \"foo\"\n\n# Status gives correct info.\nstatus engine=true\n---\nterm=2 last=3@2 commit=2@1 vote=1 engine=Status {\n    name: \"bitcask\",\n    keys: 5,\n    size: 51,\n    disk_size: 102,\n    live_disk_size: 91,\n}\n"
  },
  {
    "path": "src/raft/testscripts/log/term",
    "content": "# get_term works on empty engine.\nget_term\n---\nterm=0 vote=None\n\n# Storing a 0 term errors.\n!set_term 0\n---\nPanic: can't set term 0\n\n# set_term stores a term and empty vote, writing it to the engine\n# and flushing it to durable storage.\nset_term 3 [ops]\nget_term\n---\nengine set raft:TermVote → term=3 vote=None [\"\\x01\" → \"\\x03\\x00\"]\nengine flush\nterm=3 vote=None\n\n# set_term stores a term and vote.\nset_term 3 7 [ops]\nget_term\n---\nengine set raft:TermVote → term=3 vote=7 [\"\\x01\" → \"\\x03\\x01\\x07\"]\nengine flush\nterm=3 vote=7\n\n# set_term is idempotent, which doesn't incur an engine write.\nset_term 3 7 [ops]\nget_term\n---\nterm=3 vote=7\n\n# Moving the term into the far future is allowed.\nset_term 7\nget_term\n---\nterm=7 vote=None\n\n# Starting a new term with a vote is allowed.\nset_term 9 1\nget_term\n---\nterm=9 vote=1\n\n# Regressing the term errors.\n!set_term 8\n---\nPanic: term regression 9 → 8\n\n# Clearing the vote errors.\n!set_term 9\n---\nPanic: can't change vote\n\n# Changing the vote errors.\n!set_term 9 2\n---\nPanic: can't change vote\n\n# The above errors should not have changed the term/vote.\nget_term\ndump\n---\nterm=9 vote=1\nraft:TermVote → term=9 vote=1 [\"\\x01\" → \"\\t\\x01\\x01\"]\n"
  },
  {
    "path": "src/raft/testscripts/node/append",
    "content": "# Can append single entries in steady state.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Propose a single write.\nput 1 foo=bar\n---\nc1@1 → n1 ClientRequest id=0x01 write 0x0103666f6f03626172\nn1@1 append 2@1 put foo=bar\nn1@1 → n2 Append base=1@1 [2@1]\nn1@1 → n3 Append base=1@1 [2@1]\n\nstatus\n---\nn1@1 leader last=2@1 commit=1@1 applied=1 progress={2:1→3 3:1→3}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Append it to both followers.\ndeliver\n---\nn2@1 append 2@1 put foo=bar\nn2@1 → n1 AppendResponse match_index=2\nn3@1 append 2@1 put foo=bar\nn3@1 → n1 AppendResponse match_index=2\n\n# The leader commits and applies the write.\nstabilize\n---\nn1@1 commit 2@1\nn1@1 apply 2@1 put foo=bar\nn1@1 → c1 ClientResponse id=0x01 write 0x0102\nc1@1 put foo=bar ⇒ 2\n\nstatus\n---\nn1@1 leader last=2@1 commit=2@1 applied=2 progress={2:2→3 3:2→3}\nn2@1 follower(n1) last=2@1 commit=1@1 applied=1\nn3@1 follower(n1) last=2@1 commit=1@1 applied=1\n"
  },
  {
    "path": "src/raft/testscripts/node/append_base_missing",
    "content": "# Appends with a base beyond the node's last log entry should result in a\n# rejection at the index following the last entry, and the leader appending\n# the tail of the log.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Partition n3 so that it does not receive writes.\npartition 3\n---\nn3 ⇹ n1 n2\n\n# Replicate a couple of writes.\n(put 1 a=1)\n(put 1 b=2)\n(put 1 c=3)\n(stabilize heartbeat=true)\nstatus\n---\nn1@1 leader last=4@1 commit=4@1 applied=4 progress={2:4→5 3:1→5}\nn2@1 follower(n1) last=4@1 commit=4@1 applied=4\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Heal the partition, and propose another write.\nheal\nput 1 c=3\n---\nn1 n2 n3 fully connected\nc1@1 → n1 ClientRequest id=0x04 write 0x0101630133\nn1@1 append 5@1 put c=3\nn1@1 → n2 Append base=4@1 [5@1]\nn1@1 → n3 Append base=4@1 [5@1]\n\n# The 4@1 base is beyond n3's last index 1@1, so the append is rejected.\n# However, the follower returns reject_index=2 immediately after its\n# last index, rather than the original base index 4.\ndeliver 3\n---\nn3@1 → n1 AppendResponse reject_index=2\n\n# Because index 1 is already matched with the leader, it doesn't have to probe\n# and simply sends the entire tail, which is accepted.\ndeliver 1\nstatus 1\n---\nn1@1 → n3 Append base=1@1 [2@1 3@1 4@1 5@1]\nn1@1 leader last=5@1 commit=4@1 applied=4 progress={2:4→6 3:1→6}\n\ndeliver 3\n---\nn3@1 append 2@1 put a=1\nn3@1 append 3@1 put b=2\nn3@1 append 4@1 put c=3\nn3@1 append 5@1 put c=3\nn3@1 → n1 AppendResponse match_index=5\n\n# When n1 receives the ack, it commits and applies the write.\ndeliver 1\n---\nn1@1 commit 5@1\nn1@1 apply 5@1 put c=3\nn1@1 → c1 ClientResponse id=0x04 write 0x0105\nc1@1 put c=3 ⇒ 5\n\n# The progress is also updated.\nstatus\n---\nn1@1 leader last=5@1 commit=5@1 applied=5 progress={2:4→6 3:5→6}\nn2@1 follower(n1) last=4@1 commit=4@1 applied=4\nn3@1 follower(n1) last=5@1 commit=1@1 applied=1\n"
  },
  {
    "path": "src/raft/testscripts/node/append_base_missing_all",
    "content": "# Appends to a node with an empty log should result in a rejection of index 1,\n# allowing the leader to send the entire log.\n\ncluster nodes=3\n---\nn1@0 follower() last=0@0 commit=0@0 applied=0\nn2@0 follower() last=0@0 commit=0@0 applied=0\nn3@0 follower() last=0@0 commit=0@0 applied=0\n\n# Partition n3 so that it does not receive writes.\npartition 3\n---\nn3 ⇹ n1 n2\n\n# Elect n1 as leader.\n(campaign 1)\n(stabilize)\nstatus\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:0→2}\nn2@1 follower(n1) last=1@1 commit=0@0 applied=0\nn3@0 follower() last=0@0 commit=0@0 applied=0\n\n# Replicate a couple of writes.\n(put 1 a=1)\n(put 1 b=2)\n(put 1 c=3)\n(stabilize heartbeat=true)\nstatus\n---\nn1@1 leader last=4@1 commit=4@1 applied=4 progress={2:4→5 3:0→5}\nn2@1 follower(n1) last=4@1 commit=4@1 applied=4\nn3@0 follower() last=0@0 commit=0@0 applied=0\n\n# Heal the partition, and propose another write.\nheal\nput 1 c=3\n---\nn1 n2 n3 fully connected\nc1@1 → n1 ClientRequest id=0x04 write 0x0101630133\nn1@1 append 5@1 put c=3\nn1@1 → n2 Append base=4@1 [5@1]\nn1@1 → n3 Append base=4@1 [5@1]\n\n# n3 has no entries, so it rejects with reject_index=1.\ndeliver 3\n---\nn3@0 follower() ⇨ n3@1 follower(n1)\nn3@1 → n1 AppendResponse reject_index=1\n\n# This allows n1 to send the entire log, without having to probe.\ndeliver 1\nstatus 1\n---\nn1@1 → n3 Append base=0@0 [1@1 2@1 3@1 4@1 5@1]\nn1@1 leader last=5@1 commit=4@1 applied=4 progress={2:4→6 3:0→6}\n\ndeliver 3\n---\nn3@1 append 1@1 None\nn3@1 append 2@1 put a=1\nn3@1 append 3@1 put b=2\nn3@1 append 4@1 put c=3\nn3@1 append 5@1 put c=3\nn3@1 → n1 AppendResponse match_index=5\n\n# When n1 receives the ack, it commits and applies the write.\ndeliver 1\n---\nn1@1 commit 5@1\nn1@1 apply 5@1 put c=3\nn1@1 → c1 ClientResponse id=0x04 write 0x0105\nc1@1 put c=3 ⇒ 5\n\n# The progress is also updated.\nstatus\n---\nn1@1 leader last=5@1 commit=5@1 applied=5 progress={2:4→6 3:5→6}\nn2@1 follower(n1) last=4@1 commit=4@1 applied=4\nn3@1 follower(n1) last=5@1 commit=0@0 applied=0\n"
  },
  {
    "path": "src/raft/testscripts/node/append_commit_quorum",
    "content": "# Append results in a leader-side commit once a quorum is reached for the\n# relevant entries.\n\ncluster nodes=6 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2 4:1→2 5:1→2 6:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\nn4@1 follower(n1) last=1@1 commit=1@1 applied=1\nn5@1 follower(n1) last=1@1 commit=1@1 applied=1\nn6@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Incrementally disconnect all nodes except one and then propose a write, to\n# generate an increasing quorum index.\n\n# Replicating 2 to n2 does not commit.\npartition 3 4 5 6\n---\nn1 n2 ⇹ n3 n4 n5 n6\n\nput 1 a=1\nstabilize\n---\nc1@1 → n1 ClientRequest id=0x01 write 0x0101610131\nn1@1 append 2@1 put a=1\nn1@1 → n2 Append base=1@1 [2@1]\nn1@1 ⇥ n3 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶1̶@̶1̶ ̶[̶2̶@̶1̶]̶\nn1@1 ⇥ n4 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶1̶@̶1̶ ̶[̶2̶@̶1̶]̶\nn1@1 ⇥ n5 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶1̶@̶1̶ ̶[̶2̶@̶1̶]̶\nn1@1 ⇥ n6 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶1̶@̶1̶ ̶[̶2̶@̶1̶]̶\nn2@1 append 2@1 put a=1\nn2@1 → n1 AppendResponse match_index=2\n\nstatus\n---\nn1@1 leader last=2@1 commit=1@1 applied=1 progress={2:2→3 3:1→3 4:1→3 5:1→3 6:1→3}\nn2@1 follower(n1) last=2@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\nn4@1 follower(n1) last=1@1 commit=1@1 applied=1\nn5@1 follower(n1) last=1@1 commit=1@1 applied=1\nn6@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Replicating 2-3 to n3 does not commit.\nheal\npartition 2 4 5 6\n---\nn1 n2 n3 n4 n5 n6 fully connected\nn1 n3 ⇹ n2 n4 n5 n6\n\nput 1 b=2\nstabilize\n---\nc1@1 → n1 ClientRequest id=0x02 write 0x0101620132\nn1@1 append 3@1 put b=2\nn1@1 ⇥ n2 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶2̶@̶1̶ ̶[̶3̶@̶1̶]̶\nn1@1 → n3 Append base=2@1 [3@1]\nn1@1 ⇥ n4 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶2̶@̶1̶ ̶[̶3̶@̶1̶]̶\nn1@1 ⇥ n5 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶2̶@̶1̶ ̶[̶3̶@̶1̶]̶\nn1@1 ⇥ n6 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶2̶@̶1̶ ̶[̶3̶@̶1̶]̶\nn3@1 → n1 AppendResponse reject_index=2\nn1@1 → n3 Append base=1@1 [2@1 3@1]\nn3@1 append 2@1 put a=1\nn3@1 append 3@1 put b=2\nn3@1 → n1 AppendResponse match_index=3\n\nstatus\n---\nn1@1 leader last=3@1 commit=1@1 applied=1 progress={2:2→4 3:3→4 4:1→4 5:1→4 6:1→4}\nn2@1 follower(n1) last=2@1 commit=1@1 applied=1\nn3@1 follower(n1) last=3@1 commit=1@1 applied=1\nn4@1 follower(n1) last=1@1 commit=1@1 applied=1\nn5@1 follower(n1) last=1@1 commit=1@1 applied=1\nn6@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Replicating 2-4 to n4 commits 2.\nheal\npartition 2 3 5 6\n---\nn1 n2 n3 n4 n5 n6 fully connected\nn1 n4 ⇹ n2 n3 n5 n6\n\nput 1 c=3\nstabilize\n---\nc1@1 → n1 ClientRequest id=0x03 write 0x0101630133\nn1@1 append 4@1 put c=3\nn1@1 ⇥ n2 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶3̶@̶1̶ ̶[̶4̶@̶1̶]̶\nn1@1 ⇥ n3 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶3̶@̶1̶ ̶[̶4̶@̶1̶]̶\nn1@1 → n4 Append base=3@1 [4@1]\nn1@1 ⇥ n5 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶3̶@̶1̶ ̶[̶4̶@̶1̶]̶\nn1@1 ⇥ n6 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶3̶@̶1̶ ̶[̶4̶@̶1̶]̶\nn4@1 → n1 AppendResponse reject_index=2\nn1@1 → n4 Append base=1@1 [2@1 3@1 4@1]\nn4@1 append 2@1 put a=1\nn4@1 append 3@1 put b=2\nn4@1 append 4@1 put c=3\nn4@1 → n1 AppendResponse match_index=4\nn1@1 commit 2@1\nn1@1 apply 2@1 put a=1\nn1@1 → c1 ClientResponse id=0x01 write 0x0102\nc1@1 put a=1 ⇒ 2\n\nstatus\n---\nn1@1 leader last=4@1 commit=2@1 applied=2 progress={2:2→5 3:3→5 4:4→5 5:1→5 6:1→5}\nn2@1 follower(n1) last=2@1 commit=1@1 applied=1\nn3@1 follower(n1) last=3@1 commit=1@1 applied=1\nn4@1 follower(n1) last=4@1 commit=1@1 applied=1\nn5@1 follower(n1) last=1@1 commit=1@1 applied=1\nn6@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Replicating 2-5 to n5 commits 3.\nheal\npartition 2 3 4 6\n---\nn1 n2 n3 n4 n5 n6 fully connected\nn1 n5 ⇹ n2 n3 n4 n6\n\nput 1 d=4\nstabilize\n---\nc1@1 → n1 ClientRequest id=0x04 write 0x0101640134\nn1@1 append 5@1 put d=4\nn1@1 ⇥ n2 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶4̶@̶1̶ ̶[̶5̶@̶1̶]̶\nn1@1 ⇥ n3 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶4̶@̶1̶ ̶[̶5̶@̶1̶]̶\nn1@1 ⇥ n4 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶4̶@̶1̶ ̶[̶5̶@̶1̶]̶\nn1@1 → n5 Append base=4@1 [5@1]\nn1@1 ⇥ n6 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶4̶@̶1̶ ̶[̶5̶@̶1̶]̶\nn5@1 → n1 AppendResponse reject_index=2\nn1@1 → n5 Append base=1@1 [2@1 3@1 4@1 5@1]\nn5@1 append 2@1 put a=1\nn5@1 append 3@1 put b=2\nn5@1 append 4@1 put c=3\nn5@1 append 5@1 put d=4\nn5@1 → n1 AppendResponse match_index=5\nn1@1 commit 3@1\nn1@1 apply 3@1 put b=2\nn1@1 → c1 ClientResponse id=0x02 write 0x0103\nc1@1 put b=2 ⇒ 3\n\nstatus\n---\nn1@1 leader last=5@1 commit=3@1 applied=3 progress={2:2→6 3:3→6 4:4→6 5:5→6 6:1→6}\nn2@1 follower(n1) last=2@1 commit=1@1 applied=1\nn3@1 follower(n1) last=3@1 commit=1@1 applied=1\nn4@1 follower(n1) last=4@1 commit=1@1 applied=1\nn5@1 follower(n1) last=5@1 commit=1@1 applied=1\nn6@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Replicating 2-6 to n6 commits 4.\nheal\npartition 2 3 4 5\n---\nn1 n2 n3 n4 n5 n6 fully connected\nn1 n6 ⇹ n2 n3 n4 n5\n\nput 1 e=5\nstabilize\n---\nc1@1 → n1 ClientRequest id=0x05 write 0x0101650135\nn1@1 append 6@1 put e=5\nn1@1 ⇥ n2 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶5̶@̶1̶ ̶[̶6̶@̶1̶]̶\nn1@1 ⇥ n3 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶5̶@̶1̶ ̶[̶6̶@̶1̶]̶\nn1@1 ⇥ n4 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶5̶@̶1̶ ̶[̶6̶@̶1̶]̶\nn1@1 ⇥ n5 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶5̶@̶1̶ ̶[̶6̶@̶1̶]̶\nn1@1 → n6 Append base=5@1 [6@1]\nn6@1 → n1 AppendResponse reject_index=2\nn1@1 → n6 Append base=1@1 [2@1 3@1 4@1 5@1 6@1]\nn6@1 append 2@1 put a=1\nn6@1 append 3@1 put b=2\nn6@1 append 4@1 put c=3\nn6@1 append 5@1 put d=4\nn6@1 append 6@1 put e=5\nn6@1 → n1 AppendResponse match_index=6\nn1@1 commit 4@1\nn1@1 apply 4@1 put c=3\nn1@1 → c1 ClientResponse id=0x03 write 0x0104\nc1@1 put c=3 ⇒ 4\n\nstatus\n---\nn1@1 leader last=6@1 commit=4@1 applied=4 progress={2:2→7 3:3→7 4:4→7 5:5→7 6:6→7}\nn2@1 follower(n1) last=2@1 commit=1@1 applied=1\nn3@1 follower(n1) last=3@1 commit=1@1 applied=1\nn4@1 follower(n1) last=4@1 commit=1@1 applied=1\nn5@1 follower(n1) last=5@1 commit=1@1 applied=1\nn6@1 follower(n1) last=6@1 commit=1@1 applied=1\n\n# Healing the partition and proposing another write replicates and commits all\n# entries.\nheal\n---\nn1 n2 n3 n4 n5 n6 fully connected\n\nput 1 f=6\nstabilize\n---\nc1@1 → n1 ClientRequest id=0x06 write 0x0101660136\nn1@1 append 7@1 put f=6\nn1@1 → n2 Append base=6@1 [7@1]\nn1@1 → n3 Append base=6@1 [7@1]\nn1@1 → n4 Append base=6@1 [7@1]\nn1@1 → n5 Append base=6@1 [7@1]\nn1@1 → n6 Append base=6@1 [7@1]\nn2@1 → n1 AppendResponse reject_index=3\nn3@1 → n1 AppendResponse reject_index=4\nn4@1 → n1 AppendResponse reject_index=5\nn5@1 → n1 AppendResponse reject_index=6\nn6@1 append 7@1 put f=6\nn6@1 → n1 AppendResponse match_index=7\nn1@1 → n2 Append base=2@1 [3@1 4@1 5@1 6@1 7@1]\nn1@1 → n3 Append base=3@1 [4@1 5@1 6@1 7@1]\nn1@1 → n4 Append base=4@1 [5@1 6@1 7@1]\nn1@1 → n5 Append base=5@1 [6@1 7@1]\nn2@1 append 3@1 put b=2\nn2@1 append 4@1 put c=3\nn2@1 append 5@1 put d=4\nn2@1 append 6@1 put e=5\nn2@1 append 7@1 put f=6\nn2@1 → n1 AppendResponse match_index=7\nn3@1 append 4@1 put c=3\nn3@1 append 5@1 put d=4\nn3@1 append 6@1 put e=5\nn3@1 append 7@1 put f=6\nn3@1 → n1 AppendResponse match_index=7\nn4@1 append 5@1 put d=4\nn4@1 append 6@1 put e=5\nn4@1 append 7@1 put f=6\nn4@1 → n1 AppendResponse match_index=7\nn5@1 append 6@1 put e=5\nn5@1 append 7@1 put f=6\nn5@1 → n1 AppendResponse match_index=7\nn1@1 commit 5@1\nn1@1 apply 5@1 put d=4\nn1@1 → c1 ClientResponse id=0x04 write 0x0105\nc1@1 put d=4 ⇒ 5\nn1@1 commit 7@1\nn1@1 apply 6@1 put e=5\nn1@1 apply 7@1 put f=6\nn1@1 → c1 ClientResponse id=0x05 write 0x0106\nc1@1 put e=5 ⇒ 6\nn1@1 → c1 ClientResponse id=0x06 write 0x0107\nc1@1 put f=6 ⇒ 7\n\nstatus\n---\nn1@1 leader last=7@1 commit=7@1 applied=7 progress={2:7→8 3:7→8 4:7→8 5:7→8 6:7→8}\nn2@1 follower(n1) last=7@1 commit=1@1 applied=1\nn3@1 follower(n1) last=7@1 commit=1@1 applied=1\nn4@1 follower(n1) last=7@1 commit=1@1 applied=1\nn5@1 follower(n1) last=7@1 commit=1@1 applied=1\nn6@1 follower(n1) last=7@1 commit=1@1 applied=1\n"
  },
  {
    "path": "src/raft/testscripts/node/append_initial",
    "content": "# An initial append at base 0 can have a single or multiple entries.\n\ncluster nodes=3\n---\nn1@0 follower() last=0@0 commit=0@0 applied=0\nn2@0 follower() last=0@0 commit=0@0 applied=0\nn3@0 follower() last=0@0 commit=0@0 applied=0\n\n# Partition n3 so that is has an empty log.\npartition 3\n---\nn3 ⇹ n1 n2\n\n# n1 campaigns.\ncampaign 1\ndeliver\n---\nn1@0 follower() ⇨ n1@1 candidate\nn1@1 → n2 Campaign last=0@0\nn1@1 ⇥ n3 C̶a̶m̶p̶a̶i̶g̶n̶ ̶l̶a̶s̶t̶=̶0̶@̶0̶\nn2@0 follower() ⇨ n2@1 follower()\nn2@1 → n1 CampaignResponse vote=true\n\n# When n1 wins, it successfully appends an entry at base 0 to n2.\nstabilize\n---\nn1@1 candidate ⇨ n1@1 leader\nn1@1 append 1@1 None\nn1@1 → n2 Append base=0@0 [1@1]\nn1@1 ⇥ n3 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶0̶@̶0̶ ̶[̶1̶@̶1̶]̶\nn1@1 → n2 Heartbeat last_index=1 commit_index=0 read_seq=0\nn1@1 ⇥ n3 H̶e̶a̶r̶t̶b̶e̶a̶t̶ ̶l̶a̶s̶t̶_̶i̶n̶d̶e̶x̶=̶1̶ ̶c̶o̶m̶m̶i̶t̶_̶i̶n̶d̶e̶x̶=̶0̶ ̶r̶e̶a̶d̶_̶s̶e̶q̶=̶0̶\nn2@1 follower() ⇨ n2@1 follower(n1)\nn2@1 append 1@1 None\nn2@1 → n1 AppendResponse match_index=1\nn2@1 → n1 HeartbeatResponse match_index=1 read_seq=0\nn1@1 commit 1@1\nn1@1 apply 1@1 None\n\nstatus\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:0→2}\nn2@1 follower(n1) last=1@1 commit=0@0 applied=0\nn3@0 follower() last=0@0 commit=0@0 applied=0\n\n# Heal the partition.\nheal\n---\nn1 n2 n3 fully connected\n\n# Propose a write. This appends entry 2 to n2 at base 1, but is rejected by n3\n# which doesn't have entry 1.\nput 1 foo=bar\ndeliver\n---\nc1@1 → n1 ClientRequest id=0x01 write 0x0103666f6f03626172\nn1@1 append 2@1 put foo=bar\nn1@1 → n2 Append base=1@1 [2@1]\nn1@1 → n3 Append base=1@1 [2@1]\nn2@1 append 2@1 put foo=bar\nn2@1 → n1 AppendResponse match_index=2\nn3@0 follower() ⇨ n3@1 follower(n1)\nn3@1 → n1 AppendResponse reject_index=1\n\n# Since n3 rejected base 1, n1 sends an append with all messages, which\n# is accepted.\nstabilize\n---\nn1@1 commit 2@1\nn1@1 apply 2@1 put foo=bar\nn1@1 → c1 ClientResponse id=0x01 write 0x0102\nc1@1 put foo=bar ⇒ 2\nn1@1 → n3 Append base=0@0 [1@1 2@1]\nn3@1 append 1@1 None\nn3@1 append 2@1 put foo=bar\nn3@1 → n1 AppendResponse match_index=2\n\nlog\n---\nn1@1 term=1 last=2@1 commit=2@1 vote=Some(1)\nn1@1 entry 1@1 None\nn1@1 entry 2@1 put foo=bar\nn2@1 term=1 last=2@1 commit=0@0 vote=Some(1)\nn2@1 entry 1@1 None\nn2@1 entry 2@1 put foo=bar\nn3@1 term=1 last=2@1 commit=0@0 vote=None\nn3@1 entry 1@1 None\nn3@1 entry 2@1 put foo=bar\n"
  },
  {
    "path": "src/raft/testscripts/node/append_max_entries",
    "content": "# Large appends are limited to MAX_APPEND_ENTRIES, and each successful append\n# triggers the next append batch.\n\ncluster nodes=3 leader=1 max_append_entries=2\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Partition n3.\npartition 3\n---\nn3 ⇹ n1 n2\n\n# Make a bunch of writes.\n(put 1 a=1)\n(put 1 a=2)\n(put 1 a=3)\n(put 1 a=4)\n(put 1 a=5)\n(put 1 a=6)\n(put 1 a=7)\n(stabilize heartbeat=true)\nstatus\n---\nn1@1 leader last=8@1 commit=8@1 applied=8 progress={2:8→9 3:1→9}\nn2@1 follower(n1) last=8@1 commit=8@1 applied=8\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Heal the partition.\nheal\n---\nn1 n2 n3 fully connected\n\n# The next heartbeat triggers a probe.\nheartbeat 1\ndeliver\ndeliver\ndeliver\n---\nn1@1 → n2 Heartbeat last_index=8 commit_index=8 read_seq=0\nn1@1 → n3 Heartbeat last_index=8 commit_index=8 read_seq=0\nn2@1 → n1 HeartbeatResponse match_index=8 read_seq=0\nn3@1 → n1 HeartbeatResponse match_index=0 read_seq=0\nn1@1 → n3 Append base=7@1 []\nn3@1 → n1 AppendResponse reject_index=2\n\n# When the leader receives the probe response, it begins appending in batches of\n# max_append_entries until the follower is caught up.\nstabilize\n---\nn1@1 → n3 Append base=1@1 [2@1 3@1]\nn3@1 append 2@1 put a=1\nn3@1 append 3@1 put a=2\nn3@1 → n1 AppendResponse match_index=3\nn1@1 → n3 Append base=3@1 [4@1 5@1]\nn3@1 append 4@1 put a=3\nn3@1 append 5@1 put a=4\nn3@1 → n1 AppendResponse match_index=5\nn1@1 → n3 Append base=5@1 [6@1 7@1]\nn3@1 append 6@1 put a=5\nn3@1 append 7@1 put a=6\nn3@1 → n1 AppendResponse match_index=7\nn1@1 → n3 Append base=7@1 [8@1]\nn3@1 append 8@1 put a=7\nn3@1 → n1 AppendResponse match_index=8\n"
  },
  {
    "path": "src/raft/testscripts/node/append_pipeline",
    "content": "# Multiple appends are pipelined before acks are received, without\n# retransmitting the unacked entries.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Propose a single write. The progress next index increases to 3.\nput 1 a=1\n---\nc1@1 → n1 ClientRequest id=0x01 write 0x0101610131\nn1@1 append 2@1 put a=1\nn1@1 → n2 Append base=1@1 [2@1]\nn1@1 → n3 Append base=1@1 [2@1]\n\nstatus\n---\nn1@1 leader last=2@1 commit=1@1 applied=1 progress={2:1→3 3:1→3}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Propose two more writes. Appends are sent without past duplicates.\nput 1 b=2\nput 1 c=3\n---\nc1@1 → n1 ClientRequest id=0x02 write 0x0101620132\nn1@1 append 3@1 put b=2\nn1@1 → n2 Append base=2@1 [3@1]\nn1@1 → n3 Append base=2@1 [3@1]\nc1@1 → n1 ClientRequest id=0x03 write 0x0101630133\nn1@1 append 4@1 put c=3\nn1@1 → n2 Append base=3@1 [4@1]\nn1@1 → n3 Append base=3@1 [4@1]\n\nstatus\n---\nn1@1 leader last=4@1 commit=1@1 applied=1 progress={2:1→5 3:1→5}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# The appends are received and acked sequentially.\ndeliver\n---\nn2@1 append 2@1 put a=1\nn2@1 → n1 AppendResponse match_index=2\nn2@1 append 3@1 put b=2\nn2@1 → n1 AppendResponse match_index=3\nn2@1 append 4@1 put c=3\nn2@1 → n1 AppendResponse match_index=4\nn3@1 append 2@1 put a=1\nn3@1 → n1 AppendResponse match_index=2\nn3@1 append 3@1 put b=2\nn3@1 → n1 AppendResponse match_index=3\nn3@1 append 4@1 put c=3\nn3@1 → n1 AppendResponse match_index=4\n\n# The leader receives the acks and commits the writes one by one,\n# without retransmitting the in-flight (to it) entries.\ndeliver\n---\nn1@1 commit 2@1\nn1@1 apply 2@1 put a=1\nn1@1 → c1 ClientResponse id=0x01 write 0x0102\nc1@1 put a=1 ⇒ 2\nn1@1 commit 3@1\nn1@1 apply 3@1 put b=2\nn1@1 → c1 ClientResponse id=0x02 write 0x0103\nc1@1 put b=2 ⇒ 3\nn1@1 commit 4@1\nn1@1 apply 4@1 put c=3\nn1@1 → c1 ClientResponse id=0x03 write 0x0104\nc1@1 put c=3 ⇒ 4\n\n# All nodes are now caught up on logs (but not commit/apply, which needs a\n# heartbeat).\nstatus\n---\nn1@1 leader last=4@1 commit=4@1 applied=4 progress={2:4→5 3:4→5}\nn2@1 follower(n1) last=4@1 commit=1@1 applied=1\nn3@1 follower(n1) last=4@1 commit=1@1 applied=1\n"
  },
  {
    "path": "src/raft/testscripts/node/append_probe_divergent_first",
    "content": "# Appends to a previous leader and follower with a divergent tail all\n# the way back to the first entry works.\n\ncluster nodes=5 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2 4:1→2 5:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\nn4@1 follower(n1) last=1@1 commit=1@1 applied=1\nn5@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Partition n1-n2\npartition 1 2\n---\nn1 n2 ⇹ n3 n4 n5\n\n# Elect new leaders in the majority partition and replicate a few writes.\n# Multiple leaders ensures the log has multiple terms.\n(campaign 3)\n(stabilize)\n(put 3 a=1)\n(stabilize heartbeat=true)\nstatus\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2 4:1→2 5:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@2 leader last=3@2 commit=3@2 applied=3 progress={1:0→4 2:0→4 4:3→4 5:3→4}\nn4@2 follower(n3) last=3@2 commit=3@2 applied=3\nn5@2 follower(n3) last=3@2 commit=3@2 applied=3\n\n(campaign 4)\n(stabilize)\n(put 4 b=2)\n(stabilize heartbeat=true)\nstatus\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2 4:1→2 5:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@3 follower(n4) last=5@3 commit=5@3 applied=5\nn4@3 leader last=5@3 commit=5@3 applied=5 progress={1:0→6 2:0→6 3:5→6 5:5→6}\nn5@3 follower(n4) last=5@3 commit=5@3 applied=5\n\n(campaign 5)\n(stabilize)\n(put 5 c=3)\n(stabilize heartbeat=true)\nstatus\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2 4:1→2 5:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@4 follower(n5) last=7@4 commit=7@4 applied=7\nn4@4 follower(n5) last=7@4 commit=7@4 applied=7\nn5@4 leader last=7@4 commit=7@4 applied=7 progress={1:0→8 2:0→8 3:7→8 4:7→8}\n\n# Propose writes in the minority partition as well.\n(put 1 a=2)\n(put 1 a=3)\n(put 1 a=4)\n(put 1 a=5)\n(put 1 a=6)\n(put 1 a=7)\n(stabilize)\nstatus\n---\nn1@1 leader last=7@1 commit=1@1 applied=1 progress={2:7→8 3:1→8 4:1→8 5:1→8}\nn2@1 follower(n1) last=7@1 commit=1@1 applied=1\nn3@4 follower(n5) last=7@4 commit=7@4 applied=7\nn4@4 follower(n5) last=7@4 commit=7@4 applied=7\nn5@4 leader last=7@4 commit=7@4 applied=7 progress={1:0→8 2:0→8 3:7→8 4:7→8}\n\nlog 1 5\n---\nn1@1 term=1 last=7@1 commit=1@1 vote=Some(1)\nn1@1 entry 1@1 None\nn1@1 entry 2@1 put a=2\nn1@1 entry 3@1 put a=3\nn1@1 entry 4@1 put a=4\nn1@1 entry 5@1 put a=5\nn1@1 entry 6@1 put a=6\nn1@1 entry 7@1 put a=7\nn5@4 term=4 last=7@4 commit=7@4 vote=Some(5)\nn5@4 entry 1@1 None\nn5@4 entry 2@2 None\nn5@4 entry 3@2 put a=1\nn5@4 entry 4@3 None\nn5@4 entry 5@3 put b=2\nn5@4 entry 6@4 None\nn5@4 entry 7@4 put c=3\n\n# Heal the partition.\nheal\n---\nn1 n2 n3 n4 n5 fully connected\n\n# Propose another write on the majority leader.\nput 5 d=4\n---\nc5@4 → n5 ClientRequest id=0x0a write 0x0101640134\nn5@4 append 8@4 put d=4\nn5@4 → n1 Append base=7@4 [8@4]\nn5@4 → n2 Append base=7@4 [8@4]\nn5@4 → n3 Append base=7@4 [8@4]\nn5@4 → n4 Append base=7@4 [8@4]\n\n# Delivering the appends to n1 and n2 should reject them. It also cancels the\n# in-flight write requests on n1.\ndeliver 1 2\n---\nn1@1 leader ⇨ n1@4 follower(n5)\nn1@1 → c1 ClientResponse id=0x04 Error::Abort\nc1@1 put a=2 ⇒ Error::Abort (operation aborted)\nn1@1 → c1 ClientResponse id=0x05 Error::Abort\nc1@1 put a=3 ⇒ Error::Abort (operation aborted)\nn1@1 → c1 ClientResponse id=0x06 Error::Abort\nc1@1 put a=4 ⇒ Error::Abort (operation aborted)\nn1@1 → c1 ClientResponse id=0x07 Error::Abort\nc1@1 put a=5 ⇒ Error::Abort (operation aborted)\nn1@1 → c1 ClientResponse id=0x08 Error::Abort\nc1@1 put a=6 ⇒ Error::Abort (operation aborted)\nn1@1 → c1 ClientResponse id=0x09 Error::Abort\nc1@1 put a=7 ⇒ Error::Abort (operation aborted)\nn1@4 → n5 AppendResponse reject_index=7\nn2@1 follower(n1) ⇨ n2@4 follower(n5)\nn2@4 → n5 AppendResponse reject_index=7\n\n# n5 will probe the previous base, which is again rejected. This repeats until\n# a common base is found at 1@1.\ndeliver 5\nstatus 5\ndeliver 1 2\n---\nn5@4 → n1 Append base=6@4 []\nn5@4 → n2 Append base=6@4 []\nn5@4 leader last=8@4 commit=7@4 applied=7 progress={1:0→7 2:0→7 3:7→9 4:7→9}\nn1@4 → n5 AppendResponse reject_index=6\nn2@4 → n5 AppendResponse reject_index=6\n\ndeliver 5\ndeliver 1 2\nstatus 5\n---\nn5@4 → n1 Append base=5@3 []\nn5@4 → n2 Append base=5@3 []\nn1@4 → n5 AppendResponse reject_index=5\nn2@4 → n5 AppendResponse reject_index=5\nn5@4 leader last=8@4 commit=7@4 applied=7 progress={1:0→6 2:0→6 3:7→9 4:7→9}\n\ndeliver 5\nstatus 5\ndeliver 1 2\n---\nn5@4 → n1 Append base=4@3 []\nn5@4 → n2 Append base=4@3 []\nn5@4 leader last=8@4 commit=7@4 applied=7 progress={1:0→5 2:0→5 3:7→9 4:7→9}\nn1@4 → n5 AppendResponse reject_index=4\nn2@4 → n5 AppendResponse reject_index=4\n\ndeliver 5\nstatus 5\ndeliver 1 2\n---\nn5@4 → n1 Append base=3@2 []\nn5@4 → n2 Append base=3@2 []\nn5@4 leader last=8@4 commit=7@4 applied=7 progress={1:0→4 2:0→4 3:7→9 4:7→9}\nn1@4 → n5 AppendResponse reject_index=3\nn2@4 → n5 AppendResponse reject_index=3\n\ndeliver 5\nstatus 5\ndeliver 1 2\n---\nn5@4 → n1 Append base=2@2 []\nn5@4 → n2 Append base=2@2 []\nn5@4 leader last=8@4 commit=7@4 applied=7 progress={1:0→3 2:0→3 3:7→9 4:7→9}\nn1@4 → n5 AppendResponse reject_index=2\nn2@4 → n5 AppendResponse reject_index=2\n\ndeliver 5\nstatus 5\ndeliver 1 2\n---\nn5@4 → n1 Append base=1@1 []\nn5@4 → n2 Append base=1@1 []\nn5@4 leader last=8@4 commit=7@4 applied=7 progress={1:0→2 2:0→2 3:7→9 4:7→9}\nn1@4 → n5 AppendResponse match_index=1\nn2@4 → n5 AppendResponse match_index=1\n\n# n5 can now replicate the tail to n1 and n2, allowing n5 to commit it.\ndeliver 5\nstatus 5\ndeliver 1 2\n---\nn5@4 → n1 Append base=1@1 [2@2 3@2 4@3 5@3 6@4 7@4 8@4]\nn5@4 → n2 Append base=1@1 [2@2 3@2 4@3 5@3 6@4 7@4 8@4]\nn5@4 leader last=8@4 commit=7@4 applied=7 progress={1:1→9 2:1→9 3:7→9 4:7→9}\nn1@4 append 2@2 None\nn1@4 append 3@2 put a=1\nn1@4 append 4@3 None\nn1@4 append 5@3 put b=2\nn1@4 append 6@4 None\nn1@4 append 7@4 put c=3\nn1@4 append 8@4 put d=4\nn1@4 → n5 AppendResponse match_index=8\nn2@4 append 2@2 None\nn2@4 append 3@2 put a=1\nn2@4 append 4@3 None\nn2@4 append 5@3 put b=2\nn2@4 append 6@4 None\nn2@4 append 7@4 put c=3\nn2@4 append 8@4 put d=4\nn2@4 → n5 AppendResponse match_index=8\n\ndeliver 5\n---\nn5@4 commit 8@4\nn5@4 apply 8@4 put d=4\nn5@4 → c5 ClientResponse id=0x0a write 0x0108\nc5@4 put d=4 ⇒ 8\n\nstatus\n---\nn1@4 follower(n5) last=8@4 commit=1@1 applied=1\nn2@4 follower(n5) last=8@4 commit=1@1 applied=1\nn3@4 follower(n5) last=7@4 commit=7@4 applied=7\nn4@4 follower(n5) last=7@4 commit=7@4 applied=7\nn5@4 leader last=8@4 commit=8@4 applied=8 progress={1:8→9 2:8→9 3:7→9 4:7→9}\n\n# Stabilize the cluster.\n(stabilize heartbeat=true)\nstatus\n---\nn1@4 follower(n5) last=8@4 commit=8@4 applied=8\nn2@4 follower(n5) last=8@4 commit=8@4 applied=8\nn3@4 follower(n5) last=8@4 commit=8@4 applied=8\nn4@4 follower(n5) last=8@4 commit=8@4 applied=8\nn5@4 leader last=8@4 commit=8@4 applied=8 progress={1:8→9 2:8→9 3:8→9 4:8→9}\n"
  },
  {
    "path": "src/raft/testscripts/node/append_probe_divergent_long",
    "content": "# Appends to a previous leader and follower with a long divergent tail requires\n# the leader to repeatedly probe until it finds a common base.\n\ncluster nodes=5 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2 4:1→2 5:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\nn4@1 follower(n1) last=1@1 commit=1@1 applied=1\nn5@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Make a couple of writes to ensure a common log prefix.\n(put 1 a=1)\n(put 1 b=2)\n(stabilize)\nstatus\n---\nn1@1 leader last=3@1 commit=3@1 applied=3 progress={2:3→4 3:3→4 4:3→4 5:3→4}\nn2@1 follower(n1) last=3@1 commit=1@1 applied=1\nn3@1 follower(n1) last=3@1 commit=1@1 applied=1\nn4@1 follower(n1) last=3@1 commit=1@1 applied=1\nn5@1 follower(n1) last=3@1 commit=1@1 applied=1\n\n# Partition n1-n2\npartition 1 2\n---\nn1 n2 ⇹ n3 n4 n5\n\n# Elect new leaders in the majority partition and replicate a few writes.\n# Multiple leaders ensures the log has multiple terms.\n(campaign 3)\n(stabilize)\n(put 3 c=3)\n(stabilize heartbeat=true)\nstatus\n---\nn1@1 leader last=3@1 commit=3@1 applied=3 progress={2:3→4 3:3→4 4:3→4 5:3→4}\nn2@1 follower(n1) last=3@1 commit=1@1 applied=1\nn3@2 leader last=5@2 commit=5@2 applied=5 progress={1:0→6 2:0→6 4:5→6 5:5→6}\nn4@2 follower(n3) last=5@2 commit=5@2 applied=5\nn5@2 follower(n3) last=5@2 commit=5@2 applied=5\n\n(campaign 4)\n(stabilize)\n(put 4 d=4)\n(stabilize heartbeat=true)\nstatus\n---\nn1@1 leader last=3@1 commit=3@1 applied=3 progress={2:3→4 3:3→4 4:3→4 5:3→4}\nn2@1 follower(n1) last=3@1 commit=1@1 applied=1\nn3@3 follower(n4) last=7@3 commit=7@3 applied=7\nn4@3 leader last=7@3 commit=7@3 applied=7 progress={1:0→8 2:0→8 3:7→8 5:7→8}\nn5@3 follower(n4) last=7@3 commit=7@3 applied=7\n\n(campaign 5)\n(stabilize)\n(put 5 e=5)\n(stabilize heartbeat=true)\nstatus\n---\nn1@1 leader last=3@1 commit=3@1 applied=3 progress={2:3→4 3:3→4 4:3→4 5:3→4}\nn2@1 follower(n1) last=3@1 commit=1@1 applied=1\nn3@4 follower(n5) last=9@4 commit=9@4 applied=9\nn4@4 follower(n5) last=9@4 commit=9@4 applied=9\nn5@4 leader last=9@4 commit=9@4 applied=9 progress={1:0→10 2:0→10 3:9→10 4:9→10}\n\n# Propose writes in the minority partition as well, to build up a log\n# longer than the majority log.\n(put 1 a=2)\n(put 1 a=3)\n(put 1 a=4)\n(put 1 a=5)\n(put 1 a=6)\n(put 1 a=7)\n(put 1 a=8)\n(put 1 a=9)\n(put 1 a=10)\n(stabilize)\nstatus\n---\nn1@1 leader last=12@1 commit=3@1 applied=3 progress={2:12→13 3:3→13 4:3→13 5:3→13}\nn2@1 follower(n1) last=12@1 commit=1@1 applied=1\nn3@4 follower(n5) last=9@4 commit=9@4 applied=9\nn4@4 follower(n5) last=9@4 commit=9@4 applied=9\nn5@4 leader last=9@4 commit=9@4 applied=9 progress={1:0→10 2:0→10 3:9→10 4:9→10}\n\nlog 1 5\n---\nn1@1 term=1 last=12@1 commit=3@1 vote=Some(1)\nn1@1 entry 1@1 None\nn1@1 entry 2@1 put a=1\nn1@1 entry 3@1 put b=2\nn1@1 entry 4@1 put a=2\nn1@1 entry 5@1 put a=3\nn1@1 entry 6@1 put a=4\nn1@1 entry 7@1 put a=5\nn1@1 entry 8@1 put a=6\nn1@1 entry 9@1 put a=7\nn1@1 entry 10@1 put a=8\nn1@1 entry 11@1 put a=9\nn1@1 entry 12@1 put a=10\nn5@4 term=4 last=9@4 commit=9@4 vote=Some(5)\nn5@4 entry 1@1 None\nn5@4 entry 2@1 put a=1\nn5@4 entry 3@1 put b=2\nn5@4 entry 4@2 None\nn5@4 entry 5@2 put c=3\nn5@4 entry 6@3 None\nn5@4 entry 7@3 put d=4\nn5@4 entry 8@4 None\nn5@4 entry 9@4 put e=5\n\n# Heal the partition.\nheal\n---\nn1 n2 n3 n4 n5 fully connected\n\n# Propose another write on the majority leader.\nput 5 f=6\n---\nc5@4 → n5 ClientRequest id=0x0f write 0x0101660136\nn5@4 append 10@4 put f=6\nn5@4 → n1 Append base=9@4 [10@4]\nn5@4 → n2 Append base=9@4 [10@4]\nn5@4 → n3 Append base=9@4 [10@4]\nn5@4 → n4 Append base=9@4 [10@4]\n\n# Delivering the appends to n1 and n2 should reject them. It also cancels the\n# in-flight write requests on n1.\ndeliver 1 2\n---\nn1@1 leader ⇨ n1@4 follower(n5)\nn1@1 → c1 ClientResponse id=0x06 Error::Abort\nc1@1 put a=2 ⇒ Error::Abort (operation aborted)\nn1@1 → c1 ClientResponse id=0x07 Error::Abort\nc1@1 put a=3 ⇒ Error::Abort (operation aborted)\nn1@1 → c1 ClientResponse id=0x08 Error::Abort\nc1@1 put a=4 ⇒ Error::Abort (operation aborted)\nn1@1 → c1 ClientResponse id=0x09 Error::Abort\nc1@1 put a=5 ⇒ Error::Abort (operation aborted)\nn1@1 → c1 ClientResponse id=0x0a Error::Abort\nc1@1 put a=6 ⇒ Error::Abort (operation aborted)\nn1@1 → c1 ClientResponse id=0x0b Error::Abort\nc1@1 put a=7 ⇒ Error::Abort (operation aborted)\nn1@1 → c1 ClientResponse id=0x0c Error::Abort\nc1@1 put a=8 ⇒ Error::Abort (operation aborted)\nn1@1 → c1 ClientResponse id=0x0d Error::Abort\nc1@1 put a=9 ⇒ Error::Abort (operation aborted)\nn1@1 → c1 ClientResponse id=0x0e Error::Abort\nc1@1 put a=10 ⇒ Error::Abort (operation aborted)\nn1@4 → n5 AppendResponse reject_index=9\nn2@1 follower(n1) ⇨ n2@4 follower(n5)\nn2@4 → n5 AppendResponse reject_index=9\n\n# n5 will probe the previous base, which is again rejected. This repeats until\n# a common base is found at 3@1.\ndeliver 5\nstatus 5\ndeliver 1 2\n---\nn5@4 → n1 Append base=8@4 []\nn5@4 → n2 Append base=8@4 []\nn5@4 leader last=10@4 commit=9@4 applied=9 progress={1:0→9 2:0→9 3:9→11 4:9→11}\nn1@4 → n5 AppendResponse reject_index=8\nn2@4 → n5 AppendResponse reject_index=8\n\ndeliver 5\ndeliver 1 2\nstatus 5\n---\nn5@4 → n1 Append base=7@3 []\nn5@4 → n2 Append base=7@3 []\nn1@4 → n5 AppendResponse reject_index=7\nn2@4 → n5 AppendResponse reject_index=7\nn5@4 leader last=10@4 commit=9@4 applied=9 progress={1:0→8 2:0→8 3:9→11 4:9→11}\n\ndeliver 5\nstatus 5\ndeliver 1 2\n---\nn5@4 → n1 Append base=6@3 []\nn5@4 → n2 Append base=6@3 []\nn5@4 leader last=10@4 commit=9@4 applied=9 progress={1:0→7 2:0→7 3:9→11 4:9→11}\nn1@4 → n5 AppendResponse reject_index=6\nn2@4 → n5 AppendResponse reject_index=6\n\ndeliver 5\nstatus 5\ndeliver 1 2\n---\nn5@4 → n1 Append base=5@2 []\nn5@4 → n2 Append base=5@2 []\nn5@4 leader last=10@4 commit=9@4 applied=9 progress={1:0→6 2:0→6 3:9→11 4:9→11}\nn1@4 → n5 AppendResponse reject_index=5\nn2@4 → n5 AppendResponse reject_index=5\n\ndeliver 5\nstatus 5\ndeliver 1 2\n---\nn5@4 → n1 Append base=4@2 []\nn5@4 → n2 Append base=4@2 []\nn5@4 leader last=10@4 commit=9@4 applied=9 progress={1:0→5 2:0→5 3:9→11 4:9→11}\nn1@4 → n5 AppendResponse reject_index=4\nn2@4 → n5 AppendResponse reject_index=4\n\ndeliver 5\nstatus 5\ndeliver 1 2\n---\nn5@4 → n1 Append base=3@1 []\nn5@4 → n2 Append base=3@1 []\nn5@4 leader last=10@4 commit=9@4 applied=9 progress={1:0→4 2:0→4 3:9→11 4:9→11}\nn1@4 → n5 AppendResponse match_index=3\nn2@4 → n5 AppendResponse match_index=3\n\n# n5 can now replicate the tail to n1 and n2, allowing n5 to commit it.\ndeliver 5\nstatus 5\ndeliver 1 2\n---\nn5@4 → n1 Append base=3@1 [4@2 5@2 6@3 7@3 8@4 9@4 10@4]\nn5@4 → n2 Append base=3@1 [4@2 5@2 6@3 7@3 8@4 9@4 10@4]\nn5@4 leader last=10@4 commit=9@4 applied=9 progress={1:3→11 2:3→11 3:9→11 4:9→11}\nn1@4 append 4@2 None\nn1@4 append 5@2 put c=3\nn1@4 append 6@3 None\nn1@4 append 7@3 put d=4\nn1@4 append 8@4 None\nn1@4 append 9@4 put e=5\nn1@4 append 10@4 put f=6\nn1@4 → n5 AppendResponse match_index=10\nn2@4 append 4@2 None\nn2@4 append 5@2 put c=3\nn2@4 append 6@3 None\nn2@4 append 7@3 put d=4\nn2@4 append 8@4 None\nn2@4 append 9@4 put e=5\nn2@4 append 10@4 put f=6\nn2@4 → n5 AppendResponse match_index=10\n\ndeliver 5\n---\nn5@4 commit 10@4\nn5@4 apply 10@4 put f=6\nn5@4 → c5 ClientResponse id=0x0f write 0x010a\nc5@4 put f=6 ⇒ 10\n\nstatus\n---\nn1@4 follower(n5) last=10@4 commit=3@1 applied=3\nn2@4 follower(n5) last=10@4 commit=1@1 applied=1\nn3@4 follower(n5) last=9@4 commit=9@4 applied=9\nn4@4 follower(n5) last=9@4 commit=9@4 applied=9\nn5@4 leader last=10@4 commit=10@4 applied=10 progress={1:10→11 2:10→11 3:9→11 4:9→11}\n\n# Stabilize the cluster.\n(stabilize heartbeat=true)\nstatus\n---\nn1@4 follower(n5) last=10@4 commit=10@4 applied=10\nn2@4 follower(n5) last=10@4 commit=10@4 applied=10\nn3@4 follower(n5) last=10@4 commit=10@4 applied=10\nn4@4 follower(n5) last=10@4 commit=10@4 applied=10\nn5@4 leader last=10@4 commit=10@4 applied=10 progress={1:10→11 2:10→11 3:10→11 4:10→11}\n"
  },
  {
    "path": "src/raft/testscripts/node/append_probe_divergent_short",
    "content": "# Appends to a previous leader and follower with a shorter divergent tail skips\n# the missing entries before probing.\n\ncluster nodes=5 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2 4:1→2 5:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\nn4@1 follower(n1) last=1@1 commit=1@1 applied=1\nn5@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Make a couple of writes to ensure a common log prefix.\n(put 1 a=1)\n(put 1 b=2)\n(stabilize)\nstatus\n---\nn1@1 leader last=3@1 commit=3@1 applied=3 progress={2:3→4 3:3→4 4:3→4 5:3→4}\nn2@1 follower(n1) last=3@1 commit=1@1 applied=1\nn3@1 follower(n1) last=3@1 commit=1@1 applied=1\nn4@1 follower(n1) last=3@1 commit=1@1 applied=1\nn5@1 follower(n1) last=3@1 commit=1@1 applied=1\n\n# Partition n1-n2\npartition 1 2\n---\nn1 n2 ⇹ n3 n4 n5\n\n# Elect new leaders in the majority partition and replicate a few writes.\n# Multiple leaders ensures the log has multiple terms.\n(campaign 3)\n(stabilize)\n(put 3 c=3)\n(stabilize heartbeat=true)\nstatus\n---\nn1@1 leader last=3@1 commit=3@1 applied=3 progress={2:3→4 3:3→4 4:3→4 5:3→4}\nn2@1 follower(n1) last=3@1 commit=1@1 applied=1\nn3@2 leader last=5@2 commit=5@2 applied=5 progress={1:0→6 2:0→6 4:5→6 5:5→6}\nn4@2 follower(n3) last=5@2 commit=5@2 applied=5\nn5@2 follower(n3) last=5@2 commit=5@2 applied=5\n\n(campaign 4)\n(stabilize)\n(put 4 d=4)\n(stabilize heartbeat=true)\nstatus\n---\nn1@1 leader last=3@1 commit=3@1 applied=3 progress={2:3→4 3:3→4 4:3→4 5:3→4}\nn2@1 follower(n1) last=3@1 commit=1@1 applied=1\nn3@3 follower(n4) last=7@3 commit=7@3 applied=7\nn4@3 leader last=7@3 commit=7@3 applied=7 progress={1:0→8 2:0→8 3:7→8 5:7→8}\nn5@3 follower(n4) last=7@3 commit=7@3 applied=7\n\n(campaign 5)\n(stabilize)\n(put 5 e=5)\n(stabilize heartbeat=true)\nstatus\n---\nn1@1 leader last=3@1 commit=3@1 applied=3 progress={2:3→4 3:3→4 4:3→4 5:3→4}\nn2@1 follower(n1) last=3@1 commit=1@1 applied=1\nn3@4 follower(n5) last=9@4 commit=9@4 applied=9\nn4@4 follower(n5) last=9@4 commit=9@4 applied=9\nn5@4 leader last=9@4 commit=9@4 applied=9 progress={1:0→10 2:0→10 3:9→10 4:9→10}\n\n# Propose a single write in the minority partition. The divergent minority log\n# is much shorter than the majority log.\n(put 1 a=2)\n(stabilize)\nstatus\n---\nn1@1 leader last=4@1 commit=3@1 applied=3 progress={2:4→5 3:3→5 4:3→5 5:3→5}\nn2@1 follower(n1) last=4@1 commit=1@1 applied=1\nn3@4 follower(n5) last=9@4 commit=9@4 applied=9\nn4@4 follower(n5) last=9@4 commit=9@4 applied=9\nn5@4 leader last=9@4 commit=9@4 applied=9 progress={1:0→10 2:0→10 3:9→10 4:9→10}\n\nlog 1 5\n---\nn1@1 term=1 last=4@1 commit=3@1 vote=Some(1)\nn1@1 entry 1@1 None\nn1@1 entry 2@1 put a=1\nn1@1 entry 3@1 put b=2\nn1@1 entry 4@1 put a=2\nn5@4 term=4 last=9@4 commit=9@4 vote=Some(5)\nn5@4 entry 1@1 None\nn5@4 entry 2@1 put a=1\nn5@4 entry 3@1 put b=2\nn5@4 entry 4@2 None\nn5@4 entry 5@2 put c=3\nn5@4 entry 6@3 None\nn5@4 entry 7@3 put d=4\nn5@4 entry 8@4 None\nn5@4 entry 9@4 put e=5\n\n# Heal the partition.\nheal\n---\nn1 n2 n3 n4 n5 fully connected\n\n# Propose another write on the majority leader.\nput 5 f=6\n---\nc5@4 → n5 ClientRequest id=0x07 write 0x0101660136\nn5@4 append 10@4 put f=6\nn5@4 → n1 Append base=9@4 [10@4]\nn5@4 → n2 Append base=9@4 [10@4]\nn5@4 → n3 Append base=9@4 [10@4]\nn5@4 → n4 Append base=9@4 [10@4]\n\n# Delivering the appends to n1 and n2 should reject them, but with a\n# reject_index=5 after their last index instead of the original base 9. It also\n# cancels the in-flight write requests on n1.\ndeliver 1 2\n---\nn1@1 leader ⇨ n1@4 follower(n5)\nn1@1 → c1 ClientResponse id=0x06 Error::Abort\nc1@1 put a=2 ⇒ Error::Abort (operation aborted)\nn1@4 → n5 AppendResponse reject_index=5\nn2@1 follower(n1) ⇨ n2@4 follower(n5)\nn2@4 → n5 AppendResponse reject_index=5\n\n# n5 will probe the previous base, which is again rejected. This repeats until\n# a common base is found at 3@1.\ndeliver 5\nstatus 5\ndeliver 1 2\n---\nn5@4 → n1 Append base=4@2 []\nn5@4 → n2 Append base=4@2 []\nn5@4 leader last=10@4 commit=9@4 applied=9 progress={1:0→5 2:0→5 3:9→11 4:9→11}\nn1@4 → n5 AppendResponse reject_index=4\nn2@4 → n5 AppendResponse reject_index=4\n\ndeliver 5\ndeliver 1 2\nstatus 5\n---\nn5@4 → n1 Append base=3@1 []\nn5@4 → n2 Append base=3@1 []\nn1@4 → n5 AppendResponse match_index=3\nn2@4 → n5 AppendResponse match_index=3\nn5@4 leader last=10@4 commit=9@4 applied=9 progress={1:0→4 2:0→4 3:9→11 4:9→11}\n\n# n5 can now replicate the tail to n1 and n2, allowing n5 to commit it.\ndeliver 5\ndeliver 1 2\n---\nn5@4 → n1 Append base=3@1 [4@2 5@2 6@3 7@3 8@4 9@4 10@4]\nn5@4 → n2 Append base=3@1 [4@2 5@2 6@3 7@3 8@4 9@4 10@4]\nn1@4 append 4@2 None\nn1@4 append 5@2 put c=3\nn1@4 append 6@3 None\nn1@4 append 7@3 put d=4\nn1@4 append 8@4 None\nn1@4 append 9@4 put e=5\nn1@4 append 10@4 put f=6\nn1@4 → n5 AppendResponse match_index=10\nn2@4 append 4@2 None\nn2@4 append 5@2 put c=3\nn2@4 append 6@3 None\nn2@4 append 7@3 put d=4\nn2@4 append 8@4 None\nn2@4 append 9@4 put e=5\nn2@4 append 10@4 put f=6\nn2@4 → n5 AppendResponse match_index=10\n\ndeliver 5\n---\nn5@4 commit 10@4\nn5@4 apply 10@4 put f=6\nn5@4 → c5 ClientResponse id=0x07 write 0x010a\nc5@4 put f=6 ⇒ 10\n\nstatus\n---\nn1@4 follower(n5) last=10@4 commit=3@1 applied=3\nn2@4 follower(n5) last=10@4 commit=1@1 applied=1\nn3@4 follower(n5) last=9@4 commit=9@4 applied=9\nn4@4 follower(n5) last=9@4 commit=9@4 applied=9\nn5@4 leader last=10@4 commit=10@4 applied=10 progress={1:10→11 2:10→11 3:9→11 4:9→11}\n\n# Stabilize the cluster.\n(stabilize heartbeat=true)\nstatus\n---\nn1@4 follower(n5) last=10@4 commit=10@4 applied=10\nn2@4 follower(n5) last=10@4 commit=10@4 applied=10\nn3@4 follower(n5) last=10@4 commit=10@4 applied=10\nn4@4 follower(n5) last=10@4 commit=10@4 applied=10\nn5@4 leader last=10@4 commit=10@4 applied=10 progress={1:10→11 2:10→11 3:10→11 4:10→11}\n"
  },
  {
    "path": "src/raft/testscripts/node/append_probe_divergent_single",
    "content": "# An append replaces a conflict at the tail for a single term.\n\ncluster nodes=5 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2 4:1→2 5:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\nn4@1 follower(n1) last=1@1 commit=1@1 applied=1\nn5@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Partition n3-n5.\npartition 3 4 5\n---\nn1 n2 ⇹ n3 n4 n5\n\n# Propose and replicate a write in the minority partition.\nput 1 a=1\nstabilize\n---\nc1@1 → n1 ClientRequest id=0x01 write 0x0101610131\nn1@1 append 2@1 put a=1\nn1@1 → n2 Append base=1@1 [2@1]\nn1@1 ⇥ n3 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶1̶@̶1̶ ̶[̶2̶@̶1̶]̶\nn1@1 ⇥ n4 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶1̶@̶1̶ ̶[̶2̶@̶1̶]̶\nn1@1 ⇥ n5 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶1̶@̶1̶ ̶[̶2̶@̶1̶]̶\nn2@1 append 2@1 put a=1\nn2@1 → n1 AppendResponse match_index=2\n\nlog 1 2\n---\nn1@1 term=1 last=2@1 commit=1@1 vote=Some(1)\nn1@1 entry 1@1 None\nn1@1 entry 2@1 put a=1\nn2@1 term=1 last=2@1 commit=1@1 vote=Some(1)\nn2@1 entry 1@1 None\nn2@1 entry 2@1 put a=1\n\n# Elect n5 as a new majority partition leader. It appends an empty entry.\n(campaign 5)\n(stabilize heartbeat=true)\nstatus\n---\nn1@1 leader last=2@1 commit=1@1 applied=1 progress={2:2→3 3:1→3 4:1→3 5:1→3}\nn2@1 follower(n1) last=2@1 commit=1@1 applied=1\nn3@2 follower(n5) last=2@2 commit=2@2 applied=2\nn4@2 follower(n5) last=2@2 commit=2@2 applied=2\nn5@2 leader last=2@2 commit=2@2 applied=2 progress={1:0→3 2:0→3 3:2→3 4:2→3}\n\n# Heal the partition and propose a new write.\nheal\nput 5 b=2\n---\nn1 n2 n3 n4 n5 fully connected\nc5@2 → n5 ClientRequest id=0x02 write 0x0101620132\nn5@2 append 3@2 put b=2\nn5@2 → n1 Append base=2@2 [3@2]\nn5@2 → n2 Append base=2@2 [3@2]\nn5@2 → n3 Append base=2@2 [3@2]\nn5@2 → n4 Append base=2@2 [3@2]\n\n# Delivering the append messages to n1,n2 will make them follow n5 and\n# reject the appends due to a log mismatch.\ndeliver 1 2\n---\nn1@1 leader ⇨ n1@2 follower(n5)\nn1@1 → c1 ClientResponse id=0x01 Error::Abort\nc1@1 put a=1 ⇒ Error::Abort (operation aborted)\nn1@2 → n5 AppendResponse reject_index=2\nn2@1 follower(n1) ⇨ n2@2 follower(n5)\nn2@2 → n5 AppendResponse reject_index=2\n\n# n5 probes index 1, which succeeds. 1 and 2 still has the old logs.\ndeliver 5\ndeliver 1 2\n---\nn5@2 → n1 Append base=1@1 []\nn5@2 → n2 Append base=1@1 []\nn1@2 → n5 AppendResponse match_index=1\nn2@2 → n5 AppendResponse match_index=1\n\nlog 1 2\n---\nn1@2 term=2 last=2@1 commit=1@1 vote=None\nn1@2 entry 1@1 None\nn1@2 entry 2@1 put a=1\nn2@2 term=2 last=2@1 commit=1@1 vote=None\nn2@2 entry 1@1 None\nn2@2 entry 2@1 put a=1\n\n# n5 now replicates the tail of its log, which replaces the old logs.\ndeliver 5\ndeliver 1 2\n---\nn5@2 → n1 Append base=1@1 [2@2 3@2]\nn5@2 → n2 Append base=1@1 [2@2 3@2]\nn1@2 append 2@2 None\nn1@2 append 3@2 put b=2\nn1@2 → n5 AppendResponse match_index=3\nn2@2 append 2@2 None\nn2@2 append 3@2 put b=2\nn2@2 → n5 AppendResponse match_index=3\n\nlog 1 2\n---\nn1@2 term=2 last=3@2 commit=1@1 vote=None\nn1@2 entry 1@1 None\nn1@2 entry 2@2 None\nn1@2 entry 3@2 put b=2\nn2@2 term=2 last=3@2 commit=1@1 vote=None\nn2@2 entry 1@1 None\nn2@2 entry 2@2 None\nn2@2 entry 3@2 put b=2\n\n# Stabilize the cluster.\n(stabilize heartbeat=true)\nstatus\n---\nn1@2 follower(n5) last=3@2 commit=3@2 applied=3\nn2@2 follower(n5) last=3@2 commit=3@2 applied=3\nn3@2 follower(n5) last=3@2 commit=3@2 applied=3\nn4@2 follower(n5) last=3@2 commit=3@2 applied=3\nn5@2 leader last=3@2 commit=3@2 applied=3 progress={1:3→4 2:3→4 3:3→4 4:3→4}\n"
  },
  {
    "path": "src/raft/testscripts/node/append_response_beyond_last_index_panics",
    "content": "# A successful AppendResponse with last index beyond leader's last log\n# should panic.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Propose a write.\nput 1 foo=bar\n---\nc1@1 → n1 ClientRequest id=0x01 write 0x0103666f6f03626172\nn1@1 append 2@1 put foo=bar\nn1@1 → n2 Append base=1@1 [2@1]\nn1@1 → n3 Append base=1@1 [2@1]\n\n# An AppendResponse beyond leader's last log should panic.\n!step 1 '{\"from\":2, \"to\":1, \"term\":1, \"message\":{\"AppendResponse\":{\"match_index\":3,\"reject_index\":0}}}'\n---\nPanic: future match index\n"
  },
  {
    "path": "src/raft/testscripts/node/append_response_stale_reject",
    "content": "# A successful AppendResponse with a reject_index below the match index\n# should be ignored.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Replicate a write.\n(put 1 a=1)\n(stabilize heartbeat=true)\nstatus\n---\nn1@1 leader last=2@1 commit=2@1 applied=2 progress={2:2→3 3:2→3}\nn2@1 follower(n1) last=2@1 commit=2@1 applied=2\nn3@1 follower(n1) last=2@1 commit=2@1 applied=2\n\n# Propose a few writes.\n(put 1 b=2)\n(put 1 c=3)\nstatus\n---\nn1@1 leader last=4@1 commit=2@1 applied=2 progress={2:2→5 3:2→5}\nn2@1 follower(n1) last=2@1 commit=2@1 applied=2\nn3@1 follower(n1) last=2@1 commit=2@1 applied=2\n\n# A reject_index below the follower's progress match index is ignored.\nstep 1 '{\"from\":2,\"to\":1,\"term\":1,\"message\":{\"AppendResponse\":{\"match_index\":0,\"reject_index\":2}}}'\nstatus\n---\nn1@1 leader last=4@1 commit=2@1 applied=2 progress={2:2→5 3:2→5}\nn2@1 follower(n1) last=2@1 commit=2@1 applied=2\nn3@1 follower(n1) last=2@1 commit=2@1 applied=2\n\nstep 1 '{\"from\":2,\"to\":1,\"term\":1,\"message\":{\"AppendResponse\":{\"match_index\":0,\"reject_index\":1}}}'\nstatus\n---\nn1@1 leader last=4@1 commit=2@1 applied=2 progress={2:2→5 3:2→5}\nn2@1 follower(n1) last=2@1 commit=2@1 applied=2\nn3@1 follower(n1) last=2@1 commit=2@1 applied=2\n\n# The writes are still replicated without any probes.\nstabilize\n---\nn2@1 append 3@1 put b=2\nn2@1 → n1 AppendResponse match_index=3\nn2@1 append 4@1 put c=3\nn2@1 → n1 AppendResponse match_index=4\nn3@1 append 3@1 put b=2\nn3@1 → n1 AppendResponse match_index=3\nn3@1 append 4@1 put c=3\nn3@1 → n1 AppendResponse match_index=4\nn1@1 commit 3@1\nn1@1 apply 3@1 put b=2\nn1@1 → c1 ClientResponse id=0x02 write 0x0103\nc1@1 put b=2 ⇒ 3\nn1@1 commit 4@1\nn1@1 apply 4@1 put c=3\nn1@1 → c1 ClientResponse id=0x03 write 0x0104\nc1@1 put c=3 ⇒ 4\n"
  },
  {
    "path": "src/raft/testscripts/node/election",
    "content": "# A node campaigns and wins leadership once the election timeout passes. Uses\n# ticks directly to also test tick handling.\n\ncluster nodes=3 heartbeat_interval=1 election_timeout=2\n---\nn1@0 follower() last=0@0 commit=0@0 applied=0\nn2@0 follower() last=0@0 commit=0@0 applied=0\nn3@0 follower() last=0@0 commit=0@0 applied=0\n\n# Tick all nodes. Then tick n1 again to make it campaign.\ntick\n---\nok\n\ntick 1\n---\nn1@0 follower() ⇨ n1@1 candidate\nn1@1 → n2 Campaign last=0@0\nn1@1 → n3 Campaign last=0@0\n\n# n2,n3 grant n1 their votes.\ndeliver\n---\nn2@0 follower() ⇨ n2@1 follower()\nn2@1 → n1 CampaignResponse vote=true\nn3@0 follower() ⇨ n3@1 follower()\nn3@1 → n1 CampaignResponse vote=true\n\n# n1 wins the election and becomes leader.\ndeliver\n---\nn1@1 candidate ⇨ n1@1 leader\nn1@1 append 1@1 None\nn1@1 → n2 Append base=0@0 [1@1]\nn1@1 → n3 Append base=0@0 [1@1]\nn1@1 → n2 Heartbeat last_index=1 commit_index=0 read_seq=0\nn1@1 → n3 Heartbeat last_index=1 commit_index=0 read_seq=0\n\n# All nodes become n1 followers.\nstabilize\n---\nn2@1 follower() ⇨ n2@1 follower(n1)\nn2@1 append 1@1 None\nn2@1 → n1 AppendResponse match_index=1\nn2@1 → n1 HeartbeatResponse match_index=1 read_seq=0\nn3@1 follower() ⇨ n3@1 follower(n1)\nn3@1 append 1@1 None\nn3@1 → n1 AppendResponse match_index=1\nn3@1 → n1 HeartbeatResponse match_index=1 read_seq=0\nn1@1 commit 1@1\nn1@1 apply 1@1 None\n\n# n1's heartbeats are accepted by followers, who commit and apply the entry.\ntick 1\n---\nn1@1 → n2 Heartbeat last_index=1 commit_index=1 read_seq=0\nn1@1 → n3 Heartbeat last_index=1 commit_index=1 read_seq=0\n\nstabilize\n---\nn2@1 commit 1@1\nn2@1 apply 1@1 None\nn2@1 → n1 HeartbeatResponse match_index=1 read_seq=0\nn3@1 commit 1@1\nn3@1 apply 1@1 None\nn3@1 → n1 HeartbeatResponse match_index=1 read_seq=0\n\nstatus\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n"
  },
  {
    "path": "src/raft/testscripts/node/election_candidate_behind_leader",
    "content": "# A candidate that lags behind the leader can still win the election\n# as long as it isn't behind the quorum.\n\ncluster nodes=5 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2 4:1→2 5:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\nn4@1 follower(n1) last=1@1 commit=1@1 applied=1\nn5@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Partition n1+n2 away from the cluster.\npartition 1 2\n---\nn1 n2 ⇹ n3 n4 n5\n\n# Replica a write on n1+n2. The write can't be committed, because n1 doesn't\n# have quorum.\n(put 1 foo=bar)\n(stabilize)\nstatus\n---\nn1@1 leader last=2@1 commit=1@1 applied=1 progress={2:2→3 3:1→3 4:1→3 5:1→3}\nn2@1 follower(n1) last=2@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\nn4@1 follower(n1) last=1@1 commit=1@1 applied=1\nn5@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# List the logs on n1 n2 n3 to show the replicated but uncommitted entry.\nlog 1 2 3\n---\nn1@1 term=1 last=2@1 commit=1@1 vote=Some(1)\nn1@1 entry 1@1 None\nn1@1 entry 2@1 put foo=bar\nn2@1 term=1 last=2@1 commit=1@1 vote=Some(1)\nn2@1 entry 1@1 None\nn2@1 entry 2@1 put foo=bar\nn3@1 term=1 last=1@1 commit=1@1 vote=Some(1)\nn3@1 entry 1@1 None\n\n# Heal the partition.\nheal\n---\nn1 n2 n3 n4 n5 fully connected\n\n# Make n5 campaign. n3+n4 grant their votes, n1+n2 reject it. n1 aborts the\n# in-flight write request because the term changes.\ncampaign 5\ndeliver\n---\nn5@1 follower(n1) ⇨ n5@2 candidate\nn5@2 → n1 Campaign last=1@1\nn5@2 → n2 Campaign last=1@1\nn5@2 → n3 Campaign last=1@1\nn5@2 → n4 Campaign last=1@1\nn1@1 leader ⇨ n1@2 follower()\nn1@1 → c1 ClientResponse id=0x01 Error::Abort\nc1@1 put foo=bar ⇒ Error::Abort (operation aborted)\nn1@2 → n5 CampaignResponse vote=false\nn2@1 follower(n1) ⇨ n2@2 follower()\nn2@2 → n5 CampaignResponse vote=false\nn3@1 follower(n1) ⇨ n3@2 follower()\nn3@2 → n5 CampaignResponse vote=true\nn4@1 follower(n1) ⇨ n4@2 follower()\nn4@2 → n5 CampaignResponse vote=true\n\n# n5 wins the election and becomes leader.\nstabilize heartbeat=true\n---\nn5@2 candidate ⇨ n5@2 leader\nn5@2 append 2@2 None\nn5@2 → n1 Append base=1@1 [2@2]\nn5@2 → n2 Append base=1@1 [2@2]\nn5@2 → n3 Append base=1@1 [2@2]\nn5@2 → n4 Append base=1@1 [2@2]\nn5@2 → n1 Heartbeat last_index=2 commit_index=1 read_seq=0\nn5@2 → n2 Heartbeat last_index=2 commit_index=1 read_seq=0\nn5@2 → n3 Heartbeat last_index=2 commit_index=1 read_seq=0\nn5@2 → n4 Heartbeat last_index=2 commit_index=1 read_seq=0\nn1@2 follower() ⇨ n1@2 follower(n5)\nn1@2 append 2@2 None\nn1@2 → n5 AppendResponse match_index=2\nn1@2 → n5 HeartbeatResponse match_index=2 read_seq=0\nn2@2 follower() ⇨ n2@2 follower(n5)\nn2@2 append 2@2 None\nn2@2 → n5 AppendResponse match_index=2\nn2@2 → n5 HeartbeatResponse match_index=2 read_seq=0\nn3@2 follower() ⇨ n3@2 follower(n5)\nn3@2 append 2@2 None\nn3@2 → n5 AppendResponse match_index=2\nn3@2 → n5 HeartbeatResponse match_index=2 read_seq=0\nn4@2 follower() ⇨ n4@2 follower(n5)\nn4@2 append 2@2 None\nn4@2 → n5 AppendResponse match_index=2\nn4@2 → n5 HeartbeatResponse match_index=2 read_seq=0\nn5@2 commit 2@2\nn5@2 apply 2@2 None\nn5@2 → n1 Heartbeat last_index=2 commit_index=2 read_seq=0\nn5@2 → n2 Heartbeat last_index=2 commit_index=2 read_seq=0\nn5@2 → n3 Heartbeat last_index=2 commit_index=2 read_seq=0\nn5@2 → n4 Heartbeat last_index=2 commit_index=2 read_seq=0\nn1@2 commit 2@2\nn1@2 apply 2@2 None\nn1@2 → n5 HeartbeatResponse match_index=2 read_seq=0\nn2@2 commit 2@2\nn2@2 apply 2@2 None\nn2@2 → n5 HeartbeatResponse match_index=2 read_seq=0\nn3@2 commit 2@2\nn3@2 apply 2@2 None\nn3@2 → n5 HeartbeatResponse match_index=2 read_seq=0\nn4@2 commit 2@2\nn4@2 apply 2@2 None\nn4@2 → n5 HeartbeatResponse match_index=2 read_seq=0\n\n# n1+n2's in-flight write at log position 2 has been replaced by the\n# empty log entry appended by n5 when it became leader.\nlog 1 2\n---\nn1@2 term=2 last=2@2 commit=2@2 vote=None\nn1@2 entry 1@1 None\nn1@2 entry 2@2 None\nn2@2 term=2 last=2@2 commit=2@2 vote=None\nn2@2 entry 1@1 None\nn2@2 entry 2@2 None\n\nstatus\n---\nn1@2 follower(n5) last=2@2 commit=2@2 applied=2\nn2@2 follower(n5) last=2@2 commit=2@2 applied=2\nn3@2 follower(n5) last=2@2 commit=2@2 applied=2\nn4@2 follower(n5) last=2@2 commit=2@2 applied=2\nn5@2 leader last=2@2 commit=2@2 applied=2 progress={1:2→3 2:2→3 3:2→3 4:2→3}\n"
  },
  {
    "path": "src/raft/testscripts/node/election_candidate_behind_quorum",
    "content": "# A candidate that lags behind the quorum can't win an election.\n\ncluster nodes=5 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2 4:1→2 5:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\nn4@1 follower(n1) last=1@1 commit=1@1 applied=1\nn5@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Partition n4+n5 away from the cluster.\npartition 4 5\n---\nn4 n5 ⇹ n1 n2 n3\n\n# Replicate a write on n1. n4+n5 now lag behind the quorum. Don't yet propagate\n# the commit index to n2+n3, to make sure it won't grant the vote just because\n# n5 is caught up to their local view of the commit index.\n(put 1 foo=bar)\n(stabilize)\nstatus\n---\nn1@1 leader last=2@1 commit=2@1 applied=2 progress={2:2→3 3:2→3 4:1→3 5:1→3}\nn2@1 follower(n1) last=2@1 commit=1@1 applied=1\nn3@1 follower(n1) last=2@1 commit=1@1 applied=1\nn4@1 follower(n1) last=1@1 commit=1@1 applied=1\nn5@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Heal the partition.\nheal\n---\nn1 n2 n3 n4 n5 fully connected\n\n# Make n5 campaign. n4 grants its vote, but the others reject it because it is\n# behind the quorum. However, the term bump will convert the other nodes to\n# leaderless followers.\nheal\ncampaign 5\nstabilize\n---\nn1 n2 n3 n4 n5 fully connected\nn5@1 follower(n1) ⇨ n5@2 candidate\nn5@2 → n1 Campaign last=1@1\nn5@2 → n2 Campaign last=1@1\nn5@2 → n3 Campaign last=1@1\nn5@2 → n4 Campaign last=1@1\nn1@1 leader ⇨ n1@2 follower()\nn1@2 → n5 CampaignResponse vote=false\nn2@1 follower(n1) ⇨ n2@2 follower()\nn2@2 → n5 CampaignResponse vote=false\nn3@1 follower(n1) ⇨ n3@2 follower()\nn3@2 → n5 CampaignResponse vote=false\nn4@1 follower(n1) ⇨ n4@2 follower()\nn4@2 → n5 CampaignResponse vote=true\n\nstatus\n---\nn1@2 follower() last=2@1 commit=2@1 applied=2\nn2@2 follower() last=2@1 commit=1@1 applied=1\nn3@2 follower() last=2@1 commit=1@1 applied=1\nn4@2 follower() last=1@1 commit=1@1 applied=1\nn5@2 candidate last=1@1 commit=1@1 applied=1\n\n# n2 can campaign and win the election.\n(campaign 2)\n(stabilize heartbeat=true)\nstatus\n---\nn1@3 follower(n2) last=3@3 commit=3@3 applied=3\nn2@3 leader last=3@3 commit=3@3 applied=3 progress={1:3→4 3:3→4 4:3→4 5:3→4}\nn3@3 follower(n2) last=3@3 commit=3@3 applied=3\nn4@3 follower(n2) last=3@3 commit=3@3 applied=3\nn5@3 follower(n2) last=3@3 commit=3@3 applied=3\n"
  },
  {
    "path": "src/raft/testscripts/node/election_contested",
    "content": "# A leader can be elected even when there are multiple candidates.\n\ncluster nodes=5 election_timeout=2\n---\nn1@0 follower() last=0@0 commit=0@0 applied=0\nn2@0 follower() last=0@0 commit=0@0 applied=0\nn3@0 follower() last=0@0 commit=0@0 applied=0\nn4@0 follower() last=0@0 commit=0@0 applied=0\nn5@0 follower() last=0@0 commit=0@0 applied=0\n\n# n1 and n5 campaign.\ntick\ntick 1 5\n---\nn1@0 follower() ⇨ n1@1 candidate\nn1@1 → n2 Campaign last=0@0\nn1@1 → n3 Campaign last=0@0\nn1@1 → n4 Campaign last=0@0\nn1@1 → n5 Campaign last=0@0\nn5@0 follower() ⇨ n5@1 candidate\nn5@1 → n1 Campaign last=0@0\nn5@1 → n2 Campaign last=0@0\nn5@1 → n3 Campaign last=0@0\nn5@1 → n4 Campaign last=0@0\n\n# n1 and n5 ignore each other, since they're both campaigning.\ndeliver 1 5\n---\nn1@1 → n5 CampaignResponse vote=false\nn5@1 → n1 CampaignResponse vote=false\n\n# n1 reaches n2,n3 first, but n5 reaches n4 first.\ndeliver 2 3\ndeliver 4 from=5\ndeliver 4\n---\nn2@0 follower() ⇨ n2@1 follower()\nn2@1 → n1 CampaignResponse vote=true\nn2@1 → n5 CampaignResponse vote=false\nn3@0 follower() ⇨ n3@1 follower()\nn3@1 → n1 CampaignResponse vote=true\nn3@1 → n5 CampaignResponse vote=false\nn4@0 follower() ⇨ n4@1 follower()\nn4@1 → n5 CampaignResponse vote=true\nn4@1 → n1 CampaignResponse vote=false\n\n# n1 and n5 receive their votes. n1 has quorum and becomes leader.\ndeliver\n---\nn1@1 candidate ⇨ n1@1 leader\nn1@1 append 1@1 None\nn1@1 → n2 Append base=0@0 [1@1]\nn1@1 → n3 Append base=0@0 [1@1]\nn1@1 → n4 Append base=0@0 [1@1]\nn1@1 → n5 Append base=0@0 [1@1]\nn1@1 → n2 Heartbeat last_index=1 commit_index=0 read_seq=0\nn1@1 → n3 Heartbeat last_index=1 commit_index=0 read_seq=0\nn1@1 → n4 Heartbeat last_index=1 commit_index=0 read_seq=0\nn1@1 → n5 Heartbeat last_index=1 commit_index=0 read_seq=0\n\n# All nodes accept n1 as leader in term 1 and become followers.\nstabilize\n---\nn2@1 follower() ⇨ n2@1 follower(n1)\nn2@1 append 1@1 None\nn2@1 → n1 AppendResponse match_index=1\nn2@1 → n1 HeartbeatResponse match_index=1 read_seq=0\nn3@1 follower() ⇨ n3@1 follower(n1)\nn3@1 append 1@1 None\nn3@1 → n1 AppendResponse match_index=1\nn3@1 → n1 HeartbeatResponse match_index=1 read_seq=0\nn4@1 follower() ⇨ n4@1 follower(n1)\nn4@1 append 1@1 None\nn4@1 → n1 AppendResponse match_index=1\nn4@1 → n1 HeartbeatResponse match_index=1 read_seq=0\nn5@1 candidate ⇨ n5@1 follower(n1)\nn5@1 append 1@1 None\nn5@1 → n1 AppendResponse match_index=1\nn5@1 → n1 HeartbeatResponse match_index=1 read_seq=0\nn1@1 commit 1@1\nn1@1 apply 1@1 None\n\nstatus\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2 4:1→2 5:1→2}\nn2@1 follower(n1) last=1@1 commit=0@0 applied=0\nn3@1 follower(n1) last=1@1 commit=0@0 applied=0\nn4@1 follower(n1) last=1@1 commit=0@0 applied=0\nn5@1 follower(n1) last=1@1 commit=0@0 applied=0\n"
  },
  {
    "path": "src/raft/testscripts/node/election_tie",
    "content": "# No leader can be elected with an election tie.\n\ncluster nodes=3 election_timeout=2\n---\nn1@0 follower() last=0@0 commit=0@0 applied=0\nn2@0 follower() last=0@0 commit=0@0 applied=0\nn3@0 follower() last=0@0 commit=0@0 applied=0\n\n# Tick all nodes twice to make them all campaign.\ntick\ntick\n---\nn1@0 follower() ⇨ n1@1 candidate\nn1@1 → n2 Campaign last=0@0\nn1@1 → n3 Campaign last=0@0\nn2@0 follower() ⇨ n2@1 candidate\nn2@1 → n1 Campaign last=0@0\nn2@1 → n3 Campaign last=0@0\nn3@0 follower() ⇨ n3@1 candidate\nn3@1 → n1 Campaign last=0@0\nn3@1 → n2 Campaign last=0@0\n\n# Stabilizing the cluster will not result in a leader.\nstabilize\n---\nn1@1 → n2 CampaignResponse vote=false\nn1@1 → n3 CampaignResponse vote=false\nn2@1 → n1 CampaignResponse vote=false\nn2@1 → n3 CampaignResponse vote=false\nn3@1 → n1 CampaignResponse vote=false\nn3@1 → n2 CampaignResponse vote=false\n\nstatus\n---\nn1@1 candidate last=0@0 commit=0@0 applied=0\nn2@1 candidate last=0@0 commit=0@0 applied=0\nn3@1 candidate last=0@0 commit=0@0 applied=0\n\n# A node can call another election in a new term and win.\ntick 2\ntick 2\n---\nn2@1 candidate ⇨ n2@2 candidate\nn2@2 → n1 Campaign last=0@0\nn2@2 → n3 Campaign last=0@0\n\ndeliver\n---\nn1@1 candidate ⇨ n1@2 follower()\nn1@2 → n2 CampaignResponse vote=true\nn3@1 candidate ⇨ n3@2 follower()\nn3@2 → n2 CampaignResponse vote=true\n\ndeliver\n---\nn2@2 candidate ⇨ n2@2 leader\nn2@2 append 1@2 None\nn2@2 → n1 Append base=0@0 [1@2]\nn2@2 → n3 Append base=0@0 [1@2]\nn2@2 → n1 Heartbeat last_index=1 commit_index=0 read_seq=0\nn2@2 → n3 Heartbeat last_index=1 commit_index=0 read_seq=0\n\nstabilize\n---\nn1@2 follower() ⇨ n1@2 follower(n2)\nn1@2 append 1@2 None\nn1@2 → n2 AppendResponse match_index=1\nn1@2 → n2 HeartbeatResponse match_index=1 read_seq=0\nn3@2 follower() ⇨ n3@2 follower(n2)\nn3@2 append 1@2 None\nn3@2 → n2 AppendResponse match_index=1\nn3@2 → n2 HeartbeatResponse match_index=1 read_seq=0\nn2@2 commit 1@2\nn2@2 apply 1@2 None\n\nstatus\n---\nn1@2 follower(n2) last=1@2 commit=0@0 applied=0\nn2@2 leader last=1@2 commit=1@2 applied=1 progress={1:1→2 3:1→2}\nn3@2 follower(n2) last=1@2 commit=0@0 applied=0\n"
  },
  {
    "path": "src/raft/testscripts/node/election_tie_even",
    "content": "# No leader can be elected with an election tie between an even number of nodes.\n\ncluster nodes=4 election_timeout=2\n---\nn1@0 follower() last=0@0 commit=0@0 applied=0\nn2@0 follower() last=0@0 commit=0@0 applied=0\nn3@0 follower() last=0@0 commit=0@0 applied=0\nn4@0 follower() last=0@0 commit=0@0 applied=0\n\n# n1 and n4 campaign.\ntick\ntick 1 4\n---\nn1@0 follower() ⇨ n1@1 candidate\nn1@1 → n2 Campaign last=0@0\nn1@1 → n3 Campaign last=0@0\nn1@1 → n4 Campaign last=0@0\nn4@0 follower() ⇨ n4@1 candidate\nn4@1 → n1 Campaign last=0@0\nn4@1 → n2 Campaign last=0@0\nn4@1 → n3 Campaign last=0@0\n\n# n2 votes for n1, n3 votes for n4.\ndeliver 2\ndeliver 3 from=4\ndeliver 3\n---\nn2@0 follower() ⇨ n2@1 follower()\nn2@1 → n1 CampaignResponse vote=true\nn2@1 → n4 CampaignResponse vote=false\nn3@0 follower() ⇨ n3@1 follower()\nn3@1 → n4 CampaignResponse vote=true\nn3@1 → n1 CampaignResponse vote=false\n\n# Stabilizing the cluster will not result in a leader.\nstabilize\n---\nn1@1 → n4 CampaignResponse vote=false\nn4@1 → n1 CampaignResponse vote=false\n\nstatus\n---\nn1@1 candidate last=0@0 commit=0@0 applied=0\nn2@1 follower() last=0@0 commit=0@0 applied=0\nn3@1 follower() last=0@0 commit=0@0 applied=0\nn4@1 candidate last=0@0 commit=0@0 applied=0\n\n# A node can call another election in a new term and win.\ntick 3\ntick 3\n---\nn3@1 follower() ⇨ n3@2 candidate\nn3@2 → n1 Campaign last=0@0\nn3@2 → n2 Campaign last=0@0\nn3@2 → n4 Campaign last=0@0\n\ndeliver\n---\nn1@1 candidate ⇨ n1@2 follower()\nn1@2 → n3 CampaignResponse vote=true\nn2@1 follower() ⇨ n2@2 follower()\nn2@2 → n3 CampaignResponse vote=true\nn4@1 candidate ⇨ n4@2 follower()\nn4@2 → n3 CampaignResponse vote=true\n\ndeliver\n---\nn3@2 candidate ⇨ n3@2 leader\nn3@2 append 1@2 None\nn3@2 → n1 Append base=0@0 [1@2]\nn3@2 → n2 Append base=0@0 [1@2]\nn3@2 → n4 Append base=0@0 [1@2]\nn3@2 → n1 Heartbeat last_index=1 commit_index=0 read_seq=0\nn3@2 → n2 Heartbeat last_index=1 commit_index=0 read_seq=0\nn3@2 → n4 Heartbeat last_index=1 commit_index=0 read_seq=0\n\nstabilize\n---\nn1@2 follower() ⇨ n1@2 follower(n3)\nn1@2 append 1@2 None\nn1@2 → n3 AppendResponse match_index=1\nn1@2 → n3 HeartbeatResponse match_index=1 read_seq=0\nn2@2 follower() ⇨ n2@2 follower(n3)\nn2@2 append 1@2 None\nn2@2 → n3 AppendResponse match_index=1\nn2@2 → n3 HeartbeatResponse match_index=1 read_seq=0\nn4@2 follower() ⇨ n4@2 follower(n3)\nn4@2 append 1@2 None\nn4@2 → n3 AppendResponse match_index=1\nn4@2 → n3 HeartbeatResponse match_index=1 read_seq=0\nn3@2 commit 1@2\nn3@2 apply 1@2 None\n\nstatus\n---\nn1@2 follower(n3) last=1@2 commit=0@0 applied=0\nn2@2 follower(n3) last=1@2 commit=0@0 applied=0\nn3@2 leader last=1@2 commit=1@2 applied=1 progress={1:1→2 2:1→2 4:1→2}\nn4@2 follower(n3) last=1@2 commit=0@0 applied=0\n"
  },
  {
    "path": "src/raft/testscripts/node/heartbeat_commits_follower",
    "content": "# A heartbeat will commit and apply an entry on a follower.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Write on the leader, which replicates then commits and applies locally.\nput 1 foo=bar\nstabilize\n---\nc1@1 → n1 ClientRequest id=0x01 write 0x0103666f6f03626172\nn1@1 append 2@1 put foo=bar\nn1@1 → n2 Append base=1@1 [2@1]\nn1@1 → n3 Append base=1@1 [2@1]\nn2@1 append 2@1 put foo=bar\nn2@1 → n1 AppendResponse match_index=2\nn3@1 append 2@1 put foo=bar\nn3@1 → n1 AppendResponse match_index=2\nn1@1 commit 2@1\nn1@1 apply 2@1 put foo=bar\nn1@1 → c1 ClientResponse id=0x01 write 0x0102\nc1@1 put foo=bar ⇒ 2\n\n# The write has been replicated, but not yet committed and applied on followers.\nstatus\n---\nn1@1 leader last=2@1 commit=2@1 applied=2 progress={2:2→3 3:2→3}\nn2@1 follower(n1) last=2@1 commit=1@1 applied=1\nn3@1 follower(n1) last=2@1 commit=1@1 applied=1\n\n# A heartbeat commits and applies on followers.\nheartbeat 1\nstabilize\n---\nn1@1 → n2 Heartbeat last_index=2 commit_index=2 read_seq=0\nn1@1 → n3 Heartbeat last_index=2 commit_index=2 read_seq=0\nn2@1 commit 2@1\nn2@1 apply 2@1 put foo=bar\nn2@1 → n1 HeartbeatResponse match_index=2 read_seq=0\nn3@1 commit 2@1\nn3@1 apply 2@1 put foo=bar\nn3@1 → n1 HeartbeatResponse match_index=2 read_seq=0\n\nstatus\n---\nn1@1 leader last=2@1 commit=2@1 applied=2 progress={2:2→3 3:2→3}\nn2@1 follower(n1) last=2@1 commit=2@1 applied=2\nn3@1 follower(n1) last=2@1 commit=2@1 applied=2\n"
  },
  {
    "path": "src/raft/testscripts/node/heartbeat_converts_candidate",
    "content": "# A heartbeat from a leader should convert a candidate in the same term to a\n# follower.\n\ncluster nodes=3\n---\nn1@0 follower() last=0@0 commit=0@0 applied=0\nn2@0 follower() last=0@0 commit=0@0 applied=0\nn3@0 follower() last=0@0 commit=0@0 applied=0\n\n# Partition n3 away from the cluster.\npartition 3\n---\nn3 ⇹ n1 n2\n\n# Both n1 and n3 campaign. n2 votes for n1.\ncampaign 1 3\ndeliver\n---\nn1@0 follower() ⇨ n1@1 candidate\nn1@1 → n2 Campaign last=0@0\nn1@1 ⇥ n3 C̶a̶m̶p̶a̶i̶g̶n̶ ̶l̶a̶s̶t̶=̶0̶@̶0̶\nn3@0 follower() ⇨ n3@1 candidate\nn3@1 ⇥ n1 C̶a̶m̶p̶a̶i̶g̶n̶ ̶l̶a̶s̶t̶=̶0̶@̶0̶\nn3@1 ⇥ n2 C̶a̶m̶p̶a̶i̶g̶n̶ ̶l̶a̶s̶t̶=̶0̶@̶0̶\nn2@0 follower() ⇨ n2@1 follower()\nn2@1 → n1 CampaignResponse vote=true\n\n# n1 assumes leadership and heartbeats, committing entry 1.\n(stabilize heartbeat=true)\nstatus\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:0→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 candidate last=0@0 commit=0@0 applied=0\n\n# Heal the partition.\nheal\n---\nn1 n2 n3 fully connected\n\n# The next heartbeat from n1 converts n3 to a follower in term 1.\nheartbeat 1\nstabilize\n---\nn1@1 → n2 Heartbeat last_index=1 commit_index=1 read_seq=0\nn1@1 → n3 Heartbeat last_index=1 commit_index=1 read_seq=0\nn2@1 → n1 HeartbeatResponse match_index=1 read_seq=0\nn3@1 candidate ⇨ n3@1 follower(n1)\nn3@1 → n1 HeartbeatResponse match_index=0 read_seq=0\nn1@1 → n3 Append base=0@0 [1@1]\nn3@1 append 1@1 None\nn3@1 → n1 AppendResponse match_index=1\n\nstatus\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=0@0 applied=0\n"
  },
  {
    "path": "src/raft/testscripts/node/heartbeat_converts_follower",
    "content": "# A heartbeat from a leader should convert a follower of a different leader in a\n# past term to a follower.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Partition n2 away from the cluster.\npartition 2\n---\nn2 ⇹ n1 n3\n\n# Elect n3 as a new leader.\n(campaign 3)\n(stabilize heartbeat=true)\nstatus\n---\nn1@2 follower(n3) last=2@2 commit=2@2 applied=2\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@2 leader last=2@2 commit=2@2 applied=2 progress={1:2→3 2:0→3}\n\n# Heal the partition.\nheal\n---\nn1 n2 n3 fully connected\n\n# The next heartbeat from n3 converts n2 to a follower in term 2.\nheartbeat 3\nstabilize heartbeat=true\n---\nn3@2 → n1 Heartbeat last_index=2 commit_index=2 read_seq=0\nn3@2 → n2 Heartbeat last_index=2 commit_index=2 read_seq=0\nn1@2 → n3 HeartbeatResponse match_index=2 read_seq=0\nn2@1 follower(n1) ⇨ n2@2 follower(n3)\nn2@2 → n3 HeartbeatResponse match_index=0 read_seq=0\nn3@2 → n2 Append base=1@1 []\nn2@2 → n3 AppendResponse match_index=1\nn3@2 → n2 Append base=1@1 [2@2]\nn2@2 append 2@2 None\nn2@2 → n3 AppendResponse match_index=2\nn3@2 → n1 Heartbeat last_index=2 commit_index=2 read_seq=0\nn3@2 → n2 Heartbeat last_index=2 commit_index=2 read_seq=0\nn1@2 → n3 HeartbeatResponse match_index=2 read_seq=0\nn2@2 commit 2@2\nn2@2 apply 2@2 None\nn2@2 → n3 HeartbeatResponse match_index=2 read_seq=0\n\nstatus\n---\nn1@2 follower(n3) last=2@2 commit=2@2 applied=2\nn2@2 follower(n3) last=2@2 commit=2@2 applied=2\nn3@2 leader last=2@2 commit=2@2 applied=2 progress={1:2→3 2:2→3}\n"
  },
  {
    "path": "src/raft/testscripts/node/heartbeat_converts_follower_leaderless",
    "content": "# A heartbeat from a leader should convert a leaderless follower.\n\ncluster nodes=3\n---\nn1@0 follower() last=0@0 commit=0@0 applied=0\nn2@0 follower() last=0@0 commit=0@0 applied=0\nn3@0 follower() last=0@0 commit=0@0 applied=0\n\n# Partition n3 away from the cluster.\npartition 3\n---\nn3 ⇹ n1 n2\n\n# Elect n1 as a new leader.\n(campaign 1)\n(stabilize heartbeat=true)\nstatus\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:0→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@0 follower() last=0@0 commit=0@0 applied=0\n\n# Heal the partition.\nheal\n---\nn1 n2 n3 fully connected\n\n# The next heartbeat from n1 converts n3 to a follower in term 1.\nheartbeat 1\nstabilize\n---\nn1@1 → n2 Heartbeat last_index=1 commit_index=1 read_seq=0\nn1@1 → n3 Heartbeat last_index=1 commit_index=1 read_seq=0\nn2@1 → n1 HeartbeatResponse match_index=1 read_seq=0\nn3@0 follower() ⇨ n3@1 follower(n1)\nn3@1 → n1 HeartbeatResponse match_index=0 read_seq=0\nn1@1 → n3 Append base=0@0 [1@1]\nn3@1 append 1@1 None\nn3@1 → n1 AppendResponse match_index=1\n\nstatus\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=0@0 applied=0\n"
  },
  {
    "path": "src/raft/testscripts/node/heartbeat_converts_leader",
    "content": "# A heartbeat from a leader should convert a leader in a past term to a\n# follower.\n\ncluster nodes=3 leader=3\n---\nn1@1 follower(n3) last=1@1 commit=1@1 applied=1\nn2@1 follower(n3) last=1@1 commit=1@1 applied=1\nn3@1 leader last=1@1 commit=1@1 applied=1 progress={1:1→2 2:1→2}\n\n# Partition n3 away from the cluster.\npartition 3\n---\nn3 ⇹ n1 n2\n\n# Elect n1 as a new leader.\n(campaign 1)\n(stabilize heartbeat=true)\nstatus\n---\nn1@2 leader last=2@2 commit=2@2 applied=2 progress={2:2→3 3:0→3}\nn2@2 follower(n1) last=2@2 commit=2@2 applied=2\nn3@1 leader last=1@1 commit=1@1 applied=1 progress={1:1→2 2:1→2}\n\n# Heal the partition.\nheal\n---\nn1 n2 n3 fully connected\n\n# The next heartbeat from n1 converts n3 to a follower in term 2.\nheartbeat 1\nstabilize heartbeat=true\n---\nn1@2 → n2 Heartbeat last_index=2 commit_index=2 read_seq=0\nn1@2 → n3 Heartbeat last_index=2 commit_index=2 read_seq=0\nn2@2 → n1 HeartbeatResponse match_index=2 read_seq=0\nn3@1 leader ⇨ n3@2 follower(n1)\nn3@2 → n1 HeartbeatResponse match_index=0 read_seq=0\nn1@2 → n3 Append base=1@1 []\nn3@2 → n1 AppendResponse match_index=1\nn1@2 → n3 Append base=1@1 [2@2]\nn3@2 append 2@2 None\nn3@2 → n1 AppendResponse match_index=2\nn1@2 → n2 Heartbeat last_index=2 commit_index=2 read_seq=0\nn1@2 → n3 Heartbeat last_index=2 commit_index=2 read_seq=0\nn2@2 → n1 HeartbeatResponse match_index=2 read_seq=0\nn3@2 commit 2@2\nn3@2 apply 2@2 None\nn3@2 → n1 HeartbeatResponse match_index=2 read_seq=0\n\nstatus\n---\nn1@2 leader last=2@2 commit=2@2 applied=2 progress={2:2→3 3:2→3}\nn2@2 follower(n1) last=2@2 commit=2@2 applied=2\nn3@2 follower(n1) last=2@2 commit=2@2 applied=2\n"
  },
  {
    "path": "src/raft/testscripts/node/heartbeat_lost_append_duplicate",
    "content": "# Duplicate heartbeats and responses with a lost append will\n# trigger duplicate resends, but it will eventually resolve.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Partition the leader, submit a write whose appends are dropped,\n# then heal the partition again.\npartition 1\n---\nn1 ⇹ n2 n3\n\nput 1 foo=bar\n---\nc1@1 → n1 ClientRequest id=0x01 write 0x0103666f6f03626172\nn1@1 append 2@1 put foo=bar\nn1@1 ⇥ n2 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶1̶@̶1̶ ̶[̶2̶@̶1̶]̶\nn1@1 ⇥ n3 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶1̶@̶1̶ ̶[̶2̶@̶1̶]̶\n\nheal\n---\nn1 n2 n3 fully connected\n\n# The next heartbeat will result in match_index=0 since the followers\n# don't have the last_index. 3 heartbeats are made.\nheartbeat 1\nheartbeat 1\nheartbeat 1\ndeliver\n---\nn1@1 → n2 Heartbeat last_index=2 commit_index=1 read_seq=0\nn1@1 → n3 Heartbeat last_index=2 commit_index=1 read_seq=0\nn1@1 → n2 Heartbeat last_index=2 commit_index=1 read_seq=0\nn1@1 → n3 Heartbeat last_index=2 commit_index=1 read_seq=0\nn1@1 → n2 Heartbeat last_index=2 commit_index=1 read_seq=0\nn1@1 → n3 Heartbeat last_index=2 commit_index=1 read_seq=0\nn2@1 → n1 HeartbeatResponse match_index=0 read_seq=0\nn2@1 → n1 HeartbeatResponse match_index=0 read_seq=0\nn2@1 → n1 HeartbeatResponse match_index=0 read_seq=0\nn3@1 → n1 HeartbeatResponse match_index=0 read_seq=0\nn3@1 → n1 HeartbeatResponse match_index=0 read_seq=0\nn3@1 → n1 HeartbeatResponse match_index=0 read_seq=0\n\n# The leader has previously matched the followers at index 1.\nstatus 1\n---\nn1@1 leader last=2@1 commit=1@1 applied=1 progress={2:1→3 3:1→3}\n\n# When it receives the heartbeat responses, it sends duplicates of the missing\n# entries.\ndeliver\n---\nn1@1 → n2 Append base=1@1 [2@1]\nn1@1 → n2 Append base=1@1 [2@1]\nn1@1 → n2 Append base=1@1 [2@1]\nn1@1 → n3 Append base=1@1 [2@1]\nn1@1 → n3 Append base=1@1 [2@1]\nn1@1 → n3 Append base=1@1 [2@1]\n\n# The followers accept the duplicate appends and the leader commits and applies.\nstabilize\n---\nn2@1 append 2@1 put foo=bar\nn2@1 → n1 AppendResponse match_index=2\nn2@1 → n1 AppendResponse match_index=2\nn2@1 → n1 AppendResponse match_index=2\nn3@1 append 2@1 put foo=bar\nn3@1 → n1 AppendResponse match_index=2\nn3@1 → n1 AppendResponse match_index=2\nn3@1 → n1 AppendResponse match_index=2\nn1@1 commit 2@1\nn1@1 apply 2@1 put foo=bar\nn1@1 → c1 ClientResponse id=0x01 write 0x0102\nc1@1 put foo=bar ⇒ 2\n"
  },
  {
    "path": "src/raft/testscripts/node/heartbeat_lost_append_multiple",
    "content": "# A heartbeat response triggers a probe and resend of lost appends.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Partition the leader, submit three writes whose appends are dropped, then heal\n# the partition again.\npartition 1\n---\nn1 ⇹ n2 n3\n\nput 1 a=1\nput 1 b=2\nput 1 c=3\n---\nc1@1 → n1 ClientRequest id=0x01 write 0x0101610131\nn1@1 append 2@1 put a=1\nn1@1 ⇥ n2 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶1̶@̶1̶ ̶[̶2̶@̶1̶]̶\nn1@1 ⇥ n3 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶1̶@̶1̶ ̶[̶2̶@̶1̶]̶\nc1@1 → n1 ClientRequest id=0x02 write 0x0101620132\nn1@1 append 3@1 put b=2\nn1@1 ⇥ n2 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶2̶@̶1̶ ̶[̶3̶@̶1̶]̶\nn1@1 ⇥ n3 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶2̶@̶1̶ ̶[̶3̶@̶1̶]̶\nc1@1 → n1 ClientRequest id=0x03 write 0x0101630133\nn1@1 append 4@1 put c=3\nn1@1 ⇥ n2 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶3̶@̶1̶ ̶[̶4̶@̶1̶]̶\nn1@1 ⇥ n3 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶3̶@̶1̶ ̶[̶4̶@̶1̶]̶\n\nheal\nstatus\n---\nn1 n2 n3 fully connected\nn1@1 leader last=4@1 commit=1@1 applied=1 progress={2:1→5 3:1→5}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# The next heartbeat will result in match_index=0 since the followers\n# don't have the last_index.\nheartbeat 1\ndeliver\n---\nn1@1 → n2 Heartbeat last_index=4 commit_index=1 read_seq=0\nn1@1 → n3 Heartbeat last_index=4 commit_index=1 read_seq=0\nn2@1 → n1 HeartbeatResponse match_index=0 read_seq=0\nn3@1 → n1 HeartbeatResponse match_index=0 read_seq=0\n\n# The leader has previously matched the followers at index 1.\nstatus 1\n---\nn1@1 leader last=4@1 commit=1@1 applied=1 progress={2:1→5 3:1→5}\n\n# When it receives the heartbeat response, it probes the previous index 3.\ndeliver\n---\nn1@1 → n2 Append base=3@1 []\nn1@1 → n3 Append base=3@1 []\n\n# The followers don't have index 3. They don't have index 2 either, but they\n# do have 1, so they respond with a reject_index=2.\ndeliver\n---\nn2@1 → n1 AppendResponse reject_index=2\nn3@1 → n1 AppendResponse reject_index=2\n\n# The leader has already matched index 1, so it doesn't have to probe for it,\n# and can simply send the tail of the log.\ndeliver\n---\nn1@1 → n2 Append base=1@1 [2@1 3@1 4@1]\nn1@1 → n3 Append base=1@1 [2@1 3@1 4@1]\n\n# The followers accept the append and the leader commits and applies.\nstabilize\n---\nn2@1 append 2@1 put a=1\nn2@1 append 3@1 put b=2\nn2@1 append 4@1 put c=3\nn2@1 → n1 AppendResponse match_index=4\nn3@1 append 2@1 put a=1\nn3@1 append 3@1 put b=2\nn3@1 append 4@1 put c=3\nn3@1 → n1 AppendResponse match_index=4\nn1@1 commit 4@1\nn1@1 apply 2@1 put a=1\nn1@1 apply 3@1 put b=2\nn1@1 apply 4@1 put c=3\nn1@1 → c1 ClientResponse id=0x01 write 0x0102\nc1@1 put a=1 ⇒ 2\nn1@1 → c1 ClientResponse id=0x02 write 0x0103\nc1@1 put b=2 ⇒ 3\nn1@1 → c1 ClientResponse id=0x03 write 0x0104\nc1@1 put c=3 ⇒ 4\n"
  },
  {
    "path": "src/raft/testscripts/node/heartbeat_lost_append_single",
    "content": "# A heartbeat response triggers a resend of a lost append.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Partition the leader, submit a write whose appends are dropped,\n# then heal the partition again.\npartition 1\n---\nn1 ⇹ n2 n3\n\nput 1 foo=bar\n---\nc1@1 → n1 ClientRequest id=0x01 write 0x0103666f6f03626172\nn1@1 append 2@1 put foo=bar\nn1@1 ⇥ n2 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶1̶@̶1̶ ̶[̶2̶@̶1̶]̶\nn1@1 ⇥ n3 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶1̶@̶1̶ ̶[̶2̶@̶1̶]̶\n\nheal\n---\nn1 n2 n3 fully connected\n\n# The next heartbeat will result in match_index=0 since the followers\n# don't have the last_index.\nheartbeat 1\ndeliver\n---\nn1@1 → n2 Heartbeat last_index=2 commit_index=1 read_seq=0\nn1@1 → n3 Heartbeat last_index=2 commit_index=1 read_seq=0\nn2@1 → n1 HeartbeatResponse match_index=0 read_seq=0\nn3@1 → n1 HeartbeatResponse match_index=0 read_seq=0\n\n# The leader has previously matched the followers at index 1.\nstatus 1\n---\nn1@1 leader last=2@1 commit=1@1 applied=1 progress={2:1→3 3:1→3}\n\n# When it receives the heartbeat response, instead of probing index 1 and then\n# sending the actual entries, it simply sends the entries.\ndeliver\n---\nn1@1 → n2 Append base=1@1 [2@1]\nn1@1 → n3 Append base=1@1 [2@1]\n\n# The followers accept the append and the leader commits and applies.\nstabilize\n---\nn2@1 append 2@1 put foo=bar\nn2@1 → n1 AppendResponse match_index=2\nn3@1 append 2@1 put foo=bar\nn3@1 → n1 AppendResponse match_index=2\nn1@1 commit 2@1\nn1@1 apply 2@1 put foo=bar\nn1@1 → c1 ClientResponse id=0x01 write 0x0102\nc1@1 put foo=bar ⇒ 2\n"
  },
  {
    "path": "src/raft/testscripts/node/heartbeat_lost_read",
    "content": "# Heartbeats will recover from a lost read message.\n\ncluster nodes=5 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2 4:1→2 5:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\nn4@1 follower(n1) last=1@1 commit=1@1 applied=1\nn5@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Write a key and replicate it.\n(put 1 foo=bar)\n(stabilize heartbeat=true)\n---\nok\n\n# Partition the leader, and submit a read.\npartition 1\n---\nn1 ⇹ n2 n3 n4 n5\n\nget 1 foo\n---\nc1@1 → n1 ClientRequest id=0x02 read 0x0003666f6f\nn1@1 ⇥ n2 R̶e̶a̶d̶ ̶s̶e̶q̶=̶1̶\nn1@1 ⇥ n3 R̶e̶a̶d̶ ̶s̶e̶q̶=̶1̶\nn1@1 ⇥ n4 R̶e̶a̶d̶ ̶s̶e̶q̶=̶1̶\nn1@1 ⇥ n5 R̶e̶a̶d̶ ̶s̶e̶q̶=̶1̶\n\nheal\n---\nn1 n2 n3 n4 n5 fully connected\n\n# The next heartbeat will detect the failed read, and serve it when\n# it has a quorum.\nheartbeat 1\ndeliver\n---\nn1@1 → n2 Heartbeat last_index=2 commit_index=2 read_seq=1\nn1@1 → n3 Heartbeat last_index=2 commit_index=2 read_seq=1\nn1@1 → n4 Heartbeat last_index=2 commit_index=2 read_seq=1\nn1@1 → n5 Heartbeat last_index=2 commit_index=2 read_seq=1\nn2@1 → n1 HeartbeatResponse match_index=2 read_seq=1\nn3@1 → n1 HeartbeatResponse match_index=2 read_seq=1\nn4@1 → n1 HeartbeatResponse match_index=2 read_seq=1\nn5@1 → n1 HeartbeatResponse match_index=2 read_seq=1\n\n# The first response does not provide quorum.\ndeliver 1 from=2\n---\nok\n\n# The second does, and the read is served.\ndeliver 1 from=3\n---\nn1@1 → c1 ClientResponse id=0x02 read 0x000103626172\nc1@1 get foo ⇒ bar\n"
  },
  {
    "path": "src/raft/testscripts/node/heartbeat_match_commits",
    "content": "# A heartbeat response can advance a follower match index and commit+apply.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Submit a write to the leader.\nput 1 foo=bar\n---\nc1@1 → n1 ClientRequest id=0x01 write 0x0103666f6f03626172\nn1@1 append 2@1 put foo=bar\nn1@1 → n2 Append base=1@1 [2@1]\nn1@1 → n3 Append base=1@1 [2@1]\n\n# Partition n1 away from the followers as they send the append acks, then heal\n# the partition.\npartition 1\n---\nn1 ⇹ n2 n3\n\nstabilize\n---\nn2@1 append 2@1 put foo=bar\nn2@1 ⇥ n1 A̶p̶p̶e̶n̶d̶R̶e̶s̶p̶o̶n̶s̶e̶ ̶m̶a̶t̶c̶h̶_̶i̶n̶d̶e̶x̶=̶2̶\nn3@1 append 2@1 put foo=bar\nn3@1 ⇥ n1 A̶p̶p̶e̶n̶d̶R̶e̶s̶p̶o̶n̶s̶e̶ ̶m̶a̶t̶c̶h̶_̶i̶n̶d̶e̶x̶=̶2̶\n\nheal\n---\nn1 n2 n3 fully connected\n\n# The write has been replicated, but not yet committed and applied.\nstatus\n---\nn1@1 leader last=2@1 commit=1@1 applied=1 progress={2:1→3 3:1→3}\nn2@1 follower(n1) last=2@1 commit=1@1 applied=1\nn3@1 follower(n1) last=2@1 commit=1@1 applied=1\n\n# The leader heartbeats. The followers confirm they are caught up.\nheartbeat 1\ndeliver\n---\nn1@1 → n2 Heartbeat last_index=2 commit_index=1 read_seq=0\nn1@1 → n3 Heartbeat last_index=2 commit_index=1 read_seq=0\nn2@1 → n1 HeartbeatResponse match_index=2 read_seq=0\nn3@1 → n1 HeartbeatResponse match_index=2 read_seq=0\n\n# When the leader receives the first heartbeat, it commits and applies\n# the write.\ndeliver 1 from=2\n---\nn1@1 commit 2@1\nn1@1 apply 2@1 put foo=bar\nn1@1 → c1 ClientResponse id=0x01 write 0x0102\nc1@1 put foo=bar ⇒ 2\n\nstatus 1\n---\nn1@1 leader last=2@1 commit=2@1 applied=2 progress={2:2→3 3:1→3}\n\n# Delivery of the second heartbeat advances the match index, but\n# there is nothing more to do.\ndeliver\nstatus\n---\nn1@1 leader last=2@1 commit=2@1 applied=2 progress={2:2→3 3:2→3}\nn2@1 follower(n1) last=2@1 commit=1@1 applied=1\nn3@1 follower(n1) last=2@1 commit=1@1 applied=1\n"
  },
  {
    "path": "src/raft/testscripts/node/heartbeat_multiple_leaders_panic",
    "content": "# A heartbeat will panic if there are multiple leaders in a term.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Leader panics if it sees another leader in the same term.\n!step 1 '{\"from\":2, \"to\":1, \"term\":1, \"message\": {\"Heartbeat\":{\"last_index\":1,\"commit_index\":0, \"commit_term\":0, \"read_seq\":0}}}'\n---\nPanic: saw other leader 2 in term 1\n\n# Follower panics too.\n!step 2 '{\"from\":3, \"to\":2, \"term\":1, \"message\": {\"Heartbeat\":{\"last_index\":1,\"commit_index\":0, \"commit_term\":0, \"read_seq\":0}}}'\n---\nPanic: assertion `left == right` failed: multiple leaders in term\n  left: 3\n right: 1\n"
  },
  {
    "path": "src/raft/testscripts/node/heartbeat_old_commit_index",
    "content": "# A heartbeat with an old commit index is ignored by a follower.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Replicate a write.\n(put 1 foo=bar)\n(stabilize heartbeat=true)\nstatus\n---\nn1@1 leader last=2@1 commit=2@1 applied=2 progress={2:2→3 3:2→3}\nn2@1 follower(n1) last=2@1 commit=2@1 applied=2\nn3@1 follower(n1) last=2@1 commit=2@1 applied=2\n\n# Step a heartbeat with an outdated commit index.\nstep 2 '{\"from\":1, \"to\":2, \"term\":1, \"message\":{\"Heartbeat\":{\"last_index\":2,\"commit_index\":1,\"commit_term\":1,\"read_seq\":0}}}'\nstabilize\n---\nn2@1 → n1 HeartbeatResponse match_index=2 read_seq=0\n"
  },
  {
    "path": "src/raft/testscripts/node/heartbeat_old_last_index",
    "content": "# A heartbeat with an old last index is matched by a follower.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Replicate a write.\n(put 1 foo=bar)\n(stabilize heartbeat=true)\nstatus\n---\nn1@1 leader last=2@1 commit=2@1 applied=2 progress={2:2→3 3:2→3}\nn2@1 follower(n1) last=2@1 commit=2@1 applied=2\nn3@1 follower(n1) last=2@1 commit=2@1 applied=2\n\n# Step a heartbeat with an outdated last index.\nstep 2 '{\"from\":1, \"to\":2, \"term\":1, \"message\":{\"Heartbeat\":{\"last_index\":1,\"commit_index\":1,\"commit_term\":1,\"read_seq\":0}}}'\nstabilize\n---\nn2@1 → n1 HeartbeatResponse match_index=1 read_seq=0\n"
  },
  {
    "path": "src/raft/testscripts/node/heartbeat_probe_divergent",
    "content": "# A heartbeat while the leader is probing a follower with a long divergent tail\n# doesn't disrupt the probing, and won't result in a quadratically increasing\n# amount of probes with each heartbeat.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Make a couple of writes to ensure a common log prefix.\n(put 1 a=1)\n(put 1 b=2)\n(stabilize)\nstatus\n---\nn1@1 leader last=3@1 commit=3@1 applied=3 progress={2:3→4 3:3→4}\nn2@1 follower(n1) last=3@1 commit=1@1 applied=1\nn3@1 follower(n1) last=3@1 commit=1@1 applied=1\n\n# Partition n1\npartition 1\n---\nn1 ⇹ n2 n3\n\n# Elect new leaders in the majority partition and replicate a few writes.\n(campaign 2)\n(stabilize)\n(put 2 c=3)\n(put 2 d=4)\n(stabilize heartbeat=true)\nstatus\n---\nn1@1 leader last=3@1 commit=3@1 applied=3 progress={2:3→4 3:3→4}\nn2@2 leader last=6@2 commit=6@2 applied=6 progress={1:0→7 3:6→7}\nn3@2 follower(n2) last=6@2 commit=6@2 applied=6\n\n(campaign 3)\n(stabilize)\n(put 2 e=5)\n(put 2 f=6)\n(stabilize heartbeat=true)\nstatus\n---\nn1@1 leader last=3@1 commit=3@1 applied=3 progress={2:3→4 3:3→4}\nn2@3 follower(n3) last=9@3 commit=9@3 applied=9\nn3@3 leader last=9@3 commit=9@3 applied=9 progress={1:0→10 2:9→10}\n\n# Propose writes in the minority partition as well, to build up a divergent log.\n(put 1 a=2)\n(put 1 a=3)\n(put 1 a=4)\n(put 1 a=5)\n(put 1 a=6)\n(put 1 a=7)\n(put 1 a=8)\n(put 1 a=9)\n(stabilize)\nstatus\n---\nn1@1 leader last=11@1 commit=3@1 applied=3 progress={2:3→12 3:3→12}\nn2@3 follower(n3) last=9@3 commit=9@3 applied=9\nn3@3 leader last=9@3 commit=9@3 applied=9 progress={1:0→10 2:9→10}\n\n# Heal the partition.\nheal\n---\nn1 n2 n3 fully connected\n\n# Propose another write on the majority leader to start probing.\nput 3 g=7\n---\nc3@3 → n3 ClientRequest id=0x0f write 0x0101670137\nn3@3 append 10@3 put g=7\nn3@3 → n1 Append base=9@3 [10@3]\nn3@3 → n2 Append base=9@3 [10@3]\n\n# The append should be rejected by n1, canceling the writes.\ndeliver 1\n---\nn1@1 leader ⇨ n1@3 follower(n3)\nn1@1 → c1 ClientResponse id=0x07 Error::Abort\nc1@1 put a=2 ⇒ Error::Abort (operation aborted)\nn1@1 → c1 ClientResponse id=0x08 Error::Abort\nc1@1 put a=3 ⇒ Error::Abort (operation aborted)\nn1@1 → c1 ClientResponse id=0x09 Error::Abort\nc1@1 put a=4 ⇒ Error::Abort (operation aborted)\nn1@1 → c1 ClientResponse id=0x0a Error::Abort\nc1@1 put a=5 ⇒ Error::Abort (operation aborted)\nn1@1 → c1 ClientResponse id=0x0b Error::Abort\nc1@1 put a=6 ⇒ Error::Abort (operation aborted)\nn1@1 → c1 ClientResponse id=0x0c Error::Abort\nc1@1 put a=7 ⇒ Error::Abort (operation aborted)\nn1@1 → c1 ClientResponse id=0x0d Error::Abort\nc1@1 put a=8 ⇒ Error::Abort (operation aborted)\nn1@1 → c1 ClientResponse id=0x0e Error::Abort\nc1@1 put a=9 ⇒ Error::Abort (operation aborted)\nn1@3 → n3 AppendResponse reject_index=9\n\n# n3 begins probing, and also heartbeats.\ndeliver 3\nheartbeat 3\ndeliver 1\nstatus 3\n---\nn3@3 → n1 Append base=8@3 []\nn3@3 → n1 Heartbeat last_index=10 commit_index=9 read_seq=0\nn3@3 → n2 Heartbeat last_index=10 commit_index=9 read_seq=0\nn1@3 → n3 AppendResponse reject_index=8\nn1@3 → n3 HeartbeatResponse match_index=0 read_seq=0\nn3@3 leader last=10@3 commit=9@3 applied=9 progress={1:0→9 2:9→11}\n\n# n3 receives probe and heartbeat responses, resulting in duplicate\n# probes being sent at base index 7.\ndeliver 3\nstatus 3\n---\nn3@3 → n1 Append base=7@3 []\nn3@3 → n1 Append base=7@3 []\nn3@3 leader last=10@3 commit=9@3 applied=9 progress={1:0→8 2:9→11}\n\ndeliver 1\n---\nn1@3 → n3 AppendResponse reject_index=7\nn1@3 → n3 AppendResponse reject_index=7\n\n# However, when receiving the duplicate probe responses, they are\n# deduplicated and only a single new probe is sent.\ndeliver 3\n---\nn3@3 → n1 Append base=6@2 []\n\ndeliver 1\n---\nn1@3 → n3 AppendResponse reject_index=6\n\n# n3 heartbeats again before sending the next probe. This results in\n# two probes: the heartbeat response resends the probe at base 5, while\n# the probe response triggers a new probe at base 4.\nheartbeat 3\ndeliver 3\n---\nn3@3 → n1 Heartbeat last_index=10 commit_index=9 read_seq=0\nn3@3 → n2 Heartbeat last_index=10 commit_index=9 read_seq=0\nn3@3 → n1 Append base=5@2 []\n\ndeliver 1\n---\nn1@3 → n3 HeartbeatResponse match_index=0 read_seq=0\nn1@3 → n3 AppendResponse reject_index=5\n\ndeliver 3\n---\nn3@3 → n1 Append base=5@2 []\nn3@3 → n1 Append base=4@2 []\n\ndeliver 1\n---\nn1@3 → n3 AppendResponse reject_index=5\nn1@3 → n3 AppendResponse reject_index=4\n\n# The probe response at reject_index=5 is ignored, since we're already probed\n# it. Only a single new probe is sent at base 4.\ndeliver 3\n---\nn3@3 → n1 Append base=3@1 []\n\n# When delivered, we finally get a match, and the follower gets caught up.\ndeliver 1\n---\nn1@3 → n3 AppendResponse match_index=3\n\ndeliver 3\n---\nn3@3 → n1 Append base=3@1 [4@2 5@2 6@2 7@3 8@3 9@3 10@3]\n\ndeliver 1\n---\nn1@3 append 4@2 None\nn1@3 append 5@2 put c=3\nn1@3 append 6@2 put d=4\nn1@3 append 7@3 None\nn1@3 append 8@3 put e=5\nn1@3 append 9@3 put f=6\nn1@3 append 10@3 put g=7\nn1@3 → n3 AppendResponse match_index=10\n\ndeliver 3\n---\nn3@3 commit 10@3\nn3@3 apply 10@3 put g=7\nn3@3 → c3 ClientResponse id=0x0f write 0x010a\nc3@3 put g=7 ⇒ 10\n"
  },
  {
    "path": "src/raft/testscripts/node/old_campaign_rejected",
    "content": "# Old campaign messages (in the same term) are ignored by leaders and followers\n# once a leader is elected.\n\ncluster nodes=3\n---\nn1@0 follower() last=0@0 commit=0@0 applied=0\nn2@0 follower() last=0@0 commit=0@0 applied=0\nn3@0 follower() last=0@0 commit=0@0 applied=0\n\n# n1 and n2 campaign.\ncampaign 1 2\n---\nn1@0 follower() ⇨ n1@1 candidate\nn1@1 → n2 Campaign last=0@0\nn1@1 → n3 Campaign last=0@0\nn2@0 follower() ⇨ n2@1 candidate\nn2@1 → n1 Campaign last=0@0\nn2@1 → n3 Campaign last=0@0\n\n# n3 receives n1's Campaign message and grants its vote.\ndeliver 3 from=1\n---\nn3@0 follower() ⇨ n3@1 follower()\nn3@1 → n1 CampaignResponse vote=true\n\n# n1 becomes leader.\ndeliver 1 from=3\n---\nn1@1 candidate ⇨ n1@1 leader\nn1@1 append 1@1 None\nn1@1 → n2 Append base=0@0 [1@1]\nn1@1 → n3 Append base=0@0 [1@1]\nn1@1 → n2 Heartbeat last_index=1 commit_index=0 read_seq=0\nn1@1 → n3 Heartbeat last_index=1 commit_index=0 read_seq=0\n\n# n3 receives n1's heartbeat and becomes follower.\ndeliver 3 from=1\n---\nn3@1 follower() ⇨ n3@1 follower(n1)\nn3@1 append 1@1 None\nn3@1 → n1 AppendResponse match_index=1\nn3@1 → n1 HeartbeatResponse match_index=1 read_seq=0\n\nstatus\n---\nn1@1 leader last=1@1 commit=0@0 applied=0 progress={2:0→2 3:0→2}\nn2@1 candidate last=0@0 commit=0@0 applied=0\nn3@1 follower(n1) last=1@1 commit=0@0 applied=0\n\n# n1 and n3 receive n2's Campaign message and reject it.\ndeliver 1 3 from=2\n---\nn1@1 → n2 CampaignResponse vote=false\nn3@1 → n2 CampaignResponse vote=false\n\n# Stabilizing the cluster results in everyone following n1.\n(stabilize heartbeat=true)\nstatus\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n"
  },
  {
    "path": "src/raft/testscripts/node/old_campaign_response_ignored",
    "content": "# Old campaign responses (in the same term) are ignored by leaders and followers\n# once a leader is elected.\n\ncluster nodes=7\n---\nn1@0 follower() last=0@0 commit=0@0 applied=0\nn2@0 follower() last=0@0 commit=0@0 applied=0\nn3@0 follower() last=0@0 commit=0@0 applied=0\nn4@0 follower() last=0@0 commit=0@0 applied=0\nn5@0 follower() last=0@0 commit=0@0 applied=0\nn6@0 follower() last=0@0 commit=0@0 applied=0\nn7@0 follower() last=0@0 commit=0@0 applied=0\n\n# n1 and n2 campaign.\ncampaign 1 2\n---\nn1@0 follower() ⇨ n1@1 candidate\nn1@1 → n2 Campaign last=0@0\nn1@1 → n3 Campaign last=0@0\nn1@1 → n4 Campaign last=0@0\nn1@1 → n5 Campaign last=0@0\nn1@1 → n6 Campaign last=0@0\nn1@1 → n7 Campaign last=0@0\nn2@0 follower() ⇨ n2@1 candidate\nn2@1 → n1 Campaign last=0@0\nn2@1 → n3 Campaign last=0@0\nn2@1 → n4 Campaign last=0@0\nn2@1 → n5 Campaign last=0@0\nn2@1 → n6 Campaign last=0@0\nn2@1 → n7 Campaign last=0@0\n\n# n3-n6 vote for n1, n7 votes for n2.\ndeliver 3 4 5 6 from=1\ndeliver 7 from=2\n---\nn3@0 follower() ⇨ n3@1 follower()\nn3@1 → n1 CampaignResponse vote=true\nn4@0 follower() ⇨ n4@1 follower()\nn4@1 → n1 CampaignResponse vote=true\nn5@0 follower() ⇨ n5@1 follower()\nn5@1 → n1 CampaignResponse vote=true\nn6@0 follower() ⇨ n6@1 follower()\nn6@1 → n1 CampaignResponse vote=true\nn7@0 follower() ⇨ n7@1 follower()\nn7@1 → n2 CampaignResponse vote=true\n\n# n1 receives votes from n3-n5 and assumes leadership.\ndeliver 1 from=3\ndeliver 1 from=4\ndeliver 1 from=5\n---\nn1@1 candidate ⇨ n1@1 leader\nn1@1 append 1@1 None\nn1@1 → n2 Append base=0@0 [1@1]\nn1@1 → n3 Append base=0@0 [1@1]\nn1@1 → n4 Append base=0@0 [1@1]\nn1@1 → n5 Append base=0@0 [1@1]\nn1@1 → n6 Append base=0@0 [1@1]\nn1@1 → n7 Append base=0@0 [1@1]\nn1@1 → n2 Heartbeat last_index=1 commit_index=0 read_seq=0\nn1@1 → n3 Heartbeat last_index=1 commit_index=0 read_seq=0\nn1@1 → n4 Heartbeat last_index=1 commit_index=0 read_seq=0\nn1@1 → n5 Heartbeat last_index=1 commit_index=0 read_seq=0\nn1@1 → n6 Heartbeat last_index=1 commit_index=0 read_seq=0\nn1@1 → n7 Heartbeat last_index=1 commit_index=0 read_seq=0\n\n# n2 receives n1's heartbeats and becomes follower.\ndeliver 2 from=1\n---\nn2@1 → n1 CampaignResponse vote=false\nn2@1 candidate ⇨ n2@1 follower(n1)\nn2@1 append 1@1 None\nn2@1 → n1 AppendResponse match_index=1\nn2@1 → n1 HeartbeatResponse match_index=1 read_seq=0\n\n# n1 (leader) receives n6's vote and ignores it. n2 (follower) receives n7's\n# vote and ignores it. They remain leader and follower.\ndeliver 1 from=6\ndeliver 2 from=7\nstatus\n---\nn1@1 leader last=1@1 commit=0@0 applied=0 progress={2:0→2 3:0→2 4:0→2 5:0→2 6:0→2 7:0→2}\nn2@1 follower(n1) last=1@1 commit=0@0 applied=0\nn3@1 follower() last=0@0 commit=0@0 applied=0\nn4@1 follower() last=0@0 commit=0@0 applied=0\nn5@1 follower() last=0@0 commit=0@0 applied=0\nn6@1 follower() last=0@0 commit=0@0 applied=0\nn7@1 follower() last=0@0 commit=0@0 applied=0\n\n# Stabilizing the cluster results in everyone following n1.\n(stabilize heartbeat=true)\nstatus\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2 4:1→2 5:1→2 6:1→2 7:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\nn4@1 follower(n1) last=1@1 commit=1@1 applied=1\nn5@1 follower(n1) last=1@1 commit=1@1 applied=1\nn6@1 follower(n1) last=1@1 commit=1@1 applied=1\nn7@1 follower(n1) last=1@1 commit=1@1 applied=1\n"
  },
  {
    "path": "src/raft/testscripts/node/old_heartbeat_ignored",
    "content": "# A heartbeat from an old leader should be ignored.\n\n# Make n3 leader.\ncluster nodes=3 leader=3\n---\nn1@1 follower(n3) last=1@1 commit=1@1 applied=1\nn2@1 follower(n3) last=1@1 commit=1@1 applied=1\nn3@1 leader last=1@1 commit=1@1 applied=1 progress={1:1→2 2:1→2}\n\n# Partition n3 away from the cluster.\npartition 3\n---\nn3 ⇹ n1 n2\n\n# Elect n1 as a new leader.\n(campaign 1)\n(stabilize heartbeat=true)\nstatus\n---\nn1@2 leader last=2@2 commit=2@2 applied=2 progress={2:2→3 3:0→3}\nn2@2 follower(n1) last=2@2 commit=2@2 applied=2\nn3@1 leader last=1@1 commit=1@1 applied=1 progress={1:1→2 2:1→2}\n\n# Heal the partition.\nheal\n---\nn1 n2 n3 fully connected\n\n# The next heartbeat from n3 is ignored.\nheartbeat 3\nstabilize\n---\nn3@1 → n1 Heartbeat last_index=1 commit_index=1 read_seq=0\nn3@1 → n2 Heartbeat last_index=1 commit_index=1 read_seq=0\n\nstatus\n---\nn1@2 leader last=2@2 commit=2@2 applied=2 progress={2:2→3 3:0→3}\nn2@2 follower(n1) last=2@2 commit=2@2 applied=2\nn3@1 leader last=1@1 commit=1@1 applied=1 progress={1:1→2 2:1→2}\n"
  },
  {
    "path": "src/raft/testscripts/node/request_candidate_abort",
    "content": "# Client read/write requests fail on candidates.\n\ncluster nodes=3\n---\nn1@0 follower() last=0@0 commit=0@0 applied=0\nn2@0 follower() last=0@0 commit=0@0 applied=0\nn3@0 follower() last=0@0 commit=0@0 applied=0\n\n# n1 campaigns.\ncampaign 1\n---\nn1@0 follower() ⇨ n1@1 candidate\nn1@1 → n2 Campaign last=0@0\nn1@1 → n3 Campaign last=0@0\n\n# A read request on n1 should be rejected.\nget 1 foo\n---\nc1@1 → n1 ClientRequest id=0x01 read 0x0003666f6f\nn1@1 → c1 ClientResponse id=0x01 Error::Abort\nc1@1 get foo ⇒ Error::Abort (operation aborted)\n\n# A write request on n1 should be rejected.\nput 1 foo=bar\n---\nc1@1 → n1 ClientRequest id=0x02 write 0x0103666f6f03626172\nn1@1 → c1 ClientResponse id=0x02 Error::Abort\nc1@1 put foo=bar ⇒ Error::Abort (operation aborted)\n"
  },
  {
    "path": "src/raft/testscripts/node/request_follower",
    "content": "# Client read/write requests are proxied by followers.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# An initial get on a follower yields None.\nget 2 foo\nstabilize\n---\nc2@1 → n2 ClientRequest id=0x01 read 0x0003666f6f\nn2@1 → n1 ClientRequest id=0x01 read 0x0003666f6f\nn1@1 → n2 Read seq=1\nn1@1 → n3 Read seq=1\nn2@1 → n1 ReadResponse seq=1\nn3@1 → n1 ReadResponse seq=1\nn1@1 → n2 ClientResponse id=0x01 read 0x0000\nn2@1 → c2 ClientResponse id=0x01 read 0x0000\nc2@1 get foo ⇒ None\n\n# Write a value on the follower.\nput 2 foo=bar\nstabilize\n(stabilize heartbeat=true)\n---\nc2@1 → n2 ClientRequest id=0x02 write 0x0103666f6f03626172\nn2@1 → n1 ClientRequest id=0x02 write 0x0103666f6f03626172\nn1@1 append 2@1 put foo=bar\nn1@1 → n2 Append base=1@1 [2@1]\nn1@1 → n3 Append base=1@1 [2@1]\nn2@1 append 2@1 put foo=bar\nn2@1 → n1 AppendResponse match_index=2\nn3@1 append 2@1 put foo=bar\nn3@1 → n1 AppendResponse match_index=2\nn1@1 commit 2@1\nn1@1 apply 2@1 put foo=bar\nn1@1 → n2 ClientResponse id=0x02 write 0x0102\nn2@1 → c2 ClientResponse id=0x02 write 0x0102\nc2@1 put foo=bar ⇒ 2\n\n# Read the value back on the follower.\nget 2 foo\nstabilize\n---\nc2@1 → n2 ClientRequest id=0x03 read 0x0003666f6f\nn2@1 → n1 ClientRequest id=0x03 read 0x0003666f6f\nn1@1 → n2 Read seq=2\nn1@1 → n3 Read seq=2\nn2@1 → n1 ReadResponse seq=2\nn3@1 → n1 ReadResponse seq=2\nn1@1 → n2 ClientResponse id=0x03 read 0x000103626172\nn2@1 → c2 ClientResponse id=0x03 read 0x000103626172\nc2@1 get foo ⇒ bar\n"
  },
  {
    "path": "src/raft/testscripts/node/request_follower_campaign_abort",
    "content": "# A follower aborts in-flight requests when it steps down.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Submit a read and write on n2.\nput 2 foo=bar\nget 2 foo\n---\nc2@1 → n2 ClientRequest id=0x01 write 0x0103666f6f03626172\nn2@1 → n1 ClientRequest id=0x01 write 0x0103666f6f03626172\nc2@1 → n2 ClientRequest id=0x02 read 0x0003666f6f\nn2@1 → n1 ClientRequest id=0x02 read 0x0003666f6f\n\n# n3 campaigns before n2's requests achieve quorum.\ncampaign 3\n---\nn3@1 follower(n1) ⇨ n3@2 candidate\nn3@2 → n1 Campaign last=1@1\nn3@2 → n2 Campaign last=1@1\n\n# When n2 receives the campaign message, the requests are aborted.\ndeliver 2 from=3\n---\nn2@1 follower(n1) ⇨ n2@2 follower()\nn2@1 → c2 ClientResponse id=0x01 Error::Abort\nc2@1 put foo=bar ⇒ Error::Abort (operation aborted)\nn2@1 → c2 ClientResponse id=0x02 Error::Abort\nc2@1 get foo ⇒ Error::Abort (operation aborted)\nn2@2 → n3 CampaignResponse vote=true\n"
  },
  {
    "path": "src/raft/testscripts/node/request_follower_disconnect_stall",
    "content": "# Client read/write requests stall if the follower is disconnected from the\n# leader when the request is submitted. They are not retried, nor aborted.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Partition n3 away from the cluster.\npartition 3\n---\nn3 ⇹ n1 n2\n\n# Submit write and read requests to n3. They don't return a result.\nput 3 foo=bar\nget 3 foo\nstabilize\n---\nc3@1 → n3 ClientRequest id=0x01 write 0x0103666f6f03626172\nn3@1 ⇥ n1 C̶l̶i̶e̶n̶t̶R̶e̶q̶u̶e̶s̶t̶ ̶i̶d̶=̶0̶x̶0̶1̶ ̶w̶r̶i̶t̶e̶ ̶0̶x̶0̶1̶0̶3̶6̶6̶6̶f̶6̶f̶0̶3̶6̶2̶6̶1̶7̶2̶\nc3@1 → n3 ClientRequest id=0x02 read 0x0003666f6f\nn3@1 ⇥ n1 C̶l̶i̶e̶n̶t̶R̶e̶q̶u̶e̶s̶t̶ ̶i̶d̶=̶0̶x̶0̶2̶ ̶r̶e̶a̶d̶ ̶0̶x̶0̶0̶0̶3̶6̶6̶6̶f̶6̶f̶\n\n# Heal the partition and heartbeat. The requests still don't return a result.\nheal\n---\nn1 n2 n3 fully connected\n\nstabilize heartbeat=true\n---\nn1@1 → n2 Heartbeat last_index=1 commit_index=1 read_seq=0\nn1@1 → n3 Heartbeat last_index=1 commit_index=1 read_seq=0\nn2@1 → n1 HeartbeatResponse match_index=1 read_seq=0\nn3@1 → n1 HeartbeatResponse match_index=1 read_seq=0\n"
  },
  {
    "path": "src/raft/testscripts/node/request_follower_leaderless_abort",
    "content": "# Client read/write requests fail on leaderless followers.\n\ncluster nodes=3\n---\nn1@0 follower() last=0@0 commit=0@0 applied=0\nn2@0 follower() last=0@0 commit=0@0 applied=0\nn3@0 follower() last=0@0 commit=0@0 applied=0\n\n# A read request on n1 should be rejected.\nget 1 foo\n---\nc1@0 → n1 ClientRequest id=0x01 read 0x0003666f6f\nn1@0 → c1 ClientResponse id=0x01 Error::Abort\nc1@0 get foo ⇒ Error::Abort (operation aborted)\n\n# A write request on n1 should be rejected.\nput 1 foo=bar\n---\nc1@0 → n1 ClientRequest id=0x02 write 0x0103666f6f03626172\nn1@0 → c1 ClientResponse id=0x02 Error::Abort\nc1@0 put foo=bar ⇒ Error::Abort (operation aborted)\n"
  },
  {
    "path": "src/raft/testscripts/node/request_leader",
    "content": "# Client read/write requests succeed on leaders.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# An initial get on the leader yields None.\nget 1 foo\nstabilize\n---\nc1@1 → n1 ClientRequest id=0x01 read 0x0003666f6f\nn1@1 → n2 Read seq=1\nn1@1 → n3 Read seq=1\nn2@1 → n1 ReadResponse seq=1\nn3@1 → n1 ReadResponse seq=1\nn1@1 → c1 ClientResponse id=0x01 read 0x0000\nc1@1 get foo ⇒ None\n\n# Write a value on the leader.\nput 1 foo=bar\nstabilize\n(stabilize heartbeat=true)\n---\nc1@1 → n1 ClientRequest id=0x02 write 0x0103666f6f03626172\nn1@1 append 2@1 put foo=bar\nn1@1 → n2 Append base=1@1 [2@1]\nn1@1 → n3 Append base=1@1 [2@1]\nn2@1 append 2@1 put foo=bar\nn2@1 → n1 AppendResponse match_index=2\nn3@1 append 2@1 put foo=bar\nn3@1 → n1 AppendResponse match_index=2\nn1@1 commit 2@1\nn1@1 apply 2@1 put foo=bar\nn1@1 → c1 ClientResponse id=0x02 write 0x0102\nc1@1 put foo=bar ⇒ 2\n\n# Read the value back on the leader.\nget 1 foo\nstabilize\n---\nc1@1 → n1 ClientRequest id=0x03 read 0x0003666f6f\nn1@1 → n2 Read seq=2\nn1@1 → n3 Read seq=2\nn2@1 → n1 ReadResponse seq=2\nn3@1 → n1 ReadResponse seq=2\nn1@1 → c1 ClientResponse id=0x03 read 0x000103626172\nc1@1 get foo ⇒ bar\n"
  },
  {
    "path": "src/raft/testscripts/node/request_leader_campaign_abort",
    "content": "# A leader aborts in-flight requests when it steps down.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Submit a read and write on n1.\nput 1 foo=bar\nget 1 foo\n---\nc1@1 → n1 ClientRequest id=0x01 write 0x0103666f6f03626172\nn1@1 append 2@1 put foo=bar\nn1@1 → n2 Append base=1@1 [2@1]\nn1@1 → n3 Append base=1@1 [2@1]\nc1@1 → n1 ClientRequest id=0x02 read 0x0003666f6f\nn1@1 → n2 Read seq=1\nn1@1 → n3 Read seq=1\n\n# n2 campaigns before n1's requests achieve quorum.\ncampaign 2\n---\nn2@1 follower(n1) ⇨ n2@2 candidate\nn2@2 → n1 Campaign last=1@1\nn2@2 → n3 Campaign last=1@1\n\n# When n1 receives the campaign message, the requests are aborted.\ndeliver 1 from=2\n---\nn1@1 leader ⇨ n1@2 follower()\nn1@1 → c1 ClientResponse id=0x01 Error::Abort\nc1@1 put foo=bar ⇒ Error::Abort (operation aborted)\nn1@1 → c1 ClientResponse id=0x02 Error::Abort\nc1@1 get foo ⇒ Error::Abort (operation aborted)\nn1@2 → n2 CampaignResponse vote=false\n"
  },
  {
    "path": "src/raft/testscripts/node/request_leader_change_linearizability",
    "content": "# A new leader that's behind on commit/apply shouldn't serve stale reads.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Write an initial value, and propagate the commit index.\n(put 1 a=1)\n(stabilize heartbeat=true)\nstatus\n---\nn1@1 leader last=2@1 commit=2@1 applied=2 progress={2:2→3 3:2→3}\nn2@1 follower(n1) last=2@1 commit=2@1 applied=2\nn3@1 follower(n1) last=2@1 commit=2@1 applied=2\n\n# Write another value, but don't propagate the commit index.\n(put 1 b=2)\n(stabilize)\nstatus\n---\nn1@1 leader last=3@1 commit=3@1 applied=3 progress={2:3→4 3:3→4}\nn2@1 follower(n1) last=3@1 commit=2@1 applied=2\nn3@1 follower(n1) last=3@1 commit=2@1 applied=2\n\n# n2 now campaigns and wins, while being behind on commit/apply.\ncampaign 2\ndeliver\n---\nn2@1 follower(n1) ⇨ n2@2 candidate\nn2@2 → n1 Campaign last=3@1\nn2@2 → n3 Campaign last=3@1\nn1@1 leader ⇨ n1@2 follower()\nn1@2 → n2 CampaignResponse vote=true\nn3@1 follower(n1) ⇨ n3@2 follower()\nn3@2 → n2 CampaignResponse vote=true\n\n# The initial append doesn't make it to the followers, so its commit index\n# trails the previous leader.\npartition 2\ndeliver 2\n---\nn2 ⇹ n1 n3\nn2@2 candidate ⇨ n2@2 leader\nn2@2 append 4@2 None\nn2@2 ⇥ n1 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶3̶@̶1̶ ̶[̶4̶@̶2̶]̶\nn2@2 ⇥ n3 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶3̶@̶1̶ ̶[̶4̶@̶2̶]̶\nn2@2 ⇥ n1 H̶e̶a̶r̶t̶b̶e̶a̶t̶ ̶l̶a̶s̶t̶_̶i̶n̶d̶e̶x̶=̶4̶ ̶c̶o̶m̶m̶i̶t̶_̶i̶n̶d̶e̶x̶=̶2̶ ̶r̶e̶a̶d̶_̶s̶e̶q̶=̶0̶\nn2@2 ⇥ n3 H̶e̶a̶r̶t̶b̶e̶a̶t̶ ̶l̶a̶s̶t̶_̶i̶n̶d̶e̶x̶=̶4̶ ̶c̶o̶m̶m̶i̶t̶_̶i̶n̶d̶e̶x̶=̶2̶ ̶r̶e̶a̶d̶_̶s̶e̶q̶=̶0̶\n\nheal\nstatus\n---\nn1 n2 n3 fully connected\nn1@2 follower() last=3@1 commit=3@1 applied=3\nn2@2 leader last=4@2 commit=2@1 applied=2 progress={1:0→5 3:0→5}\nn3@2 follower() last=3@1 commit=2@1 applied=2\n\n# Reading from n2 should not result in a stale read even if followers\n# confirm the read sequence.\nget 2 b\ndeliver\ndeliver\n---\nc2@2 → n2 ClientRequest id=0x03 read 0x000162\nn2@2 → n1 Read seq=1\nn2@2 → n3 Read seq=1\nn1@2 follower() ⇨ n1@2 follower(n2)\nn1@2 → n2 ReadResponse seq=1\nn3@2 follower() ⇨ n3@2 follower(n2)\nn3@2 → n2 ReadResponse seq=1\n\n# The leader heartbeats and detects the lost appends.\nheartbeat 2\ndeliver\ndeliver\ndeliver\n---\nn2@2 → n1 Heartbeat last_index=4 commit_index=2 read_seq=1\nn2@2 → n3 Heartbeat last_index=4 commit_index=2 read_seq=1\nn1@2 → n2 HeartbeatResponse match_index=0 read_seq=1\nn3@2 → n2 HeartbeatResponse match_index=0 read_seq=1\nn2@2 → n1 Append base=3@1 []\nn2@2 → n3 Append base=3@1 []\nn1@2 → n2 AppendResponse match_index=3\nn3@2 → n2 AppendResponse match_index=3\n\n# It resends the missing log entry.\ndeliver\ndeliver\n---\nn2@2 → n1 Append base=3@1 [4@2]\nn2@2 → n3 Append base=3@1 [4@2]\nn1@2 append 4@2 None\nn1@2 → n2 AppendResponse match_index=4\nn3@2 append 4@2 None\nn3@2 → n2 AppendResponse match_index=4\n\n# Once the leader receives the acks it commits the entry. The read can now be\n# served, resulting in an up-to-date b=2.\nstabilize\n---\nn2@2 commit 4@2\nn2@2 apply 3@1 put b=2\nn2@2 apply 4@2 None\nn2@2 → c2 ClientResponse id=0x03 read 0x00010132\nc2@2 get b ⇒ 2\n"
  },
  {
    "path": "src/raft/testscripts/node/request_leader_disconnect",
    "content": "# Client read/write requests succeed if the leader is disconnected from the\n# quorum when the request is submitted but it later reconnects.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Partition n1 away from the cluster.\npartition 1\n---\nn1 ⇹ n2 n3\n\n# Submit write and read requests to n1. They don't return a result.\nput 1 foo=bar\nget 1 foo\nstabilize\n---\nc1@1 → n1 ClientRequest id=0x01 write 0x0103666f6f03626172\nn1@1 append 2@1 put foo=bar\nn1@1 ⇥ n2 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶1̶@̶1̶ ̶[̶2̶@̶1̶]̶\nn1@1 ⇥ n3 A̶p̶p̶e̶n̶d̶ ̶b̶a̶s̶e̶=̶1̶@̶1̶ ̶[̶2̶@̶1̶]̶\nc1@1 → n1 ClientRequest id=0x02 read 0x0003666f6f\nn1@1 ⇥ n2 R̶e̶a̶d̶ ̶s̶e̶q̶=̶1̶\nn1@1 ⇥ n3 R̶e̶a̶d̶ ̶s̶e̶q̶=̶1̶\n\n# Heal the partition and heartbeat. The requests eventually return results.\nheal\n---\nn1 n2 n3 fully connected\n\nstabilize heartbeat=true\n---\nn1@1 → n2 Heartbeat last_index=2 commit_index=1 read_seq=1\nn1@1 → n3 Heartbeat last_index=2 commit_index=1 read_seq=1\nn2@1 → n1 HeartbeatResponse match_index=0 read_seq=1\nn3@1 → n1 HeartbeatResponse match_index=0 read_seq=1\nn1@1 → c1 ClientResponse id=0x02 read 0x0000\nc1@1 get foo ⇒ None\nn1@1 → n2 Append base=1@1 [2@1]\nn1@1 → n3 Append base=1@1 [2@1]\nn2@1 append 2@1 put foo=bar\nn2@1 → n1 AppendResponse match_index=2\nn3@1 append 2@1 put foo=bar\nn3@1 → n1 AppendResponse match_index=2\nn1@1 commit 2@1\nn1@1 apply 2@1 put foo=bar\nn1@1 → c1 ClientResponse id=0x01 write 0x0102\nc1@1 put foo=bar ⇒ 2\n"
  },
  {
    "path": "src/raft/testscripts/node/request_leader_read_quorum",
    "content": "# Client read requests are only processed once a quorum confirms the read sequence.\n\ncluster nodes=5 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2 4:1→2 5:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\nn4@1 follower(n1) last=1@1 commit=1@1 applied=1\nn5@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Write foo=bar.\n(put 1 foo=bar)\n(stabilize heartbeat=true)\n---\nok\n\n# Read it once.\n(get 1 foo)\n(stabilize)\n---\nok\n\n# Attempt to read it again. The read only returns once a quorum have\n# confirmed the read sequence.\nget 1 foo\n---\nc1@1 → n1 ClientRequest id=0x03 read 0x0003666f6f\nn1@1 → n2 Read seq=2\nn1@1 → n3 Read seq=2\nn1@1 → n4 Read seq=2\nn1@1 → n5 Read seq=2\n\ndeliver 2\ndeliver 1\n---\nn2@1 → n1 ReadResponse seq=2\n\ndeliver 3\ndeliver 1\n---\nn3@1 → n1 ReadResponse seq=2\nn1@1 → c1 ClientResponse id=0x03 read 0x000103626172\nc1@1 get foo ⇒ bar\n\n(stabilize)\n---\nok\n"
  },
  {
    "path": "src/raft/testscripts/node/request_leader_read_quorum_sequence",
    "content": "# Client read requests are only served once a quorum confirm the read sequence\n# number, including higher sequence numbers.\n\ncluster nodes=5 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2 4:1→2 5:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\nn4@1 follower(n1) last=1@1 commit=1@1 applied=1\nn5@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Write foo=bar and read it back.\n(put 1 foo=bar)\n(stabilize heartbeat=true)\n(get 1 foo)\n(stabilize)\n---\nok\n\n# Send a heartbeat with sequence number 1, and deliver it to all followers.\nheartbeat 1\ndeliver\n---\nn1@1 → n2 Heartbeat last_index=2 commit_index=2 read_seq=1\nn1@1 → n3 Heartbeat last_index=2 commit_index=2 read_seq=1\nn1@1 → n4 Heartbeat last_index=2 commit_index=2 read_seq=1\nn1@1 → n5 Heartbeat last_index=2 commit_index=2 read_seq=1\nn2@1 → n1 HeartbeatResponse match_index=2 read_seq=1\nn3@1 → n1 HeartbeatResponse match_index=2 read_seq=1\nn4@1 → n1 HeartbeatResponse match_index=2 read_seq=1\nn5@1 → n1 HeartbeatResponse match_index=2 read_seq=1\n\n# Partition n1 away.\npartition 1\n---\nn1 ⇹ n2 n3 n4 n5\n\n# Perform a read at sequence number 2. The read messages are lost.\nget 1 foo\n---\nc1@1 → n1 ClientRequest id=0x03 read 0x0003666f6f\nn1@1 ⇥ n2 R̶e̶a̶d̶ ̶s̶e̶q̶=̶2̶\nn1@1 ⇥ n3 R̶e̶a̶d̶ ̶s̶e̶q̶=̶2̶\nn1@1 ⇥ n4 R̶e̶a̶d̶ ̶s̶e̶q̶=̶2̶\nn1@1 ⇥ n5 R̶e̶a̶d̶ ̶s̶e̶q̶=̶2̶\n\n# Deliver the heartbeat responses at sequence number 1. These should not satisfy\n# the read at sequence number 2.\ndeliver 1\n---\nok\n\n# Heal the partition and perform another read at sequence number 3. Followers\n# respond to the reads at sequence number 3.\nheal\nget 1 foo\n---\nn1 n2 n3 n4 n5 fully connected\nc1@1 → n1 ClientRequest id=0x04 read 0x0003666f6f\nn1@1 → n2 Read seq=3\nn1@1 → n3 Read seq=3\nn1@1 → n4 Read seq=3\nn1@1 → n5 Read seq=3\n\ndeliver\n---\nn2@1 → n1 ReadResponse seq=3\nn3@1 → n1 ReadResponse seq=3\nn4@1 → n1 ReadResponse seq=3\nn5@1 → n1 ReadResponse seq=3\n\n# Once n1 receives two responses it has a read quorum and serves both the read\n# at seqnums 2 (id=0x03) and 3 (id=0x04).\ndeliver 1 from=3\ndeliver 1 from=5\n---\nn1@1 → c1 ClientResponse id=0x03 read 0x000103626172\nc1@1 get foo ⇒ bar\nn1@1 → c1 ClientResponse id=0x04 read 0x000103626172\nc1@1 get foo ⇒ bar\n"
  },
  {
    "path": "src/raft/testscripts/node/request_leader_single",
    "content": "# Client read/write requests succeed on a lone leader.\n\ncluster nodes=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={}\n\n# An initial get on the leader yields None.\nget 1 foo\nstabilize\n---\nc1@1 → n1 ClientRequest id=0x01 read 0x0003666f6f\nn1@1 → c1 ClientResponse id=0x01 read 0x0000\nc1@1 get foo ⇒ None\n\n# Write a value on the leader.\nput 1 foo=bar\nstabilize heartbeat=true\n---\nc1@1 → n1 ClientRequest id=0x02 write 0x0103666f6f03626172\nn1@1 append 2@1 put foo=bar\nn1@1 commit 2@1\nn1@1 apply 2@1 put foo=bar\nn1@1 → c1 ClientResponse id=0x02 write 0x0102\nc1@1 put foo=bar ⇒ 2\n\n# Read the value back on the leader.\nget 1 foo\nstabilize\n---\nc1@1 → n1 ClientRequest id=0x03 read 0x0003666f6f\nn1@1 → c1 ClientResponse id=0x03 read 0x000103626172\nc1@1 get foo ⇒ bar\n"
  },
  {
    "path": "src/raft/testscripts/node/request_status",
    "content": "# Status requests return the cluster status.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Partition away n3, so not all nodes have the same log position.\npartition 3\n---\nn3 ⇹ n1 n2\n\n# Replicate a write, but not the commit index.\n(put 1 foo=bar)\n(stabilize)\nstatus\n---\nn1@1 leader last=2@1 commit=2@1 applied=2 progress={2:2→3 3:1→3}\nn2@1 follower(n1) last=2@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Run a status request on the leader.\nstatus request=true 1\nstabilize\n---\nc1@1 → n1 ClientRequest id=0x02 status\nn1@1 → c1 ClientResponse id=0x02 status Status { leader: 1, term: 1, match_index: {1: 2, 2: 2, 3: 1}, commit_index: 2, applied_index: 2, storage: Status { name: \"bitcask\", keys: 4, size: 41, disk_size: 84, live_disk_size: 73 } }\nc1@1 status ⇒ Status {\n    leader: 1,\n    term: 1,\n    match_index: {\n        1: 2,\n        2: 2,\n        3: 1,\n    },\n    commit_index: 2,\n    applied_index: 2,\n    storage: Status {\n        name: \"bitcask\",\n        keys: 4,\n        size: 41,\n        disk_size: 84,\n        live_disk_size: 73,\n    },\n}\n\n# Run a status request on a follower.\nstatus request=true 2\nstabilize\n---\nc2@1 → n2 ClientRequest id=0x03 status\nn2@1 → n1 ClientRequest id=0x03 status\nn1@1 → n2 ClientResponse id=0x03 status Status { leader: 1, term: 1, match_index: {1: 2, 2: 2, 3: 1}, commit_index: 2, applied_index: 2, storage: Status { name: \"bitcask\", keys: 4, size: 41, disk_size: 84, live_disk_size: 73 } }\nn2@1 → c2 ClientResponse id=0x03 status Status { leader: 1, term: 1, match_index: {1: 2, 2: 2, 3: 1}, commit_index: 2, applied_index: 2, storage: Status { name: \"bitcask\", keys: 4, size: 41, disk_size: 84, live_disk_size: 73 } }\nc2@1 status ⇒ Status {\n    leader: 1,\n    term: 1,\n    match_index: {\n        1: 2,\n        2: 2,\n        3: 1,\n    },\n    commit_index: 2,\n    applied_index: 2,\n    storage: Status {\n        name: \"bitcask\",\n        keys: 4,\n        size: 41,\n        disk_size: 84,\n        live_disk_size: 73,\n    },\n}\n"
  },
  {
    "path": "src/raft/testscripts/node/request_status_single",
    "content": "# Status requests return the cluster status on a single node.\n\ncluster nodes=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={}\n\n# Perform a write.\n(put 1 foo=bar)\n(stabilize)\nstatus\n---\nn1@1 leader last=2@1 commit=2@1 applied=2 progress={}\n\n# Run a status request on the leader.\nstatus request=true 1\nstabilize\n---\nc1@1 → n1 ClientRequest id=0x02 status\nn1@1 → c1 ClientResponse id=0x02 status Status { leader: 1, term: 1, match_index: {1: 2}, commit_index: 2, applied_index: 2, storage: Status { name: \"bitcask\", keys: 4, size: 41, disk_size: 84, live_disk_size: 73 } }\nc1@1 status ⇒ Status {\n    leader: 1,\n    term: 1,\n    match_index: {\n        1: 2,\n    },\n    commit_index: 2,\n    applied_index: 2,\n    storage: Status {\n        name: \"bitcask\",\n        keys: 4,\n        size: 41,\n        disk_size: 84,\n        live_disk_size: 73,\n    },\n}\n"
  },
  {
    "path": "src/raft/testscripts/node/restart",
    "content": "# Restarting a cluster that's fully caught up retains the existing state and\n# allows trivially electing a new leader.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Replicate a couple of writes.\n(put 1 a=1)\n(put 1 b=2)\n(stabilize heartbeat=true)\n---\nok\n\n# Dump the current status, log, and state.\nstatus\n---\nn1@1 leader last=3@1 commit=3@1 applied=3 progress={2:3→4 3:3→4}\nn2@1 follower(n1) last=3@1 commit=3@1 applied=3\nn3@1 follower(n1) last=3@1 commit=3@1 applied=3\n\nlog\n---\nn1@1 term=1 last=3@1 commit=3@1 vote=Some(1)\nn1@1 entry 1@1 None\nn1@1 entry 2@1 put a=1\nn1@1 entry 3@1 put b=2\nn2@1 term=1 last=3@1 commit=3@1 vote=Some(1)\nn2@1 entry 1@1 None\nn2@1 entry 2@1 put a=1\nn2@1 entry 3@1 put b=2\nn3@1 term=1 last=3@1 commit=3@1 vote=Some(1)\nn3@1 entry 1@1 None\nn3@1 entry 2@1 put a=1\nn3@1 entry 3@1 put b=2\n\nstate\n---\nn1@1 applied=3\nn1@1 state a=1\nn1@1 state b=2\nn2@1 applied=3\nn2@1 state a=1\nn2@1 state b=2\nn3@1 applied=3\nn3@1 state a=1\nn3@1 state b=2\n\n# Restart the nodes. They retain the same status, logs, and state.\nrestart\n---\nn1@1 follower() last=3@1 commit=3@1 applied=3\nn2@1 follower() last=3@1 commit=3@1 applied=3\nn3@1 follower() last=3@1 commit=3@1 applied=3\n\nlog\n---\nn1@1 term=1 last=3@1 commit=3@1 vote=Some(1)\nn1@1 entry 1@1 None\nn1@1 entry 2@1 put a=1\nn1@1 entry 3@1 put b=2\nn2@1 term=1 last=3@1 commit=3@1 vote=Some(1)\nn2@1 entry 1@1 None\nn2@1 entry 2@1 put a=1\nn2@1 entry 3@1 put b=2\nn3@1 term=1 last=3@1 commit=3@1 vote=Some(1)\nn3@1 entry 1@1 None\nn3@1 entry 2@1 put a=1\nn3@1 entry 3@1 put b=2\n\nstate\n---\nn1@1 applied=3\nn1@1 state a=1\nn1@1 state b=2\nn2@1 applied=3\nn2@1 state a=1\nn2@1 state b=2\nn3@1 applied=3\nn3@1 state a=1\nn3@1 state b=2\n\n# Elect a new leader.\ncampaign 3\nstabilize heartbeat=true\n---\nn3@1 follower() ⇨ n3@2 candidate\nn3@2 → n1 Campaign last=3@1\nn3@2 → n2 Campaign last=3@1\nn1@1 follower() ⇨ n1@2 follower()\nn1@2 → n3 CampaignResponse vote=true\nn2@1 follower() ⇨ n2@2 follower()\nn2@2 → n3 CampaignResponse vote=true\nn3@2 candidate ⇨ n3@2 leader\nn3@2 append 4@2 None\nn3@2 → n1 Append base=3@1 [4@2]\nn3@2 → n2 Append base=3@1 [4@2]\nn3@2 → n1 Heartbeat last_index=4 commit_index=3 read_seq=0\nn3@2 → n2 Heartbeat last_index=4 commit_index=3 read_seq=0\nn1@2 follower() ⇨ n1@2 follower(n3)\nn1@2 append 4@2 None\nn1@2 → n3 AppendResponse match_index=4\nn1@2 → n3 HeartbeatResponse match_index=4 read_seq=0\nn2@2 follower() ⇨ n2@2 follower(n3)\nn2@2 append 4@2 None\nn2@2 → n3 AppendResponse match_index=4\nn2@2 → n3 HeartbeatResponse match_index=4 read_seq=0\nn3@2 commit 4@2\nn3@2 → n1 Heartbeat last_index=4 commit_index=4 read_seq=0\nn3@2 → n2 Heartbeat last_index=4 commit_index=4 read_seq=0\nn1@2 commit 4@2\nn1@2 → n3 HeartbeatResponse match_index=4 read_seq=0\nn2@2 commit 4@2\nn2@2 → n3 HeartbeatResponse match_index=4 read_seq=0\n\nstatus\n---\nn1@2 follower(n3) last=4@2 commit=4@2 applied=4\nn2@2 follower(n3) last=4@2 commit=4@2 applied=4\nn3@2 leader last=4@2 commit=4@2 applied=4 progress={1:4→5 2:4→5}\n"
  },
  {
    "path": "src/raft/testscripts/node/restart_apply",
    "content": "# Restarting a node and wiping its state machine will reapply the state.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Replicate a couple of writes.\n(put 1 a=1)\n(put 1 b=2)\n(stabilize heartbeat=true)\n---\nok\n\n# Restart n3 and clear its state machine. The node will apply all pending\n# entries when restarting.\nrestart 3 applied_index=0\n---\nn3@1 apply 1@1 None\nn3@1 apply 2@1 put a=1\nn3@1 apply 3@1 put b=2\nn3@1 follower() last=3@1 commit=3@1 applied=3\n\nstate 3\n---\nn3@1 applied=3\nn3@1 state a=1\nn3@1 state b=2\n\n# Restart n3 and lose the last write. It will also be reapplied.\nrestart 3 applied_index=2\n---\nn3@1 apply 3@1 put b=2\nn3@1 follower() last=3@1 commit=3@1 applied=3\n\nstate 3\n---\nn3@1 applied=3\nn3@1 state a=1\nn3@1 state b=2\n"
  },
  {
    "path": "src/raft/testscripts/node/restart_commit_recover",
    "content": "# Restarting the cluster and wiping the commit indexes allows\n# a new leader to recover the commit index.\n\ncluster nodes=3 leader=1\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Replicate a couple of writes, but don't propagate the commit index.\n(put 1 a=1)\n(put 1 b=2)\n(stabilize)\nstatus\n---\nn1@1 leader last=3@1 commit=3@1 applied=3 progress={2:3→4 3:3→4}\nn2@1 follower(n1) last=3@1 commit=1@1 applied=1\nn3@1 follower(n1) last=3@1 commit=1@1 applied=1\n\n# Restart all nodes and wipe the commit index.\nrestart commit_index=0\n---\nn1@1 follower() last=3@1 commit=0@0 applied=3\nn2@1 follower() last=3@1 commit=0@0 applied=1\nn3@1 follower() last=3@1 commit=0@0 applied=1\n\n# n3 campaigns for leadership and recovers the commit index.\ncampaign 3\nstabilize\n---\nn3@1 follower() ⇨ n3@2 candidate\nn3@2 → n1 Campaign last=3@1\nn3@2 → n2 Campaign last=3@1\nn1@1 follower() ⇨ n1@2 follower()\nn1@2 → n3 CampaignResponse vote=true\nn2@1 follower() ⇨ n2@2 follower()\nn2@2 → n3 CampaignResponse vote=true\nn3@2 candidate ⇨ n3@2 leader\nn3@2 append 4@2 None\nn3@2 → n1 Append base=3@1 [4@2]\nn3@2 → n2 Append base=3@1 [4@2]\nn3@2 → n1 Heartbeat last_index=4 commit_index=0 read_seq=0\nn3@2 → n2 Heartbeat last_index=4 commit_index=0 read_seq=0\nn1@2 follower() ⇨ n1@2 follower(n3)\nn1@2 append 4@2 None\nn1@2 → n3 AppendResponse match_index=4\nn1@2 → n3 HeartbeatResponse match_index=4 read_seq=0\nn2@2 follower() ⇨ n2@2 follower(n3)\nn2@2 append 4@2 None\nn2@2 → n3 AppendResponse match_index=4\nn2@2 → n3 HeartbeatResponse match_index=4 read_seq=0\nn3@2 commit 4@2\n\nstatus\n---\nn1@2 follower(n3) last=4@2 commit=0@0 applied=3\nn2@2 follower(n3) last=4@2 commit=0@0 applied=1\nn3@2 leader last=4@2 commit=4@2 applied=4 progress={1:4→5 2:4→5}\n\n# A heartbeat propagates the commit index.\nheartbeat 3\nstabilize\n---\nn3@2 → n1 Heartbeat last_index=4 commit_index=4 read_seq=0\nn3@2 → n2 Heartbeat last_index=4 commit_index=4 read_seq=0\nn1@2 commit 4@2\nn1@2 → n3 HeartbeatResponse match_index=4 read_seq=0\nn2@2 commit 4@2\nn2@2 → n3 HeartbeatResponse match_index=4 read_seq=0\n"
  },
  {
    "path": "src/raft/testscripts/node/restart_term_vote",
    "content": "# The term/vote is retained across a restart.\n\ncluster nodes=3\n---\nn1@0 follower() last=0@0 commit=0@0 applied=0\nn2@0 follower() last=0@0 commit=0@0 applied=0\nn3@0 follower() last=0@0 commit=0@0 applied=0\n\n# Start a new election on n1.\ncampaign 1\n---\nn1@0 follower() ⇨ n1@1 candidate\nn1@1 → n2 Campaign last=0@0\nn1@1 → n3 Campaign last=0@0\n\n# n3 votes for n1, and then restarts.\ndeliver 3\n---\nn3@0 follower() ⇨ n3@1 follower()\nn3@1 → n1 CampaignResponse vote=true\n\nrestart 3\n---\nn3@1 follower() last=0@0 commit=0@0 applied=0\n\n# n3 still has a record of the term and vote in the log.\nlog 3\n---\nn3@1 term=1 last=0@0 commit=0@0 vote=Some(1)\n\n# n2 also campaigns. n3 does not grant its vote.\ncampaign 2\n---\nn2@0 follower() ⇨ n2@1 candidate\nn2@1 → n1 Campaign last=0@0\nn2@1 → n3 Campaign last=0@0\n\ndeliver 3\n---\nn3@1 → n2 CampaignResponse vote=false\n\n# n1 wins leadership.\n(stabilize)\nstatus\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=0@0 applied=0\nn3@1 follower(n1) last=1@1 commit=0@0 applied=0\n"
  },
  {
    "path": "src/raft/testscripts/node/tick_candidate",
    "content": "# Ticking a candidate will eventually hold a new election in a later term.\n\ncluster nodes=3 heartbeat_interval=1 election_timeout=2\n---\nn1@0 follower() last=0@0 commit=0@0 applied=0\nn2@0 follower() last=0@0 commit=0@0 applied=0\nn3@0 follower() last=0@0 commit=0@0 applied=0\n\n# n1 campaigns.\ncampaign 1\n---\nn1@0 follower() ⇨ n1@1 candidate\nn1@1 → n2 Campaign last=0@0\nn1@1 → n3 Campaign last=0@0\n\n# A single tick does nothing.\ntick 1\n---\nok\n\n# Another tick campaigns in a later term.\ntick 1\n---\nn1@1 candidate ⇨ n1@2 candidate\nn1@2 → n2 Campaign last=0@0\nn1@2 → n3 Campaign last=0@0\n\nstatus\n---\nn1@2 candidate last=0@0 commit=0@0 applied=0\nn2@0 follower() last=0@0 commit=0@0 applied=0\nn3@0 follower() last=0@0 commit=0@0 applied=0\n"
  },
  {
    "path": "src/raft/testscripts/node/tick_follower",
    "content": "# Ticking a follower will transition it to candidate if it hasn't\n# heard from the leader in a while.\n\ncluster nodes=3 leader=1 heartbeat_interval=1 election_timeout=2\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# A single follower tick does nothing.\ntick 2\n---\nok\n\n# If n1 heartbeats, the election counter is reset, and another n2 tick does nothing.\nheartbeat 1\nstabilize\n---\nn1@1 → n2 Heartbeat last_index=1 commit_index=1 read_seq=0\nn1@1 → n3 Heartbeat last_index=1 commit_index=1 read_seq=0\nn2@1 → n1 HeartbeatResponse match_index=1 read_seq=0\nn3@1 → n1 HeartbeatResponse match_index=1 read_seq=0\n\ntick 2\n---\nok\n\n# Ticking n2 again exceeds the election timeout, making it campaign.\ntick 2\n---\nn2@1 follower(n1) ⇨ n2@2 candidate\nn2@2 → n1 Campaign last=1@1\nn2@2 → n3 Campaign last=1@1\n\nstatus\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@2 candidate last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n"
  },
  {
    "path": "src/raft/testscripts/node/tick_follower_leaderless",
    "content": "# Ticking a leaderless follower will eventually transition it to candidate.\n\ncluster nodes=3 heartbeat_interval=1 election_timeout=2\n---\nn1@0 follower() last=0@0 commit=0@0 applied=0\nn2@0 follower() last=0@0 commit=0@0 applied=0\nn3@0 follower() last=0@0 commit=0@0 applied=0\n\n# A single follower tick does nothing.\ntick 2\n---\nok\n\n# Another tick makes it campaign.\ntick 2\n---\nn2@0 follower() ⇨ n2@1 candidate\nn2@1 → n1 Campaign last=0@0\nn2@1 → n3 Campaign last=0@0\n\nstatus\n---\nn1@0 follower() last=0@0 commit=0@0 applied=0\nn2@1 candidate last=0@0 commit=0@0 applied=0\nn3@0 follower() last=0@0 commit=0@0 applied=0\n"
  },
  {
    "path": "src/raft/testscripts/node/tick_leader",
    "content": "# Ticking a leader should cause it to emit heartbeats, even when it doesn't\n# hear back from any followers.\n\ncluster nodes=3 leader=1 heartbeat_interval=1 election_timeout=2\n---\nn1@1 leader last=1@1 commit=1@1 applied=1 progress={2:1→2 3:1→2}\nn2@1 follower(n1) last=1@1 commit=1@1 applied=1\nn3@1 follower(n1) last=1@1 commit=1@1 applied=1\n\n# Ticking n1 will emit a heartbeat.\ntick 1\n---\nn1@1 → n2 Heartbeat last_index=1 commit_index=1 read_seq=0\nn1@1 → n3 Heartbeat last_index=1 commit_index=1 read_seq=0\n\n# Ticking n1 again will emit further heartbeats, even when it hasn't heard from\n# any followers.\ntick 1\ntick 1\ntick 1\n---\nn1@1 → n2 Heartbeat last_index=1 commit_index=1 read_seq=0\nn1@1 → n3 Heartbeat last_index=1 commit_index=1 read_seq=0\nn1@1 → n2 Heartbeat last_index=1 commit_index=1 read_seq=0\nn1@1 → n3 Heartbeat last_index=1 commit_index=1 read_seq=0\nn1@1 → n2 Heartbeat last_index=1 commit_index=1 read_seq=0\nn1@1 → n3 Heartbeat last_index=1 commit_index=1 read_seq=0\n"
  },
  {
    "path": "src/server.rs",
    "content": "use std::collections::HashMap;\nuse std::io::{BufReader, BufWriter, Write as _};\nuse std::net::{TcpListener, TcpStream, ToSocketAddrs};\nuse std::time::Duration;\n\nuse crossbeam::channel::{Receiver, Sender};\nuse log::{debug, error, info};\nuse serde::{Deserialize, Serialize};\nuse uuid::Uuid;\n\nuse crate::encoding::{self, Value as _};\nuse crate::error::Result;\nuse crate::raft;\nuse crate::sql;\nuse crate::sql::engine::{Catalog as _, Engine as _};\nuse crate::sql::execution::StatementResult;\nuse crate::sql::types::{Row, Table};\nuse crate::storage;\n\n/// The outbound Raft peer channel capacity. This buffers messages when a Raft\n/// peer is slow or unavailable. Beyond this, messages will be dropped.\nconst RAFT_PEER_CHANNEL_CAPACITY: usize = 1000;\n\n/// The retry interval when connecting to a Raft peer.\nconst RAFT_PEER_RETRY_INTERVAL: Duration = Duration::from_secs(1);\n\n/// A toyDB server. Routes messages to/from an inner Raft node.\n///\n/// * Listens for inbound SQL connections from clients via TCP and passes\n///   requests to the local Raft node.\n///\n/// * Listens for inbound Raft connections from other toyDB nodes via TCP and\n///   passes messages to the local Raft node.\n///\n/// * Connects to other toyDB nodes via TCP and sends outbound Raft messages\n///   from the local Raft node.\npub struct Server {\n    /// The inner Raft node.\n    node: raft::Node,\n    /// Outbound messages from the Raft node.\n    node_rx: Receiver<raft::Envelope>,\n    /// Raft peer IDs and addresses.\n    peers: HashMap<raft::NodeID, String>,\n}\n\nimpl Server {\n    /// Creates a new toyDB server.\n    pub fn new(\n        id: raft::NodeID,\n        peers: HashMap<raft::NodeID, String>,\n        raft_log: raft::Log,\n        raft_state: Box<dyn raft::State>,\n    ) -> Result<Self> {\n        let (node_tx, node_rx) = crossbeam::channel::unbounded();\n        let node = raft::Node::new(\n            id,\n            peers.keys().copied().collect(),\n            raft_log,\n            raft_state,\n            node_tx,\n            raft::Options::default(),\n        )?;\n        Ok(Self { node, peers, node_rx })\n    }\n\n    /// Serves Raft and SQL requests indefinitely. Consumes the server.\n    pub fn serve(self, raft_addr: impl ToSocketAddrs, sql_addr: impl ToSocketAddrs) -> Result<()> {\n        let raft_listener = TcpListener::bind(raft_addr)?;\n        let sql_listener = TcpListener::bind(sql_addr)?;\n        info!(\n            \"Listening on {} (SQL) and {} (Raft)\",\n            sql_listener.local_addr()?,\n            raft_listener.local_addr()?\n        );\n\n        std::thread::scope(move |s| {\n            let id = self.node.id();\n            let (raft_request_tx, raft_request_rx) = crossbeam::channel::unbounded();\n            let (raft_step_tx, raft_step_rx) = crossbeam::channel::unbounded();\n\n            // Serve inbound Raft connections.\n            s.spawn(move || Self::raft_accept(raft_listener, raft_step_tx));\n\n            // Establish outbound Raft connections to peers.\n            let mut raft_peers_tx = HashMap::new();\n            for (id, addr) in self.peers.into_iter() {\n                let (raft_peer_tx, raft_peer_rx) =\n                    crossbeam::channel::bounded(RAFT_PEER_CHANNEL_CAPACITY);\n                raft_peers_tx.insert(id, raft_peer_tx);\n                s.spawn(move || Self::raft_send_peer(addr, raft_peer_rx));\n            }\n\n            // Route Raft messages between the local node, peers, and clients.\n            s.spawn(move || {\n                Self::raft_route(\n                    self.node,\n                    self.node_rx,\n                    raft_step_rx,\n                    raft_peers_tx,\n                    raft_request_rx,\n                )\n            });\n\n            // Serve inbound SQL connections.\n            let sql_engine = sql::engine::Raft::new(raft_request_tx);\n            s.spawn(move || Self::sql_accept(id, sql_listener, sql_engine));\n        });\n\n        Ok(())\n    }\n\n    /// Accepts new inbound Raft connections from peers and spawns threads\n    /// routing inbound messages to the local Raft node.\n    fn raft_accept(listener: TcpListener, raft_step_tx: Sender<raft::Envelope>) {\n        std::thread::scope(|s| {\n            loop {\n                let (socket, peer) = match listener.accept() {\n                    Ok((socket, peer)) => (socket, peer),\n                    Err(err) => {\n                        error!(\"Raft peer accept failed: {err}\");\n                        continue;\n                    }\n                };\n                let raft_step_tx = raft_step_tx.clone();\n                s.spawn(move || {\n                    debug!(\"Raft peer {peer} connected\");\n                    match Self::raft_receive_peer(socket, raft_step_tx) {\n                        Ok(()) => debug!(\"Raft peer {peer} disconnected\"),\n                        Err(err) => error!(\"Raft peer {peer} error: {err}\"),\n                    }\n                });\n            }\n        });\n    }\n\n    /// Receives inbound messages from a peer via TCP, and queues them for\n    /// stepping into the Raft node.\n    fn raft_receive_peer(socket: TcpStream, raft_step_tx: Sender<raft::Envelope>) -> Result<()> {\n        let mut socket = BufReader::new(socket);\n        while let Some(message) = raft::Envelope::maybe_decode_from(&mut socket)? {\n            raft_step_tx.send(message)?;\n        }\n        Ok(())\n    }\n\n    /// Sends outbound messages to a peer via TCP. Retries indefinitely if the\n    /// connection fails.\n    fn raft_send_peer(addr: String, raft_node_rx: Receiver<raft::Envelope>) {\n        loop {\n            let mut socket = match TcpStream::connect(&addr) {\n                Ok(socket) => BufWriter::new(socket),\n                Err(err) => {\n                    error!(\"Failed connecting to Raft peer {addr}: {err}\");\n                    std::thread::sleep(RAFT_PEER_RETRY_INTERVAL);\n                    continue;\n                }\n            };\n            while let Ok(message) = raft_node_rx.recv() {\n                if let Err(err) = message.encode_into(&mut socket).and_then(|_| Ok(socket.flush()?))\n                {\n                    error!(\"Failed sending to Raft peer {addr}: {err}\");\n                    break;\n                }\n            }\n            debug!(\"Disconnected from Raft peer {addr}\");\n        }\n    }\n\n    /// Routes Raft messages:\n    ///\n    /// * node_rx: outbound messages from the local Raft node. Routed to peers\n    ///   via TCP, or to local clients via a response channel.\n    ///\n    /// * request_rx: inbound requests from local SQL clients. Stepped into\n    ///   the local Raft node as ClientRequest messages. Responses are returned\n    ///   via the provided response channel.\n    ///\n    /// * peers_rx: inbound messages from remote Raft peers. Stepped into the\n    ///   local Raft node.\n    ///\n    /// * peers_tx: outbound per-peer channels sent via TCP connections.\n    ///   Messages from the local node's node_rx are sent here.\n    ///\n    /// Panics on any errors, since the Raft node can't recover from failed\n    /// state transitions.\n    fn raft_route(\n        mut node: raft::Node,\n        node_rx: Receiver<raft::Envelope>,\n        peers_rx: Receiver<raft::Envelope>,\n        mut peers_tx: HashMap<raft::NodeID, Sender<raft::Envelope>>,\n        request_rx: Receiver<(raft::Request, Sender<Result<raft::Response>>)>,\n    ) {\n        // Track response channels by request ID. The Raft node will emit\n        // ClientResponse messages that we forward to the response channel.\n        let mut response_txs = HashMap::<raft::RequestID, Sender<Result<raft::Response>>>::new();\n\n        let ticker = crossbeam::channel::tick(raft::TICK_INTERVAL);\n        loop {\n            crossbeam::select! {\n                // Periodically tick the node.\n                recv(ticker) -> _ => node = node.tick().expect(\"tick failed\"),\n\n                // Step messages from peers into the node.\n                recv(peers_rx) -> result => {\n                    let msg = result.expect(\"peers_rx disconnected\");\n                    node = node.step(msg).expect(\"step failed\");\n                },\n\n                // Send outbound messages from the node to the appropriate peer.\n                // If we receive a client response addressed to the local node,\n                // forward it to the waiting client via the response channel.\n                recv(node_rx) -> result => {\n                    let msg = result.expect(\"node_rx disconnected\");\n                    if msg.to == node.id()\n                        && let raft::Message::ClientResponse{ id, response } = msg.message\n                    {\n                        if let Some(response_tx) = response_txs.remove(&id) {\n                            response_tx.send(response).expect(\"response_tx disconnected\");\n                        }\n                        continue\n                    }\n                    let peer_tx = peers_tx.get_mut(&msg.to).expect(\"unknown peer\");\n                    match peer_tx.try_send(msg) {\n                        Ok(()) => {},\n                        Err(crossbeam::channel::TrySendError::Full(_)) => {\n                            error!(\"Raft peer channel full, dropping message\");\n                        },\n                        Err(crossbeam::channel::TrySendError::Disconnected(_)) => {\n                            panic!(\"peer_tx disconnected\");\n                        },\n                    };\n                }\n\n                // Track inbound client requests and step them into the node.\n                recv(request_rx) -> result => {\n                    let (request, response_tx) = result.expect(\"request_rx disconnected\");\n                    let id = Uuid::new_v4();\n                    let msg = raft::Envelope{\n                        from: node.id(),\n                        to: node.id(),\n                        term: node.term(),\n                        message: raft::Message::ClientRequest{id, request},\n                    };\n                    node = node.step(msg).expect(\"step failed\");\n                    response_txs.insert(id, response_tx);\n                }\n            }\n        }\n    }\n\n    /// Accepts new SQL client connections and spawns session threads for them.\n    fn sql_accept(id: raft::NodeID, listener: TcpListener, sql_engine: sql::engine::Raft) {\n        std::thread::scope(|s| {\n            loop {\n                let (socket, peer) = match listener.accept() {\n                    Ok((socket, peer)) => (socket, peer),\n                    Err(err) => {\n                        error!(\"Client accept failed: {err}\");\n                        continue;\n                    }\n                };\n                let session = sql_engine.session();\n                s.spawn(move || {\n                    debug!(\"Client {peer} connected\");\n                    match Self::sql_session(id, socket, session) {\n                        Ok(()) => debug!(\"Client {peer} disconnected\"),\n                        Err(err) => error!(\"Client {peer} error: {err}\"),\n                    }\n                });\n            }\n        })\n    }\n\n    /// Processes a client SQL session, executing SQL statements against the\n    /// Raft node.\n    fn sql_session(\n        id: raft::NodeID,\n        socket: TcpStream,\n        mut session: sql::execution::Session<sql::engine::Raft>,\n    ) -> Result<()> {\n        let mut reader = BufReader::new(socket.try_clone()?);\n        let mut writer = BufWriter::new(socket);\n\n        while let Some(request) = Request::maybe_decode_from(&mut reader)? {\n            // Execute request.\n            debug!(\"Received request {request:?}\");\n            let response = match request {\n                Request::Execute(query) => session.execute(&query).map(Response::Execute),\n                Request::GetTable(table) => {\n                    session.with_txn(true, |txn| txn.must_get_table(&table)).map(Response::GetTable)\n                }\n                Request::ListTables => session\n                    .with_txn(true, |txn| {\n                        Ok(txn.list_tables()?.into_iter().map(|t| t.name).collect())\n                    })\n                    .map(Response::ListTables),\n                Request::Status => session\n                    .status()\n                    .map(|s| Status { server: id, raft: s.raft, mvcc: s.mvcc })\n                    .map(Response::Status),\n            };\n\n            // Process response.\n            debug!(\"Returning response {response:?}\");\n            response.encode_into(&mut writer)?;\n            writer.flush()?;\n        }\n        Ok(())\n    }\n}\n\n/// A SQL client request.\n#[derive(Debug, Serialize, Deserialize)]\npub enum Request {\n    /// Executes a SQL statement.\n    Execute(String),\n    /// Fetches the given table schema.\n    GetTable(String),\n    /// Lists all tables.\n    ListTables,\n    /// Returns server status.\n    Status,\n}\n\nimpl encoding::Value for Request {}\n\n/// A SQL server response.\n#[derive(Debug, Serialize, Deserialize)]\npub enum Response {\n    Execute(StatementResult),\n    Row(Option<Row>),\n    GetTable(Table),\n    ListTables(Vec<String>),\n    Status(Status),\n}\n\nimpl encoding::Value for Response {}\n\n/// SQL server status.\n#[derive(Debug, PartialEq, Serialize, Deserialize)]\npub struct Status {\n    pub server: raft::NodeID,\n    pub raft: raft::Status,\n    pub mvcc: storage::mvcc::Status,\n}\n"
  },
  {
    "path": "src/sql/engine/engine.rs",
    "content": "use std::collections::{BTreeMap, BTreeSet};\n\nuse crate::errinput;\nuse crate::error::Result;\nuse crate::sql::execution::Session;\nuse crate::sql::types::{Expression, Row, Rows, Table, Value};\nuse crate::storage::mvcc;\n\n/// A SQL engine. This provides low-level CRUD (create, read, update, delete)\n/// operations for table rows, a schema catalog for accessing and modifying\n/// table schemas, and interactive SQL sessions that execute client SQL\n/// statements. All engine access is transactional with snapshot isolation.\npub trait Engine<'a>: Sized {\n    /// The engine's transaction type. This provides both row-level CRUD operations and\n    /// transactional access to the schema catalog.\n    type Transaction: Transaction + 'a;\n\n    /// Begins a read-write transaction.\n    fn begin(&'a self) -> Result<Self::Transaction>;\n    /// Begins a read-only transaction.\n    fn begin_read_only(&'a self) -> Result<Self::Transaction>;\n    /// Begins a read-only transaction as of a historical version.\n    fn begin_as_of(&'a self, version: mvcc::Version) -> Result<Self::Transaction>;\n\n    /// Creates a client session for executing SQL statements.\n    fn session(&'a self) -> Session<'a, Self> {\n        Session::new(self)\n    }\n}\n\n/// A SQL transaction. Executes transactional CRUD operations on table rows.\n/// Provides snapshot isolation (see `storage::mvcc` module for details).\n///\n/// All methods operate on row batches rather than single rows to amortize the\n/// cost. With the Raft engine, each call results in a Raft roundtrip, and we'd\n/// rather not have to do that for every single row that's modified.\npub trait Transaction: Catalog {\n    /// The transaction's internal MVCC state.\n    fn state(&self) -> &mvcc::TransactionState;\n\n    /// Commits the transaction.\n    fn commit(self) -> Result<()>;\n    /// Rolls back the transaction.\n    fn rollback(self) -> Result<()>;\n\n    /// Deletes table rows by primary key, if they exist.\n    fn delete(&self, table: &str, ids: &[Value]) -> Result<()>;\n    /// Fetches table rows by primary key, if they exist.\n    fn get(&self, table: &str, ids: &[Value]) -> Result<Vec<Row>>;\n    /// Inserts new table rows.\n    fn insert(&self, table: &str, rows: Vec<Row>) -> Result<()>;\n    /// Looks up a set of primary keys by index values. BTreeSet for testing.\n    fn lookup_index(&self, table: &str, column: &str, values: &[Value]) -> Result<BTreeSet<Value>>;\n    /// Scans a table's rows, optionally applying the given filter.\n    fn scan(&self, table: &str, filter: Option<Expression>) -> Result<Rows>;\n    /// Updates table rows by primary key. BTreeMap for testing.\n    fn update(&self, table: &str, rows: BTreeMap<Value, Row>) -> Result<()>;\n}\n\n/// The catalog stores table schema information. It must be implemented for\n/// Transaction, and is thus fully transactional. For simplicity, it only\n/// supports creating and dropping tables -- there are no ALTER TABLE schema\n/// changes, nor CREATE INDEX.\npub trait Catalog {\n    /// Creates a new table. Errors if it already exists.\n    fn create_table(&self, table: Table) -> Result<()>;\n    /// Drops a table. Errors if it does not exist, unless if_exists is true.\n    /// Returns true if the table existed and was deleted.\n    fn drop_table(&self, table: &str, if_exists: bool) -> Result<bool>;\n    /// Fetches a table schema, or None if it doesn't exist.\n    fn get_table(&self, table: &str) -> Result<Option<Table>>;\n    /// Returns a list of all table schemas.\n    fn list_tables(&self) -> Result<Vec<Table>>;\n\n    /// Fetches a table schema, or errors if it does not exist.\n    fn must_get_table(&self, table: &str) -> Result<Table> {\n        self.get_table(table)?.ok_or_else(|| errinput!(\"table {table} does not exist\"))\n    }\n}\n"
  },
  {
    "path": "src/sql/engine/local.rs",
    "content": "use std::borrow::Cow;\nuse std::collections::{BTreeMap, BTreeSet};\nuse std::slice;\n\nuse itertools::Itertools as _;\nuse serde::{Deserialize, Serialize};\n\nuse super::Catalog;\nuse crate::encoding::{self, Key as _, Value as _};\nuse crate::errinput;\nuse crate::error::Result;\nuse crate::sql::types::{Expression, Row, Rows, Table, Value};\nuse crate::storage::{self, mvcc};\n\n/// SQL engine keys, using the Keycode order-preserving encoding. For\n/// simplicity, table and column names are used directly as identifiers, instead\n/// of e.g. numeric IDs. It is not possible to change table/column names, so\n/// this is fine, if somewhat inefficient.\n///\n/// Uses Cow to allow encoding borrowed values but decoding owned values.\n#[derive(Debug, Deserialize, Serialize)]\npub enum Key<'a> {\n    /// A table schema, keyed by table name. The value is a `sql::types::Table`.\n    Table(Cow<'a, str>),\n    /// A column index entry, keyed by table name, column name, and index value.\n    /// The value is a `BTreeSet` of `sql::types::Value` primary key values.\n    Index(Cow<'a, str>, Cow<'a, str>, Cow<'a, Value>),\n    /// A table row, keyed by table name and primary key value. The value is a\n    /// `sql::types::Row`.\n    Row(Cow<'a, str>, Cow<'a, Value>),\n}\n\nimpl<'a> encoding::Key<'a> for Key<'a> {}\n\n/// Key prefixes, allowing prefix scans of specific parts of the keyspace. These\n/// must match the keys -- in particular, the enum variant indexes must match,\n/// since it's part of the encoded key.\n#[derive(Deserialize, Serialize)]\nenum KeyPrefix<'a> {\n    /// All table schemas.\n    Table,\n    /// All column index entries, keyed by table and column name.\n    Index(Cow<'a, str>, Cow<'a, str>),\n    /// All table rows, keyed by table name.\n    Row(Cow<'a, str>),\n}\n\nimpl<'a> encoding::Key<'a> for KeyPrefix<'a> {}\n\n/// A SQL engine using local storage. This provides the main SQL storage logic.\n/// The Raft SQL engine dispatches to this for node-local SQL storage, executing\n/// the same writes across each nodes' instance of `Local`.\npub struct Local<E: storage::Engine + 'static> {\n    /// The local MVCC storage engine.\n    pub mvcc: mvcc::MVCC<E>,\n}\n\nimpl<E: storage::Engine> Local<E> {\n    /// Creates a new local SQL engine using the given storage engine.\n    pub fn new(engine: E) -> Self {\n        Self { mvcc: mvcc::MVCC::new(engine) }\n    }\n\n    /// Resumes a transaction from the given state. This is usually kept within\n    /// `mvcc::Transaction`, but the Raft-based engine can't retain the MVCC\n    /// transaction across requests since it may be executed on different leader\n    /// nodes, so it instead keeps the state client-side in the session.\n    pub fn resume(&self, state: mvcc::TransactionState) -> Result<Transaction<E>> {\n        Ok(Transaction::new(self.mvcc.resume(state)?))\n    }\n\n    /// Gets an unversioned key, or None if it doesn't exist.\n    pub fn get_unversioned(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {\n        self.mvcc.get_unversioned(key)\n    }\n\n    /// Sets an unversioned key.\n    pub fn set_unversioned(&self, key: &[u8], value: Vec<u8>) -> Result<()> {\n        self.mvcc.set_unversioned(key, value)\n    }\n}\n\nimpl<E: storage::Engine> super::Engine<'_> for Local<E> {\n    type Transaction = Transaction<E>;\n\n    fn begin(&self) -> Result<Self::Transaction> {\n        Ok(Self::Transaction::new(self.mvcc.begin()?))\n    }\n\n    fn begin_read_only(&self) -> Result<Self::Transaction> {\n        Ok(Self::Transaction::new(self.mvcc.begin_read_only()?))\n    }\n\n    fn begin_as_of(&self, version: mvcc::Version) -> Result<Self::Transaction> {\n        Ok(Self::Transaction::new(self.mvcc.begin_as_of(version)?))\n    }\n}\n\n/// A SQL transaction, wrapping an MVCC transaction.\npub struct Transaction<E: storage::Engine + 'static> {\n    txn: mvcc::Transaction<E>,\n}\n\nimpl<E: storage::Engine> Transaction<E> {\n    /// Creates a new SQL transaction using the given MVCC transaction.\n    fn new(txn: mvcc::Transaction<E>) -> Self {\n        Self { txn }\n    }\n\n    /// Returns the transaction's internal state.\n    pub fn state(&self) -> &mvcc::TransactionState {\n        self.txn.state()\n    }\n\n    /// Fetches the matching primary keys for the given secondary index value,\n    /// or an empty set if there is none.\n    fn get_index(&self, table: &str, column: &str, value: &Value) -> Result<BTreeSet<Value>> {\n        debug_assert!(self.has_index(table, column)?, \"no index on {table}.{column}\");\n        Ok(self\n            .txn\n            .get(&Key::Index(table.into(), column.into(), value.into()).encode())?\n            .map(|v| BTreeSet::decode(&v))\n            .transpose()?\n            .unwrap_or_default())\n    }\n\n    /// Fetches a single row by primary key, or None if it doesn't exist.\n    fn get_row(&self, table: &str, id: &Value) -> Result<Option<Row>> {\n        self.txn\n            .get(&Key::Row(table.into(), id.into()).encode())?\n            .map(|v| Row::decode(&v))\n            .transpose()\n    }\n\n    /// Returns true if a secondary index exists for the given column.\n    fn has_index(&self, table: &str, column: &str) -> Result<bool> {\n        let table = self.must_get_table(table)?;\n        Ok(table.columns.iter().find(|c| c.name == column).map(|c| c.index).unwrap_or(false))\n    }\n\n    /// Stores a secondary index entry for the given column value, replacing the\n    /// existing entry if any.\n    fn set_index(\n        &self,\n        table: &str,\n        column: &str,\n        value: &Value,\n        ids: BTreeSet<Value>,\n    ) -> Result<()> {\n        debug_assert!(self.has_index(table, column)?, \"no index on {table}.{column}\");\n        let key = Key::Index(table.into(), column.into(), value.into()).encode();\n        if ids.is_empty() {\n            self.txn.delete(&key)?;\n        } else {\n            self.txn.set(&key, ids.encode())?;\n        }\n        Ok(())\n    }\n\n    /// Returns all tables referencing a table, as (table, column index) pairs.\n    /// This includes any references from the table itself.\n    fn table_references(&self, table: &str) -> Result<Vec<(Table, Vec<usize>)>> {\n        Ok(self\n            .list_tables()?\n            .into_iter()\n            .map(|t| {\n                let references = t\n                    .columns\n                    .iter()\n                    .enumerate()\n                    .filter(|(_, c)| c.references.as_deref() == Some(table))\n                    .map(|(i, _)| i)\n                    .collect_vec();\n                (t, references)\n            })\n            .filter(|(_, references)| !references.is_empty())\n            .collect())\n    }\n}\n\nimpl<E: storage::Engine> super::Transaction for Transaction<E> {\n    fn state(&self) -> &mvcc::TransactionState {\n        self.txn.state()\n    }\n\n    fn commit(self) -> Result<()> {\n        self.txn.commit()\n    }\n\n    fn rollback(self) -> Result<()> {\n        self.txn.rollback()\n    }\n\n    fn delete(&self, table: &str, ids: &[Value]) -> Result<()> {\n        let table = self.must_get_table(table)?;\n        let indexes = table.columns.iter().enumerate().filter(|(_, c)| c.index).collect_vec();\n\n        // Check for foreign key references to the deleted rows.\n        for (source, refs) in self.table_references(&table.name)? {\n            let self_reference = source.name == table.name;\n            for i in refs {\n                let column = &source.columns[i];\n                let mut source_ids = if i == source.primary_key {\n                    // If the reference is from a primary key column, do a lookup.\n                    self.get(&source.name, ids)?\n                        .into_iter()\n                        .map(|row| row.into_iter().nth(i).expect(\"short row\"))\n                        .collect()\n                } else {\n                    // Otherwise (commonly), do a secondary index lookup.\n                    // All foreign keys have a secondary index.\n                    self.lookup_index(&source.name, &column.name, ids)?\n                };\n                // We can ignore any references between the deleted rows,\n                // including a row referencing itself.\n                if self_reference {\n                    for id in ids {\n                        source_ids.remove(id);\n                    }\n                }\n                // Error if the delete would violate referential integrity.\n                if let Some(source_id) = source_ids.first() {\n                    let table = source.name;\n                    let column = &source.columns[source.primary_key].name;\n                    return errinput!(\"row referenced by {table}.{column}={source_id}\");\n                }\n            }\n        }\n\n        for id in ids {\n            // Update any secondary index entries.\n            if !indexes.is_empty()\n                && let Some(row) = self.get_row(&table.name, id)?\n            {\n                for (i, column) in indexes.iter().copied() {\n                    let mut ids = self.get_index(&table.name, &column.name, &row[i])?;\n                    ids.remove(id);\n                    self.set_index(&table.name, &column.name, &row[i], ids)?;\n                }\n            }\n\n            // Delete the row.\n            self.txn.delete(&Key::Row((&table.name).into(), id.into()).encode())?;\n        }\n        Ok(())\n    }\n\n    fn get(&self, table: &str, ids: &[Value]) -> Result<Vec<Row>> {\n        ids.iter().filter_map(|id| self.get_row(table, id).transpose()).collect()\n    }\n\n    fn insert(&self, table: &str, rows: Vec<Row>) -> Result<()> {\n        let table = self.must_get_table(table)?;\n        for row in rows {\n            // Insert the row.\n            table.validate_row(&row, false, self)?;\n            let id = &row[table.primary_key];\n            self.txn.set(&Key::Row((&table.name).into(), id.into()).encode(), row.encode())?;\n\n            // Update any secondary index entries.\n            for (i, column) in table.columns.iter().enumerate().filter(|(_, c)| c.index) {\n                let mut ids = self.get_index(&table.name, &column.name, &row[i])?;\n                ids.insert(id.clone());\n                self.set_index(&table.name, &column.name, &row[i], ids)?;\n            }\n        }\n        Ok(())\n    }\n\n    fn lookup_index(&self, table: &str, column: &str, values: &[Value]) -> Result<BTreeSet<Value>> {\n        debug_assert!(self.has_index(table, column)?, \"no index on {table}.{column}\");\n        values.iter().map(|v| self.get_index(table, column, v)).flatten_ok().collect()\n    }\n\n    fn scan(&self, table: &str, filter: Option<Expression>) -> Result<Rows> {\n        // TODO: this could be simpler if process_results() implemented Clone.\n        let rows = self\n            .txn\n            .scan_prefix(&KeyPrefix::Row(table.into()).encode())\n            .map(|result| result.and_then(|(_, value)| Row::decode(&value)));\n        let Some(filter) = filter else {\n            return Ok(Box::new(rows));\n        };\n        let rows = rows.filter_map(move |result| {\n            result\n                .and_then(|row| match filter.evaluate(Some(&row))? {\n                    Value::Boolean(true) => Ok(Some(row)),\n                    Value::Boolean(false) | Value::Null => Ok(None),\n                    value => errinput!(\"filter returned {value}, expected boolean\"),\n                })\n                .transpose()\n        });\n        Ok(Box::new(rows))\n    }\n\n    fn update(&self, table: &str, rows: BTreeMap<Value, Row>) -> Result<()> {\n        let table = self.must_get_table(table)?;\n        for (id, row) in rows {\n            // If the primary key changes, we simply do a delete and insert.\n            // This simplifies constraint validation.\n            if id != row[table.primary_key] {\n                self.delete(&table.name, &[id])?;\n                self.insert(&table.name, vec![row])?;\n                continue;\n            }\n\n            // Validate the row, but don't write it yet since we may need to\n            // read the existing value to update secondary indexes.\n            table.validate_row(&row, true, self)?;\n\n            // Update indexes, knowing that the primary key has not changed.\n            let indexes = table.columns.iter().enumerate().filter(|(_, c)| c.index).collect_vec();\n            if !indexes.is_empty() {\n                let old = self.get(&table.name, slice::from_ref(&id))?.remove(0);\n                for (i, column) in indexes {\n                    // If the value didn't change, we don't have to do anything.\n                    if old[i] == row[i] {\n                        continue;\n                    }\n\n                    // Remove the old value from the index entry.\n                    let mut ids = self.get_index(&table.name, &column.name, &old[i])?;\n                    ids.remove(&id);\n                    self.set_index(&table.name, &column.name, &old[i], ids)?;\n\n                    // Insert the new value into the index entry.\n                    let mut ids = self.get_index(&table.name, &column.name, &row[i])?;\n                    ids.insert(id.clone());\n                    self.set_index(&table.name, &column.name, &row[i], ids)?;\n                }\n            }\n\n            // Update the row.\n            self.txn.set(&Key::Row((&table.name).into(), (&id).into()).encode(), row.encode())?;\n        }\n        Ok(())\n    }\n}\n\nimpl<E: storage::Engine> Catalog for Transaction<E> {\n    fn create_table(&self, table: Table) -> Result<()> {\n        if self.get_table(&table.name)?.is_some() {\n            return errinput!(\"table {} already exists\", table.name);\n        }\n        table.validate(self)?;\n        self.txn.set(&Key::Table((&table.name).into()).encode(), table.encode())\n    }\n\n    fn drop_table(&self, table: &str, if_exists: bool) -> Result<bool> {\n        let Some(table) = self.get_table(table)? else {\n            if if_exists {\n                return Ok(false);\n            }\n            return errinput!(\"table {table} does not exist\");\n        };\n\n        // Check for foreign key references.\n        if let Some((source, refs)) =\n            self.table_references(&table.name)?.iter().find(|(t, _)| t.name != table.name)\n        {\n            return errinput!(\n                \"table {} is referenced from {}.{}\",\n                table.name,\n                source.name,\n                source.columns[refs[0]].name\n            );\n        }\n\n        // Delete the table schema entry.\n        self.txn.delete(&Key::Table((&table.name).into()).encode())?;\n\n        // Delete the table rows.\n        let prefix = &KeyPrefix::Row((&table.name).into()).encode();\n        let mut keys = self.txn.scan_prefix(prefix).map_ok(|(key, _)| key);\n        while let Some(key) = keys.next().transpose()? {\n            self.txn.delete(&key)?;\n        }\n\n        // Delete any secondary index entries.\n        for column in table.columns.iter().filter(|c| c.index) {\n            let prefix = &KeyPrefix::Index((&table.name).into(), (&column.name).into()).encode();\n            let mut keys = self.txn.scan_prefix(prefix).map_ok(|(key, _)| key);\n            while let Some(key) = keys.next().transpose()? {\n                self.txn.delete(&key)?;\n            }\n        }\n        Ok(true)\n    }\n\n    fn get_table(&self, table: &str) -> Result<Option<Table>> {\n        self.txn.get(&Key::Table(table.into()).encode())?.map(|v| Table::decode(&v)).transpose()\n    }\n\n    fn list_tables(&self) -> Result<Vec<Table>> {\n        self.txn\n            .scan_prefix(&KeyPrefix::Table.encode())\n            .map(|r| r.and_then(|(_, v)| Table::decode(&v)))\n            .collect()\n    }\n}\n"
  },
  {
    "path": "src/sql/engine/mod.rs",
    "content": "//! The SQL engine provides SQL data storage and access, as well as session and\n//! transaction management. The `Local` engine provides node-local on-disk\n//! storage, while the `Raft` engine submits commands through Raft consensus\n//! before dispatching to the `Local` engine on each node.\n\nmod engine;\nmod local;\nmod raft;\n\npub use engine::{Catalog, Engine, Transaction};\npub use local::{Key, Local};\npub use raft::{Raft, Status, Write};\n"
  },
  {
    "path": "src/sql/engine/raft.rs",
    "content": "use std::borrow::Cow;\nuse std::collections::{BTreeMap, BTreeSet};\n\nuse crossbeam::channel::Sender;\nuse serde::de::DeserializeOwned;\nuse serde::{Deserialize, Serialize};\n\nuse super::{Catalog, Engine as _, Transaction as _};\nuse crate::encoding::{self, Value as _, bincode};\nuse crate::errdata;\nuse crate::error::Result;\nuse crate::raft;\nuse crate::sql::types::{Expression, Row, Rows, Table, Value};\nuse crate::storage::{self, mvcc};\n\n/// A read command, submitted via Raft and executed on the leader. Each command\n/// corresponds to a SQL engine method and parameters. Uses Cows to allow\n/// borrowed encoding and owned decoding.\n#[derive(Debug, Serialize, Deserialize)]\npub enum Read<'a> {\n    BeginReadOnly {\n        as_of: Option<mvcc::Version>,\n    },\n    Status,\n\n    Get {\n        txn: Cow<'a, mvcc::TransactionState>,\n        table: Cow<'a, str>,\n        ids: Cow<'a, [Value]>,\n    },\n    LookupIndex {\n        txn: Cow<'a, mvcc::TransactionState>,\n        table: Cow<'a, str>,\n        column: Cow<'a, str>,\n        values: Cow<'a, [Value]>,\n    },\n    Scan {\n        txn: Cow<'a, mvcc::TransactionState>,\n        table: Cow<'a, str>,\n        filter: Option<Expression>,\n    },\n\n    GetTable {\n        txn: Cow<'a, mvcc::TransactionState>,\n        table: Cow<'a, str>,\n    },\n    ListTables {\n        txn: Cow<'a, mvcc::TransactionState>,\n    },\n}\n\nimpl encoding::Value for Read<'_> {}\n\n/// A write command, submitted via Raft and executed on all nodes. Each command\n/// corresponds to a SQL engine method and parameters. Uses Cows to allow\n/// borrowed encoding and owned decoding.\n#[derive(Debug, Serialize, Deserialize)]\npub enum Write<'a> {\n    Begin,\n    Commit(Cow<'a, mvcc::TransactionState>),\n    Rollback(Cow<'a, mvcc::TransactionState>),\n\n    Delete { txn: Cow<'a, mvcc::TransactionState>, table: Cow<'a, str>, ids: Cow<'a, [Value]> },\n    Insert { txn: Cow<'a, mvcc::TransactionState>, table: Cow<'a, str>, rows: Vec<Row> },\n    Update { txn: Cow<'a, mvcc::TransactionState>, table: Cow<'a, str>, rows: BTreeMap<Value, Row> },\n\n    CreateTable { txn: Cow<'a, mvcc::TransactionState>, schema: Table },\n    DropTable { txn: Cow<'a, mvcc::TransactionState>, table: Cow<'a, str>, if_exists: bool },\n}\n\nimpl encoding::Value for Write<'_> {}\n\n/// Raft SQL engine status.\n#[derive(Serialize, Deserialize)]\npub struct Status {\n    pub raft: raft::Status,\n    pub mvcc: mvcc::Status,\n}\n\n/// A Raft-based SQL engine. This dispatches to the `Local` engine for local\n/// storage and processing on each node, but sends read and write commands\n/// through Raft for distributed consensus.\n///\n/// The `Raft` engine itself is simply a Raft client which sends `raft::Request`\n/// to the local Raft node for processing. These requests are applied to the\n/// Raft SQL engine's `State` state machine running below Raft on each node,\n/// which executes the commands on a `Local` SQL engine using a\n/// `storage::Engine` for local storage.\n///\n/// For more details on how SQL statements flow through the engine, see the\n/// `sql` module documentation.\npub struct Raft {\n    /// Sends requests to the local Raft node, along with a response channel.\n    tx: Sender<(raft::Request, Sender<Result<raft::Response>>)>,\n}\n\nimpl Raft {\n    /// The unversioned key used to store the applied index. Just uses a string\n    /// for simplicity.\n    pub const APPLIED_INDEX_KEY: &'static [u8] = b\"applied_index\";\n\n    /// Creates a new Raft-based SQL engine, with a channel to send requests to\n    /// the local Raft node.\n    pub fn new(tx: Sender<(raft::Request, Sender<Result<raft::Response>>)>) -> Self {\n        Self { tx }\n    }\n\n    /// Creates the Raft-managed state machine for the Raft engine. Receives\n    /// commands from the Raft engine and executes them on a `Local` engine.\n    pub fn new_state<E: storage::Engine>(engine: E) -> Result<State<E>> {\n        State::new(engine)\n    }\n\n    /// Executes a request against the Raft cluster, waiting for the response.\n    fn request(&self, request: raft::Request) -> Result<raft::Response> {\n        let (response_tx, response_rx) = crossbeam::channel::bounded(1);\n        self.tx.send((request, response_tx))?;\n        response_rx.recv()?\n    }\n\n    /// Writes through Raft, deserializing the response into the return type.\n    fn write<V: DeserializeOwned>(&self, write: Write) -> Result<V> {\n        match self.request(raft::Request::Write(write.encode()))? {\n            raft::Response::Write(response) => bincode::deserialize(&response),\n            response => errdata!(\"unexpected Raft write response {response:?}\"),\n        }\n    }\n\n    /// Reads from Raft, deserializing the response into the return type.\n    fn read<V: DeserializeOwned>(&self, read: Read) -> Result<V> {\n        match self.request(raft::Request::Read(read.encode()))? {\n            raft::Response::Read(response) => bincode::deserialize(&response),\n            response => errdata!(\"unexpected Raft read response {response:?}\"),\n        }\n    }\n\n    /// Raft SQL engine status.\n    pub fn status(&self) -> Result<Status> {\n        let raft = match self.request(raft::Request::Status)? {\n            raft::Response::Status(status) => status,\n            response => return errdata!(\"unexpected Raft status response {response:?}\"),\n        };\n        let mvcc = self.read(Read::Status)?;\n        Ok(Status { raft, mvcc })\n    }\n}\n\nimpl<'a> super::Engine<'a> for Raft {\n    type Transaction = Transaction<'a>;\n\n    fn begin(&'a self) -> Result<Self::Transaction> {\n        Transaction::begin(self, false, None)\n    }\n\n    fn begin_read_only(&'a self) -> Result<Self::Transaction> {\n        Transaction::begin(self, true, None)\n    }\n\n    fn begin_as_of(&'a self, version: mvcc::Version) -> Result<Self::Transaction> {\n        Transaction::begin(self, true, Some(version))\n    }\n}\n\n/// A Raft SQL engine transaction.\n///\n/// This keeps track of the transaction state in memory. An `mvcc::Transaction`\n/// normally manages this, but since `mvcc::Transaction` runs below Raft, it\n/// can't maintain this state between individual requests (which could execute\n/// on different leaders). Instead, it uses `mvcc::Transaction::resume` to\n/// resume the transaction from the provided transaction state for each request.\npub struct Transaction<'a> {\n    /// The Raft SQL engine client, used to communicate with Raft.\n    raft: &'a Raft,\n    /// The MVCC transaction state.\n    state: mvcc::TransactionState,\n}\n\nimpl<'a> Transaction<'a> {\n    /// Starts a transaction in the given mode.\n    fn begin(raft: &'a Raft, read_only: bool, as_of: Option<mvcc::Version>) -> Result<Self> {\n        assert!(as_of.is_none() || read_only, \"can't use as_of without read_only\");\n        // Read-only transactions don't allocate a new MVCC version, so they\n        // don't write anything -- they just grab the current transaction state.\n        // Submit them as reads to avoid a replication roundtrip.\n        let state = if read_only || as_of.is_some() {\n            raft.read(Read::BeginReadOnly { as_of })?\n        } else {\n            raft.write(Write::Begin)?\n        };\n        Ok(Self { raft, state })\n    }\n}\n\nimpl super::Transaction for Transaction<'_> {\n    fn state(&self) -> &mvcc::TransactionState {\n        &self.state\n    }\n\n    fn commit(self) -> Result<()> {\n        if self.state.read_only {\n            return Ok(()); // noop\n        }\n        self.raft.write(Write::Commit(self.state.into()))\n    }\n\n    fn rollback(self) -> Result<()> {\n        if self.state.read_only {\n            return Ok(()); // noop\n        }\n        self.raft.write(Write::Rollback(self.state.into()))\n    }\n\n    fn delete(&self, table: &str, ids: &[Value]) -> Result<()> {\n        self.raft.write(Write::Delete {\n            txn: (&self.state).into(),\n            table: table.into(),\n            ids: ids.into(),\n        })\n    }\n\n    fn get(&self, table: &str, ids: &[Value]) -> Result<Vec<Row>> {\n        self.raft.read(Read::Get {\n            txn: (&self.state).into(),\n            table: table.into(),\n            ids: ids.into(),\n        })\n    }\n\n    fn insert(&self, table: &str, rows: Vec<Row>) -> Result<()> {\n        self.raft.write(Write::Insert { txn: (&self.state).into(), table: table.into(), rows })\n    }\n\n    fn lookup_index(&self, table: &str, column: &str, values: &[Value]) -> Result<BTreeSet<Value>> {\n        self.raft.read(Read::LookupIndex {\n            txn: (&self.state).into(),\n            table: table.into(),\n            column: column.into(),\n            values: values.into(),\n        })\n    }\n\n    fn scan(&self, table: &str, filter: Option<Expression>) -> Result<Rows> {\n        let scan: Vec<Row> = self.raft.read(Read::Scan {\n            txn: (&self.state).into(),\n            table: table.into(),\n            filter,\n        })?;\n        Ok(Box::new(scan.into_iter().map(Ok)))\n    }\n\n    fn update(&self, table: &str, rows: BTreeMap<Value, Row>) -> Result<()> {\n        self.raft.write(Write::Update { txn: (&self.state).into(), table: table.into(), rows })\n    }\n}\n\nimpl Catalog for Transaction<'_> {\n    fn create_table(&self, schema: Table) -> Result<()> {\n        self.raft.write(Write::CreateTable { txn: (&self.state).into(), schema })\n    }\n\n    fn drop_table(&self, table: &str, if_exists: bool) -> Result<bool> {\n        self.raft.write(Write::DropTable {\n            txn: (&self.state).into(),\n            table: table.into(),\n            if_exists,\n        })\n    }\n\n    fn get_table(&self, table: &str) -> Result<Option<Table>> {\n        self.raft.read(Read::GetTable { txn: (&self.state).into(), table: table.into() })\n    }\n\n    fn list_tables(&self) -> Result<Vec<Table>> {\n        self.raft.read(Read::ListTables { txn: (&self.state).into() })\n    }\n}\n\n/// The state machine for the Raft SQL engine. Receives commands via Raft and\n/// dispatches to a `Local` SQL engine which does the actual work, using a\n/// `storage::Engine` for storage.\n///\n/// For simplicity, we don't attempt to stream large requests or responses,\n/// instead just delivering them as one large chunk. This means that e.g. a full\n/// table scan will pull the entire table into memory, serialize it, and send it\n/// across the network as one message, but that's fine for toyDB.\npub struct State<E: storage::Engine + 'static> {\n    /// The local SQL engine, used for actual storage.\n    local: super::Local<E>,\n    /// The last applied index. This tells Raft which command to apply next.\n    applied_index: raft::Index,\n}\n\nimpl<E: storage::Engine> State<E> {\n    /// Creates a new Raft state maching using the given storage engine for\n    /// local storage.\n    pub fn new(engine: E) -> Result<Self> {\n        let local = super::Local::new(engine);\n        let applied_index = local\n            .get_unversioned(Raft::APPLIED_INDEX_KEY)?\n            .map(|b| bincode::deserialize(&b))\n            .transpose()?\n            .unwrap_or_default();\n        Ok(State { local, applied_index })\n    }\n\n    /// Executes a write command. This is executed on all nodes, but the\n    /// response is returned from the Raft leader.\n    ///\n    /// The response is encoded using Bincode. The caller will know what\n    /// response type to expect for each command and deserialize into it.\n    fn write(&self, command: Write) -> Result<Vec<u8>> {\n        Ok(match command {\n            Write::Begin => self.local.begin()?.state().encode(),\n            Write::Commit(txn) => {\n                bincode::serialize(&self.local.resume(txn.into_owned())?.commit()?)\n            }\n            Write::Rollback(txn) => {\n                bincode::serialize(&self.local.resume(txn.into_owned())?.rollback()?)\n            }\n\n            Write::Delete { txn, table, ids } => {\n                bincode::serialize(&self.local.resume(txn.into_owned())?.delete(&table, &ids)?)\n            }\n            Write::Insert { txn, table, rows } => {\n                bincode::serialize(&self.local.resume(txn.into_owned())?.insert(&table, rows)?)\n            }\n            Write::Update { txn, table, rows } => {\n                bincode::serialize(&self.local.resume(txn.into_owned())?.update(&table, rows)?)\n            }\n\n            Write::CreateTable { txn, schema } => {\n                bincode::serialize(&self.local.resume(txn.into_owned())?.create_table(schema)?)\n            }\n            Write::DropTable { txn, table, if_exists } => bincode::serialize(\n                &self.local.resume(txn.into_owned())?.drop_table(&table, if_exists)?,\n            ),\n        })\n    }\n}\n\nimpl<E: storage::Engine> raft::State for State<E> {\n    fn get_applied_index(&self) -> raft::Index {\n        self.applied_index\n    }\n\n    fn apply(&mut self, entry: raft::Entry) -> Result<Vec<u8>> {\n        assert_eq!(entry.index, self.applied_index + 1, \"entry index not after applied index\");\n\n        let result = match &entry.command {\n            Some(command) => match self.write(Write::decode(command)?) {\n                // Panic on non-deterministic apply failures, to prevent node\n                // state divergence. See `raft::State` docs for details.\n                Err(e) if !e.is_deterministic() => panic!(\"non-deterministic apply failure: {e}\"),\n                result => result,\n            },\n            // Raft submits noop commands on leader changes. Ignore them, but\n            // record the applied index below.\n            None => Ok(Vec::new()),\n        };\n\n        // Persist the applied index. We don't have to flush, because it's ok to\n        // lose a tail of the state machine writes (e.g. if the machine\n        // crashes). Raft will replay the log from the last known applied index.\n        self.applied_index = entry.index;\n        self.local.set_unversioned(Raft::APPLIED_INDEX_KEY, bincode::serialize(&entry.index))?;\n        result\n    }\n\n    fn read(&self, command: Vec<u8>) -> Result<Vec<u8>> {\n        Ok(match Read::decode(&command)? {\n            Read::BeginReadOnly { as_of } => {\n                let txn = match as_of {\n                    Some(version) => self.local.begin_as_of(version)?,\n                    None => self.local.begin_read_only()?,\n                };\n                txn.state().encode()\n            }\n            Read::Status => self.local.mvcc.status()?.encode(),\n\n            Read::Get { txn, table, ids } => {\n                self.local.resume(txn.into_owned())?.get(&table, &ids)?.encode()\n            }\n            Read::LookupIndex { txn, table, column, values } => self\n                .local\n                .resume(txn.into_owned())?\n                .lookup_index(&table, &column, &values)?\n                .encode(),\n            Read::Scan { txn, table, filter } => {\n                // For simplicity, buffer the entire scan. See `State` comment.\n                self.local\n                    .resume(txn.into_owned())?\n                    .scan(&table, filter)?\n                    .collect::<Result<Vec<Row>>>()?\n                    .encode()\n            }\n\n            Read::GetTable { txn, table } => {\n                self.local.resume(txn.into_owned())?.get_table(&table)?.encode()\n            }\n            Read::ListTables { txn } => {\n                self.local.resume(txn.into_owned())?.list_tables()?.encode()\n            }\n        })\n    }\n}\n"
  },
  {
    "path": "src/sql/execution/aggregator.rs",
    "content": "use std::collections::BTreeMap;\n\nuse itertools::Itertools as _;\n\nuse crate::error::Result;\nuse crate::sql::planner::Aggregate;\nuse crate::sql::types::{Expression, Row, Rows, Value};\n\n/// Computes bucketed aggregates for input rows. For example, this query would\n/// compute COUNT and SUM aggregates bucketed by category and brand:\n///\n/// SELECT COUNT(*), SUM(price) FROM products GROUP BY category, brand\npub struct Aggregator {\n    /// GROUP BY expressions.\n    group_by: Vec<Expression>,\n    /// Aggregates to compute.\n    aggregates: Vec<Aggregate>,\n    /// Accumulators indexed by group_by bucket.\n    buckets: BTreeMap<Vec<Value>, Vec<Accumulator>>,\n}\n\nimpl Aggregator {\n    /// Creates a new aggregator for the given GROUP BY buckets and aggregates.\n    pub fn new(group_by: Vec<Expression>, aggregates: Vec<Aggregate>) -> Self {\n        Self { group_by, aggregates, buckets: BTreeMap::new() }\n    }\n\n    /// Adds a row to the aggregator.\n    pub fn add(&mut self, row: &Row) -> Result<()> {\n        // Compute the bucket values.\n        let bucket = self.group_by.iter().map(|expr| expr.evaluate(Some(row))).try_collect()?;\n\n        // Look up the bucket accumulators, or create a new bucket.\n        let accumulators = self\n            .buckets\n            .entry(bucket)\n            .or_insert_with(|| self.aggregates.iter().map(Accumulator::new).collect())\n            .iter_mut();\n\n        // Collect expressions to evaluate.\n        let exprs = self.aggregates.iter().map(|a| a.expr());\n\n        // Accumulate the evaluated values.\n        for (accumulator, expr) in accumulators.zip_eq(exprs) {\n            accumulator.add(expr.evaluate(Some(row))?)?;\n        }\n        Ok(())\n    }\n\n    /// Adds rows to the aggregator.\n    pub fn add_rows(&mut self, rows: Rows) -> Result<()> {\n        for row in rows {\n            self.add(&row?)?;\n        }\n        Ok(())\n    }\n\n    /// Returns a row iterator over the aggregate result.\n    pub fn into_rows(self) -> Rows {\n        // If there were no rows and no group_by expressions, return a row of\n        // empty accumulators (e.g. SELECT COUNT(*) FROM t WHERE FALSE).\n        if self.buckets.is_empty() && self.group_by.is_empty() {\n            let result =\n                self.aggregates.iter().map(Accumulator::new).map(|acc| acc.value()).try_collect();\n            return Box::new(std::iter::once(result));\n        }\n\n        // Emit the group_by and aggregate values for each bucket. We use an\n        // intermediate vec since btree_map::IntoIter doesn't implement Clone\n        // (required by Rows).\n        let buckets = self.buckets.into_iter().collect_vec();\n        Box::new(buckets.into_iter().map(|(bucket, accumulators)| {\n            bucket\n                .into_iter()\n                .map(Ok)\n                .chain(accumulators.into_iter().map(|acc| acc.value()))\n                .collect()\n        }))\n    }\n}\n\n/// Accumulates aggregate values. Uses an enum rather than a trait since we need\n/// to keep these in a vector (could use boxed trait objects too).\n#[derive(Clone)]\nenum Accumulator {\n    Average { count: i64, sum: Value },\n    Count(i64),\n    Max(Option<Value>),\n    Min(Option<Value>),\n    Sum(Option<Value>),\n}\n\nimpl Accumulator {\n    /// Creates a new accumulator from an aggregate kind.\n    fn new(aggregate: &Aggregate) -> Self {\n        match aggregate {\n            Aggregate::Average(_) => Self::Average { count: 0, sum: Value::Integer(0) },\n            Aggregate::Count(_) => Self::Count(0),\n            Aggregate::Max(_) => Self::Max(None),\n            Aggregate::Min(_) => Self::Min(None),\n            Aggregate::Sum(_) => Self::Sum(None),\n        }\n    }\n\n    /// Adds a value to the accumulator.\n    fn add(&mut self, value: Value) -> Result<()> {\n        // Aggregates ignore NULL values.\n        if value == Value::Null {\n            return Ok(());\n        }\n        match self {\n            Self::Average { sum, count } => (*sum, *count) = (sum.checked_add(&value)?, *count + 1),\n            Self::Count(count) => *count += 1,\n            Self::Max(max @ None) => *max = Some(value),\n            Self::Max(Some(max)) if value > *max => *max = value,\n            Self::Max(Some(_)) => {}\n            Self::Min(min @ None) => *min = Some(value),\n            Self::Min(Some(min)) if value < *min => *min = value,\n            Self::Min(Some(_)) => {}\n            Self::Sum(sum @ None) => *sum = Some(Value::Integer(0).checked_add(&value)?),\n            Self::Sum(Some(sum)) => *sum = sum.checked_add(&value)?,\n        }\n        Ok(())\n    }\n\n    /// Returns the aggregate value.\n    fn value(self) -> Result<Value> {\n        Ok(match self {\n            Self::Average { count: 0, sum: _ } => Value::Null,\n            Self::Average { count, sum } => sum.checked_div(&Value::Integer(count))?,\n            Self::Count(count) => count.into(),\n            Self::Max(Some(value)) | Self::Min(Some(value)) | Self::Sum(Some(value)) => value,\n            Self::Max(None) | Self::Min(None) | Self::Sum(None) => Value::Null,\n        })\n    }\n}\n"
  },
  {
    "path": "src/sql/execution/executor.rs",
    "content": "use std::cmp::Ordering;\nuse std::collections::{BTreeMap, HashMap};\n\nuse itertools::{Itertools as _, izip};\n\nuse super::aggregator::Aggregator;\nuse super::join::{HashJoiner, NestedLoopJoiner};\nuse crate::errinput;\nuse crate::error::Result;\nuse crate::sql::engine::Transaction;\nuse crate::sql::planner::{Direction, Node, Plan};\nuse crate::sql::types::{Expression, Label, Row, Rows, Table, Value};\n\n/// Executes statement plans.\n///\n/// The plan root specifies the action to take (e.g. SELECT, INSERT, UPDATE,\n/// etc). It has a nested tree of child nodes that process rows.\n///\n/// Nodes are executed recursively, and return row iterators. Parent nodes\n/// recursively pull input rows from their child nodes, process them, and pass\n/// them on to their parent node.\n///\n/// Below is an example of an (unoptimized) query plan:\n///\n/// SELECT title, released, genres.name AS genre\n/// FROM movies INNER JOIN genres ON movies.genre_id = genres.id\n/// WHERE released >= 2000\n/// ORDER BY released\n///\n/// Select\n/// └─ Order: movies.released desc\n///    └─ Projection: movies.title, movies.released, genres.name as genre\n///       └─ Filter: movies.released >= 2000\n///          └─ NestedLoopJoin: inner on movies.genre_id = genres.id\n///             ├─ Scan: movies\n///             └─ Scan: genres\n///\n/// Rows flow from the tree leaves to the root:\n///\n/// 1. Scan nodes read rows from movies and genres.\n/// 2. NestedLoopJoin joins the rows from movies and genres.\n/// 3. Filter discards rows with release dates older than 2000.\n/// 4. Projection picks out the requested column values from the rows.\n/// 5. Order sorts the rows by release date.\n/// 6. Select returns the final rows to the client.\npub struct Executor<'a, T: Transaction> {\n    /// The transaction used to execute the plan.\n    txn: &'a T,\n}\n\nimpl<'a, T: Transaction> Executor<'a, T> {\n    /// Creates a new executor.\n    pub fn new(txn: &'a T) -> Self {\n        Self { txn }\n    }\n\n    /// Executes a plan, returning an execution result.\n    pub fn execute(&mut self, plan: Plan) -> Result<ExecutionResult> {\n        Ok(match plan {\n            // CREATE TABLE\n            Plan::CreateTable { schema } => {\n                let name = schema.name.clone();\n                self.txn.create_table(schema)?;\n                ExecutionResult::CreateTable { name }\n            }\n\n            // DROP TABLE\n            Plan::DropTable { name, if_exists } => {\n                let existed = self.txn.drop_table(&name, if_exists)?;\n                ExecutionResult::DropTable { name, existed }\n            }\n\n            // DELETE\n            Plan::Delete { table, primary_key, source } => {\n                let source = self.execute_node(source)?;\n                let count = self.delete(&table, primary_key, source)?;\n                ExecutionResult::Delete { count }\n            }\n\n            // INSERT\n            Plan::Insert { table, column_map, source } => {\n                let source = self.execute_node(source)?;\n                let count = self.insert(table, column_map, source)?;\n                ExecutionResult::Insert { count }\n            }\n\n            // SELECT\n            Plan::Select(root) => {\n                let columns = (0..root.columns()).map(|i| root.column_label(i)).collect();\n                let rows = self.execute_node(root)?;\n                ExecutionResult::Select { columns, rows }\n            }\n\n            // UPDATE\n            Plan::Update { table, primary_key, source, expressions } => {\n                let source = self.execute_node(source)?;\n                let count = self.update(&table.name, primary_key, source, expressions)?;\n                ExecutionResult::Update { count }\n            }\n        })\n    }\n\n    /// Recursively executes a query plan node, returning a row iterator.\n    fn execute_node(&mut self, node: Node) -> Result<Rows> {\n        Ok(match node {\n            // GROUP BY and aggregate functions.\n            Node::Aggregate { source, group_by, aggregates } => {\n                let source = self.execute_node(*source)?;\n                let mut aggregator = Aggregator::new(group_by, aggregates);\n                aggregator.add_rows(source)?;\n                aggregator.into_rows()\n            }\n\n            // WHERE and similar filtering.\n            Node::Filter { source, predicate } => {\n                let source = self.execute_node(*source)?;\n                Box::new(source.filter_map(move |result| {\n                    result\n                        .and_then(|row| match predicate.evaluate(Some(&row))? {\n                            Value::Boolean(true) => Ok(Some(row)),\n                            Value::Boolean(false) | Value::Null => Ok(None),\n                            value => errinput!(\"filter returned {value}, expected boolean\",),\n                        })\n                        .transpose()\n                }))\n            }\n\n            // JOIN using a hash join.\n            Node::HashJoin { left, left_column, right, right_column, outer } => {\n                let right_columns = right.columns();\n                let left = self.execute_node(*left)?;\n                let right = self.execute_node(*right)?;\n                Box::new(HashJoiner::new(\n                    left,\n                    left_column,\n                    right,\n                    right_column,\n                    right_columns,\n                    outer,\n                )?)\n            }\n\n            // Looks up primary keys by secondary index values.\n            Node::IndexLookup { table, column, values, alias: _ } => {\n                let column = table.columns.into_iter().nth(column).expect(\"invalid column\").name;\n                let ids =\n                    self.txn.lookup_index(&table.name, &column, &values)?.into_iter().collect_vec();\n                Box::new(self.txn.get(&table.name, &ids)?.into_iter().map(Ok))\n            }\n\n            // Looks up rows by primary key.\n            Node::KeyLookup { table, keys, alias: _ } => {\n                Box::new(self.txn.get(&table.name, &keys)?.into_iter().map(Ok))\n            }\n\n            // LIMIT\n            Node::Limit { source, limit } => Box::new(self.execute_node(*source)?.take(limit)),\n\n            // JOIN using a nested loop join.\n            Node::NestedLoopJoin { left, right, predicate, outer } => {\n                let right_columns = right.columns();\n                let left = self.execute_node(*left)?;\n                let right = self.execute_node(*right)?;\n                Box::new(NestedLoopJoiner::new(left, right, right_columns, predicate, outer))\n            }\n\n            // An empty row iterator.\n            Node::Nothing { .. } => Box::new(std::iter::empty()),\n\n            // OFFSET\n            Node::Offset { source, offset } => Box::new(self.execute_node(*source)?.skip(offset)),\n\n            // ORDER BY\n            Node::Order { source, key } => {\n                let source = self.execute_node(*source)?;\n                Box::new(Self::order(source, key)?)\n            }\n\n            // Projects columns from the source, and evaluates expressions.\n            Node::Projection { source, expressions, aliases: _ } => {\n                let source = self.execute_node(*source)?;\n                Box::new(source.map(move |result| {\n                    let row = result?;\n                    expressions.iter().map(|expr| expr.evaluate(Some(&row))).collect()\n                }))\n            }\n\n            // Remaps source column indexes to new target column indexes.\n            Node::Remap { source, targets } => {\n                let source = self.execute_node(*source)?;\n                let size = targets.iter().copied().flatten().map(|i| i + 1).max().unwrap_or(0);\n                Box::new(source.map_ok(move |row| {\n                    let mut remapped = vec![Value::Null; size];\n                    for (target, value) in targets.iter().copied().zip_eq(row) {\n                        if let Some(target) = target {\n                            remapped[target] = value;\n                        }\n                    }\n                    remapped\n                }))\n            }\n\n            // Scans a table, optionally filtering rows.\n            Node::Scan { table, filter, alias: _ } => Box::new(self.txn.scan(&table.name, filter)?),\n\n            // Emits constant values.\n            Node::Values { rows } => Box::new(\n                rows.into_iter()\n                    .map(|row| row.into_iter().map(|expr| expr.evaluate(None)).collect()),\n            ),\n        })\n    }\n\n    /// DELETE: deletes rows, taking primary keys from the source at the given\n    /// primary_key column index. Returns the number of rows deleted.\n    fn delete(&self, table: &str, primary_key: usize, source: Rows) -> Result<u64> {\n        let ids: Vec<Value> = source\n            .map_ok(|row| row.into_iter().nth(primary_key).expect(\"short row\"))\n            .try_collect()?;\n        let count = ids.len() as u64;\n        self.txn.delete(table, &ids)?;\n        Ok(count)\n    }\n\n    /// INSERT: inserts rows into a table from the given source.\n    ///\n    /// If given, column_map contains the mapping of table → source columns for\n    /// all columns in source. Otherwise, every column in source corresponds to\n    /// those in table, but a tail of source columns may be missing.\n    fn insert(\n        &self,\n        table: Table,\n        column_map: Option<HashMap<usize, usize>>,\n        mut source: Rows,\n    ) -> Result<u64> {\n        let mut rows = Vec::new();\n        while let Some(values) = source.next().transpose()? {\n            // Fast path: the row is already complete, with no column mapping.\n            if values.len() == table.columns.len() && column_map.is_none() {\n                rows.push(values);\n                continue;\n            }\n            if values.len() > table.columns.len() {\n                return errinput!(\"too many values for table {}\", table.name);\n            }\n            if let Some(column_map) = &column_map\n                && column_map.len() != values.len()\n            {\n                return errinput!(\"column and value counts do not match\");\n            }\n\n            // Map source columns to table columns, and fill in default values.\n            let mut row = Vec::with_capacity(table.columns.len());\n            for (i, column) in table.columns.iter().enumerate() {\n                if column_map.is_none() && i < values.len() {\n                    // Pass through the source column to the table column.\n                    row.push(values[i].clone())\n                } else if let Some(vi) = column_map.as_ref().and_then(|c| c.get(&i)).copied() {\n                    // Map the source column to the table column.\n                    row.push(values[vi].clone())\n                } else if let Some(default) = &column.default {\n                    // Column not given in source, use the default.\n                    row.push(default.clone())\n                } else {\n                    return errinput!(\"no value given for column {} with no default\", column.name);\n                }\n            }\n            rows.push(row);\n        }\n        let count = rows.len() as u64;\n        self.txn.insert(&table.name, rows)?;\n        Ok(count)\n    }\n\n    /// UPDATE: updates rows passed in from the source. Returns the number of\n    /// rows updated.\n    fn update(\n        &self,\n        table: &str,\n        primary_key: usize,\n        mut source: Rows,\n        expressions: Vec<(usize, Expression)>,\n    ) -> Result<u64> {\n        let mut updates = BTreeMap::new();\n        while let Some(row) = source.next().transpose()? {\n            let mut update = row.clone();\n            for (column, expr) in &expressions {\n                update[*column] = expr.evaluate(Some(&row))?;\n            }\n            let id = row.into_iter().nth(primary_key).expect(\"short row\");\n            updates.insert(id, update);\n        }\n        let count = updates.len() as u64;\n        self.txn.update(table, updates)?;\n        Ok(count)\n    }\n\n    /// Sorts the input rows.\n    fn order(source: Rows, order: Vec<(Expression, Direction)>) -> Result<Rows> {\n        // We can't use sorted_by_cached_key(), since expression evaluation is\n        // fallible, and since we may have to vary the sort direction of each\n        // expression. Collect the rows and pre-computed sort keys into a vec.\n        let mut rows: Vec<(Row, Vec<Value>)> = source\n            .map(|result| {\n                result.and_then(|row| {\n                    let sort_keys =\n                        order.iter().map(|(expr, _)| expr.evaluate(Some(&row))).try_collect()?;\n                    Ok((row, sort_keys))\n                })\n            })\n            .try_collect()?;\n\n        rows.sort_by(|(_, a_keys), (_, b_keys)| {\n            let dirs = order.iter().map(|(_, dir)| dir).copied();\n            for (a_key, b_key, dir) in izip!(a_keys, b_keys, dirs) {\n                let mut ordering = a_key.cmp(b_key);\n                if dir == Direction::Descending {\n                    ordering = ordering.reverse();\n                }\n                if ordering != Ordering::Equal {\n                    return ordering;\n                }\n            }\n            Ordering::Equal\n        });\n\n        Ok(Box::new(rows.into_iter().map(|(row, _)| Ok(row))))\n    }\n}\n\n/// A plan execution result.\npub enum ExecutionResult {\n    CreateTable { name: String },\n    DropTable { name: String, existed: bool },\n    Delete { count: u64 },\n    Insert { count: u64 },\n    Update { count: u64 },\n    Select { columns: Vec<Label>, rows: Rows },\n}\n"
  },
  {
    "path": "src/sql/execution/join.rs",
    "content": "use std::collections::HashMap;\nuse std::iter::Peekable;\n\nuse crate::errinput;\nuse crate::error::Result;\nuse crate::sql::types::{Expression, Row, Rows, Value};\n\n/// NestedLoopJoiner implements nested loop joins.\n///\n/// For every row in the left source, iterate over the right source and join\n/// them. Rows are filtered on the join predicate, if given.\n///\n/// If outer is true, and there are no matches in the right source for a row in\n/// the left source, a joined row with NULL values for the right source is\n/// returned (typically used for a LEFT JOIN).\n///\n/// This could be trivially implemented with carthesian_product(), but we need\n/// to handle the left outer join case where there is no match in the right\n/// source.\n#[derive(Clone)]\npub struct NestedLoopJoiner {\n    /// The left source.\n    left: Peekable<Rows>,\n    /// The right source.\n    right: Rows,\n    /// The original right iterator state. Can be cloned to reset the\n    /// right source to its original state.\n    right_original: Rows,\n    /// The number of columns in the right source.\n    right_columns: usize,\n    /// True if a right match has been seen for the current left row.\n    right_matched: bool,\n    /// The join predicate.\n    predicate: Option<Expression>,\n    /// If true, emit a row when there is no match in the right source.\n    outer: bool,\n}\n\nimpl NestedLoopJoiner {\n    /// Creates a new nested loop joiner.\n    pub fn new(\n        left: Rows,\n        right: Rows,\n        right_columns: usize,\n        predicate: Option<Expression>,\n        outer: bool,\n    ) -> Self {\n        let left = left.peekable();\n        let right_original = right.clone();\n        Self { left, right, right_original, right_columns, right_matched: false, predicate, outer }\n    }\n\n    // Returns the next joined row, if any.\n    fn try_next(&mut self) -> Result<Option<Row>> {\n        // While there is a valid left row, look for a right-hand match to return.\n        while let Some(Ok(left)) = self.left.peek() {\n            // If there is a match in the remaining right rows, return it.\n            while let Some(right) = self.right.next().transpose()? {\n                let row = left.iter().cloned().chain(right).collect();\n                if let Some(predicate) = &self.predicate {\n                    match predicate.evaluate(Some(&row))? {\n                        Value::Boolean(true) => {}\n                        Value::Boolean(false) | Value::Null => continue,\n                        v => return errinput!(\"join predicate returned {v}, expected boolean\"),\n                    }\n                }\n                self.right_matched = true;\n                return Ok(Some(row));\n            }\n\n            // If there was no right match for the left row, and this is an\n            // outer join, emit a row with right NULLs.\n            if !self.right_matched && self.outer {\n                self.right_matched = true;\n                return Ok(Some(\n                    left.iter()\n                        .cloned()\n                        .chain(std::iter::repeat_n(Value::Null, self.right_columns))\n                        .collect(),\n                ));\n            }\n\n            // We reached the end of the right source. Reset it and move onto\n            // the next left row.\n            self.right = self.right_original.clone();\n            self.right_matched = false;\n            self.left.next().transpose()?;\n        }\n\n        // Otherwise, there's either a None or Err in left. Return it.\n        self.left.next().transpose()\n    }\n}\n\nimpl Iterator for NestedLoopJoiner {\n    type Item = Result<Row>;\n\n    fn next(&mut self) -> Option<Self::Item> {\n        self.try_next().transpose()\n    }\n}\n\n/// HashJoiner implements hash joins.\n///\n/// This builds a hash table of rows from the right source keyed on the join\n/// value, then iterates over the left source and looks up matching rows in the\n/// hash table.\n///\n/// If outer is true, and there is no match in the right source for a row in the\n/// left source, a row with NULL values for the right source is emitted instead.\n#[derive(Clone)]\npub struct HashJoiner {\n    /// The left source.\n    left: Rows,\n    /// The left column to join on.\n    left_column: usize,\n    /// The right hash map to join on.\n    right: HashMap<Value, Vec<Row>>,\n    /// The number of columns in the right source.\n    right_columns: usize,\n    /// If true, emit a row when there is no match in the right source.\n    outer: bool,\n    /// Any pending matches to emit.\n    pending: Rows,\n}\n\nimpl HashJoiner {\n    /// Creates a new hash joiner.\n    pub fn new(\n        left: Rows,\n        left_column: usize,\n        mut right: Rows,\n        right_column: usize,\n        right_columns: usize,\n        outer: bool,\n    ) -> Result<Self> {\n        // Build a hash map from the right source.\n        let mut right_map: HashMap<Value, Vec<Row>> = HashMap::new();\n        while let Some(row) = right.next().transpose()? {\n            let value = row[right_column].clone();\n            if value.is_undefined() {\n                continue; // undefined will never match anything\n            }\n            right_map.entry(value).or_default().push(row);\n        }\n\n        let pending = Box::new(std::iter::empty());\n\n        Ok(Self { left, left_column, right: right_map, right_columns, outer, pending })\n    }\n\n    // Returns the next joined row, if any.\n    fn try_next(&mut self) -> Result<Option<Row>> {\n        // If there's a pending row stashed from a previous call, return it.\n        if let Some(row) = self.pending.next().transpose()? {\n            return Ok(Some(row));\n        }\n\n        // Find the next left row to join with.\n        while let Some(left) = self.left.next().transpose()? {\n            if let Some(right) = self.right.get(&left[self.left_column]).cloned() {\n                // Join with all right matches and stash them in pending.\n                self.pending = Box::new(\n                    right\n                        .into_iter()\n                        .map(move |right| left.iter().cloned().chain(right).collect())\n                        .map(Ok),\n                );\n                return self.pending.next().transpose();\n            } else if self.outer {\n                // If there is no match for the left row, but it's an outer\n                // join, emit a row with right NULLs.\n                return Ok(Some(\n                    left.into_iter()\n                        .chain(std::iter::repeat_n(Value::Null, self.right_columns))\n                        .collect(),\n                ));\n            }\n        }\n\n        Ok(None)\n    }\n}\n\nimpl Iterator for HashJoiner {\n    type Item = Result<Row>;\n\n    fn next(&mut self) -> Option<Self::Item> {\n        self.try_next().transpose()\n    }\n}\n"
  },
  {
    "path": "src/sql/execution/mod.rs",
    "content": "//! Executes statements and plans.\n\nmod aggregator;\nmod executor;\nmod join;\nmod session;\n\npub use executor::{ExecutionResult, Executor};\npub use session::{Session, StatementResult};\n"
  },
  {
    "path": "src/sql/execution/session.rs",
    "content": "use itertools::Itertools as _;\nuse log::error;\nuse serde::{Deserialize, Serialize};\n\nuse crate::error::{Error, Result};\nuse crate::sql::engine::{Engine, Raft, Status, Transaction as _};\nuse crate::sql::execution::ExecutionResult;\nuse crate::sql::parser::{Parser, ast};\nuse crate::sql::planner::Plan;\nuse crate::sql::types::{Label, Row, Rows, Value};\nuse crate::storage::mvcc;\nuse crate::{errdata, errinput};\n\n/// A SQL client session. Parses and executes raw SQL statements and handles\n/// transaction control.\npub struct Session<'a, E: Engine<'a>> {\n    /// The SQL engine.\n    engine: &'a E,\n    /// The current transaction, if any.\n    txn: Option<E::Transaction>,\n}\n\nimpl<'a, E: Engine<'a>> Session<'a, E> {\n    /// Creates a new session using the given SQL engine.\n    pub fn new(engine: &'a E) -> Self {\n        Self { engine, txn: None }\n    }\n\n    /// Executes a client statement.\n    pub fn execute(&mut self, statement: &str) -> Result<StatementResult> {\n        // Parse and execute the statement. Transaction control is handled here,\n        // other statements are handled by the SQL executor.\n        Ok(match Parser::parse(statement)? {\n            // BEGIN: starts a new transaction and returns its state.\n            ast::Statement::Begin { read_only, as_of } => {\n                if self.txn.is_some() {\n                    return errinput!(\"already in a transaction\");\n                }\n                let txn = match (read_only, as_of) {\n                    (false, None) => self.engine.begin()?,\n                    (true, None) => self.engine.begin_read_only()?,\n                    (true, Some(as_of)) => self.engine.begin_as_of(as_of)?,\n                    (false, Some(_)) => {\n                        return errinput!(\"can't start read-write transaction in a given version\");\n                    }\n                };\n                let state = txn.state().clone();\n                self.txn = Some(txn);\n                StatementResult::Begin(state)\n            }\n\n            // COMMIT: commits the currently open transaction, if any.\n            ast::Statement::Commit => {\n                let Some(txn) = self.txn.take() else {\n                    return errinput!(\"not in a transaction\");\n                };\n                let version = txn.state().version;\n                txn.commit()?;\n                StatementResult::Commit { version }\n            }\n\n            // ROLLBACK: rolls back the currently open transaction, if any.\n            ast::Statement::Rollback => {\n                let Some(txn) = self.txn.take() else {\n                    return errinput!(\"not in a transaction\");\n                };\n                let version = txn.state().version;\n                txn.rollback()?;\n                StatementResult::Rollback { version }\n            }\n\n            // EXPLAIN: returns the given SQL query's plan.\n            ast::Statement::Explain(statement) => self.with_txn(true, |txn| {\n                Ok(StatementResult::Explain(Plan::build(*statement, txn)?.optimize()?))\n            })?,\n\n            // Other statements (SELECT etc.) are handled by the SQL executor.\n            statement => {\n                let read_only = matches!(statement, ast::Statement::Select { .. });\n                self.with_txn(read_only, |txn| {\n                    Plan::build(statement, txn)?.optimize()?.execute(txn)?.try_into()\n                })?\n            }\n        })\n    }\n\n    /// Runs a closure in the session's transaction, if there is one, otherwise\n    /// a temporary implicit transaction. If read_only is true, uses a read-only\n    /// implicit transaction. Does not automatically retry errors.\n    pub fn with_txn<F, T>(&mut self, read_only: bool, f: F) -> Result<T>\n    where\n        F: FnOnce(&mut E::Transaction) -> Result<T>,\n    {\n        // Use the current explicit transaction, if there is one.\n        if let Some(ref mut txn) = self.txn {\n            return f(txn);\n        }\n        // Otherwise, use an implicit transaction. Doing this session-side\n        // results in additional Raft roundtrips to begin and complete the\n        // transaction -- we could avoid this if the Raft SQL state machine\n        // supported implicit transactions, but we keep it simple.\n        let mut txn = match read_only {\n            true => self.engine.begin_read_only()?,\n            false => self.engine.begin()?,\n        };\n        let result = f(&mut txn);\n        match result {\n            Ok(_) => txn.commit()?,\n            Err(_) => txn.rollback()?,\n        }\n        result\n    }\n}\n\nimpl Session<'_, Raft> {\n    /// Returns the Raft SQL engine status.\n    pub fn status(&self) -> Result<Status> {\n        self.engine.status()\n    }\n}\n\n/// If the session has an open transaction when dropped, roll it back.\nimpl<'a, E: Engine<'a>> Drop for Session<'a, E> {\n    fn drop(&mut self) {\n        let Some(txn) = self.txn.take() else { return };\n        if let Err(error) = txn.rollback() {\n            error!(\"implicit transaction rollback failed: {error}\")\n        }\n    }\n}\n\n/// A session statement result, returned over the network to SQL clients.\n#[derive(Debug, PartialEq, Serialize, Deserialize)]\npub enum StatementResult {\n    Begin(mvcc::TransactionState),\n    Commit { version: mvcc::Version },\n    Rollback { version: mvcc::Version },\n    Explain(Plan),\n    CreateTable { name: String },\n    DropTable { name: String, existed: bool },\n    Delete { count: u64 },\n    Insert { count: u64 },\n    Update { count: u64 },\n    // For simplicity, we buffer and send the entire set of rows as a vector\n    // instead of streaming them to the client. Streaming reads haven't been\n    // implemented from Raft either, so they're buffered all the way through.\n    Select { columns: Vec<Label>, rows: Vec<Row> },\n}\n\n/// Converts an execution result into a statement result.\nimpl TryFrom<ExecutionResult> for StatementResult {\n    type Error = Error;\n\n    fn try_from(result: ExecutionResult) -> Result<Self> {\n        Ok(match result {\n            ExecutionResult::CreateTable { name } => Self::CreateTable { name },\n            ExecutionResult::DropTable { name, existed } => Self::DropTable { name, existed },\n            ExecutionResult::Delete { count } => Self::Delete { count },\n            ExecutionResult::Insert { count } => Self::Insert { count },\n            ExecutionResult::Update { count } => Self::Update { count },\n            ExecutionResult::Select { rows, columns } => {\n                // We buffer the entire set of rows, for simplicity.\n                Self::Select { columns, rows: rows.try_collect()? }\n            }\n        })\n    }\n}\n\n/// Attempts to convert a SELECT result into a row iterator.\nimpl TryFrom<StatementResult> for Rows {\n    type Error = Error;\n\n    fn try_from(result: StatementResult) -> Result<Self> {\n        let StatementResult::Select { rows, .. } = result else {\n            return errdata!(\"expected select result, found {result:?}\");\n        };\n        Ok(Box::new(rows.into_iter().map(Ok)))\n    }\n}\n\n/// Extracts the first row from a SELECT result.\nimpl TryFrom<StatementResult> for Row {\n    type Error = Error;\n\n    fn try_from(result: StatementResult) -> Result<Self> {\n        let mut rows: Rows = result.try_into()?;\n        rows.next().transpose()?.ok_or_else(|| errdata!(\"no rows returned\"))\n    }\n}\n\n/// Extracts the value of the first column in the first row.\nimpl TryFrom<StatementResult> for Value {\n    type Error = Error;\n\n    fn try_from(result: StatementResult) -> Result<Self> {\n        let row: Row = result.try_into()?;\n        row.into_iter().next().ok_or_else(|| errdata!(\"no columns returned\"))\n    }\n}\n\n/// Extracts the first boolean value of the first column in the first row.\nimpl TryFrom<StatementResult> for bool {\n    type Error = Error;\n\n    fn try_from(result: StatementResult) -> Result<Self> {\n        Value::try_from(result)?.try_into()\n    }\n}\n\n/// Extracts the first f64 value of the first column in the first row.\nimpl TryFrom<StatementResult> for f64 {\n    type Error = Error;\n\n    fn try_from(result: StatementResult) -> Result<Self> {\n        Value::try_from(result)?.try_into()\n    }\n}\n\n/// Extracts the first i64 value of the first column in the first row.\nimpl TryFrom<StatementResult> for i64 {\n    type Error = Error;\n\n    fn try_from(result: StatementResult) -> Result<Self> {\n        Value::try_from(result)?.try_into()\n    }\n}\n\n/// Extracts the first string value of the first column in the first row.\nimpl TryFrom<StatementResult> for String {\n    type Error = Error;\n\n    fn try_from(result: StatementResult) -> Result<Self> {\n        Value::try_from(result)?.try_into()\n    }\n}\n"
  },
  {
    "path": "src/sql/mod.rs",
    "content": "//! Implements a SQL execution engine. A SQL statement flows through the engine\n//! as follows:\n//!\n//! 1. The `toySQL` client connects to the server, which creates a new\n//!    `sql::execution::Session` in `Server::sql_session`.\n//!\n//! 2. `toySQL` submits a SQL `SELECT` string, which the server executes via\n//!    `Session::execute`.\n//!\n//! 3. `Session::execute` calls `Parser::parse` to parse the SQL `SELECT` string\n//!    into an `ast::Statement::Select` AST (Abstract Syntax Tree). The parser\n//!    uses the `Lexer` for initial tokenization.\n//!     \n//! 4. `Session::execute` obtains a new read-only `sql::engine::Transaction` via\n//!    `Session::with_txn`. We'll gloss over the details here.\n//!\n//! 5. `Session::execute` calls `Plan::build` to construct an execution plan\n//!    from the AST via the `Planner`, using the `Transaction`'s\n//!    `sql::engine::Catalog` trait to look up table schema information.\n//!\n//! 6. `Session::execute` calls `Plan::optimize` to optimize the execution plan\n//!    via the optimizers in `sql::planner::optimizer`. This e.g. performs\n//!    filter pushdown to filter rows during storage scans, uses secondary\n//!    indexes where appropriate, and chooses more efficient join types.\n//!\n//! 7. `Session::execute` calls `Plan::execute` to actually execute the plan,\n//!    using the `Transaction` to access the `sql::engine::Engine`.  It uses the\n//!    executors in `sql::execution` to recursively execute the\n//!    `sql::planner::Node` nodes, which stream and process `sql::types::Row`\n//!    vectors via `sql::types::Rows` iterators.\n//!\n//! 8. At the tip of the execution plan there's typically a `Node::Scan` which\n//!    performs full table scans from storage. It is executed by\n//!    `sql::execution::source::scan`, which calls `Transaction::scan`.\n//!\n//! 9. The upper `sql::engine::Raft` engine submits a `Read::Scan` request to\n//!    Raft via `Raft::read` and `Raft::execute`. This is submitted through the\n//!    crossbeam channel `Raft::tx`, which is routed to the local Raft node in\n//!    `Server::raft_route` via `raft::Node::step`.\n//!\n//! 10. We'll skip Raft details, but see the `raft` module documentation. The\n//!     `Read::Scan` request eventually makes its way to the SQL state machine\n//!     `sql::engine::raft::State` that's managed by Raft. Since this is a read\n//!     request, it is executed only on the leader node, calling `State::read`.\n//!\n//! 11. `State` wraps the `sql::engine::Local` SQL execution engine that runs\n//!     on each node, using local storage. `State::read` calls\n//!     `Transaction::scan` using a `Local::Transaction`.\n//!\n//! 12. The `Local` engine uses a `storage::BitCask` engine for local storage,\n//!     with `storage::mvcc` providing transactions. See their documentation\n//!     for details.\n//!\n//! 13. `Transaction::scan` uses `sql::engine::KeyPrefix::Table` to obtain the\n//!     key prefix for the scanned table, encoded via `encoding::keycode`. It\n//!     scans rows under this prefix by calling `MVCC::scan_prefix`, which in\n//!     turn dispatches to `BitCask::scan_prefix`. It returns a row iterator.\n//!\n//! 14. A row iterator is propagated back up through the stack:\n//!     `BitCask` → `MVCC` → `Local` → `State` → `Raft` → `scan` → `Plan::execute`\n//!\n//! 15. `Plan::execute` collects the results in a `ExecutionResult::Select`,\n//!     and returns it to `Session::execute`. It in turns returns it to\n//!     `Server::sql_session`, which encodes it and sends it across the wire\n//!     to `toySQL`, which displays them to the user.\n//!\n//! TODO: expand this into a \"Life of a SQL statement\" document.\n\npub mod engine;\npub mod execution;\npub mod parser;\npub mod planner;\npub mod types;\n\n/// SQL tests are implemented as goldenscripts under src/sql/testscripts.\n#[cfg(test)]\nmod tests {\n    use std::collections::HashMap;\n    use std::error::Error;\n    use std::fmt::Write as _;\n    use std::path::Path;\n    use std::result::Result;\n\n    use crossbeam::channel::Receiver;\n    use itertools::Itertools as _;\n    use tempfile::TempDir;\n    use test_each_file::test_each_path;\n\n    use super::engine::Catalog as _;\n    use super::execution::{Session, StatementResult};\n    use super::parser::Parser;\n    use super::planner::{OPTIMIZERS, Plan};\n    use crate::encoding::format::{self, Formatter as _};\n    use crate::sql::engine::{Engine, Local};\n    use crate::sql::planner::{Planner, Scope};\n    use crate::storage::engine::test as testengine;\n    use crate::storage::{self, Engine as _};\n\n    // Run goldenscript tests in src/sql/testscripts.\n    test_each_path! { in \"src/sql/testscripts/expressions\" as expressions => test_goldenscript_expr }\n    test_each_path! { in \"src/sql/testscripts/optimizers\" as optimizers => test_goldenscript }\n    test_each_path! { in \"src/sql/testscripts/queries\" as queries => test_goldenscript }\n    test_each_path! { in \"src/sql/testscripts/schema\" as schema => test_goldenscript }\n    test_each_path! { in \"src/sql/testscripts/transactions\" as transactions => test_goldenscript }\n    test_each_path! { in \"src/sql/testscripts/writes\" as writes => test_goldenscript }\n\n    /// Runs SQL goldenscripts.\n    fn test_goldenscript(path: &Path) {\n        // The runner's Session can't borrow from an Engine in the same struct,\n        // so pass an engine reference. Use both BitCask and Memory engines and\n        // mirror operations across them. Emit engine operations to op_rx.\n        let (op_tx, op_rx) = crossbeam::channel::unbounded();\n        let tempdir = TempDir::with_prefix(\"toydb\").expect(\"tempdir failed\");\n        let bitcask =\n            storage::BitCask::new(tempdir.path().join(\"bitcask\")).expect(\"bitcask failed\");\n        let memory = storage::Memory::new();\n        let engine =\n            Local::new(testengine::Emit::new(testengine::Mirror::new(bitcask, memory), op_tx));\n        let mut runner = SQLRunner::new(&engine, op_rx);\n\n        goldenscript::run(&mut runner, path).expect(\"goldenscript failed\")\n    }\n\n    /// Runs expression goldenscripts.\n    fn test_goldenscript_expr(path: &Path) {\n        goldenscript::run(&mut ExpressionRunner, path).expect(\"goldenscript failed\")\n    }\n\n    /// The SQL test runner.\n    struct SQLRunner<'a> {\n        engine: &'a TestEngine,\n        sessions: HashMap<String, Session<'a, TestEngine>>,\n        op_rx: Receiver<testengine::Operation>,\n    }\n\n    type TestEngine =\n        Local<testengine::Emit<testengine::Mirror<storage::BitCask, storage::Memory>>>;\n\n    impl<'a> SQLRunner<'a> {\n        fn new(engine: &'a TestEngine, op_rx: Receiver<testengine::Operation>) -> Self {\n            Self { engine, sessions: HashMap::new(), op_rx }\n        }\n    }\n\n    impl goldenscript::Runner for SQLRunner<'_> {\n        fn run(&mut self, command: &goldenscript::Command) -> Result<String, Box<dyn Error>> {\n            let mut output = String::new();\n\n            // Obtain a session based on the command prefix (\"\" if none).\n            let prefix = command.prefix.clone().unwrap_or_default();\n            let session = self.sessions.entry(prefix).or_insert_with(|| self.engine.session());\n\n            // Handle runner commands.\n            match command.name.as_str() {\n                // dump\n                \"dump\" => {\n                    command.consume_args().reject_rest()?;\n                    let mut engine = self.engine.mvcc.engine.lock().expect(\"mutex failed\");\n                    let mut iter = engine.scan(..);\n                    while let Some((key, value)) = iter.next().transpose()? {\n                        let fmtkv = format::MVCC::<format::SQL>::key_value(&key, &value);\n                        let rawkv = format::Raw::key_value(&key, &value);\n                        writeln!(output, \"{fmtkv} [{rawkv}]\",)?;\n                    }\n                    return Ok(output);\n                }\n\n                // schema [TABLE...]\n                \"schema\" => {\n                    let mut args = command.consume_args();\n                    let tables = args.rest_pos().iter().map(|arg| arg.value.clone()).collect_vec();\n                    args.reject_rest()?;\n\n                    let schemas = if tables.is_empty() {\n                        session.with_txn(true, |txn| txn.list_tables())?\n                    } else {\n                        tables\n                            .into_iter()\n                            .map(|t| session.with_txn(true, |txn| txn.must_get_table(&t)))\n                            .try_collect()?\n                    };\n                    return Ok(schemas.into_iter().join(\"\\n\"));\n                }\n\n                // Otherwise, fall through to SQL execution.\n                _ => {}\n            }\n\n            // The entire command is the SQL statement. There are no args.\n            if !command.args.is_empty() {\n                return Err(\"SQL statements should be given as a command with no args\".into());\n            }\n            let input = &command.name;\n            let mut tags = command.tags.clone();\n\n            // Output the plan if requested.\n            if tags.remove(\"plan\") {\n                let ast = Parser::parse(input)?;\n                let plan =\n                    session.with_txn(true, |txn| Planner::new(txn).build(ast)?.optimize())?;\n                writeln!(output, \"{plan}\")?;\n            }\n\n            // Output plan optimizations if requested.\n            if tags.remove(\"opt\") {\n                if tags.contains(\"plan\") {\n                    return Err(\"using both plan and opt is redundant\".into());\n                }\n                let ast = Parser::parse(input)?;\n                let plan = session.with_txn(true, |txn| Planner::new(txn).build(ast))?;\n                let Plan::Select(mut root) = plan else {\n                    return Err(\"can only use opt with SELECT plans\".into());\n                };\n                writeln!(output, \"{}\", format!(\"Initial:\\n{root}\").replace('\\n', \"\\n   \"))?;\n                for optimizer in OPTIMIZERS.iter() {\n                    let prev = root.clone();\n                    root = optimizer.optimize(root)?;\n                    if root != prev {\n                        writeln!(\n                            output,\n                            \"{}\",\n                            format!(\"{optimizer:?}:\\n{root}\").replace('\\n', \"\\n   \")\n                        )?;\n                    }\n                }\n            }\n\n            // Execute the statement.\n            let result = session.execute(input)?;\n\n            // Output engine ops if requested.\n            if tags.remove(\"ops\") {\n                while let Ok(op) = self.op_rx.try_recv() {\n                    match op {\n                        testengine::Operation::Delete { key } => {\n                            let fmtkey = format::MVCC::<format::SQL>::key(&key);\n                            let rawkey = format::Raw::key(&key);\n                            writeln!(output, \"delete {fmtkey} [{rawkey}]\")?;\n                        }\n                        testengine::Operation::Flush => writeln!(output, \"flush\")?,\n                        testengine::Operation::Set { key, value } => {\n                            let fmtkv = format::MVCC::<format::SQL>::key_value(&key, &value);\n                            let rawkv = format::Raw::key_value(&key, &value);\n                            writeln!(output, \"set {fmtkv} [{rawkv}]\")?;\n                        }\n                    }\n                }\n            }\n\n            // Output the result if requested. SELECT results are always output.\n            match result {\n                StatementResult::Select { columns, rows } => {\n                    if tags.remove(\"header\") {\n                        writeln!(output, \"{}\", columns.into_iter().join(\", \"))?;\n                    }\n                    for row in rows {\n                        writeln!(output, \"{}\", row.into_iter().join(\", \"))?;\n                    }\n                }\n                result if tags.remove(\"result\") => writeln!(output, \"{result:?}\")?,\n                _ => {}\n            }\n\n            // Reject unknown tags.\n            if let Some(tag) = tags.iter().next() {\n                return Err(format!(\"unknown tag {tag}\").into());\n            }\n\n            Ok(output)\n        }\n\n        /// Drain unprocessed operations after each command.\n        fn end_command(&mut self, _: &goldenscript::Command) -> Result<String, Box<dyn Error>> {\n            while self.op_rx.try_recv().is_ok() {}\n            Ok(String::new())\n        }\n    }\n\n    /// A test runner for expressions. Evaluates expressions to values, and\n    /// optionally emits the expression tree.\n    struct ExpressionRunner;\n\n    type Catalog<'a> = <Local<storage::Memory> as Engine<'a>>::Transaction;\n\n    impl goldenscript::Runner for ExpressionRunner {\n        fn run(&mut self, command: &goldenscript::Command) -> Result<String, Box<dyn Error>> {\n            let mut output = String::new();\n\n            // The entire command is the expression to evaluate. There are no args.\n            if !command.args.is_empty() {\n                return Err(\"expressions should be given as a command with no args\".into());\n            }\n            let input = &command.name;\n            let mut tags = command.tags.clone();\n\n            // Parse and build the expression.\n            let ast = Parser::parse_expr(input)?;\n            let expr = Planner::<Catalog>::build_expression(ast, &Scope::new())?;\n\n            // Evaluate the expression.\n            let value = expr.evaluate(None)?;\n            write!(output, \"{value}\")?;\n\n            // If requested, convert the expression to conjunctive normal form\n            // and dump it. Assert that it produces the same result.\n            if tags.remove(\"cnf\") {\n                let cnf = expr.clone().into_cnf();\n                assert_eq!(value, cnf.evaluate(None)?, \"CNF result differs\");\n                write!(output, \" ← {cnf}\")?;\n            }\n\n            // If requested, debug-dump the parsed expression.\n            if tags.remove(\"expr\") {\n                write!(output, \" ← {:?}\", expr)?;\n            }\n            writeln!(output)?;\n\n            // Reject unknown tags.\n            if let Some(tag) = tags.iter().next() {\n                return Err(format!(\"unknown tag {tag}\").into());\n            }\n\n            Ok(output)\n        }\n    }\n}\n"
  },
  {
    "path": "src/sql/parser/ast.rs",
    "content": "use std::collections::BTreeMap;\nuse std::hash::{Hash, Hasher};\n\nuse crate::sql::types::DataType;\n\n/// SQL statements are represented as an Abstract Syntax Tree (AST). The\n/// statement is the root node of this tree, and describes the syntactic\n/// structure of a SQL statement. It is built from a raw SQL string by the\n/// parser, and passed on to the planner which validates it and builds an\n/// execution plan from it.\n#[derive(Debug)]\npub enum Statement {\n    /// BEGIN: begins a new transaction.\n    Begin {\n        /// READ ONLY: if true, begin a read-only transaction.\n        read_only: bool,\n        /// AS OF: if given, the MVCC version to read at.\n        as_of: Option<u64>,\n    },\n    /// COMMIT: commits a transaction.\n    Commit,\n    /// ROLLBACK: rolls back a transaction.\n    Rollback,\n    /// EXPLAIN: explains a SQL statement's execution plan.\n    Explain(Box<Statement>),\n    /// CREATE TABLE: creates a new table.\n    CreateTable {\n        /// The table name.\n        name: String,\n        /// Column specifications.\n        columns: Vec<Column>,\n    },\n    /// DROP TABLE: drops a table.\n    DropTable {\n        /// The table to drop.\n        name: String,\n        /// IF EXISTS: if true, don't error if the table doesn't exist.\n        if_exists: bool,\n    },\n    /// DELETE: deletes rows from a table.\n    Delete {\n        /// The table to delete from.\n        table: String,\n        /// WHERE: optional condition to match rows to delete.\n        r#where: Option<Expression>,\n    },\n    /// INSERT INTO: inserts new rows into a table.\n    Insert {\n        /// Table to insert into.\n        table: String,\n        /// Columns to insert values into. If None, all columns are used.\n        columns: Option<Vec<String>>,\n        /// Row values to insert.\n        values: Vec<Vec<Expression>>,\n    },\n    /// UPDATE: updates rows in a table.\n    Update {\n        table: String,\n        set: BTreeMap<String, Option<Expression>>, // column → value, None for default value\n        r#where: Option<Expression>,\n    },\n    /// SELECT: selects rows, possibly from a table.\n    Select {\n        /// Expressions to select, with an optional column alias.\n        select: Vec<(Expression, Option<String>)>,\n        /// FROM: tables to select from.\n        from: Vec<From>,\n        /// WHERE: optional condition to filter rows.\n        r#where: Option<Expression>,\n        /// GROUP BY: expressions to group and aggregate by.\n        group_by: Vec<Expression>,\n        /// HAVING: expression to filter groups by.\n        having: Option<Expression>,\n        /// ORDER BY: expresisions to sort by, with direction.\n        order_by: Vec<(Expression, Direction)>,\n        /// OFFSET: row offset to start from.\n        offset: Option<Expression>,\n        /// LIMIT: maximum number of rows to return.\n        limit: Option<Expression>,\n    },\n}\n\n/// A FROM item.\n#[derive(Debug)]\npub enum From {\n    /// A table.\n    Table {\n        /// The table name.\n        name: String,\n        /// An optional alias for the table.\n        alias: Option<String>,\n    },\n    /// A join of two or more tables (may be nested).\n    Join {\n        /// The left table to join,\n        left: Box<From>,\n        /// The right table to join.\n        right: Box<From>,\n        /// The join type.\n        r#type: JoinType,\n        /// The join condition. None for a cross join.\n        predicate: Option<Expression>,\n    },\n}\n\n/// A CREATE TABLE column definition.\n#[derive(Debug)]\npub struct Column {\n    pub name: String,\n    pub datatype: DataType,\n    pub primary_key: bool,\n    pub nullable: Option<bool>,\n    pub default: Option<Expression>,\n    pub unique: bool,\n    pub index: bool,\n    pub references: Option<String>,\n}\n\n/// JOIN types.\n#[derive(Debug, PartialEq)]\npub enum JoinType {\n    Cross,\n    Inner,\n    Left,\n    Right,\n}\n\nimpl JoinType {\n    // If true, the join is an outer join, where rows with no join matches are\n    // emitted with a NULL match.\n    pub fn is_outer(&self) -> bool {\n        match self {\n            Self::Left | Self::Right => true,\n            Self::Cross | Self::Inner => false,\n        }\n    }\n}\n\n/// ORDER BY direction.\n#[derive(Debug, Default)]\npub enum Direction {\n    #[default]\n    Ascending,\n    Descending,\n}\n\n/// SQL expressions, e.g. `a + 7 > b`. Can be nested.\n#[derive(Clone, Debug, Eq, Hash, PartialEq)]\npub enum Expression {\n    /// All columns, i.e. *.\n    All,\n    /// A column reference, optionally qualified with a table name.\n    Column(Option<String>, String),\n    /// A literal value.\n    Literal(Literal),\n    /// A function call (name and parameters).\n    Function(String, Vec<Expression>),\n    /// An operator.\n    Operator(Operator),\n}\n\n/// Expression literal values.\n#[derive(Clone, Debug)]\npub enum Literal {\n    Null,\n    Boolean(bool),\n    Integer(i64),\n    Float(f64),\n    String(String),\n}\n\n/// To allow using expressions and literals in e.g. hashmaps, implement simple\n/// equality by value for all types, including Null and f64::NAN. This only\n/// checks that the values are the same, and ignores SQL semantics for e.g. NULL\n/// and NaN (which is handled by SQL expression evaluation).\nimpl PartialEq for Literal {\n    fn eq(&self, other: &Self) -> bool {\n        match (self, other) {\n            (Self::Null, Self::Null) => true,\n            (Self::Boolean(l), Self::Boolean(r)) => l == r,\n            (Self::Integer(l), Self::Integer(r)) => l == r,\n            (Self::Float(l), Self::Float(r)) => l.to_bits() == r.to_bits(),\n            (Self::String(l), Self::String(r)) => l == r,\n            (_, _) => false,\n        }\n    }\n}\n\nimpl Eq for Literal {}\n\nimpl Hash for Literal {\n    fn hash<H: Hasher>(&self, state: &mut H) {\n        core::mem::discriminant(self).hash(state);\n        match self {\n            Self::Null => {}\n            Self::Boolean(v) => v.hash(state),\n            Self::Integer(v) => v.hash(state),\n            Self::Float(v) => v.to_bits().hash(state),\n            Self::String(v) => v.hash(state),\n        }\n    }\n}\n\n/// Expression operators.\n///\n/// Since this is a recursive data structure, we have to box each child\n/// expression, which incurs a heap allocation. There are clever ways to get\n/// around this, but we keep it simple.\n#[derive(Clone, Debug, Eq, Hash, PartialEq)]\npub enum Operator {\n    And(Box<Expression>, Box<Expression>), // a AND b\n    Not(Box<Expression>),                  // NOT a\n    Or(Box<Expression>, Box<Expression>),  // a OR b\n\n    Equal(Box<Expression>, Box<Expression>),       // a = b\n    GreaterThan(Box<Expression>, Box<Expression>), // a > b\n    GreaterThanOrEqual(Box<Expression>, Box<Expression>), // a >= b\n    Is(Box<Expression>, Literal),                  // IS NULL or IS NAN\n    LessThan(Box<Expression>, Box<Expression>),    // a < b\n    LessThanOrEqual(Box<Expression>, Box<Expression>), // a <= b\n    NotEqual(Box<Expression>, Box<Expression>),    // a != b\n\n    Add(Box<Expression>, Box<Expression>),          // a + b\n    Divide(Box<Expression>, Box<Expression>),       // a / b\n    Exponentiate(Box<Expression>, Box<Expression>), // a ^ b\n    Factorial(Box<Expression>),                     // a!\n    Identity(Box<Expression>),                      // +a\n    Multiply(Box<Expression>, Box<Expression>),     // a * b\n    Negate(Box<Expression>),                        // -a\n    Remainder(Box<Expression>, Box<Expression>),    // a % b\n    Subtract(Box<Expression>, Box<Expression>),     // a - b\n\n    Like(Box<Expression>, Box<Expression>), // a LIKE b\n}\n\nimpl Expression {\n    /// Walks the expression tree depth-first, calling a closure for every node.\n    /// Halts and returns false if the closure returns false.\n    pub fn walk(&self, visitor: &mut impl FnMut(&Expression) -> bool) -> bool {\n        use Operator::*;\n\n        if !visitor(self) {\n            return false;\n        }\n\n        match self {\n            Self::Operator(op) => match op {\n                Add(lhs, rhs)\n                | And(lhs, rhs)\n                | Divide(lhs, rhs)\n                | Equal(lhs, rhs)\n                | Exponentiate(lhs, rhs)\n                | GreaterThan(lhs, rhs)\n                | GreaterThanOrEqual(lhs, rhs)\n                | LessThan(lhs, rhs)\n                | LessThanOrEqual(lhs, rhs)\n                | Like(lhs, rhs)\n                | Multiply(lhs, rhs)\n                | NotEqual(lhs, rhs)\n                | Or(lhs, rhs)\n                | Remainder(lhs, rhs)\n                | Subtract(lhs, rhs) => lhs.walk(visitor) && rhs.walk(visitor),\n\n                Factorial(expr) | Identity(expr) | Is(expr, _) | Negate(expr) | Not(expr) => {\n                    expr.walk(visitor)\n                }\n            },\n\n            Self::Function(_, exprs) => exprs.iter().any(|expr| expr.walk(visitor)),\n\n            Self::All | Self::Column(_, _) | Self::Literal(_) => true,\n        }\n    }\n\n    /// Walks the expression tree depth-first while calling a closure until it\n    /// returns true. This is the inverse of walk().\n    pub fn contains(&self, visitor: &impl Fn(&Expression) -> bool) -> bool {\n        !self.walk(&mut |expr| !visitor(expr))\n    }\n\n    /// Find and collects expressions for which the given closure returns true,\n    /// adding them to c. Does not recurse into matching expressions.\n    pub fn collect(&self, visitor: &impl Fn(&Expression) -> bool, exprs: &mut Vec<Expression>) {\n        use Operator::*;\n\n        if visitor(self) {\n            exprs.push(self.clone());\n            return;\n        }\n\n        match self {\n            Self::Operator(op) => match op {\n                Add(lhs, rhs)\n                | And(lhs, rhs)\n                | Divide(lhs, rhs)\n                | Equal(lhs, rhs)\n                | Exponentiate(lhs, rhs)\n                | GreaterThan(lhs, rhs)\n                | GreaterThanOrEqual(lhs, rhs)\n                | LessThan(lhs, rhs)\n                | LessThanOrEqual(lhs, rhs)\n                | Like(lhs, rhs)\n                | Multiply(lhs, rhs)\n                | NotEqual(lhs, rhs)\n                | Or(lhs, rhs)\n                | Remainder(lhs, rhs)\n                | Subtract(lhs, rhs) => {\n                    lhs.collect(visitor, exprs);\n                    rhs.collect(visitor, exprs);\n                }\n                Factorial(expr) | Identity(expr) | Is(expr, _) | Negate(expr) | Not(expr) => {\n                    expr.collect(visitor, exprs);\n                }\n            },\n\n            Self::Function(_, args) => args.iter().for_each(|arg| arg.collect(visitor, exprs)),\n\n            Self::All | Self::Column(_, _) | Self::Literal(_) => {}\n        }\n    }\n}\n\nimpl core::convert::From<Literal> for Expression {\n    fn from(literal: Literal) -> Self {\n        Self::Literal(literal)\n    }\n}\n\nimpl core::convert::From<Operator> for Expression {\n    fn from(op: Operator) -> Self {\n        Self::Operator(op)\n    }\n}\n\nimpl core::convert::From<Operator> for Box<Expression> {\n    fn from(value: Operator) -> Self {\n        Box::new(value.into())\n    }\n}\n"
  },
  {
    "path": "src/sql/parser/lexer.rs",
    "content": "use std::fmt::Display;\nuse std::iter::Peekable;\nuse std::str::Chars;\n\nuse crate::errinput;\nuse crate::error::Result;\n\n/// A lexical token.\n///\n/// These carry owned String clones rather than &str references into the\n/// original input string, because the lexer may need to modify the string (e.g.\n/// to parse escaped quotes in strings, lowercase identifiers, etc). We could\n/// use `Cow<str>` to avoid this in the common case, but we'll end up using\n/// owned strings in the final parsed AST anyway to avoid propagating these\n/// lifetimes throughout the entire SQL execution engine, so we keep it simple.\n#[derive(Clone, Debug, PartialEq)]\npub enum Token {\n    /// A numeric string, with digits, decimal points, and/or exponents. Leading\n    /// signs (e.g. -) are separate tokens.\n    Number(String),\n    /// A Unicode string, with quotes stripped and escape sequences resolved.\n    String(String),\n    /// An identifier, with any quotes stripped. Lowercased if not quoted.\n    Ident(String),\n    /// A SQL keyword.\n    Keyword(Keyword),\n    Period,             // .\n    Equal,              // =\n    NotEqual,           // !=\n    GreaterThan,        // >\n    GreaterThanOrEqual, // >=\n    LessThan,           // <\n    LessThanOrEqual,    // <=\n    LessOrGreaterThan,  // <>\n    Plus,               // +\n    Minus,              // -\n    Asterisk,           // *\n    Slash,              // /\n    Caret,              // ^\n    Percent,            // %\n    Exclamation,        // !\n    Question,           // ?\n    Comma,              // ,\n    Semicolon,          // ;\n    OpenParen,          // (\n    CloseParen,         // )\n}\n\nimpl Display for Token {\n    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {\n        f.write_str(match self {\n            Self::Number(n) => n,\n            Self::String(s) => s,\n            Self::Ident(s) => s,\n            Self::Keyword(k) => return k.fmt(f),\n            Self::Period => \".\",\n            Self::Equal => \"=\",\n            Self::NotEqual => \"!=\",\n            Self::GreaterThan => \">\",\n            Self::GreaterThanOrEqual => \">=\",\n            Self::LessThan => \"<\",\n            Self::LessThanOrEqual => \"<=\",\n            Self::LessOrGreaterThan => \"<>\",\n            Self::Plus => \"+\",\n            Self::Minus => \"-\",\n            Self::Asterisk => \"*\",\n            Self::Slash => \"/\",\n            Self::Caret => \"^\",\n            Self::Percent => \"%\",\n            Self::Exclamation => \"!\",\n            Self::Question => \"?\",\n            Self::Comma => \",\",\n            Self::Semicolon => \";\",\n            Self::OpenParen => \"(\",\n            Self::CloseParen => \")\",\n        })\n    }\n}\n\nimpl From<Keyword> for Token {\n    fn from(keyword: Keyword) -> Self {\n        Self::Keyword(keyword)\n    }\n}\n\n/// Reserved SQL keywords.\n#[derive(Clone, Copy, Debug, PartialEq)]\npub enum Keyword {\n    And,\n    As,\n    Asc,\n    Begin,\n    Bool,\n    Boolean,\n    By,\n    Commit,\n    Create,\n    Cross,\n    Default,\n    Delete,\n    Desc,\n    Double,\n    Drop,\n    Exists,\n    Explain,\n    False,\n    Float,\n    From,\n    Group,\n    Having,\n    If,\n    Index,\n    Infinity,\n    Inner,\n    Insert,\n    Int,\n    Integer,\n    Into,\n    Is,\n    Join,\n    Key,\n    Left,\n    Like,\n    Limit,\n    NaN,\n    Not,\n    Null,\n    Of,\n    Offset,\n    On,\n    Only,\n    Or,\n    Order,\n    Outer,\n    Primary,\n    Read,\n    References,\n    Right,\n    Rollback,\n    Select,\n    Set,\n    String,\n    System,\n    Table,\n    Text,\n    Time,\n    Transaction,\n    True,\n    Unique,\n    Update,\n    Values,\n    Varchar,\n    Where,\n    Write,\n}\n\nimpl TryFrom<&str> for Keyword {\n    // Use a cheap static error string. This just indicates it's not a keyword.\n    type Error = &'static str;\n\n    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {\n        // Only compare lowercase, which is enforced by the lexer. This avoids\n        // allocating a string to change the case. Assert this.\n        debug_assert!(value.chars().all(|c| !c.is_uppercase()), \"keyword must be lowercase\");\n        Ok(match value {\n            \"as\" => Self::As,\n            \"asc\" => Self::Asc,\n            \"and\" => Self::And,\n            \"begin\" => Self::Begin,\n            \"bool\" => Self::Bool,\n            \"boolean\" => Self::Boolean,\n            \"by\" => Self::By,\n            \"commit\" => Self::Commit,\n            \"create\" => Self::Create,\n            \"cross\" => Self::Cross,\n            \"default\" => Self::Default,\n            \"delete\" => Self::Delete,\n            \"desc\" => Self::Desc,\n            \"double\" => Self::Double,\n            \"drop\" => Self::Drop,\n            \"exists\" => Self::Exists,\n            \"explain\" => Self::Explain,\n            \"false\" => Self::False,\n            \"float\" => Self::Float,\n            \"from\" => Self::From,\n            \"group\" => Self::Group,\n            \"having\" => Self::Having,\n            \"if\" => Self::If,\n            \"index\" => Self::Index,\n            \"infinity\" => Self::Infinity,\n            \"inner\" => Self::Inner,\n            \"insert\" => Self::Insert,\n            \"int\" => Self::Int,\n            \"integer\" => Self::Integer,\n            \"into\" => Self::Into,\n            \"is\" => Self::Is,\n            \"join\" => Self::Join,\n            \"key\" => Self::Key,\n            \"left\" => Self::Left,\n            \"like\" => Self::Like,\n            \"limit\" => Self::Limit,\n            \"nan\" => Self::NaN,\n            \"not\" => Self::Not,\n            \"null\" => Self::Null,\n            \"of\" => Self::Of,\n            \"offset\" => Self::Offset,\n            \"on\" => Self::On,\n            \"only\" => Self::Only,\n            \"or\" => Self::Or,\n            \"order\" => Self::Order,\n            \"outer\" => Self::Outer,\n            \"primary\" => Self::Primary,\n            \"read\" => Self::Read,\n            \"references\" => Self::References,\n            \"right\" => Self::Right,\n            \"rollback\" => Self::Rollback,\n            \"select\" => Self::Select,\n            \"set\" => Self::Set,\n            \"string\" => Self::String,\n            \"system\" => Self::System,\n            \"table\" => Self::Table,\n            \"text\" => Self::Text,\n            \"time\" => Self::Time,\n            \"transaction\" => Self::Transaction,\n            \"true\" => Self::True,\n            \"unique\" => Self::Unique,\n            \"update\" => Self::Update,\n            \"values\" => Self::Values,\n            \"varchar\" => Self::Varchar,\n            \"where\" => Self::Where,\n            \"write\" => Self::Write,\n            _ => return Err(\"not a keyword\"),\n        })\n    }\n}\n\nimpl Display for Keyword {\n    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {\n        // Display keywords as uppercase.\n        f.write_str(match self {\n            Self::As => \"AS\",\n            Self::Asc => \"ASC\",\n            Self::And => \"AND\",\n            Self::Begin => \"BEGIN\",\n            Self::Bool => \"BOOL\",\n            Self::Boolean => \"BOOLEAN\",\n            Self::By => \"BY\",\n            Self::Commit => \"COMMIT\",\n            Self::Create => \"CREATE\",\n            Self::Cross => \"CROSS\",\n            Self::Default => \"DEFAULT\",\n            Self::Delete => \"DELETE\",\n            Self::Desc => \"DESC\",\n            Self::Double => \"DOUBLE\",\n            Self::Drop => \"DROP\",\n            Self::Exists => \"EXISTS\",\n            Self::Explain => \"EXPLAIN\",\n            Self::False => \"FALSE\",\n            Self::Float => \"FLOAT\",\n            Self::From => \"FROM\",\n            Self::Group => \"GROUP\",\n            Self::Having => \"HAVING\",\n            Self::If => \"IF\",\n            Self::Index => \"INDEX\",\n            Self::Infinity => \"INFINITY\",\n            Self::Inner => \"INNER\",\n            Self::Insert => \"INSERT\",\n            Self::Int => \"INT\",\n            Self::Integer => \"INTEGER\",\n            Self::Into => \"INTO\",\n            Self::Is => \"IS\",\n            Self::Join => \"JOIN\",\n            Self::Key => \"KEY\",\n            Self::Left => \"LEFT\",\n            Self::Like => \"LIKE\",\n            Self::Limit => \"LIMIT\",\n            Self::NaN => \"NAN\",\n            Self::Not => \"NOT\",\n            Self::Null => \"NULL\",\n            Self::Of => \"OF\",\n            Self::Offset => \"OFFSET\",\n            Self::On => \"ON\",\n            Self::Only => \"ONLY\",\n            Self::Outer => \"OUTER\",\n            Self::Or => \"OR\",\n            Self::Order => \"ORDER\",\n            Self::Primary => \"PRIMARY\",\n            Self::Read => \"READ\",\n            Self::References => \"REFERENCES\",\n            Self::Right => \"RIGHT\",\n            Self::Rollback => \"ROLLBACK\",\n            Self::Select => \"SELECT\",\n            Self::Set => \"SET\",\n            Self::String => \"STRING\",\n            Self::System => \"SYSTEM\",\n            Self::Table => \"TABLE\",\n            Self::Text => \"TEXT\",\n            Self::Time => \"TIME\",\n            Self::Transaction => \"TRANSACTION\",\n            Self::True => \"TRUE\",\n            Self::Unique => \"UNIQUE\",\n            Self::Update => \"UPDATE\",\n            Self::Values => \"VALUES\",\n            Self::Varchar => \"VARCHAR\",\n            Self::Where => \"WHERE\",\n            Self::Write => \"WRITE\",\n        })\n    }\n}\n\n/// The lexer (lexical analyzer) preprocesses raw SQL strings into a sequence of\n/// lexical tokens (e.g. keyword, number, string, etc), which are passed on to\n/// the SQL parser. In doing so, it strips away basic syntactic noise such as\n/// whitespace, case, and quotes, and performs initial symbol validation.\npub struct Lexer<'a> {\n    chars: Peekable<Chars<'a>>,\n}\n\n/// The lexer is used as a token iterator.\nimpl Iterator for Lexer<'_> {\n    type Item = Result<Token>;\n\n    fn next(&mut self) -> Option<Result<Token>> {\n        match self.scan() {\n            Ok(Some(token)) => Some(Ok(token)),\n            // If there's any remaining chars, the lexer didn't recognize them.\n            Ok(None) => self.chars.peek().map(|c| errinput!(\"unexpected character {c}\")),\n            Err(err) => Some(Err(err)),\n        }\n    }\n}\n\nimpl<'a> Lexer<'a> {\n    /// Creates a new lexer for the given string.\n    pub fn new(input: &'a str) -> Lexer<'a> {\n        Lexer { chars: input.chars().peekable() }\n    }\n\n    /// Returns the next character if it satisfies the predicate.\n    fn next_if(&mut self, predicate: impl Fn(char) -> bool) -> Option<char> {\n        self.chars.peek().filter(|&&c| predicate(c))?;\n        self.chars.next()\n    }\n\n    /// Applies a closure to the next character, returning its result and\n    /// consuming the next character if it's Some.\n    fn next_if_map<T>(&mut self, map: impl Fn(char) -> Option<T>) -> Option<T> {\n        let value = self.chars.peek().copied().and_then(map)?;\n        self.chars.next();\n        Some(value)\n    }\n\n    /// Returns true if the next character is the given character, consuming it.\n    fn next_is(&mut self, c: char) -> bool {\n        self.next_if(|n| n == c).is_some()\n    }\n\n    /// Scans the next token, if any.\n    fn scan(&mut self) -> Result<Option<Token>> {\n        // Ignore whitespace.\n        self.skip_whitespace();\n        let Some(c) = self.chars.peek() else {\n            return Ok(None);\n        };\n        // The first character tells us the token kind. Scan it accordingly.\n        match c {\n            '\\'' => self.scan_string(),\n            '\"' => self.scan_ident_quoted(),\n            '0'..='9' => Ok(self.scan_number()),\n            c if c.is_alphabetic() => Ok(self.scan_ident_or_keyword()),\n            _ => Ok(self.scan_symbol()),\n        }\n    }\n\n    /// Scans the next identifier or keyword, if any. It's converted to\n    /// lowercase, by SQL convention.\n    fn scan_ident_or_keyword(&mut self) -> Option<Token> {\n        // The first character must be alphabetic. The rest can be numeric.\n        let mut name = self.next_if(|c| c.is_alphabetic())?.to_lowercase().to_string();\n        while let Some(c) = self.next_if(|c| c.is_alphanumeric() || c == '_') {\n            name.extend(c.to_lowercase())\n        }\n        // Check if the identifier matches a keyword.\n        if let Ok(keyword) = Keyword::try_from(name.as_str()) {\n            return Some(Token::Keyword(keyword));\n        }\n        Some(Token::Ident(name))\n    }\n\n    /// Scans the next quoted identifier, if any. Case is preserved.\n    fn scan_ident_quoted(&mut self) -> Result<Option<Token>> {\n        if !self.next_is('\"') {\n            return Ok(None);\n        }\n        let mut ident = String::new();\n        loop {\n            match self.chars.next() {\n                // \"\" is the escape sequence for \".\n                Some('\"') if self.next_is('\"') => ident.push('\"'),\n                Some('\"') => break,\n                Some(c) => ident.push(c),\n                None => return errinput!(\"unexpected end of quoted identifier\"),\n            }\n        }\n        Ok(Some(Token::Ident(ident)))\n    }\n\n    /// Scans the next number, if any.\n    fn scan_number(&mut self) -> Option<Token> {\n        // Scan the integer part. There must be at least one digit.\n        let mut number = self.next_if(|c| c.is_ascii_digit())?.to_string();\n        while let Some(c) = self.next_if(|c| c.is_ascii_digit()) {\n            number.push(c)\n        }\n        // Scan the fractional part, if any.\n        if self.next_is('.') {\n            number.push('.');\n            while let Some(dec) = self.next_if(|c| c.is_ascii_digit()) {\n                number.push(dec)\n            }\n        }\n        // Scan the exponent, if any.\n        if let Some(exp) = self.next_if(|c| c == 'e' || c == 'E') {\n            number.push(exp);\n            if let Some(sign) = self.next_if(|c| c == '+' || c == '-') {\n                number.push(sign)\n            }\n            while let Some(c) = self.next_if(|c| c.is_ascii_digit()) {\n                number.push(c)\n            }\n        }\n        Some(Token::Number(number))\n    }\n\n    /// Scans the next quoted string literal, if any.\n    fn scan_string(&mut self) -> Result<Option<Token>> {\n        if !self.next_is('\\'') {\n            return Ok(None);\n        }\n        let mut string = String::new();\n        loop {\n            match self.chars.next() {\n                // '' is the escape sequence for '.\n                Some('\\'') if self.next_is('\\'') => string.push('\\''),\n                Some('\\'') => break,\n                Some(c) => string.push(c),\n                None => return errinput!(\"unexpected end of string literal\"),\n            }\n        }\n        Ok(Some(Token::String(string)))\n    }\n\n    /// Scans the next symbol token, if any.\n    fn scan_symbol(&mut self) -> Option<Token> {\n        let mut token = self.next_if_map(|c| {\n            Some(match c {\n                '.' => Token::Period,\n                '=' => Token::Equal,\n                '>' => Token::GreaterThan,\n                '<' => Token::LessThan,\n                '+' => Token::Plus,\n                '-' => Token::Minus,\n                '*' => Token::Asterisk,\n                '/' => Token::Slash,\n                '^' => Token::Caret,\n                '%' => Token::Percent,\n                '!' => Token::Exclamation,\n                '?' => Token::Question,\n                ',' => Token::Comma,\n                ';' => Token::Semicolon,\n                '(' => Token::OpenParen,\n                ')' => Token::CloseParen,\n                _ => return None,\n            })\n        })?;\n        // Handle two-character tokens, e.g. !=.\n        token = match token {\n            Token::Exclamation if self.next_is('=') => Token::NotEqual,\n            Token::GreaterThan if self.next_is('=') => Token::GreaterThanOrEqual,\n            Token::LessThan if self.next_is('>') => Token::LessOrGreaterThan,\n            Token::LessThan if self.next_is('=') => Token::LessThanOrEqual,\n            token => token,\n        };\n        Some(token)\n    }\n\n    /// Skips any whitespace.\n    fn skip_whitespace(&mut self) {\n        while self.next_if(|c| c.is_whitespace()).is_some() {}\n    }\n}\n\n/// Returns true if the entire given string is a single valid identifier.\npub fn is_ident(ident: &str) -> bool {\n    let mut lexer = Lexer::new(ident);\n    let Some(Ok(Token::Ident(_))) = lexer.next() else {\n        return false;\n    };\n    lexer.next().is_none() // if further tokens, it's not a lone identifier\n}\n"
  },
  {
    "path": "src/sql/parser/mod.rs",
    "content": "//! Parses raw SQL strings into a structured Abstract Syntax Tree.\n\npub mod ast;\nmod lexer;\nmod parser;\n\npub use lexer::{Keyword, Lexer, Token, is_ident};\npub use parser::Parser;\n"
  },
  {
    "path": "src/sql/parser/parser.rs",
    "content": "use std::iter::Peekable;\nuse std::ops::Add;\n\nuse super::{Keyword, Lexer, Token, ast};\nuse crate::errinput;\nuse crate::error::Result;\nuse crate::sql::types::DataType;\n\n/// The SQL parser takes tokens from the lexer and parses the SQL syntax into an\n/// Abstract Syntax Tree (AST).\n///\n/// The AST represents the syntactic structure of a SQL query (e.g. the SELECT\n/// and FROM clauses, values, arithmetic expressions, etc.). However, it only\n/// ensures the syntax is well-formed, and does not know whether e.g. a given\n/// table or column exists or which kind of join to use -- that is the job of\n/// the planner.\npub struct Parser<'a> {\n    pub lexer: Peekable<Lexer<'a>>,\n}\n\nimpl Parser<'_> {\n    /// Parses the input string into a SQL statement AST. The entire string must\n    /// be parsed as a single statement, ending with an optional semicolon.\n    pub fn parse(statement: &str) -> Result<ast::Statement> {\n        let mut parser = Self::new(statement);\n        let statement = parser.parse_statement()?;\n        parser.skip(Token::Semicolon);\n        if let Some(token) = parser.lexer.next().transpose()? {\n            return errinput!(\"unexpected token {token}\");\n        }\n        Ok(statement)\n    }\n\n    /// Parse the input string into a SQL expression AST. The entire string must\n    /// be parsed as a single expression. Only used in tests.\n    #[cfg(test)]\n    pub fn parse_expr(expr: &str) -> Result<ast::Expression> {\n        let mut parser = Self::new(expr);\n        let expression = parser.parse_expression()?;\n        if let Some(token) = parser.lexer.next().transpose()? {\n            return errinput!(\"unexpected token {token}\");\n        }\n        Ok(expression)\n    }\n\n    /// Creates a new parser for the given raw SQL string.\n    fn new(input: &str) -> Parser<'_> {\n        Parser { lexer: Lexer::new(input).peekable() }\n    }\n\n    /// Fetches the next lexer token, or errors if none is found.\n    fn next(&mut self) -> Result<Token> {\n        self.lexer.next().transpose()?.ok_or_else(|| errinput!(\"unexpected end of input\"))\n    }\n\n    /// Returns the next identifier, or errors if not found.\n    fn next_ident(&mut self) -> Result<String> {\n        match self.next()? {\n            Token::Ident(ident) => Ok(ident),\n            token => errinput!(\"expected identifier, got {token}\"),\n        }\n    }\n\n    /// Returns the next lexer token if it satisfies the predicate.\n    fn next_if(&mut self, predicate: impl Fn(&Token) -> bool) -> Option<Token> {\n        self.peek().ok()?.filter(|t| predicate(t))?;\n        self.next().ok()\n    }\n\n    /// Passes the next lexer token through the closure, consuming it if the\n    /// closure returns Some. Returns the result of the closure.\n    fn next_if_map<T>(&mut self, f: impl Fn(&Token) -> Option<T>) -> Option<T> {\n        self.peek().ok()?.map(f)?.inspect(|_| drop(self.next()))\n    }\n\n    /// Returns the next keyword if there is one.\n    fn next_if_keyword(&mut self) -> Option<Keyword> {\n        self.next_if_map(|token| match token {\n            Token::Keyword(keyword) => Some(*keyword),\n            _ => None,\n        })\n    }\n\n    /// Consumes the next lexer token if it is the given token, returning true.\n    fn next_is(&mut self, token: Token) -> bool {\n        self.next_if(|t| t == &token).is_some()\n    }\n\n    /// Consumes the next lexer token if it's the expected token, or errors.\n    fn expect(&mut self, expect: Token) -> Result<()> {\n        let token = self.next()?;\n        if token != expect {\n            return errinput!(\"expected token {expect}, found {token}\");\n        }\n        Ok(())\n    }\n\n    /// Consumes the next lexer token if it is the given token. Equivalent to\n    /// next_is(), but expresses intent better.\n    fn skip(&mut self, token: Token) {\n        self.next_is(token);\n    }\n\n    /// Peeks the next lexer token if any, but transposes it for convenience.\n    fn peek(&mut self) -> Result<Option<&Token>> {\n        self.lexer.peek().map(|r| r.as_ref().map_err(|err| err.clone())).transpose()\n    }\n\n    /// Parses a SQL statement.\n    fn parse_statement(&mut self) -> Result<ast::Statement> {\n        let Some(token) = self.peek()? else {\n            return errinput!(\"unexpected end of input\");\n        };\n        match token {\n            Token::Keyword(Keyword::Begin) => self.parse_begin(),\n            Token::Keyword(Keyword::Commit) => self.parse_commit(),\n            Token::Keyword(Keyword::Rollback) => self.parse_rollback(),\n            Token::Keyword(Keyword::Explain) => self.parse_explain(),\n\n            Token::Keyword(Keyword::Create) => self.parse_create_table(),\n            Token::Keyword(Keyword::Drop) => self.parse_drop_table(),\n\n            Token::Keyword(Keyword::Delete) => self.parse_delete(),\n            Token::Keyword(Keyword::Insert) => self.parse_insert(),\n            Token::Keyword(Keyword::Select) => self.parse_select(),\n            Token::Keyword(Keyword::Update) => self.parse_update(),\n\n            token => errinput!(\"unexpected token {token}\"),\n        }\n    }\n\n    /// Parses a BEGIN statement.\n    fn parse_begin(&mut self) -> Result<ast::Statement> {\n        self.expect(Keyword::Begin.into())?;\n        self.skip(Keyword::Transaction.into());\n\n        let mut read_only = false;\n        if self.next_is(Keyword::Read.into()) {\n            match self.next()? {\n                Token::Keyword(Keyword::Only) => read_only = true,\n                Token::Keyword(Keyword::Write) => {}\n                token => return errinput!(\"unexpected token {token}\"),\n            }\n        }\n\n        let mut as_of = None;\n        if self.next_is(Keyword::As.into()) {\n            self.expect(Keyword::Of.into())?;\n            self.expect(Keyword::System.into())?;\n            self.expect(Keyword::Time.into())?;\n            match self.next()? {\n                Token::Number(n) => as_of = Some(n.parse()?),\n                token => return errinput!(\"unexpected token {token}, wanted number\"),\n            }\n        }\n        Ok(ast::Statement::Begin { read_only, as_of })\n    }\n\n    /// Parses a COMMIT statement.\n    fn parse_commit(&mut self) -> Result<ast::Statement> {\n        self.expect(Keyword::Commit.into())?;\n        Ok(ast::Statement::Commit)\n    }\n\n    /// Parses a ROLLBACK statement.\n    fn parse_rollback(&mut self) -> Result<ast::Statement> {\n        self.expect(Keyword::Rollback.into())?;\n        Ok(ast::Statement::Rollback)\n    }\n\n    /// Parses an EXPLAIN statement.\n    fn parse_explain(&mut self) -> Result<ast::Statement> {\n        self.expect(Keyword::Explain.into())?;\n        if self.next_is(Keyword::Explain.into()) {\n            return errinput!(\"cannot nest EXPLAIN statements\");\n        }\n        Ok(ast::Statement::Explain(Box::new(self.parse_statement()?)))\n    }\n\n    /// Parses a CREATE TABLE statement.\n    fn parse_create_table(&mut self) -> Result<ast::Statement> {\n        self.expect(Keyword::Create.into())?;\n        self.expect(Keyword::Table.into())?;\n        let name = self.next_ident()?;\n        self.expect(Token::OpenParen)?;\n        let mut columns = Vec::new();\n        loop {\n            columns.push(self.parse_create_table_column()?);\n            if !self.next_is(Token::Comma) {\n                break;\n            }\n        }\n        self.expect(Token::CloseParen)?;\n        Ok(ast::Statement::CreateTable { name, columns })\n    }\n\n    /// Parses a CREATE TABLE column definition.\n    fn parse_create_table_column(&mut self) -> Result<ast::Column> {\n        let name = self.next_ident()?;\n        let datatype = match self.next()? {\n            Token::Keyword(Keyword::Bool | Keyword::Boolean) => DataType::Boolean,\n            Token::Keyword(Keyword::Float | Keyword::Double) => DataType::Float,\n            Token::Keyword(Keyword::Int | Keyword::Integer) => DataType::Integer,\n            Token::Keyword(Keyword::String | Keyword::Text | Keyword::Varchar) => DataType::String,\n            token => return errinput!(\"unexpected token {token}\"),\n        };\n        let mut column = ast::Column {\n            name,\n            datatype,\n            primary_key: false,\n            nullable: None,\n            default: None,\n            unique: false,\n            index: false,\n            references: None,\n        };\n        while let Some(keyword) = self.next_if_keyword() {\n            match keyword {\n                Keyword::Primary => {\n                    self.expect(Keyword::Key.into())?;\n                    column.primary_key = true;\n                }\n                Keyword::Null => {\n                    if column.nullable.is_some() {\n                        return errinput!(\"nullability already set for column {}\", column.name);\n                    }\n                    column.nullable = Some(true)\n                }\n                Keyword::Not => {\n                    self.expect(Keyword::Null.into())?;\n                    if column.nullable.is_some() {\n                        return errinput!(\"nullability already set for column {}\", column.name);\n                    }\n                    column.nullable = Some(false)\n                }\n                Keyword::Default => column.default = Some(self.parse_expression()?),\n                Keyword::Unique => column.unique = true,\n                Keyword::Index => column.index = true,\n                Keyword::References => column.references = Some(self.next_ident()?),\n                keyword => return errinput!(\"unexpected keyword {keyword}\"),\n            }\n        }\n        Ok(column)\n    }\n\n    /// Parses a DROP TABLE statement.\n    fn parse_drop_table(&mut self) -> Result<ast::Statement> {\n        self.expect(Token::Keyword(Keyword::Drop))?;\n        self.expect(Token::Keyword(Keyword::Table))?;\n        let mut if_exists = false;\n        if self.next_is(Keyword::If.into()) {\n            self.expect(Token::Keyword(Keyword::Exists))?;\n            if_exists = true;\n        }\n        let name = self.next_ident()?;\n        Ok(ast::Statement::DropTable { name, if_exists })\n    }\n\n    /// Parses a DELETE statement.\n    fn parse_delete(&mut self) -> Result<ast::Statement> {\n        self.expect(Keyword::Delete.into())?;\n        self.expect(Keyword::From.into())?;\n        let table = self.next_ident()?;\n        Ok(ast::Statement::Delete { table, r#where: self.parse_where_clause()? })\n    }\n\n    /// Parses an INSERT statement.\n    fn parse_insert(&mut self) -> Result<ast::Statement> {\n        self.expect(Keyword::Insert.into())?;\n        self.expect(Keyword::Into.into())?;\n        let table = self.next_ident()?;\n\n        let mut columns = None;\n        if self.next_is(Token::OpenParen) {\n            let columns = columns.insert(Vec::new());\n            loop {\n                columns.push(self.next_ident()?);\n                if !self.next_is(Token::Comma) {\n                    break;\n                }\n            }\n            self.expect(Token::CloseParen)?;\n        }\n\n        self.expect(Keyword::Values.into())?;\n\n        let mut values = Vec::new();\n        loop {\n            let mut row = Vec::new();\n            self.expect(Token::OpenParen)?;\n            loop {\n                row.push(self.parse_expression()?);\n                if !self.next_is(Token::Comma) {\n                    break;\n                }\n            }\n            self.expect(Token::CloseParen)?;\n            values.push(row);\n            if !self.next_is(Token::Comma) {\n                break;\n            }\n        }\n\n        Ok(ast::Statement::Insert { table, columns, values })\n    }\n\n    /// Parses an UPDATE statement.\n    fn parse_update(&mut self) -> Result<ast::Statement> {\n        self.expect(Keyword::Update.into())?;\n        let table = self.next_ident()?;\n        self.expect(Keyword::Set.into())?;\n        let mut set = std::collections::BTreeMap::new();\n        loop {\n            let column = self.next_ident()?;\n            self.expect(Token::Equal)?;\n            let expr = (!self.next_is(Keyword::Default.into()))\n                .then(|| self.parse_expression())\n                .transpose()?;\n            if set.contains_key(&column) {\n                return errinput!(\"column {column} set multiple times\");\n            }\n            set.insert(column, expr);\n            if !self.next_is(Token::Comma) {\n                break;\n            }\n        }\n        Ok(ast::Statement::Update { table, set, r#where: self.parse_where_clause()? })\n    }\n\n    /// Parses a SELECT statement.\n    fn parse_select(&mut self) -> Result<ast::Statement> {\n        Ok(ast::Statement::Select {\n            select: self.parse_select_clause()?,\n            from: self.parse_from_clause()?,\n            r#where: self.parse_where_clause()?,\n            group_by: self.parse_group_by_clause()?,\n            having: self.parse_having_clause()?,\n            order_by: self.parse_order_by_clause()?,\n            limit: self.parse_limit_clause()?,\n            offset: self.parse_offset_clause()?,\n        })\n    }\n\n    /// Parses a SELECT clause, if present.\n    fn parse_select_clause(&mut self) -> Result<Vec<(ast::Expression, Option<String>)>> {\n        if !self.next_is(Keyword::Select.into()) {\n            return Ok(Vec::new());\n        }\n        let mut select = Vec::new();\n        loop {\n            let expr = self.parse_expression()?;\n            let mut alias = None;\n            if self.next_is(Keyword::As.into()) || matches!(self.peek()?, Some(Token::Ident(_))) {\n                if expr == ast::Expression::All {\n                    return errinput!(\"can't alias *\");\n                }\n                alias = Some(self.next_ident()?);\n            }\n            select.push((expr, alias));\n            if !self.next_is(Token::Comma) {\n                break;\n            }\n        }\n        Ok(select)\n    }\n\n    /// Parses a FROM clause, if present.\n    fn parse_from_clause(&mut self) -> Result<Vec<ast::From>> {\n        if !self.next_is(Keyword::From.into()) {\n            return Ok(Vec::new());\n        }\n        let mut from = Vec::new();\n        loop {\n            let mut from_item = self.parse_from_table()?;\n            while let Some(r#type) = self.parse_from_join()? {\n                let left = Box::new(from_item);\n                let right = Box::new(self.parse_from_table()?);\n                let mut predicate = None;\n                if r#type != ast::JoinType::Cross {\n                    self.expect(Keyword::On.into())?;\n                    predicate = Some(self.parse_expression()?)\n                }\n                from_item = ast::From::Join { left, right, r#type, predicate };\n            }\n            from.push(from_item);\n            if !self.next_is(Token::Comma) {\n                break;\n            }\n        }\n        Ok(from)\n    }\n\n    // Parses a FROM table.\n    fn parse_from_table(&mut self) -> Result<ast::From> {\n        let name = self.next_ident()?;\n        let mut alias = None;\n        if self.next_is(Keyword::As.into()) || matches!(self.peek()?, Some(Token::Ident(_))) {\n            alias = Some(self.next_ident()?)\n        };\n        Ok(ast::From::Table { name, alias })\n    }\n\n    // Parses a FROM JOIN type, if present.\n    fn parse_from_join(&mut self) -> Result<Option<ast::JoinType>> {\n        if self.next_is(Keyword::Join.into()) {\n            return Ok(Some(ast::JoinType::Inner));\n        }\n        if self.next_is(Keyword::Cross.into()) {\n            self.expect(Keyword::Join.into())?;\n            return Ok(Some(ast::JoinType::Cross));\n        }\n        if self.next_is(Keyword::Inner.into()) {\n            self.expect(Keyword::Join.into())?;\n            return Ok(Some(ast::JoinType::Inner));\n        }\n        if self.next_is(Keyword::Left.into()) {\n            self.skip(Keyword::Outer.into());\n            self.expect(Keyword::Join.into())?;\n            return Ok(Some(ast::JoinType::Left));\n        }\n        if self.next_is(Keyword::Right.into()) {\n            self.skip(Keyword::Outer.into());\n            self.expect(Keyword::Join.into())?;\n            return Ok(Some(ast::JoinType::Right));\n        }\n        Ok(None)\n    }\n\n    /// Parses a WHERE clause, if present.\n    fn parse_where_clause(&mut self) -> Result<Option<ast::Expression>> {\n        if !self.next_is(Keyword::Where.into()) {\n            return Ok(None);\n        }\n        Ok(Some(self.parse_expression()?))\n    }\n\n    /// Parses a GROUP BY clause, if present.\n    fn parse_group_by_clause(&mut self) -> Result<Vec<ast::Expression>> {\n        if !self.next_is(Keyword::Group.into()) {\n            return Ok(Vec::new());\n        }\n        let mut group_by = Vec::new();\n        self.expect(Keyword::By.into())?;\n        loop {\n            group_by.push(self.parse_expression()?);\n            if !self.next_is(Token::Comma) {\n                break;\n            }\n        }\n        Ok(group_by)\n    }\n\n    /// Parses a HAVING clause, if present.\n    fn parse_having_clause(&mut self) -> Result<Option<ast::Expression>> {\n        if !self.next_is(Keyword::Having.into()) {\n            return Ok(None);\n        }\n        Ok(Some(self.parse_expression()?))\n    }\n\n    /// Parses an ORDER BY clause, if present.\n    fn parse_order_by_clause(&mut self) -> Result<Vec<(ast::Expression, ast::Direction)>> {\n        if !self.next_is(Keyword::Order.into()) {\n            return Ok(Vec::new());\n        }\n        let mut order_by = Vec::new();\n        self.expect(Keyword::By.into())?;\n        loop {\n            let expr = self.parse_expression()?;\n            let order = self\n                .next_if_map(|token| match token {\n                    Token::Keyword(Keyword::Asc) => Some(ast::Direction::Ascending),\n                    Token::Keyword(Keyword::Desc) => Some(ast::Direction::Descending),\n                    _ => None,\n                })\n                .unwrap_or_default();\n            order_by.push((expr, order));\n            if !self.next_is(Token::Comma) {\n                break;\n            }\n        }\n        Ok(order_by)\n    }\n\n    /// Parses a LIMIT clause, if present.\n    fn parse_limit_clause(&mut self) -> Result<Option<ast::Expression>> {\n        if !self.next_is(Keyword::Limit.into()) {\n            return Ok(None);\n        }\n        Ok(Some(self.parse_expression()?))\n    }\n\n    /// Parses an OFFSET clause, if present.\n    fn parse_offset_clause(&mut self) -> Result<Option<ast::Expression>> {\n        if !self.next_is(Keyword::Offset.into()) {\n            return Ok(None);\n        }\n        Ok(Some(self.parse_expression()?))\n    }\n\n    /// Parses an expression using the precedence climbing algorithm. See:\n    ///\n    /// <https://en.wikipedia.org/wiki/Operator-precedence_parser#Precedence_climbing_method>\n    /// <https://eli.thegreenplace.net/2012/08/02/parsing-expressions-by-precedence-climbing>\n    ///\n    /// Expressions are made up of two main entities:\n    ///\n    /// * Atoms: values, variables, functions, and parenthesized expressions.\n    /// * Operators: performs operations on atoms and sub-expressions.\n    ///   * Prefix operators: e.g. `-a` or `NOT a`.\n    ///   * Infix operators: e.g. `a + b`  or `a AND b`.\n    ///   * Postfix operators: e.g. `a!` or `a IS NULL`.\n    ///\n    /// During parsing, we have to respect the mathematical precedence and\n    /// associativity of operators. Consider e.g.:\n    ///\n    /// 2 ^ 3 ^ 2 - 4 * 3\n    ///\n    /// By the rules of precedence and associativity, this expression should\n    /// be interpreted as:\n    ///\n    /// (2 ^ (3 ^ 2)) - (4 * 3)\n    ///\n    /// Specifically, the exponentiation operator ^ is right-associative, so it\n    /// should be 2 ^ (3 ^ 2) = 512, not (2 ^ 3) ^ 2 = 64. Similarly,\n    /// exponentiation and multiplication have higher precedence than\n    /// subtraction, so it should be (2 ^ 3 ^ 2) - (4 * 3) = 500, not\n    /// 2 ^ 3 ^ (2 - 4) * 3 = -3.24.\n    ///\n    /// To use precedence climbing, we first need to specify the relative\n    /// precedence of operators as a number, where 1 is the lowest precedence:\n    ///\n    /// * 1: OR\n    /// * 2: AND\n    /// * 3: NOT\n    /// * 4: =, !=, LIKE, IS\n    /// * 5: <, <=, >, >=\n    /// * 6: +, -\n    /// * 7: *, /, %\n    /// * 8: ^\n    /// * 9: !\n    /// * 10: +, - (prefix)\n    ///\n    /// We also have to specify the associativity of operators:\n    ///\n    /// * Right-associative: ^ and all prefix operators.\n    /// * Left-associative: all other operators.\n    ///\n    /// Left-associative operators get a +1 to their precedence, so that they\n    /// bind tighter to their left operand than right-associative operators.\n    ///\n    /// The precedence climbing algorithm works by recursively parsing the\n    /// left-hand side of an expression (including any prefix operators), any\n    /// infix operators and recursive right-hand side expressions, and finally\n    /// any postfix operators.\n    ///\n    /// The grouping is determined by where the right-hand side recursion\n    /// terminates. The algorithm will greedily consume as many operators as\n    /// possible, but only as long as their precedence is greater than or equal\n    /// to the precedence of the previous operator (hence the name \"climbing\").\n    /// When we find an operator with lower precedence, we return the current\n    /// expression up the recursion stack and resume parsing the operator at a\n    /// lower precedence.\n    ///\n    /// The precedence levels for the previous example are as follows:\n    ///\n    ///     -----          Precedence 9: ^ right-associativity\n    /// ---------          Precedence 9: ^\n    ///             -----  Precedence 7: *\n    /// -----------------  Precedence 6: -\n    /// 2 ^ 3 ^ 2 - 4 * 3\n    ///\n    /// Let's walk through the recursive parsing of this expression:\n    ///\n    /// parse_expression_at(prec=0)\n    ///   lhs = parse_expression_atom() = 2\n    ///   op = parse_infix_operator(prec=0) = ^ (prec=9)\n    ///   rhs = parse_expression_at(prec=9)\n    ///     lhs = parse_expression_atom() = 3\n    ///     op = parse_infix_operator(prec=9) = ^ (prec=9)\n    ///     rhs = parse_expression_at(prec=9)\n    ///       lhs = parse_expression_atom() = 2\n    ///       op = parse_infix_operator(prec=9) = None (reject - at prec=6)\n    ///       return lhs = 2\n    ///     lhs = (lhs op rhs) = (3 ^ 2)\n    ///     op = parse_infix_operator(prec=9) = None (reject - at prec=6)\n    ///     return lhs = (3 ^ 2)\n    ///   lhs = (lhs op rhs) = (2 ^ (3 ^ 2))\n    ///   op = parse_infix_operator(prec=0) = - (prec=6)\n    ///   rhs = parse_expression_at(prec=6)\n    ///     lhs = parse_expression_atom() = 4\n    ///     op = parse_infix_operator(prec=6) = * (prec=7)\n    ///     rhs = parse_expression_at(prec=7)\n    ///       lhs = parse_expression_atom() = 3\n    ///       op = parse_infix_operator(prec=7) = None (end of expression)\n    ///       return lhs = 3\n    ///     lhs = (lhs op rhs) = (4 * 3)\n    ///     op = parse_infix_operator(prec=6) = None (end of expression)\n    ///     return lhs = (4 * 3)\n    ///   lhs = (lhs op rhs) = ((2 ^ (3 ^ 2)) - (4 * 3))\n    ///   op = parse_infix_operator(prec=0) = None (end of expression)\n    ///   return lhs = ((2 ^ (3 ^ 2)) - (4 * 3))\n    fn parse_expression(&mut self) -> Result<ast::Expression> {\n        self.parse_expression_at(0)\n    }\n\n    /// Parses an expression at the given minimum precedence.\n    fn parse_expression_at(&mut self, min_precedence: Precedence) -> Result<ast::Expression> {\n        // If the left-hand side is a prefix operator, recursively parse it and\n        // its operand. Otherwise, parse the left-hand side as an atom.\n        let mut lhs = if let Some(prefix) = self.parse_prefix_operator_at(min_precedence) {\n            let next_precedence = prefix.precedence() + prefix.associativity();\n            let rhs = self.parse_expression_at(next_precedence)?;\n            prefix.into_expression(rhs)\n        } else {\n            self.parse_expression_atom()?\n        };\n\n        // Apply any postfix operators to the left-hand side.\n        while let Some(postfix) = self.parse_postfix_operator_at(min_precedence)? {\n            lhs = postfix.into_expression(lhs)\n        }\n\n        // Repeatedly apply any infix operators to the left-hand side as long as\n        // their precedence is greater than or equal to the current minimum\n        // precedence (i.e. that of the upstack operator).\n        //\n        // The right-hand side expression parsing will recursively apply any\n        // infix operators at or above this operator's precedence to the\n        // right-hand side.\n        while let Some(infix) = self.parse_infix_operator_at(min_precedence) {\n            let next_precedence = infix.precedence() + infix.associativity();\n            let rhs = self.parse_expression_at(next_precedence)?;\n            lhs = infix.into_expression(lhs, rhs);\n        }\n\n        // Apply any postfix operators after the binary operator. Consider e.g.\n        // 1 + NULL IS NULL.\n        while let Some(postfix) = self.parse_postfix_operator_at(min_precedence)? {\n            lhs = postfix.into_expression(lhs)\n        }\n\n        Ok(lhs)\n    }\n\n    /// Parses an expression atom. This is either:\n    ///\n    /// * A literal value.\n    /// * A column name.\n    /// * A function call.\n    /// * A parenthesized expression.\n    fn parse_expression_atom(&mut self) -> Result<ast::Expression> {\n        Ok(match self.next()? {\n            // All columns.\n            Token::Asterisk => ast::Expression::All,\n\n            // Literal value.\n            Token::Number(n) if n.chars().all(|c| c.is_ascii_digit()) => {\n                ast::Literal::Integer(n.parse()?).into()\n            }\n            Token::Number(n) => ast::Literal::Float(n.parse()?).into(),\n            Token::String(s) => ast::Literal::String(s).into(),\n            Token::Keyword(Keyword::True) => ast::Literal::Boolean(true).into(),\n            Token::Keyword(Keyword::False) => ast::Literal::Boolean(false).into(),\n            Token::Keyword(Keyword::Infinity) => ast::Literal::Float(f64::INFINITY).into(),\n            Token::Keyword(Keyword::NaN) => ast::Literal::Float(f64::NAN).into(),\n            Token::Keyword(Keyword::Null) => ast::Literal::Null.into(),\n\n            // Function call.\n            Token::Ident(name) if self.next_is(Token::OpenParen) => {\n                let mut args = Vec::new();\n                while !self.next_is(Token::CloseParen) {\n                    if !args.is_empty() {\n                        self.expect(Token::Comma)?;\n                    }\n                    args.push(self.parse_expression()?);\n                }\n                ast::Expression::Function(name, args)\n            }\n\n            // Column name, either qualified as table.column or unqualified.\n            Token::Ident(table) if self.next_is(Token::Period) => {\n                ast::Expression::Column(Some(table), self.next_ident()?)\n            }\n            Token::Ident(column) => ast::Expression::Column(None, column),\n\n            // Parenthesized expression.\n            Token::OpenParen => {\n                let expr = self.parse_expression()?;\n                self.expect(Token::CloseParen)?;\n                expr\n            }\n\n            token => return errinput!(\"expected expression atom, found {token}\"),\n        })\n    }\n\n    /// Parses a prefix operator, if there is one and its precedence is at least\n    /// min_precedence.\n    fn parse_prefix_operator_at(&mut self, min_precedence: Precedence) -> Option<PrefixOperator> {\n        self.next_if_map(|token| {\n            let operator = match token {\n                Token::Keyword(Keyword::Not) => PrefixOperator::Not,\n                Token::Minus => PrefixOperator::Minus,\n                Token::Plus => PrefixOperator::Plus,\n                _ => return None,\n            };\n            Some(operator).filter(|op| op.precedence() >= min_precedence)\n        })\n    }\n\n    /// Parses an infix operator, if there is one and its precedence is at least\n    /// min_precedence.\n    fn parse_infix_operator_at(&mut self, min_precedence: Precedence) -> Option<InfixOperator> {\n        self.next_if_map(|token| {\n            let operator = match token {\n                Token::Asterisk => InfixOperator::Multiply,\n                Token::Caret => InfixOperator::Exponentiate,\n                Token::Equal => InfixOperator::Equal,\n                Token::GreaterThan => InfixOperator::GreaterThan,\n                Token::GreaterThanOrEqual => InfixOperator::GreaterThanOrEqual,\n                Token::Keyword(Keyword::And) => InfixOperator::And,\n                Token::Keyword(Keyword::Like) => InfixOperator::Like,\n                Token::Keyword(Keyword::Or) => InfixOperator::Or,\n                Token::LessOrGreaterThan => InfixOperator::NotEqual,\n                Token::LessThan => InfixOperator::LessThan,\n                Token::LessThanOrEqual => InfixOperator::LessThanOrEqual,\n                Token::Minus => InfixOperator::Subtract,\n                Token::NotEqual => InfixOperator::NotEqual,\n                Token::Percent => InfixOperator::Remainder,\n                Token::Plus => InfixOperator::Add,\n                Token::Slash => InfixOperator::Divide,\n                _ => return None,\n            };\n            Some(operator).filter(|op| op.precedence() >= min_precedence)\n        })\n    }\n\n    /// Parses a postfix operator, if there is one and its precedence is at\n    /// least min_precedence.\n    fn parse_postfix_operator_at(\n        &mut self,\n        min_precedence: Precedence,\n    ) -> Result<Option<PostfixOperator>> {\n        // Handle IS (NOT) NULL/NAN separately, since it's multiple tokens.\n        if self.peek()? == Some(&Token::Keyword(Keyword::Is)) {\n            // We can't consume tokens unless the precedence is satisfied, so we\n            // assume IS NULL (they all have the same precedence).\n            if PostfixOperator::Is(ast::Literal::Null).precedence() < min_precedence {\n                return Ok(None);\n            }\n            self.expect(Keyword::Is.into())?;\n            let not = self.next_is(Keyword::Not.into());\n            let value = match self.next()? {\n                Token::Keyword(Keyword::NaN) => ast::Literal::Float(f64::NAN),\n                Token::Keyword(Keyword::Null) => ast::Literal::Null,\n                token => return errinput!(\"unexpected token {token}\"),\n            };\n            let operator = match not {\n                false => PostfixOperator::Is(value),\n                true => PostfixOperator::IsNot(value),\n            };\n            return Ok(Some(operator));\n        }\n\n        Ok(self.next_if_map(|token| {\n            let operator = match token {\n                Token::Exclamation => PostfixOperator::Factorial,\n                _ => return None,\n            };\n            Some(operator).filter(|op| op.precedence() >= min_precedence)\n        }))\n    }\n}\n\n/// Operator precedence.\ntype Precedence = u8;\n\n/// Operator associativity.\nenum Associativity {\n    Left,\n    Right,\n}\n\nimpl Add<Associativity> for Precedence {\n    type Output = Self;\n\n    fn add(self, rhs: Associativity) -> Self {\n        // Left-associative operators have increased precedence, so they bind\n        // tighter to their left-hand side.\n        self + match rhs {\n            Associativity::Left => 1,\n            Associativity::Right => 0,\n        }\n    }\n}\n\n/// Prefix operators.\nenum PrefixOperator {\n    Minus, // -a\n    Not,   // NOT a\n    Plus,  // +a\n}\n\nimpl PrefixOperator {\n    /// The operator precedence.\n    fn precedence(&self) -> Precedence {\n        match self {\n            Self::Not => 3,\n            Self::Minus | Self::Plus => 10,\n        }\n    }\n\n    // The operator associativity. Prefix operators are right-associative by\n    // definition.\n    fn associativity(&self) -> Associativity {\n        Associativity::Right\n    }\n\n    /// Builds an AST expression for the operator.\n    fn into_expression(self, rhs: ast::Expression) -> ast::Expression {\n        let rhs = Box::new(rhs);\n        match self {\n            Self::Plus => ast::Operator::Identity(rhs).into(),\n            Self::Minus => ast::Operator::Negate(rhs).into(),\n            Self::Not => ast::Operator::Not(rhs).into(),\n        }\n    }\n}\n\n/// Infix operators.\nenum InfixOperator {\n    Add,                // a + b\n    And,                // a AND b\n    Divide,             // a / b\n    Equal,              // a = b\n    Exponentiate,       // a ^ b\n    GreaterThan,        // a > b\n    GreaterThanOrEqual, // a >= b\n    LessThan,           // a < b\n    LessThanOrEqual,    // a <= b\n    Like,               // a LIKE b\n    Multiply,           // a * b\n    NotEqual,           // a != b\n    Or,                 // a OR b\n    Remainder,          // a % b\n    Subtract,           // a - b\n}\n\nimpl InfixOperator {\n    /// The operator precedence.\n    ///\n    /// Mostly follows Postgres, except IS and LIKE having same precedence as =.\n    /// This is similar to SQLite and MySQL.\n    fn precedence(&self) -> Precedence {\n        match self {\n            Self::Or => 1,\n            Self::And => 2,\n            // Self::Not => 3\n            Self::Equal | Self::NotEqual | Self::Like => 4, // also Self::Is\n            Self::GreaterThan\n            | Self::GreaterThanOrEqual\n            | Self::LessThan\n            | Self::LessThanOrEqual => 5,\n            Self::Add | Self::Subtract => 6,\n            Self::Multiply | Self::Divide | Self::Remainder => 7,\n            Self::Exponentiate => 8,\n        }\n    }\n\n    /// The operator associativity.\n    fn associativity(&self) -> Associativity {\n        match self {\n            Self::Exponentiate => Associativity::Right,\n            _ => Associativity::Left,\n        }\n    }\n\n    /// Builds an AST expression for the infix operator.\n    fn into_expression(self, lhs: ast::Expression, rhs: ast::Expression) -> ast::Expression {\n        let (lhs, rhs) = (Box::new(lhs), Box::new(rhs));\n        match self {\n            Self::Add => ast::Operator::Add(lhs, rhs).into(),\n            Self::And => ast::Operator::And(lhs, rhs).into(),\n            Self::Divide => ast::Operator::Divide(lhs, rhs).into(),\n            Self::Equal => ast::Operator::Equal(lhs, rhs).into(),\n            Self::Exponentiate => ast::Operator::Exponentiate(lhs, rhs).into(),\n            Self::GreaterThan => ast::Operator::GreaterThan(lhs, rhs).into(),\n            Self::GreaterThanOrEqual => ast::Operator::GreaterThanOrEqual(lhs, rhs).into(),\n            Self::LessThan => ast::Operator::LessThan(lhs, rhs).into(),\n            Self::LessThanOrEqual => ast::Operator::LessThanOrEqual(lhs, rhs).into(),\n            Self::Like => ast::Operator::Like(lhs, rhs).into(),\n            Self::Multiply => ast::Operator::Multiply(lhs, rhs).into(),\n            Self::NotEqual => ast::Operator::NotEqual(lhs, rhs).into(),\n            Self::Or => ast::Operator::Or(lhs, rhs).into(),\n            Self::Remainder => ast::Operator::Remainder(lhs, rhs).into(),\n            Self::Subtract => ast::Operator::Subtract(lhs, rhs).into(),\n        }\n    }\n}\n\n/// Postfix operators.\nenum PostfixOperator {\n    Factorial,           // a!\n    Is(ast::Literal),    // a IS NULL | NAN\n    IsNot(ast::Literal), // a IS NOT NULL | NAN\n}\n\nimpl PostfixOperator {\n    // The operator precedence.\n    fn precedence(&self) -> Precedence {\n        match self {\n            Self::Is(_) | Self::IsNot(_) => 4,\n            Self::Factorial => 9,\n        }\n    }\n\n    /// Builds an AST expression for the operator.\n    fn into_expression(self, lhs: ast::Expression) -> ast::Expression {\n        let lhs = Box::new(lhs);\n        match self {\n            Self::Factorial => ast::Operator::Factorial(lhs).into(),\n            Self::Is(v) => ast::Operator::Is(lhs, v).into(),\n            Self::IsNot(v) => ast::Operator::Not(ast::Operator::Is(lhs, v).into()).into(),\n        }\n    }\n}\n"
  },
  {
    "path": "src/sql/planner/mod.rs",
    "content": "//! The planner builds and optimizes an execution plan based on a SQL\n//! statement's Abstract Syntax Tree (AST) generated by the parser.\n\nmod optimizer;\nmod plan;\nmod planner;\n\n#[cfg(test)]\npub use optimizer::OPTIMIZERS;\npub use plan::{Aggregate, Direction, Node, Plan};\npub use planner::{Planner, Scope};\n"
  },
  {
    "path": "src/sql/planner/optimizer.rs",
    "content": "use std::collections::HashMap;\nuse std::fmt::Debug;\nuse std::sync::LazyLock;\n\nuse super::Node;\nuse crate::error::Result;\nuse crate::sql::types::{Expression, Label, Value};\n\n/// The set of optimizers, and the order in which they are applied.\npub static OPTIMIZERS: LazyLock<Vec<Box<dyn Optimizer>>> = LazyLock::new(|| {\n    vec![\n        Box::new(ConstantFolding),\n        Box::new(FilterPushdown),\n        Box::new(IndexLookup),\n        Box::new(HashJoin),\n        Box::new(ShortCircuit),\n    ]\n});\n\n/// A node optimizer, which recursively transforms a plan node to make plan\n/// execution more efficient where possible.\npub trait Optimizer: Debug + Send + Sync {\n    /// Optimizes a node, returning the optimized node.\n    fn optimize(&self, node: Node) -> Result<Node>;\n}\n\n/// Folds constant expressions by pre-evaluating them once now, instead of\n/// re-evaluating them for every row during execution.\n#[derive(Debug)]\npub struct ConstantFolding;\n\nimpl Optimizer for ConstantFolding {\n    fn optimize(&self, node: Node) -> Result<Node> {\n        // Recursively transform expressions in the node tree. Post-order to\n        // partially fold child expressions as far as possible, and avoid\n        // quadratic costs.\n        node.transform(&|node| node.transform_expressions(&Ok, &Self::fold), &Ok)\n    }\n}\n\nimpl ConstantFolding {\n    /// Folds constant expressions in a node.\n    pub fn fold(mut expr: Expression) -> Result<Expression> {\n        use Expression::*;\n        use Value::*;\n\n        // If the expression is constant, evaluate it.\n        //\n        // This is a very simple approach, which doesn't handle more complex\n        // cases such as 1 + a - 2 (which would require rearranging the\n        // expression as 1 - 2 + a to evaluate the 1 - 2 branch).\n        //\n        // TODO: consider doing something better.\n        if !expr.contains(&|expr| matches!(expr, Column(_))) {\n            return expr.evaluate(None).map(Constant);\n        }\n\n        // If the expression is a logical operator, and one of the sides is\n        // constant, we may be able to evaluate it even if it has a column\n        // reference. For example, a AND FALSE is always FALSE, regardless of\n        // what a is.\n        expr = match expr {\n            And(lhs, rhs) => match (*lhs, *rhs) {\n                // If either side of an AND is false, the AND is false.\n                (Constant(Boolean(false)), _) | (_, Constant(Boolean(false))) => {\n                    Constant(Boolean(false))\n                }\n                // If either side of an AND is true, the AND is redundant.\n                (Constant(Boolean(true)), expr) | (expr, Constant(Boolean(true))) => expr,\n                (lhs, rhs) => And(lhs.into(), rhs.into()),\n            },\n\n            Or(lhs, rhs) => match (*lhs, *rhs) {\n                // If either side of an OR is true, the OR is true.\n                (Constant(Boolean(true)), _) | (_, Constant(Boolean(true))) => {\n                    Constant(Boolean(true))\n                }\n                // If either side of an OR is false, the OR is redundant.\n                (Constant(Boolean(false)), expr) | (expr, Constant(Boolean(false))) => expr,\n                (lhs, rhs) => Or(lhs.into(), rhs.into()),\n            },\n\n            expr => expr,\n        };\n\n        Ok(expr)\n    }\n}\n\n/// Pushes filter predicates down into child nodes where possible. In\n/// particular, this can perform filtering during storage scans (below Raft),\n/// instead of reading and transmitting all rows across the network before\n/// filtering, by pushing a predicate from a Filter node down into a Scan node.\n#[derive(Debug)]\npub struct FilterPushdown;\n\nimpl Optimizer for FilterPushdown {\n    fn optimize(&self, node: Node) -> Result<Node> {\n        // Push down before descending, so we can keep recursively pushing down.\n        node.transform(&|node| Ok(Self::push_filters(node)), &Ok)\n    }\n}\n\nimpl FilterPushdown {\n    /// Pushes filter predicates down into child nodes where possible.\n    fn push_filters(mut node: Node) -> Node {\n        node = Self::maybe_push_filter(node);\n        node = Self::maybe_push_join(node);\n        node\n    }\n\n    /// Pushes an expression into a node if possible. Otherwise, returns the the\n    /// unpushed expression.\n    fn push_into(expr: Expression, target: &mut Node) -> Option<Expression> {\n        match target {\n            Node::Filter { predicate, .. } => {\n                // Temporarily replace the predicate to take ownership.\n                let rhs = std::mem::replace(predicate, Expression::Constant(Value::Null));\n                *predicate = Expression::And(expr.into(), rhs.into());\n            }\n            Node::NestedLoopJoin { predicate, .. } => {\n                *predicate = match predicate.take() {\n                    Some(predicate) => Some(Expression::And(expr.into(), predicate.into())),\n                    None => Some(expr),\n                };\n            }\n            Node::Scan { filter, .. } => {\n                *filter = match filter.take() {\n                    Some(filter) => Some(Expression::And(expr.into(), filter.into())),\n                    None => Some(expr),\n                };\n            }\n            // Unable to push down, just return the original expression.\n            _ => return Some(expr),\n        }\n        None\n    }\n\n    /// Pushes a filter node predicate down into its source, if possible.\n    fn maybe_push_filter(node: Node) -> Node {\n        let Node::Filter { mut source, predicate } = node else {\n            return node;\n        };\n        // Attempt to push the filter into the source, or return the original.\n        if let Some(predicate) = Self::push_into(predicate, &mut source) {\n            return Node::Filter { source, predicate };\n        }\n        // Push succeded, return the source that was pushed into. When we\n        // replace this filter node with the source node, Node.transform() will\n        // skip the source node since it now takes the place of the original\n        // filter node. Transform the source manually.\n        Self::push_filters(*source)\n    }\n\n    // Pushes down parts of a join predicate into the left or right sources\n    // where possible.\n    fn maybe_push_join(node: Node) -> Node {\n        let Node::NestedLoopJoin { mut left, mut right, predicate: Some(predicate), outer } = node\n        else {\n            return node;\n        };\n        // Convert the predicate into conjunctive normal form (an AND vector).\n        let cnf = predicate.into_cnf_vec();\n\n        // Push down expressions that don't reference both sources. Constant\n        // expressions can be pushed down into both.\n        let (mut push_left, mut push_right, mut predicate) = (Vec::new(), Vec::new(), Vec::new());\n        for expr in cnf {\n            let (mut ref_left, mut ref_right) = (false, false);\n            expr.walk(&mut |expr| {\n                if let Expression::Column(index) = expr {\n                    ref_left = ref_left || *index < left.columns();\n                    ref_right = ref_right || *index >= left.columns();\n                }\n                !(ref_left && ref_right) // exit once both are referenced\n            });\n            match (ref_left, ref_right) {\n                (true, true) => predicate.push(expr),\n                (true, false) => push_left.push(expr),\n                (false, true) => push_right.push(expr),\n                (false, false) => {\n                    push_left.push(expr.clone());\n                    push_right.push(expr);\n                }\n            }\n        }\n\n        // In the remaining cross-source expressions, look for equijoins where\n        // one side also has constant value lookups. In this case we can copy\n        // the constant lookups to the other side, to allow index lookups. This\n        // commonly happens when joining a foreign key (which is indexed) on a\n        // primary key, and we want to make use of the foreign key index, e.g.:\n        //\n        // SELECT m.name, g.name FROM movies m JOIN genres g ON m.genre_id = g.id AND g.id = 7;\n        let left_lookups: HashMap<usize, usize> = push_left // column → push_left index\n            .iter()\n            .enumerate()\n            .filter_map(|(i, expr)| expr.is_column_lookup().map(|column| (column, i)))\n            .collect();\n        let right_lookups: HashMap<usize, usize> = push_right // column → push_right index\n            .iter()\n            .enumerate()\n            .filter_map(|(i, expr)| expr.is_column_lookup().map(|column| (column, i)))\n            .collect();\n\n        for expr in &predicate {\n            // Find equijoins.\n            let Expression::Equal(lhs, rhs) = expr else { continue };\n            let Expression::Column(mut l) = **lhs else { continue };\n            let Expression::Column(mut r) = **rhs else { continue };\n\n            // The lhs may be a reference to the right source; swap them.\n            if l > r {\n                (l, r) = (r, l)\n            }\n\n            // Check if either side is a column lookup, and copy it over.\n            if let Some(expr) = left_lookups.get(&l).map(|i| push_left[*i].clone()) {\n                push_right.push(expr.replace_column(l, r));\n            }\n            if let Some(expr) = right_lookups.get(&r).map(|i| push_right[*i].clone()) {\n                push_left.push(expr.replace_column(r, l));\n            }\n        }\n\n        // Push predicates down into the sources if possible.\n        if let Some(expr) = Expression::and_vec(push_left)\n            && let Some(expr) = Self::push_into(expr, &mut left)\n        {\n            // Pushdown failed, put it back into the join predicate.\n            predicate.push(expr)\n        }\n\n        if let Some(mut expr) = Expression::and_vec(push_right) {\n            // Right columns have indexes in the joined row; shift them left.\n            expr = expr.shift_column(-(left.columns() as isize));\n            if let Some(mut expr) = Self::push_into(expr, &mut right) {\n                // Pushdown failed, undo the column index shift.\n                expr = expr.shift_column(left.columns() as isize);\n                predicate.push(expr)\n            }\n        }\n\n        // Leave any remaining predicates in the join node.\n        let predicate = Expression::and_vec(predicate);\n        Node::NestedLoopJoin { left, right, predicate, outer }\n    }\n}\n\n/// Uses a primary key or secondary index lookup where possible.\n#[derive(Debug)]\npub struct IndexLookup;\n\nimpl Optimizer for IndexLookup {\n    fn optimize(&self, node: Node) -> Result<Node> {\n        // Recursively transform expressions in the node tree. Post-order to\n        // partially fold child expressions as far as possible, and avoid\n        // quadratic costs.\n        node.transform(&|node| Ok(Self::index_lookup(node)), &Ok)\n    }\n}\n\nimpl IndexLookup {\n    /// Rewrites a filtered scan node into a key or index lookup if possible.\n    fn index_lookup(mut node: Node) -> Node {\n        // Only handle scan filters. Assume FilterPushdown has pushed filters\n        // into scan nodes first.\n        let Node::Scan { table, alias, filter: Some(filter) } = node else {\n            return node;\n        };\n\n        // Convert the filter into conjunctive normal form (a list of ANDs).\n        let mut cnf = filter.clone().into_cnf_vec();\n\n        // Find the first expression that's either a primary key or secondary\n        // index lookup. We could be more clever here, but this is fine.\n        let Some((i, column)) = cnf.iter().enumerate().find_map(|(i, expr)| {\n            expr.is_column_lookup()\n                .filter(|&c| c == table.primary_key || table.columns[c].index)\n                .map(|column| (i, column))\n        }) else {\n            // No index lookups found, return the original node.\n            return Node::Scan { table, alias, filter: Some(filter) };\n        };\n\n        // Extract the lookup values and expression from the cnf vector.\n        let values = cnf.remove(i).into_column_values(column);\n\n        // Build the primary key or secondary index lookup node.\n        if column == table.primary_key {\n            node = Node::KeyLookup { table, keys: values, alias };\n        } else {\n            node = Node::IndexLookup { table, column, values, alias };\n        }\n\n        // If there's any remaining CNF expressions, add a filter node for them.\n        if let Some(predicate) = Expression::and_vec(cnf) {\n            node = Node::Filter { source: Box::new(node), predicate };\n        }\n\n        node\n    }\n}\n\n/// Uses a hash join instead of a nested loop join for single-column equijoins.\n#[derive(Debug)]\npub struct HashJoin;\n\nimpl Optimizer for HashJoin {\n    fn optimize(&self, node: Node) -> Result<Node> {\n        node.transform(&|node| Ok(Self::hash_join(node)), &Ok)\n    }\n}\n\nimpl HashJoin {\n    /// Rewrites a nested loop join into a hash join if possible.\n    pub fn hash_join(node: Node) -> Node {\n        let Node::NestedLoopJoin {\n            left,\n            right,\n            predicate: Some(Expression::Equal(lhs, rhs)),\n            outer,\n        } = node\n        else {\n            return node;\n        };\n\n        match (*lhs, *rhs) {\n            // If this is a single-column equijoin, use a hash join.\n            (Expression::Column(mut left_column), Expression::Column(mut right_column)) => {\n                // The LHS column may be a column in the right table; swap them.\n                if right_column < left_column {\n                    (left_column, right_column) = (right_column, left_column);\n                }\n                // The NestedLoopJoin predicate uses column indexes in the\n                // joined row, while the HashJoin uses column indexes in each\n                // individual table. Adjust the RHS column reference.\n                right_column -= left.columns();\n                Node::HashJoin { left, left_column, right, right_column, outer }\n            }\n            // Otherwise, retain the nested loop join.\n            (lhs, rhs) => {\n                let predicate = Some(Expression::Equal(lhs.into(), rhs.into()));\n                Node::NestedLoopJoin { left, right, predicate, outer }\n            }\n        }\n    }\n}\n\n/// Short-circuits useless nodes and expressions (for example a Filter node that\n/// always evaluates to false), by removing them and/or replacing them with\n/// Nothing nodes that yield no rows.\n#[derive(Debug)]\npub struct ShortCircuit;\n\nimpl Optimizer for ShortCircuit {\n    fn optimize(&self, node: Node) -> Result<Node> {\n        // Post-order transform, to pull Nothing nodes upwards in the tree.\n        node.transform(&Ok, &|node| Ok(Self::short_circuit(node)))\n    }\n}\n\nimpl ShortCircuit {\n    /// Short-circuits useless nodes. Assumes the node has already been\n    /// optimized by ConstantFolding.\n    fn short_circuit(mut node: Node) -> Node {\n        use Expression::*;\n        use Value::*;\n\n        node = match node {\n            // Filter nodes that always yield true are unnecessary: remove them.\n            Node::Filter { source, predicate: Constant(Boolean(true)) } => *source,\n\n            // Predicates that always yield true are unnecessary: remove them.\n            Node::Scan { table, filter: Some(Constant(Boolean(true))), alias } => {\n                Node::Scan { table, filter: None, alias }\n            }\n            Node::NestedLoopJoin {\n                left,\n                right,\n                predicate: Some(Constant(Boolean(true))),\n                outer,\n            } => Node::NestedLoopJoin { left, right, predicate: None, outer },\n\n            // Remove noop projections that simply pass through the source columns.\n            Node::Projection { source, expressions, aliases }\n                if source.columns() == expressions.len()\n                    && aliases.iter().all(|alias| *alias == Label::None)\n                    && expressions\n                        .iter()\n                        .enumerate()\n                        .all(|(i, expr)| *expr == Expression::Column(i)) =>\n            {\n                *source\n            }\n\n            node => node,\n        };\n\n        // Short-circuit nodes that don't produce anything by replacing them\n        // with a Nothing node.\n        let is_empty = match &node {\n            Node::Filter { predicate: Constant(Boolean(false) | Null), .. } => true,\n            Node::IndexLookup { values, .. } if values.is_empty() => true,\n            Node::KeyLookup { keys, .. } if keys.is_empty() => true,\n            Node::Limit { limit: 0, .. } => true,\n            Node::NestedLoopJoin { predicate: Some(Constant(Boolean(false) | Null)), .. } => true,\n            Node::Scan { filter: Some(Constant(Boolean(false) | Null)), .. } => true,\n            Node::Values { rows } if rows.is_empty() => true,\n\n            // Nodes that pull from a Nothing node can't produce anything.\n            //\n            // NB: does not short-circuit aggregation, since an aggregation over 0\n            // rows should produce a result.\n            Node::Filter { source, .. }\n            | Node::HashJoin { left: source, .. }\n            | Node::HashJoin { right: source, .. }\n            | Node::NestedLoopJoin { left: source, .. }\n            | Node::NestedLoopJoin { right: source, .. }\n            | Node::Offset { source, .. }\n            | Node::Order { source, .. }\n            | Node::Projection { source, .. }\n                if matches!(**source, Node::Nothing { .. }) =>\n            {\n                true\n            }\n\n            _ => false,\n        };\n\n        if is_empty {\n            let columns = (0..node.columns()).map(|i| node.column_label(i)).collect();\n            return Node::Nothing { columns };\n        }\n\n        node\n    }\n}\n"
  },
  {
    "path": "src/sql/planner/plan.rs",
    "content": "use std::collections::HashMap;\nuse std::fmt::Display;\n\nuse itertools::Itertools as _;\nuse serde::{Deserialize, Serialize};\n\nuse super::optimizer::OPTIMIZERS;\nuse super::planner::Planner;\nuse crate::error::Result;\nuse crate::sql::engine::{Catalog, Transaction};\nuse crate::sql::execution::{ExecutionResult, Executor};\nuse crate::sql::parser::ast;\nuse crate::sql::types::{Expression, Label, Table, Value};\n\n/// A statement execution plan.\n///\n/// The plan root specifies the action to take (e.g. SELECT, INSERT, UPDATE,\n/// etc). It has a nested tree of child nodes that stream an process rows.\n///\n/// Below is an example of an (unoptimized) query plan:\n///\n/// SELECT title, released, genres.name AS genre\n/// FROM movies INNER JOIN genres ON movies.genre_id = genres.id\n/// WHERE released >= 2000\n/// ORDER BY released\n///\n/// Select\n/// └─ Order: movies.released desc\n///    └─ Projection: movies.title, movies.released, genres.name as genre\n///       └─ Filter: movies.released >= 2000\n///          └─ NestedLoopJoin: inner on movies.genre_id = genres.id\n///             ├─ Scan: movies\n///             └─ Scan: genres\n///\n/// Rows flow from the tree leaves to the root:\n///\n/// 1. Scan nodes read rows from movies and genres.\n/// 2. NestedLoopJoin joins the rows from movies and genres.\n/// 3. Filter discards rows with release dates older than 2000.\n/// 4. Projection picks out the requested column values from the rows.\n/// 5. Order sorts the rows by release date.\n/// 6. Select returns the final rows to the client.\n#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]\npub enum Plan {\n    /// A CREATE TABLE plan. Creates a new table with the given schema. Errors\n    /// if the table already exists or the schema is invalid.\n    CreateTable { schema: Table },\n\n    /// A DROP TABLE plan. Drops the given table. Errors if the table does not\n    /// exist, unless if_exists is true.\n    DropTable { name: String, if_exists: bool },\n\n    /// A DELETE plan. Deletes rows in table that match the rows from source.\n    /// primary_key specifies the primary key column index in the source rows.\n    Delete { table: String, primary_key: usize, source: Node },\n\n    /// An INSERT plan. Inserts rows from source (typically a Values node) into\n    /// table. If column_map is given, it maps table → source column indexes and\n    /// must have one entry for every column in source. Table columns not\n    /// present in source will get the column's default value if set, or error.\n    Insert { table: Table, column_map: Option<HashMap<usize, usize>>, source: Node },\n\n    /// An UPDATE plan. Updates rows in table that match the rows from source,\n    /// where primary_key specifies the primary key column index in the source\n    /// rows. The given column/expression pairs specify the row updates to make,\n    /// evaluated using the existing source row, which must be a complete row\n    /// from the update table.\n    Update { table: Table, primary_key: usize, source: Node, expressions: Vec<(usize, Expression)> },\n\n    /// A SELECT plan. Recursively executes the query plan tree and returns the\n    /// resulting rows.\n    Select(Node),\n}\n\nimpl Plan {\n    /// Builds a plan from an AST statement.\n    pub fn build(statement: ast::Statement, catalog: &impl Catalog) -> Result<Self> {\n        Planner::new(catalog).build(statement)\n    }\n\n    /// Executes the plan, consuming it.\n    pub fn execute(self, txn: &impl Transaction) -> Result<ExecutionResult> {\n        Executor::new(txn).execute(self)\n    }\n\n    /// Optimizes the plan, consuming it. See OPTIMIZERS for the list of\n    /// optimizers.\n    pub fn optimize(self) -> Result<Self> {\n        let optimize = |node| OPTIMIZERS.iter().try_fold(node, |node, opt| opt.optimize(node));\n        Ok(match self {\n            Self::CreateTable { .. } | Self::DropTable { .. } => self,\n            Self::Delete { table, primary_key, source } => {\n                Self::Delete { table, primary_key, source: optimize(source)? }\n            }\n            Self::Insert { table, column_map, source } => {\n                Self::Insert { table, column_map, source: optimize(source)? }\n            }\n            Self::Update { table, primary_key, source, expressions } => {\n                Self::Update { table, primary_key, source: optimize(source)?, expressions }\n            }\n            Self::Select(root) => Self::Select(optimize(root)?),\n        })\n    }\n}\n\n/// A query plan node. Returns a row iterator, and can be nested.\n#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]\npub enum Node {\n    /// Aggregates values for the given group_by buckets, across all rows in the\n    /// source node. The group_by columns are emitted first, followed by the\n    /// aggregate columns, in the given order.\n    Aggregate { source: Box<Node>, group_by: Vec<Expression>, aggregates: Vec<Aggregate> },\n\n    /// Filters source rows, by discarding rows for which the predicate\n    /// evaluates to false.\n    Filter { source: Box<Node>, predicate: Expression },\n\n    /// Joins the left and right sources on the given columns by building an\n    /// in-memory hashmap of the right source and looking up matches for each\n    /// row in the left source. When outer is true (e.g. LEFT JOIN), a left row\n    /// without a right match is emitted anyway, with NULLs for the right row.\n    HashJoin {\n        left: Box<Node>,\n        left_column: usize,\n        right: Box<Node>,\n        right_column: usize,\n        outer: bool,\n    },\n\n    /// Looks up the given values in a secondary index and emits matching rows.\n    /// NULL and NaN values are considered equal, to allow IS NULL and IS NAN\n    /// index lookups, as is -0.0 and 0.0.\n    IndexLookup { table: Table, column: usize, values: Vec<Value>, alias: Option<String> },\n\n    /// Looks up the given primary keys and emits their rows.\n    KeyLookup { table: Table, keys: Vec<Value>, alias: Option<String> },\n\n    /// Only emits the first limit rows from the source, discards the rest.\n    Limit { source: Box<Node>, limit: usize },\n\n    /// Joins the left and right sources on the given predicate by buffering the\n    /// right source and iterating over it for every row in the left source.\n    /// When outer is true (e.g. LEFT JOIN), a left row without a right match is\n    /// emitted anyway, with NULLs for the right row.\n    NestedLoopJoin { left: Box<Node>, right: Box<Node>, predicate: Option<Expression>, outer: bool },\n\n    /// Nothing does not emit anything, and is used to short-circuit nodes that\n    /// can't emit anything during optimization. It retains the column names of\n    /// any replaced nodes for results headers and plan formatting.\n    Nothing { columns: Vec<Label> },\n\n    /// Discards the first offset rows from source, emits the rest.\n    Offset { source: Box<Node>, offset: usize },\n\n    /// Sorts the source rows by the given sort key. Buffers the entire row set\n    /// in memory.\n    Order { source: Box<Node>, key: Vec<(Expression, Direction)> },\n\n    /// Projects the input rows by evaluating the given expressions. Aliases are\n    /// only used when displaying the plan.\n    Projection { source: Box<Node>, expressions: Vec<Expression>, aliases: Vec<Label> },\n\n    /// Remaps source columns to the given target column index, or None to drop\n    /// the column. Unspecified target columns yield Value::Null. The source →\n    /// target mapping ensures a source column can only be mapped to a single\n    /// target column, allowing the value to be moved rather than cloned.\n    Remap { source: Box<Node>, targets: Vec<Option<usize>> },\n\n    /// A full table scan, with an optional pushed-down filter. The schema is\n    /// used during plan optimization. The alias is only used for formatting.\n    Scan { table: Table, filter: Option<Expression>, alias: Option<String> },\n\n    /// A constant set of values.\n    Values { rows: Vec<Vec<Expression>> },\n}\n\nimpl Node {\n    /// Returns the number of columns emitted by the node.\n    pub fn columns(&self) -> usize {\n        match self {\n            // Source nodes emit all table columns.\n            Self::IndexLookup { table, .. }\n            | Self::KeyLookup { table, .. }\n            | Self::Scan { table, .. } => table.columns.len(),\n\n            // These nodes modify the set of columns.\n            Self::Aggregate { aggregates, group_by, .. } => aggregates.len() + group_by.len(),\n            Self::Projection { expressions, .. } => expressions.len(),\n            Self::Remap { targets, .. } => {\n                targets.iter().copied().flatten().map(|i| i + 1).max().unwrap_or(0)\n            }\n\n            // Join nodes emit the combined columns.\n            Self::HashJoin { left, right, .. } | Self::NestedLoopJoin { left, right, .. } => {\n                left.columns() + right.columns()\n            }\n\n            // Constant nodes have a predefined number of columns.\n            Self::Nothing { columns } => columns.len(),\n            Self::Values { rows } => rows.first().map(|row| row.len()).unwrap_or(0),\n\n            // Simple nodes just pass through the source columns.\n            Self::Filter { source, .. }\n            | Self::Limit { source, .. }\n            | Self::Offset { source, .. }\n            | Self::Order { source, .. } => source.columns(),\n        }\n    }\n\n    /// Returns a label for a column, if any, by tracing the column through the\n    /// plan tree. Only used for query result headers and plan display purposes,\n    /// not to look up expression columns (see Scope).\n    pub fn column_label(&self, index: usize) -> Label {\n        match self {\n            // Source nodes use the table/column name.\n            Self::IndexLookup { table, alias, .. }\n            | Self::KeyLookup { table, alias, .. }\n            | Self::Scan { table, alias, .. } => Label::Qualified(\n                alias.as_ref().unwrap_or(&table.name).clone(),\n                table.columns[index].name.clone(),\n            ),\n\n            // These nodes rearrange columns. Route them to the correct upstream\n            // column where appropriate.\n            Self::Aggregate { source, group_by, .. } => match group_by.get(index) {\n                Some(Expression::Column(index)) => source.column_label(*index),\n                Some(_) | None => Label::None,\n            },\n            Self::Projection { source, expressions, aliases } => match aliases.get(index) {\n                Some(Label::None) | None => match expressions.get(index) {\n                    // Unaliased column references route to the source.\n                    Some(Expression::Column(index)) => source.column_label(*index),\n                    // Unaliased expressions don't have a name.\n                    Some(_) | None => Label::None,\n                },\n                // Aliased columns use the alias.\n                Some(alias) => alias.clone(),\n            },\n            Self::Remap { source, targets } => targets\n                .iter()\n                .copied()\n                .position(|t| t == Some(index))\n                .map(|i| source.column_label(i))\n                .unwrap_or(Label::None),\n\n            // Joins dispatch to the appropriate source.\n            Self::HashJoin { left, right, .. } | Self::NestedLoopJoin { left, right, .. } => {\n                if index < left.columns() {\n                    left.column_label(index)\n                } else {\n                    right.column_label(index - left.columns())\n                }\n            }\n\n            // Simple nodes just dispatch to the source.\n            Self::Filter { source, .. }\n            | Self::Limit { source, .. }\n            | Self::Offset { source, .. }\n            | Self::Order { source, .. } => source.column_label(index),\n\n            // Nothing nodes contain the original columns of replaced nodes.\n            Self::Nothing { columns } => columns.get(index).cloned().unwrap_or(Label::None),\n\n            // And some don't have any names at all.\n            Self::Values { .. } => Label::None,\n        }\n    }\n\n    /// Recursively transforms query nodes depth-first by applying the given\n    /// closures before and after descending.\n    pub fn transform(\n        mut self,\n        before: &impl Fn(Self) -> Result<Self>,\n        after: &impl Fn(Self) -> Result<Self>,\n    ) -> Result<Self> {\n        // Helper for transforming boxed nodes.\n        let xform = |mut node: Box<Node>| -> Result<Box<Node>> {\n            *node = node.transform(before, after)?;\n            Ok(node)\n        };\n\n        self = before(self)?;\n        self = match self {\n            Self::Aggregate { source, group_by, aggregates } => {\n                Self::Aggregate { source: xform(source)?, group_by, aggregates }\n            }\n            Self::Filter { source, predicate } => {\n                Self::Filter { source: xform(source)?, predicate }\n            }\n            Self::HashJoin { left, left_column, right, right_column, outer } => Self::HashJoin {\n                left: xform(left)?,\n                left_column,\n                right: xform(right)?,\n                right_column,\n                outer,\n            },\n            Self::Limit { source, limit } => Self::Limit { source: xform(source)?, limit },\n            Self::NestedLoopJoin { left, right, predicate, outer } => {\n                Self::NestedLoopJoin { left: xform(left)?, right: xform(right)?, predicate, outer }\n            }\n            Self::Offset { source, offset } => Self::Offset { source: xform(source)?, offset },\n            Self::Order { source, key } => Self::Order { source: xform(source)?, key },\n            Self::Projection { source, expressions, aliases } => {\n                Self::Projection { source: xform(source)?, expressions, aliases }\n            }\n            Self::Remap { source, targets } => Self::Remap { source: xform(source)?, targets },\n\n            Self::IndexLookup { .. }\n            | Self::KeyLookup { .. }\n            | Self::Nothing { .. }\n            | Self::Scan { .. }\n            | Self::Values { .. } => self,\n        };\n        self = after(self)?;\n        Ok(self)\n    }\n\n    /// Recursively transforms all node expressions by calling the given\n    /// closures on them before and after descending.\n    pub fn transform_expressions(\n        self,\n        before: &impl Fn(Expression) -> Result<Expression>,\n        after: &impl Fn(Expression) -> Result<Expression>,\n    ) -> Result<Self> {\n        Ok(match self {\n            Self::Filter { source, mut predicate } => {\n                predicate = predicate.transform(before, after)?;\n                Self::Filter { source, predicate }\n            }\n            Self::NestedLoopJoin { left, right, predicate: Some(predicate), outer } => {\n                let predicate = Some(predicate.transform(before, after)?);\n                Self::NestedLoopJoin { left, right, predicate, outer }\n            }\n            Self::Order { source, mut key } => {\n                key = key\n                    .into_iter()\n                    .map(|(expr, dir)| expr.transform(before, after).map(|expr| (expr, dir)))\n                    .try_collect()?;\n                Self::Order { source, key }\n            }\n            Self::Projection { source, mut expressions, aliases } => {\n                expressions = expressions\n                    .into_iter()\n                    .map(|expr| expr.transform(before, after))\n                    .try_collect()?;\n                Self::Projection { source, expressions, aliases }\n            }\n            Self::Scan { table, alias, filter: Some(filter) } => {\n                let filter = Some(filter.transform(before, after)?);\n                Self::Scan { table, alias, filter }\n            }\n            Self::Values { mut rows } => {\n                rows = rows\n                    .into_iter()\n                    .map(|row| row.into_iter().map(|expr| expr.transform(before, after)).collect())\n                    .try_collect()?;\n                Self::Values { rows }\n            }\n\n            Self::Aggregate { .. }\n            | Self::HashJoin { .. }\n            | Self::IndexLookup { .. }\n            | Self::KeyLookup { .. }\n            | Self::Limit { .. }\n            | Self::NestedLoopJoin { predicate: None, .. }\n            | Self::Nothing { .. }\n            | Self::Offset { .. }\n            | Self::Remap { .. }\n            | Self::Scan { filter: None, .. } => self,\n        })\n    }\n}\n\n/// An aggregate function.\n#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]\npub enum Aggregate {\n    Average(Expression),\n    Count(Expression),\n    Max(Expression),\n    Min(Expression),\n    Sum(Expression),\n}\n\nimpl Aggregate {\n    fn format(&self, node: &Node) -> String {\n        match self {\n            Self::Average(expr) => format!(\"avg({})\", expr.display(node)),\n            Self::Count(expr) => format!(\"count({})\", expr.display(node)),\n            Self::Max(expr) => format!(\"max({})\", expr.display(node)),\n            Self::Min(expr) => format!(\"min({})\", expr.display(node)),\n            Self::Sum(expr) => format!(\"sum({})\", expr.display(node)),\n        }\n    }\n\n    /// Returns the inner expression.\n    pub fn expr(&self) -> &Expression {\n        match self {\n            Self::Average(expr)\n            | Self::Count(expr)\n            | Self::Max(expr)\n            | Self::Min(expr)\n            | Self::Sum(expr) => expr,\n        }\n    }\n}\n\n/// A sort order direction.\n#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]\npub enum Direction {\n    Ascending,\n    Descending,\n}\n\nimpl Display for Direction {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::Ascending => f.write_str(\"asc\"),\n            Self::Descending => f.write_str(\"desc\"),\n        }\n    }\n}\n\nimpl From<ast::Direction> for Direction {\n    fn from(dir: ast::Direction) -> Self {\n        match dir {\n            ast::Direction::Ascending => Self::Ascending,\n            ast::Direction::Descending => Self::Descending,\n        }\n    }\n}\n\n/// Formats the plan as an EXPLAIN tree.\nimpl Display for Plan {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::CreateTable { schema } => write!(f, \"CreateTable: {}\", schema.name),\n            Self::DropTable { name: table, .. } => write!(f, \"DropTable: {table}\"),\n            Self::Delete { table, source, .. } => {\n                write!(f, \"Delete: {table}\")?;\n                source.format(f, \"\", false, true)\n            }\n            Self::Insert { table, source, .. } => {\n                write!(f, \"Insert: {}\", table.name)?;\n                source.format(f, \"\", false, true)\n            }\n            Self::Update { table, source, expressions, .. } => {\n                let expressions = expressions\n                    .iter()\n                    .map(|(i, expr)| format!(\"{}={}\", table.columns[*i].name, expr.display(source)))\n                    .join(\", \");\n                write!(f, \"Update: {} ({expressions})\", table.name)?;\n                source.format(f, \"\", false, true)\n            }\n            Self::Select(root) => root.format(f, \"\", true, true),\n        }\n    }\n}\n\nimpl Display for Node {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        self.format(f, \"\", true, true)\n    }\n}\n\nimpl Node {\n    /// Recursively formats the node. Prefix is used for tree branch lines. root\n    /// is true if this is the root (first) node, and last_child is true if this\n    /// is the last child node of the parent.\n    pub fn format(\n        &self,\n        f: &mut std::fmt::Formatter<'_>,\n        prefix: &str,\n        root: bool,\n        last_child: bool,\n    ) -> std::fmt::Result {\n        // If this is not the root node, emit a newline after the previous node.\n        // This avoids a spurious newline at the end of the plan.\n        if !root {\n            writeln!(f)?;\n        }\n\n        // Prefix the node with a tree branch line. Modify the prefix for any\n        // child nodes we'll recurse into.\n        let prefix = if !last_child {\n            write!(f, \"{prefix}├─ \")?;\n            format!(\"{prefix}│  \")\n        } else if !root {\n            write!(f, \"{prefix}└─ \")?;\n            format!(\"{prefix}   \")\n        } else {\n            write!(f, \"{prefix}\")?;\n            prefix.to_string()\n        };\n\n        // Format the node.\n        match self {\n            Self::Aggregate { source, aggregates, group_by } => {\n                let aggregates = group_by\n                    .iter()\n                    .map(|group_by| group_by.display(source).to_string())\n                    .chain(aggregates.iter().map(|agg| agg.format(source)))\n                    .join(\", \");\n                write!(f, \"Aggregate: {aggregates}\")?;\n                source.format(f, &prefix, false, true)?;\n            }\n\n            Self::Filter { source, predicate } => {\n                write!(f, \"Filter: {}\", predicate.display(source))?;\n                source.format(f, &prefix, false, true)?;\n            }\n\n            Self::HashJoin { left, left_column, right, right_column, outer } => {\n                let kind = if *outer { \"outer\" } else { \"inner\" };\n                let left_column = match left.column_label(*left_column) {\n                    Label::None => format!(\"left #{left_column}\"),\n                    label => format!(\"{label}\"),\n                };\n                let right_column = match right.column_label(*right_column) {\n                    Label::None => format!(\"right #{right_column}\"),\n                    label => format!(\"{label}\"),\n                };\n                write!(f, \"HashJoin: {kind} on {left_column} = {right_column}\")?;\n                left.format(f, &prefix, false, false)?;\n                right.format(f, &prefix, false, true)?;\n            }\n\n            Self::IndexLookup { table, column, alias, values } => {\n                let column = &table.columns[*column].name;\n                write!(f, \"IndexLookup: {}.{column}\", table.name)?;\n                if let Some(alias) = alias {\n                    write!(f, \" as {alias}.{column}\")?;\n                }\n                if !values.is_empty() && values.len() < 10 {\n                    write!(f, \" ({})\", values.iter().join(\", \"))?;\n                } else {\n                    write!(f, \" ({} values)\", values.len())?;\n                }\n            }\n\n            Self::KeyLookup { table, alias, keys } => {\n                write!(f, \"KeyLookup: {}\", table.name)?;\n                if let Some(alias) = alias {\n                    write!(f, \" as {alias}\")?;\n                }\n                if !keys.is_empty() && keys.len() < 10 {\n                    write!(f, \" ({})\", keys.iter().join(\", \"))?;\n                } else {\n                    write!(f, \" ({} keys)\", keys.len())?;\n                }\n            }\n\n            Self::Limit { source, limit } => {\n                write!(f, \"Limit: {limit}\")?;\n                source.format(f, &prefix, false, true)?;\n            }\n\n            Self::NestedLoopJoin { left, right, predicate, outer, .. } => {\n                let kind = if *outer { \"outer\" } else { \"inner\" };\n                write!(f, \"NestedLoopJoin: {kind}\")?;\n                if let Some(predicate) = predicate {\n                    write!(f, \" on {}\", predicate.display(self))?;\n                }\n                left.format(f, &prefix, false, false)?;\n                right.format(f, &prefix, false, true)?;\n            }\n\n            Self::Nothing { .. } => write!(f, \"Nothing\")?,\n\n            Self::Offset { source, offset } => {\n                write!(f, \"Offset: {offset}\")?;\n                source.format(f, &prefix, false, true)?;\n            }\n\n            Self::Order { source, key: orders } => {\n                let orders = orders\n                    .iter()\n                    .map(|(expr, dir)| format!(\"{} {dir}\", expr.display(source)))\n                    .join(\", \");\n                write!(f, \"Order: {orders}\")?;\n                source.format(f, &prefix, false, true)?;\n            }\n\n            Self::Projection { source, expressions, aliases } => {\n                let expressions = expressions\n                    .iter()\n                    .enumerate()\n                    .map(|(i, expr)| match aliases.get(i) {\n                        Some(Label::None) | None => expr.display(source).to_string(),\n                        Some(alias) => format!(\"{} as {alias}\", expr.display(source)),\n                    })\n                    .join(\", \");\n                write!(f, \"Projection: {expressions}\")?;\n                source.format(f, &prefix, false, true)?;\n            }\n\n            Self::Remap { source, targets } => {\n                let remap = invert_remap(targets)\n                    .into_iter()\n                    .map(|from| match from {\n                        Some(from) => match source.column_label(from) {\n                            Label::None => format!(\"#{from}\"),\n                            label => label.to_string(),\n                        },\n                        None => \"Null\".to_string(),\n                    })\n                    .join(\", \");\n                write!(f, \"Remap: {remap}\")?;\n                let dropped = targets\n                    .iter()\n                    .enumerate()\n                    .filter_map(|(i, v)| {\n                        v.is_none().then_some(match source.column_label(i) {\n                            Label::None => format!(\"#{i}\"),\n                            label => format!(\"{label}\"),\n                        })\n                    })\n                    .join(\", \");\n                if !dropped.is_empty() {\n                    write!(f, \" (dropped: {dropped})\")?;\n                }\n                source.format(f, &prefix, false, true)?;\n            }\n\n            Self::Scan { table, alias, filter } => {\n                write!(f, \"Scan: {}\", table.name)?;\n                if let Some(alias) = alias {\n                    write!(f, \" as {alias}\")?;\n                }\n                if let Some(filter) = filter {\n                    write!(f, \" ({})\", filter.display(self))?;\n                }\n            }\n\n            Self::Values { rows, .. } => {\n                write!(f, \"Values: \")?;\n                match rows.len() {\n                    1 if rows[0].is_empty() => write!(f, \"blank row\")?,\n                    1 => write!(f, \"{}\", rows[0].iter().map(|e| e.display(self)).join(\", \"))?,\n                    n => write!(f, \"{n} rows\")?,\n                }\n            }\n        };\n        Ok(())\n    }\n}\n\n/// Inverts a Remap targets vector to a vector of source indexes, with None\n/// for columns that weren't targeted.\npub fn invert_remap(targets: &[Option<usize>]) -> Vec<Option<usize>> {\n    let size = targets.iter().copied().flatten().map(|i| i + 1).max().unwrap_or(0);\n    let mut sources = vec![None; size];\n    for (from, to) in targets.iter().copied().enumerate() {\n        if let Some(to) = to {\n            sources[to] = Some(from);\n        }\n    }\n    sources\n}\n"
  },
  {
    "path": "src/sql/planner/planner.rs",
    "content": "use std::collections::{BTreeMap, HashMap, HashSet};\n\nuse itertools::{Either, Itertools as _};\n\nuse super::plan::{Aggregate, Node, Plan, invert_remap};\nuse crate::errinput;\nuse crate::error::Result;\nuse crate::sql::engine::Catalog;\nuse crate::sql::parser::ast;\nuse crate::sql::types::{Column, Expression, Label, Table, Value};\n\n/// The planner builds an execution plan from a parsed Abstract Syntax Tree,\n/// using the catalog for schema information.\n///\n/// To build the plan, it recursively traverses the AST and transforms AST nodes\n/// into plan nodes. The planner also resolves column names to column indexes,\n/// using a Scope to track currently visible columns and tables at each node.\npub struct Planner<'a, C: Catalog> {\n    catalog: &'a C,\n}\n\nimpl<'a, C: Catalog> Planner<'a, C> {\n    /// Creates a new planner.\n    pub fn new(catalog: &'a C) -> Self {\n        Self { catalog }\n    }\n\n    /// Builds a plan for an AST statement.\n    pub fn build(&mut self, statement: ast::Statement) -> Result<Plan> {\n        use ast::Statement::*;\n        match statement {\n            CreateTable { name, columns } => self.build_create_table(name, columns),\n            DropTable { name, if_exists } => self.build_drop_table(name, if_exists),\n\n            Delete { table, r#where } => self.build_delete(table, r#where),\n            Insert { table, columns, values } => self.build_insert(table, columns, values),\n            Update { table, set, r#where } => self.build_update(table, set, r#where),\n            Select { select, from, r#where, group_by, having, order_by, offset, limit } => {\n                self.build_select(select, from, r#where, group_by, having, order_by, offset, limit)\n            }\n\n            // Transaction and explain statements are handled by Session.\n            Begin { .. } | Commit | Rollback | Explain(_) => {\n                panic!(\"unexpected statement {statement:?}\")\n            }\n        }\n    }\n\n    /// Builds a CREATE TABLE plan.\n    fn build_create_table(&self, name: String, columns: Vec<ast::Column>) -> Result<Plan> {\n        // Most schema validation happens during execution via Table.validate().\n        let Some(primary_key) = columns.iter().position(|c| c.primary_key) else {\n            return errinput!(\"no primary key for table {name}\");\n        };\n        if columns.iter().filter(|c| c.primary_key).count() > 1 {\n            return errinput!(\"multiple primary keys for table {name}\");\n        }\n        let columns = columns\n            .into_iter()\n            .map(|c| {\n                let nullable = c.nullable.unwrap_or(!c.primary_key);\n                Ok(Column {\n                    name: c.name,\n                    datatype: c.datatype,\n                    nullable,\n                    default: match c.default {\n                        Some(expr) => Some(Self::build_constant_value(expr)?),\n                        None if nullable => Some(Value::Null),\n                        None => None,\n                    },\n                    unique: c.unique || c.primary_key,\n                    index: (c.index || c.unique || c.references.is_some()) && !c.primary_key,\n                    references: c.references,\n                })\n            })\n            .collect::<Result<_>>()?;\n        Ok(Plan::CreateTable { schema: Table { name, primary_key, columns } })\n    }\n\n    /// Builds a DROP TABLE plan.\n    fn build_drop_table(&self, name: String, if_exists: bool) -> Result<Plan> {\n        Ok(Plan::DropTable { name, if_exists })\n    }\n\n    /// Builds a DELETE plan.\n    fn build_delete(&self, table: String, r#where: Option<ast::Expression>) -> Result<Plan> {\n        let table = self.catalog.must_get_table(&table)?;\n        let scope = Scope::from_table(&table)?;\n        let filter = r#where.map(|expr| Self::build_expression(expr, &scope)).transpose()?;\n        Ok(Plan::Delete {\n            table: table.name.clone(),\n            primary_key: table.primary_key,\n            source: Node::Scan { table, alias: None, filter },\n        })\n    }\n\n    /// Builds an INSERT plan.\n    fn build_insert(\n        &self,\n        table: String,\n        columns: Option<Vec<String>>,\n        values: Vec<Vec<ast::Expression>>,\n    ) -> Result<Plan> {\n        let table = self.catalog.must_get_table(&table)?;\n        let mut column_map = None;\n        if let Some(columns) = columns {\n            let column_map = column_map.insert(HashMap::new());\n            for (vindex, name) in columns.into_iter().enumerate() {\n                let Some(cindex) = table.columns.iter().position(|c| c.name == name) else {\n                    return errinput!(\"unknown column {name} in table {}\", table.name);\n                };\n                if column_map.insert(cindex, vindex).is_some() {\n                    return errinput!(\"column {name} given multiple times\");\n                }\n            }\n        }\n        let scope = Scope::new();\n        let rows = values\n            .into_iter()\n            .map(|exprs| {\n                exprs.into_iter().map(|expr| Self::build_expression(expr, &scope)).collect()\n            })\n            .try_collect()?;\n        Ok(Plan::Insert { table, column_map, source: Node::Values { rows } })\n    }\n\n    /// Builds an UPDATE plan.\n    fn build_update(\n        &self,\n        table: String,\n        set: BTreeMap<String, Option<ast::Expression>>,\n        r#where: Option<ast::Expression>,\n    ) -> Result<Plan> {\n        let table = self.catalog.must_get_table(&table)?;\n        let scope = Scope::from_table(&table)?;\n        let filter = r#where.map(|expr| Self::build_expression(expr, &scope)).transpose()?;\n        let mut expressions = Vec::with_capacity(set.len());\n        for (column, expr) in set {\n            let index = scope.lookup_column(None, &column)?;\n            let expr = match expr {\n                Some(expr) => Self::build_expression(expr, &scope)?,\n                None => match &table.columns[index].default {\n                    Some(default) => Expression::Constant(default.clone()),\n                    None => return errinput!(\"column {column} has no default value\"),\n                },\n            };\n            expressions.push((index, expr));\n        }\n        Ok(Plan::Update {\n            table: table.clone(),\n            primary_key: table.primary_key,\n            source: Node::Scan { table, alias: None, filter },\n            expressions,\n        })\n    }\n\n    /// Builds a SELECT plan.\n    #[allow(clippy::too_many_arguments)]\n    fn build_select(\n        &self,\n        mut select: Vec<(ast::Expression, Option<String>)>,\n        from: Vec<ast::From>,\n        r#where: Option<ast::Expression>,\n        group_by: Vec<ast::Expression>,\n        having: Option<ast::Expression>,\n        order_by: Vec<(ast::Expression, ast::Direction)>,\n        offset: Option<ast::Expression>,\n        limit: Option<ast::Expression>,\n    ) -> Result<Plan> {\n        let mut scope = Scope::new();\n\n        // Build FROM clause.\n        let mut node = if !from.is_empty() {\n            self.build_from_clause(from, &mut scope)?\n        } else {\n            // For a constant SELECT, emit a single empty row to project with.\n            // This allows using aggregate functions and WHERE as normal.\n            Node::Values { rows: vec![vec![]] }\n        };\n\n        // Expand out SELECT * to all FROM columns if there are multiple SELECT\n        // expressions or a GROUP BY clause (to ensure all columns are in GROUP\n        // BY). For simplicity, expressions only supports scalar values, so we\n        // special-case the * tuple here.\n        if select.contains(&(ast::Expression::All, None)) {\n            if node.columns() == 0 {\n                return errinput!(\"SELECT * requires a FROM clause\");\n            }\n            if select.len() > 1 || !group_by.is_empty() {\n                select = select\n                    .into_iter()\n                    .flat_map(|(expr, alias)| match expr {\n                        ast::Expression::All => Either::Left(\n                            (0..node.columns()).map(|i| (node.column_label(i).into(), None)),\n                        ),\n                        expr => Either::Right(std::iter::once((expr, alias))),\n                    })\n                    .collect();\n            }\n        }\n\n        // Build WHERE clause.\n        if let Some(r#where) = r#where {\n            let predicate = Self::build_expression(r#where, &scope)?;\n            node = Node::Filter { source: Box::new(node), predicate };\n        }\n\n        // Build aggregate functions and GROUP BY clause.\n        let aggregates = Self::collect_aggregates(&select, &having, &order_by);\n        if !group_by.is_empty() || !aggregates.is_empty() {\n            node = self.build_aggregate(node, group_by, aggregates, &mut scope)?;\n        }\n\n        // Build SELECT clause. We can omit this for a trivial SELECT *.\n        if select.as_slice() != [(ast::Expression::All, None)] {\n            // Prepare the post-projection scope.\n            let mut child_scope = scope.project(&select);\n\n            // Build the SELECT column expressions and aliases.\n            let mut expressions = Vec::with_capacity(select.len());\n            let mut aliases = Vec::with_capacity(select.len());\n            for (expr, alias) in select {\n                expressions.push(Self::build_expression(expr, &scope)?);\n                aliases.push(Label::from(alias));\n            }\n\n            // Add hidden columns for HAVING and ORDER BY columns not in SELECT.\n            let hidden = self.build_select_hidden(&having, &order_by, &scope, &mut child_scope);\n            aliases.extend(std::iter::repeat_n(Label::None, hidden.len()));\n            expressions.extend(hidden);\n\n            scope = child_scope;\n            node = Node::Projection { source: Box::new(node), expressions, aliases };\n        }\n\n        // Build HAVING clause.\n        if let Some(having) = having {\n            if scope.aggregates.is_empty() {\n                return errinput!(\"HAVING requires GROUP BY or aggregate function\");\n            }\n            let predicate = Self::build_expression(having, &scope)?;\n            node = Node::Filter { source: Box::new(node), predicate };\n        }\n\n        // Build ORDER BY clause.\n        if !order_by.is_empty() {\n            let key = order_by\n                .into_iter()\n                .map(|(expr, dir)| Ok((Self::build_expression(expr, &scope)?, dir.into())))\n                .collect::<Result<_>>()?;\n            node = Node::Order { source: Box::new(node), key };\n        }\n\n        // Build OFFSET clause.\n        if let Some(offset) = offset {\n            let offset = match Self::build_constant_value(offset)? {\n                Value::Integer(offset) if offset >= 0 => offset as usize,\n                offset => return errinput!(\"invalid offset {offset}\"),\n            };\n            node = Node::Offset { source: Box::new(node), offset }\n        }\n\n        // Build LIMIT clause.\n        if let Some(limit) = limit {\n            let limit = match Self::build_constant_value(limit)? {\n                Value::Integer(limit) if limit >= 0 => limit as usize,\n                limit => return errinput!(\"invalid limit {limit}\"),\n            };\n            node = Node::Limit { source: Box::new(node), limit }\n        }\n\n        // Remove any hidden columns before emitting the result.\n        if let Some(targets) = scope.remap_hidden() {\n            node = Node::Remap { source: Box::new(node), targets }\n        }\n\n        Ok(Plan::Select(node))\n    }\n\n    /// Builds a FROM clause consisting of one or more items. Each item is\n    /// either a table or a join of two or more tables. All items are implicitly\n    /// joined, e.g. \"SELECT * FROM a, b\" is an implicit full join of a and b.\n    fn build_from_clause(&self, from: Vec<ast::From>, scope: &mut Scope) -> Result<Node> {\n        // Build the first FROM item. A FROM clause must have at least one.\n        let mut items = from.into_iter();\n        let mut node = match items.next() {\n            Some(from) => self.build_from(from, scope)?,\n            None => return errinput!(\"no from items given\"),\n        };\n\n        // Build and implicitly join additional items.\n        for from in items {\n            let right = self.build_from(from, scope)?;\n            node = Node::NestedLoopJoin {\n                left: Box::new(node),\n                right: Box::new(right),\n                predicate: None,\n                outer: false,\n            };\n        }\n        Ok(node)\n    }\n\n    /// Builds FROM items, which can either be a single table or a chained join\n    /// of multiple tables, e.g. \"SELECT * FROM a LEFT JOIN b ON b.a_id = a.id\".\n    fn build_from(&self, from: ast::From, parent_scope: &mut Scope) -> Result<Node> {\n        // Each from item is built in its own scope, such that a join node only\n        // sees the columns of its children. It's then merged into the parent.\n        let mut scope = Scope::new();\n\n        let node = match from {\n            // A full table scan.\n            ast::From::Table { name, alias } => {\n                let table = self.catalog.must_get_table(&name)?;\n                scope.add_table(&table, alias.as_deref())?;\n                Node::Scan { table, alias, filter: None }\n            }\n\n            // A two-way join. The left or right nodes may be chained joins.\n            ast::From::Join { mut left, mut right, r#type, predicate } => {\n                // Right joins are built as a left join then column swap.\n                if r#type == ast::JoinType::Right {\n                    (left, right) = (right, left)\n                }\n\n                // Build the left and right nodes.\n                let left = Box::new(self.build_from(*left, &mut scope)?);\n                let right = Box::new(self.build_from(*right, &mut scope)?);\n                let (left_size, right_size) = (left.columns(), right.columns());\n\n                // Build the join node.\n                let predicate = predicate.map(|e| Self::build_expression(e, &scope)).transpose()?;\n                let outer = r#type.is_outer();\n                let mut node = Node::NestedLoopJoin { left, right, predicate, outer };\n\n                // For right joins, swap the columns.\n                if r#type == ast::JoinType::Right {\n                    let size = left_size + right_size;\n                    let targets = (0..size).map(|i| Some((i + right_size) % size)).collect_vec();\n                    scope = scope.remap(&targets);\n                    node = Node::Remap { source: Box::new(node), targets }\n                }\n                node\n            }\n        };\n\n        parent_scope.merge(scope)?;\n        Ok(node)\n    }\n\n    /// Builds an aggregate node, which computes aggregates for a set of GROUP\n    /// BY buckets. The aggregate functions have been collected from the SELECT,\n    /// HAVING, and ORDER BY clauses.\n    ///\n    /// The ast::Expression for each aggregate function and GROUP BY expression\n    /// is tracked in the Scope and mapped to the column index. Later nodes\n    /// (i.e. SELECT, HAVING, and ORDER BY) can look up the column index of\n    /// aggregate expressions while building expressions. Consider e.g.:\n    ///\n    /// SELECT SUM(a) / COUNT(*) FROM t GROUP BY b % 10 HAVING b % 10 >= 5 ORDER BY MAX(c)\n    ///\n    /// This will build an Aggregate node for SUM(a), COUNT(*), MAX(c) bucketed\n    /// by b % 10. The SELECT can look up up SUM(a) and COUNT(*) to compute the\n    /// division, and HAVING can look up b % 10 to compute the predicate.\n    fn build_aggregate(\n        &self,\n        source: Node,\n        mut group_by: Vec<ast::Expression>,\n        mut aggregates: Vec<ast::Expression>,\n        scope: &mut Scope,\n    ) -> Result<Node> {\n        // Construct a child scope with the group_by and aggregate AST\n        // expressions, for lookups. Discard duplicate expressions.\n        let mut child_scope = scope.spawn();\n        group_by.retain(|expr| child_scope.add_aggregate(expr, scope).is_some());\n        aggregates.retain(|expr| child_scope.add_aggregate(expr, scope).is_some());\n\n        // Build the node from the remaining unique expressions.\n        let group_by =\n            group_by.into_iter().map(|expr| Self::build_expression(expr, scope)).try_collect()?;\n        let aggregates = aggregates\n            .into_iter()\n            .map(|expr| Self::build_aggregate_function(expr, scope))\n            .try_collect()?;\n\n        *scope = child_scope;\n        Ok(Node::Aggregate { source: Box::new(source), group_by, aggregates })\n    }\n\n    /// Builds an aggregate function from an AST expression.\n    fn build_aggregate_function(expr: ast::Expression, scope: &Scope) -> Result<Aggregate> {\n        let ast::Expression::Function(name, mut args) = expr else {\n            panic!(\"aggregate expression must be function\");\n        };\n        if args.len() != 1 {\n            return errinput!(\"{name} takes 1 argument\");\n        }\n        if args[0].contains(&|expr| Self::is_aggregate_function(expr)) {\n            return errinput!(\"aggregate functions can't be nested\");\n        }\n        // Special-case COUNT(*) since expressions don't support tuples.\n        let expr = match (name.as_str(), args.remove(0)) {\n            (\"count\", ast::Expression::All) => Expression::Constant(Value::Boolean(true)),\n            (_, arg) => Self::build_expression(arg, scope)?,\n        };\n        Ok(match name.as_str() {\n            \"avg\" => Aggregate::Average(expr),\n            \"count\" => Aggregate::Count(expr),\n            \"min\" => Aggregate::Min(expr),\n            \"max\" => Aggregate::Max(expr),\n            \"sum\" => Aggregate::Sum(expr),\n            name => return errinput!(\"unknown aggregate function {name}\"),\n        })\n    }\n\n    /// Checks whether a given AST expression is an aggregate function.\n    fn is_aggregate_function(expr: &ast::Expression) -> bool {\n        if let ast::Expression::Function(name, _) = expr {\n            return [\"avg\", \"count\", \"max\", \"min\", \"sum\"].contains(&name.as_str());\n        }\n        false\n    }\n\n    /// Collects aggregate functions from SELECT, HAVING, and ORDER BY clauses.\n    fn collect_aggregates(\n        select: &[(ast::Expression, Option<String>)],\n        having: &Option<ast::Expression>,\n        order_by: &[(ast::Expression, ast::Direction)],\n    ) -> Vec<ast::Expression> {\n        let select = select.iter().map(|(expr, _)| expr);\n        let having = having.iter();\n        let order_by = order_by.iter().map(|(expr, _)| expr);\n        let mut aggregates = Vec::new();\n        for expr in select.chain(having).chain(order_by) {\n            expr.collect(&|expr| Self::is_aggregate_function(expr), &mut aggregates)\n        }\n        aggregates\n    }\n\n    /// Builds hidden columns for a projection to pass through columns that are\n    /// used by downstream nodes. Consider e.g.:\n    ///\n    /// SELECT id FROM table ORDER BY value\n    ///\n    /// The ORDER BY node is evaluated after the SELECT projection (it may need\n    /// to order on projected columns), but \"value\" isn't projected and thus\n    /// isn't available to the ORDER BY node. We add a hidden \"value\" column to\n    /// the projection to satisfy the ORDER BY.\n    ///\n    /// Hidden columns are tracked in the scope and stripped before the result\n    /// is returned to the client.\n    fn build_select_hidden(\n        &self,\n        having: &Option<ast::Expression>,\n        order_by: &[(ast::Expression, ast::Direction)],\n        scope: &Scope,\n        child_scope: &mut Scope,\n    ) -> Vec<Expression> {\n        let mut hidden = Vec::new();\n        for expr in having.iter().chain(order_by.iter().map(|(expr, _)| expr)) {\n            expr.walk(&mut |expr| {\n                // If this is an aggregate or GROUP BY expression that isn't\n                // already available in the child scope, add a hidden column.\n                if let Some(index) = scope.lookup_aggregate(expr)\n                    && child_scope.lookup_aggregate(expr).is_none()\n                {\n                    child_scope.add_passthrough(scope, index, true);\n                    hidden.push(Expression::Column(index));\n                    return true;\n                }\n\n                // Look for column references that don't exist post-projection,\n                // but that do exist in the parent, and add hidden columns.\n                let ast::Expression::Column(table, column) = expr else {\n                    return true;\n                };\n                if child_scope.lookup_column(table.as_deref(), column).is_ok() {\n                    return true;\n                }\n                let Ok(index) = scope.lookup_column(table.as_deref(), column) else {\n                    // If the parent lookup fails too (i.e. unknown column),\n                    // ignore the error. It will be surfaced during building.\n                    return true;\n                };\n                child_scope.add_passthrough(scope, index, true);\n                hidden.push(Expression::Column(index));\n                true\n            });\n        }\n        hidden\n    }\n\n    /// Builds an expression from an AST expression, looking up columns and\n    /// aggregate expressions in the scope.\n    pub fn build_expression(expr: ast::Expression, scope: &Scope) -> Result<Expression> {\n        use Expression::*;\n\n        // Look up aggregate functions or GROUP BY expressions. These were added\n        // to the scope when building the Aggregate node, if any.\n        if let Some(index) = scope.lookup_aggregate(&expr) {\n            return Ok(Column(index));\n        }\n\n        // Helper for building a boxed expression.\n        let build = |expr: Box<ast::Expression>| -> Result<Box<Expression>> {\n            Ok(Box::new(Self::build_expression(*expr, scope)?))\n        };\n\n        Ok(match expr {\n            // For simplicity, expression evaluation only supports scalar\n            // values, not compound types like tuples. Support for * is\n            // therefore special-cased in SELECT and COUNT(*).\n            ast::Expression::All => return errinput!(\"unsupported use of *\"),\n            ast::Expression::Literal(l) => Constant(match l {\n                ast::Literal::Null => Value::Null,\n                ast::Literal::Boolean(b) => Value::Boolean(b),\n                ast::Literal::Integer(i) => Value::Integer(i),\n                ast::Literal::Float(f) => Value::Float(f),\n                ast::Literal::String(s) => Value::String(s),\n            }),\n            ast::Expression::Column(table, name) => {\n                Column(scope.lookup_column(table.as_deref(), &name)?)\n            }\n            ast::Expression::Function(name, mut args) => match (name.as_str(), args.len()) {\n                // NB: aggregate functions are processed above.\n                (\"sqrt\", 1) => SquareRoot(build(Box::new(args.remove(0)))?),\n                (name, n) => return errinput!(\"unknown function {name} with {n} arguments\"),\n            },\n            ast::Expression::Operator(op) => match op {\n                ast::Operator::And(lhs, rhs) => And(build(lhs)?, build(rhs)?),\n                ast::Operator::Not(expr) => Not(build(expr)?),\n                ast::Operator::Or(lhs, rhs) => Or(build(lhs)?, build(rhs)?),\n\n                ast::Operator::Equal(lhs, rhs) => Equal(build(lhs)?, build(rhs)?),\n                ast::Operator::GreaterThan(lhs, rhs) => GreaterThan(build(lhs)?, build(rhs)?),\n                ast::Operator::GreaterThanOrEqual(lhs, rhs) => Or(\n                    GreaterThan(build(lhs.clone())?, build(rhs.clone())?).into(),\n                    Equal(build(lhs)?, build(rhs)?).into(),\n                ),\n                ast::Operator::Is(expr, literal) => {\n                    let expr = build(expr)?;\n                    let value = match literal {\n                        ast::Literal::Null => Value::Null,\n                        ast::Literal::Float(f) if f.is_nan() => Value::Float(f),\n                        value => panic!(\"invalid IS value {value:?}\"), // enforced by parser\n                    };\n                    Is(expr, value)\n                }\n                ast::Operator::LessThan(lhs, rhs) => LessThan(build(lhs)?, build(rhs)?),\n                ast::Operator::LessThanOrEqual(lhs, rhs) => Or(\n                    LessThan(build(lhs.clone())?, build(rhs.clone())?).into(),\n                    Equal(build(lhs)?, build(rhs)?).into(),\n                ),\n                ast::Operator::Like(lhs, rhs) => Like(build(lhs)?, build(rhs)?),\n                ast::Operator::NotEqual(lhs, rhs) => Not(Equal(build(lhs)?, build(rhs)?).into()),\n\n                ast::Operator::Add(lhs, rhs) => Add(build(lhs)?, build(rhs)?),\n                ast::Operator::Divide(lhs, rhs) => Divide(build(lhs)?, build(rhs)?),\n                ast::Operator::Exponentiate(lhs, rhs) => Exponentiate(build(lhs)?, build(rhs)?),\n                ast::Operator::Factorial(expr) => Factorial(build(expr)?),\n                ast::Operator::Identity(expr) => Identity(build(expr)?),\n                ast::Operator::Remainder(lhs, rhs) => Remainder(build(lhs)?, build(rhs)?),\n                ast::Operator::Multiply(lhs, rhs) => Multiply(build(lhs)?, build(rhs)?),\n                ast::Operator::Negate(expr) => Negate(build(expr)?),\n                ast::Operator::Subtract(lhs, rhs) => Subtract(build(lhs)?, build(rhs)?),\n            },\n        })\n    }\n\n    /// Builds a constant value from an AST expression by evaluating it. The\n    /// expression can't contain column references or aggregate functions.\n    fn build_constant_value(expr: ast::Expression) -> Result<Value> {\n        Self::build_expression(expr, &Scope::new())?.evaluate(None)\n    }\n}\n\n/// A scope maps column/table names to input column indexes, for lookups during\n/// expression construction. It also tracks aggregate and GROUP BY expressions,\n/// as well as hidden columns (e.g. ORDER BY columns that aren't projected in\n/// the SELECT clause).\n///\n/// When building expressions, the scope is used to resolve column names to\n/// column indexes, which are placed in the plan and used during execution.\n/// Expression evaluation generally happens in the context of an input row. This\n/// row may come directly from a single table, or it may be the result of a long\n/// chain of joins and projections. The scope keeps track of which columns are\n/// currently visible and what names they have.\n#[derive(Default)]\npub struct Scope {\n    /// The currently visible columns. If empty, only constant expressions can\n    /// be used (no column references).\n    columns: Vec<Label>,\n    /// Index of currently visible tables, by query name (e.g. may be aliased).\n    tables: HashSet<String>,\n    /// Index of fully qualified table.column names to column indexes. Qualified\n    /// names are always unique within a scope.\n    qualified: HashMap<(String, String), usize>,\n    /// Index of unqualified column names to column indexes. If a name points\n    /// to multiple columns, lookups will fail with an ambiguous name error.\n    unqualified: HashMap<String, Vec<usize>>,\n    /// Index of aggregate and GROUP BY expressions to column indexes. This is\n    /// used to track output columns of Aggregate nodes and look them up from\n    /// expressions in downstream SELECT, HAVING, and ORDER BY clauses. If the\n    /// node contains an (inner) Aggregate node, this is never empty.\n    aggregates: HashMap<ast::Expression, usize>,\n    /// Hidden columns. These are used to pass e.g. ORDER BY and HAVING\n    /// expressions through SELECT projection nodes if the expressions aren't\n    /// already projected. They should be removed before emitting results.\n    hidden: HashSet<usize>,\n}\n\nimpl Scope {\n    /// Creates a new, empty scope.\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    /// Creates a scope from a table, using the table's original name.\n    fn from_table(table: &Table) -> Result<Self> {\n        let mut scope = Self::new();\n        scope.add_table(table, None)?;\n        Ok(scope)\n    }\n\n    /// Creates a new child scope that inherits from the parent scope.\n    pub fn spawn(&self) -> Self {\n        let mut child = Scope::new();\n        child.tables = self.tables.clone(); // retain table names\n        child\n    }\n\n    /// Adds a table to the scope. The label is either the table's original name\n    /// or an alias, and must be unique. All table columns are added, in order.\n    fn add_table(&mut self, table: &Table, alias: Option<&str>) -> Result<()> {\n        let name = alias.unwrap_or(&table.name);\n        if self.tables.contains(name) {\n            return errinput!(\"duplicate table name {name}\");\n        }\n        for column in &table.columns {\n            self.add_column(Label::Qualified(name.to_string(), column.name.clone()));\n        }\n        self.tables.insert(name.to_string());\n        Ok(())\n    }\n\n    /// Appends a column with the given label to the scope. Returns the column\n    /// index.\n    fn add_column(&mut self, label: Label) -> usize {\n        let index = self.columns.len();\n        if let Label::Qualified(table, column) = &label {\n            self.qualified.insert((table.clone(), column.clone()), index);\n        }\n        if let Label::Qualified(_, name) | Label::Unqualified(name) = &label {\n            self.unqualified.entry(name.clone()).or_default().push(index)\n        }\n        self.columns.push(label);\n        index\n    }\n\n    /// Looks up a column index by name, if possible.\n    fn lookup_column(&self, table: Option<&str>, name: &str) -> Result<usize> {\n        let fmtname = || table.map(|table| format!(\"{table}.{name}\")).unwrap_or(name.to_string());\n        if self.columns.is_empty() {\n            return errinput!(\"expression must be constant, found column {}\", fmtname());\n        }\n        if let Some(table) = table {\n            if !self.tables.contains(table) {\n                return errinput!(\"unknown table {table}\");\n            }\n            if let Some(index) = self.qualified.get(&(table.to_string(), name.to_string())) {\n                return Ok(*index);\n            }\n        } else if let Some(indexes) = self.unqualified.get(name) {\n            if indexes.len() > 1 {\n                return errinput!(\"ambiguous column {name}\");\n            }\n            return Ok(indexes[0]);\n        }\n        if !self.aggregates.is_empty() {\n            return errinput!(\n                \"column {} must be used in an aggregate or GROUP BY expression\",\n                fmtname()\n            );\n        }\n        errinput!(\"unknown column {}\", fmtname())\n    }\n\n    /// Adds an aggregate expression to the scope, returning the new column\n    /// index or None if the expression already exists. This is either an\n    /// aggregate function or a GROUP BY expression, used to look up the\n    /// aggregate output column from e.g. SELECT, HAVING, and ORDER BY.\n    fn add_aggregate(&mut self, expr: &ast::Expression, parent: &Scope) -> Option<usize> {\n        if self.aggregates.contains_key(expr) {\n            return None;\n        }\n        // If this is a simple column reference (i.e. GROUP BY foo), pass\n        // through the column label from the parent scope for lookups.\n        let mut label = Label::None;\n        if let ast::Expression::Column(table, column) = expr {\n            // Ignore errors, they will be emitted when building the expression.\n            if let Ok(index) = parent.lookup_column(table.as_deref(), column.as_str()) {\n                label = parent.columns[index].clone();\n            }\n        }\n        let index = self.add_column(label);\n        self.aggregates.insert(expr.clone(), index);\n        Some(index)\n    }\n\n    /// Looks up an aggregate column index by aggregate function or GROUP BY\n    /// expression.\n    fn lookup_aggregate(&self, expr: &ast::Expression) -> Option<usize> {\n        self.aggregates.get(expr).copied()\n    }\n\n    /// Adds a column that passes through a column from the parent scope,\n    /// retaining its properties. If hide is true, the column is hidden.\n    fn add_passthrough(&mut self, parent: &Scope, parent_index: usize, hide: bool) -> usize {\n        let index = self.add_column(parent.columns[parent_index].clone());\n        for (expr, i) in &parent.aggregates {\n            if *i == parent_index {\n                self.aggregates.entry(expr.clone()).or_insert(index);\n            }\n        }\n        if hide || parent.hidden.contains(&parent_index) {\n            self.hidden.insert(index);\n        }\n        index\n    }\n\n    /// Merges two scopes, by appending the given scope to self.\n    fn merge(&mut self, scope: Scope) -> Result<()> {\n        for table in scope.tables {\n            if self.tables.contains(&table) {\n                return errinput!(\"duplicate table name {table}\");\n            }\n            self.tables.insert(table);\n        }\n        let offset = self.columns.len();\n        for label in scope.columns {\n            self.add_column(label);\n        }\n        for (expr, index) in scope.aggregates {\n            self.aggregates.entry(expr).or_insert(index + offset);\n        }\n        self.hidden.extend(scope.hidden.into_iter().map(|index| index + offset));\n        Ok(())\n    }\n\n    /// Projects the scope via the given expressions and aliases, creating a new\n    /// child scope with one column per expression. These may be a simple column\n    /// reference (e.g. \"SELECT a, b FROM table\"), which passes through the\n    /// corresponding column from the original scope and retains its qualified\n    /// and unqualified names. Otherwise, for non-trivial column references, a\n    /// new column is created for the expression. Explicit aliases may be given.\n    fn project(&self, expressions: &[(ast::Expression, Option<String>)]) -> Self {\n        let mut child = self.spawn();\n        for (expr, alias) in expressions {\n            // Use the alias if given, or look up any column references.\n            let mut label = Label::None;\n            if let Some(alias) = alias {\n                label = Label::Unqualified(alias.clone());\n            } else if let ast::Expression::Column(table, column) = expr {\n                // Ignore errors, they will be surfaced in build_expression().\n                if let Ok(index) = self.lookup_column(table.as_deref(), column.as_str()) {\n                    label = self.columns[index].clone();\n                }\n            }\n            let index = child.add_column(label);\n            // If this is an aggregate query, then all projected expressions\n            // must also be aggregates by definition (an aggregate node can only\n            // emit aggregate functions or GROUP BY expressions).\n            if !self.aggregates.is_empty() {\n                child.aggregates.entry(expr.clone()).or_insert(index);\n            }\n        }\n        child\n    }\n\n    /// Remaps the scope using the given targets.\n    fn remap(&self, targets: &[Option<usize>]) -> Self {\n        let mut child = self.spawn();\n        for index in invert_remap(targets).into_iter().flatten() {\n            child.add_passthrough(self, index, false);\n        }\n        child\n    }\n\n    /// Removes hidden columns from the scope, returning their indexes or None\n    /// if no columns are hidden.\n    fn remove_hidden(&mut self) -> Option<HashSet<usize>> {\n        if self.hidden.is_empty() {\n            return None;\n        }\n        let hidden = std::mem::take(&mut self.hidden);\n        let mut index = 0;\n        self.columns.retain(|_| {\n            let retain = !hidden.contains(&index);\n            index += 1;\n            retain\n        });\n        self.qualified.retain(|_, index| !hidden.contains(index));\n        self.unqualified.iter_mut().for_each(|(_, vec)| vec.retain(|i| !hidden.contains(i)));\n        self.unqualified.retain(|_, vec| !vec.is_empty());\n        self.aggregates.retain(|_, index| !hidden.contains(index));\n        Some(hidden)\n    }\n\n    /// Removes hidden columns from the scope and returns the remaining column\n    /// indexes as a Remap targets vector, or None if no columns are hidden. A\n    /// Remap targets vector maps parent column indexes to child column indexes,\n    /// or None if a column should be dropped.\n    fn remap_hidden(&mut self) -> Option<Vec<Option<usize>>> {\n        let size = self.columns.len();\n        let hidden = self.remove_hidden()?;\n        let mut targets = vec![None; size];\n        let mut index = 0;\n        for (old_index, target) in targets.iter_mut().enumerate() {\n            if !hidden.contains(&old_index) {\n                *target = Some(index);\n                index += 1;\n            }\n        }\n        Some(targets)\n    }\n}\n"
  },
  {
    "path": "src/sql/testscripts/expressions/cnf",
    "content": "# Tests conversion of logical expressions into canonical normal form.\n\n# Noop for non-boolean expressions.\n[cnf]> 1 + 2\n---\n3 ← 1 + 2\n\n# Applies De Morgan's laws.\n[cnf]> NOT (TRUE AND FALSE)\n---\nTRUE ← NOT TRUE OR NOT FALSE\n\n[cnf]> NOT (TRUE OR FALSE)\n---\nFALSE ← NOT TRUE AND NOT FALSE\n\n# NOTs are pushed into the expression.\n[cnf]> NOT (TRUE AND TRUE AND TRUE OR TRUE)\n---\nFALSE ← (NOT TRUE OR NOT TRUE OR NOT TRUE) AND NOT TRUE\n\n# ORs are converted to ANDs by the distributive law.\n[cnf]> (TRUE AND FALSE) OR (FALSE AND TRUE)\n---\nFALSE ← (TRUE OR FALSE) AND (TRUE OR TRUE) AND (FALSE OR FALSE) AND (FALSE OR TRUE)\n\n# This is also true when combined with De Morgan's laws.\n[cnf]> NOT ((TRUE OR FALSE) AND (TRUE OR FALSE))\n---\nFALSE ← (NOT TRUE OR NOT TRUE) AND (NOT TRUE OR NOT FALSE) AND (NOT FALSE OR NOT TRUE) AND (NOT FALSE OR NOT FALSE)\n"
  },
  {
    "path": "src/sql/testscripts/expressions/func",
    "content": "# Tests function calls.\n\n# Function names are case-insensitive.\n> sqrt(1)\n> SQRT(1)\n---\n1.0\n1.0\n\n# A space is allowed around the arguments.\n> sqrt ( 1 )\n---\n1.0\n\n# Wrong number of arguments errors.\n!> sqrt()\n!> sqrt(1, 2)\n---\nError: invalid input: unknown function sqrt with 0 arguments\nError: invalid input: unknown function sqrt with 2 arguments\n\n# Unknown functions error.\n!> unknown()\n!> unknown(1, 2, 3)\n---\nError: invalid input: unknown function unknown with 0 arguments\nError: invalid input: unknown function unknown with 3 arguments\n\n# Parse errors.\n!> unknown(1, 2, 3\n!> unknown(1, 2, 3,)\n!> unknown(1, 2 3)\n---\nError: invalid input: unexpected end of input\nError: invalid input: expected expression atom, found )\nError: invalid input: expected token ,, found 3\n"
  },
  {
    "path": "src/sql/testscripts/expressions/func_sqrt",
    "content": "# Tests sqrt().\n\n# Integers work, and return floats.\n[expr]> sqrt(2)\n[expr]> sqrt(100)\n---\n1.4142135623730951 ← SquareRoot(Constant(Integer(2)))\n10.0 ← SquareRoot(Constant(Integer(100)))\n\n# Negative integers error, but 0 is valid.\n!> sqrt(-1)\n> sqrt(0)\n---\nError: invalid input: can't take square root of -1\n0.0\n\n# Floats work.\n> sqrt(3.14)\n> sqrt(100.0)\n---\n1.772004514666935\n10.0\n\n# Negative floats work, but return NAN.\n> sqrt(-1.0)\n---\nNaN\n\n# Test various special float values.\n> sqrt(-0.0)\n> sqrt(0.0)\n> sqrt(NAN)\n> sqrt(INFINITY)\n> sqrt(-INFINITY)\n---\n-0.0\n0.0\nNaN\ninf\nNaN\n\n# NULL is passed through.\n> sqrt(NULL)\n---\nNULL\n\n# Strings and booleans error.\n!> sqrt(TRUE)\n!> sqrt('foo')\n---\nError: invalid input: can't take square root of TRUE\nError: invalid input: can't take square root of 'foo'\n"
  },
  {
    "path": "src/sql/testscripts/expressions/literals",
    "content": "# Tests parsing and evaluation of literals and constants.\n\n# Boolean and float constants.\ntrue\nfalse\nnull\ninfinity\nnan\n---\nTRUE\nFALSE\nNULL\ninf\nNaN\n\n# Constants are case-insensitive.\nNULL\nNaN\n---\nNULL\nNaN\n\n# Integers.\n3\n314\n03\n---\n3\n314\n3\n\n# Floats with decimal points.\n3.72\n3.\n3.0\n---\n3.72\n3.0\n3.0\n\n# Negative or explicit positive numbers are parsed as prefix operators.\n[expr]> -3\n[expr]> +3\n[expr]> -3.14\n[expr]> +3.14\n---\n-3 ← Negate(Constant(Integer(3)))\n3 ← Identity(Constant(Integer(3)))\n-3.14 ← Negate(Constant(Float(3.14)))\n3.14 ← Identity(Constant(Float(3.14)))\n\n# Floats with exponents.\n3.14e3\n2.718E-2\n---\n3140.0\n0.02718\n\n# Integer overflow/underflow.\n>  9223372036854775807\n!> 9223372036854775808\n>  -9223372036854775807\n!> -9223372036854775808\n---\n9223372036854775807\nError: invalid input: number too large to fit in target type\n-9223372036854775807\nError: invalid input: number too large to fit in target type\n\n# Float overflow/underflow.\n> 1.23456789012345e308\n> 1e309\n> -1.23456789012345e308\n> -1e309\n---\n1.23456789012345e308\ninf\n-1.23456789012345e308\n-inf\n\n# Float precision.\n> 1.23456789012345e-307\n> -1.23456789012345e-307\n> 1.23456789012345e-323\n> 0.12345678901234567890\n> 1e-325\n---\n1.23456789012345e-307\n-1.23456789012345e-307\n1e-323\n0.12345678901234568\n0.0\n\n# Strings, using single quotes. Only '' is supported as an escape sequence.\n> 'Hi! 👋'\n> 'Has ''single'' and \"double\" quotes'\n> 'Try \\n newlines and \\t tabs'\n---\n'Hi! 👋'\n'Has \\'single\\' and \\\"double\\\" quotes'\n'Try \\\\n newlines and \\\\t tabs'\n\n# Double quotes are identifiers, not string literals. This fails to evaluate as\n# a constant expression.\n!> \"Hi!\"\n---\nError: invalid input: expression must be constant, found column Hi!\n"
  },
  {
    "path": "src/sql/testscripts/expressions/op_compare_equal",
    "content": "# Tests the = equality operator.\n\n# Booleans.\n> TRUE = TRUE\n> TRUE = FALSE\n> FALSE = TRUE\n---\nTRUE\nFALSE\nFALSE\n\n# Integers.\n> 1 = 1\n> 1 = 2\n---\nTRUE\nFALSE\n\n# Floats.\n> 3.14 = 3.14\n> 3.14 = 2.718\n---\nTRUE\nFALSE\n\n# Float special values.\n> 0.0 = -0.0\n> INFINITY = INFINITY\n> NAN = NAN\n---\nTRUE\nTRUE\nFALSE\n\n# Mixed integers and floats.\n> 3.0 = 3\n> 3.01 = 3\n> 3 = 3.01\n> -0.0 = 0\n---\nTRUE\nFALSE\nFALSE\nTRUE\n\n# Strings.\n> 'abc' = 'abc'\n> 'abc' = 'ab'\n> 'abc' = 'abcd'\n> 'abc' = 'ABC'\n> '😀' = '😀'\n> '😀' = '🙁'\n---\nTRUE\nFALSE\nFALSE\nFALSE\nTRUE\nFALSE\n\n# NULLs.\n> 1 = NULL\n> 3.14 = NULL\n> FALSE = NULL\n> '' = NULL\n> NULL = NULL\n> NAN = NULL\n> INFINITY = NULL\n---\nNULL\nNULL\nNULL\nNULL\nNULL\nNULL\nNULL\n\n# Type mismatches.\n!> true = 1\n!> 'true' = true\n---\nError: invalid input: can't compare TRUE and 1\nError: invalid input: can't compare 'true' and TRUE\n"
  },
  {
    "path": "src/sql/testscripts/expressions/op_compare_greater",
    "content": "# Tests the > greater than operator.\n\n# Booleans.\n> TRUE > FALSE\n> FALSE > TRUE\n> TRUE > TRUE\n> FALSE > FALSE\n---\nTRUE\nFALSE\nFALSE\nFALSE\n\n# Integers.\n> 3 > 2\n> 3 > 3\n> 3 > 4\n> -1 > 0\n> 0 > -1\n---\nTRUE\nFALSE\nFALSE\nFALSE\nTRUE\n\n# Floats.\n> 3.14 > 3.13\n> 3.14 > 3.14\n> 3.14 > 3.15\n> 0.0 > -0.0\n---\nTRUE\nFALSE\nFALSE\nFALSE\n\n# Float special values.\n> INFINITY > 1e300\n> INFINITY > INFINITY\n> INFINITY > -INFINITY\n> NAN > NAN\n> NAN > INFINITY\n> INFINITY > NAN\n> NAN > 0.0\n---\nTRUE\nFALSE\nTRUE\nFALSE\nFALSE\nFALSE\nFALSE\n\n# Mixed integer/float values.\n> 3 > 3.0\n> 3 > 2.9\n> 3 > 3.1\n> 0 > -0.0\n---\nFALSE\nTRUE\nFALSE\nFALSE\n\n# Strings.\n> 'abc' > 'abc'\n> 'abc' > 'abb'\n> 'abc' > 'ab'\n> 'b' > 'abc'\n---\nFALSE\nTRUE\nTRUE\nTRUE\n\n# Empty strings.\n> '' > ''\n> 'a' > ''\n> '' > 'a'\n---\nFALSE\nTRUE\nFALSE\n\n# String case comparisons.\n> 'a' > 'B'\n> 'z' > 'B'\n> 'A' > 'b'\n> 'Z' > 'b'\n---\nTRUE\nTRUE\nFALSE\nFALSE\n\n# Unicode strings.\n> '🙁' > '😀'\n> '😀' > '😀'\n> '😀' > '🙁'\n---\nTRUE\nFALSE\nFALSE\n\n# NULLs.\n> TRUE > NULL\n> NULL > TRUE\n> 1 > NULL\n> NULL > 1\n> 3.14 > NULL\n> NULL > 3.14\n> '' > NULl\n> NULL > ''\n> NULL > NULL\n> NULL > NAN\n---\nNULL\nNULL\nNULL\nNULL\nNULL\nNULL\nNULL\nNULL\nNULL\nNULL\n\n# Type conflicts.\n!> TRUE > 1\n!> TRUE > ''\n!> '' > 1\n---\nError: invalid input: can't compare TRUE and 1\nError: invalid input: can't compare TRUE and ''\nError: invalid input: can't compare '' and 1\n"
  },
  {
    "path": "src/sql/testscripts/expressions/op_compare_greater_equal",
    "content": "# Tests the >= greater than operator.\n\n# This is implemented as > OR =, just verify this for a few basic cases.\n\n[expr]> 0 >= 1\n[expr]> 0 >= 0\n[expr]> 0 >= -1\n---\nFALSE ← Or(GreaterThan(Constant(Integer(0)), Constant(Integer(1))), Equal(Constant(Integer(0)), Constant(Integer(1))))\nTRUE ← Or(GreaterThan(Constant(Integer(0)), Constant(Integer(0))), Equal(Constant(Integer(0)), Constant(Integer(0))))\nTRUE ← Or(GreaterThan(Constant(Integer(0)), Negate(Constant(Integer(1)))), Equal(Constant(Integer(0)), Negate(Constant(Integer(1)))))\n\n[expr]> -0.0 >= 0.0\n[expr]> INFINITY >= INFINITY\n[expr]> NAN >= NAN\n---\nTRUE ← Or(GreaterThan(Negate(Constant(Float(0.0))), Constant(Float(0.0))), Equal(Negate(Constant(Float(0.0))), Constant(Float(0.0))))\nTRUE ← Or(GreaterThan(Constant(Float(inf)), Constant(Float(inf))), Equal(Constant(Float(inf)), Constant(Float(inf))))\nFALSE ← Or(GreaterThan(Constant(Float(NaN)), Constant(Float(NaN))), Equal(Constant(Float(NaN)), Constant(Float(NaN))))\n\n[expr]> NULL >= 1\n[expr]> NULL >= NAN\n[expr]> NULL >= NULL\n---\nNULL ← Or(GreaterThan(Constant(Null), Constant(Integer(1))), Equal(Constant(Null), Constant(Integer(1))))\nNULL ← Or(GreaterThan(Constant(Null), Constant(Float(NaN))), Equal(Constant(Null), Constant(Float(NaN))))\nNULL ← Or(GreaterThan(Constant(Null), Constant(Null)), Equal(Constant(Null), Constant(Null)))\n"
  },
  {
    "path": "src/sql/testscripts/expressions/op_compare_is_nan",
    "content": "# Tests the IS NAN equality operator.\n\n> 0.0 IS NAN\n> NAN IS NAN\n> NULL IS NAN\n---\nFALSE\nTRUE\nNULL\n\n!> FALSE IS NAN\n!> 0 IS NAN\n!> '' IS NAN\n!> 'nan' IS NAN\n---\nError: invalid input: IS NAN can't be used with BOOLEAN\nError: invalid input: IS NAN can't be used with INTEGER\nError: invalid input: IS NAN can't be used with STRING\nError: invalid input: IS NAN can't be used with STRING\n"
  },
  {
    "path": "src/sql/testscripts/expressions/op_compare_is_null",
    "content": "# Tests the IS NULL equality operator.\n\n> FALSE IS NULL\n> 0 IS NULL\n> 0.0 IS NULL\n> '' IS NULL\n> 'null' IS NULL\n> NAN IS NULL\n> NULL IS NULL\n---\nFALSE\nFALSE\nFALSE\nFALSE\nFALSE\nFALSE\nTRUE\n"
  },
  {
    "path": "src/sql/testscripts/expressions/op_compare_lesser",
    "content": "# Tests the < less than operator.\n\n# Booleans.\n> FALSE < TRUE\n> TRUE < FALSE\n> TRUE < TRUE\n> FALSE < FALSE\n---\nTRUE\nFALSE\nFALSE\nFALSE\n\n# Integers.\n> 3 < 2\n> 3 < 3\n> 3 < 4\n> -1 < 0\n> 0 < -1\n---\nFALSE\nFALSE\nTRUE\nTRUE\nFALSE\n\n# Floats.\n> 3.14 < 3.13\n> 3.14 < 3.14\n> 3.14 < 3.15\n> -0.0 < 0.0\n---\nFALSE\nFALSE\nTRUE\nFALSE\n\n# Float special values.\n> 1e300 < INFINITY\n> INFINITY < INFINITY\n> -INFINITY < INFINITY\n> NAN < NAN\n> NAN < INFINITY\n> INFINITY < NAN\n> 0.0 < NAN\n---\nTRUE\nFALSE\nTRUE\nFALSE\nFALSE\nFALSE\nFALSE\n\n# Mixed integer/float values.\n> 3 < 2.9\n> 3 < 3.0\n> 3 < 3.1\n> -0.0 < 0\n---\nFALSE\nFALSE\nTRUE\nFALSE\n\n# Strings.\n> 'abc' < 'abc'\n> 'abb' < 'abc'\n> 'ab' < 'abc'\n> 'abc' < 'b'\n---\nFALSE\nTRUE\nTRUE\nTRUE\n\n# Empty strings.\n> '' < ''\n> '' < 'a'\n> 'a' < ''\n---\nFALSE\nTRUE\nFALSE\n\n# String case comparisons.\n> 'B' < 'a'\n> 'B' < 'z'\n> 'B' < 'A'\n> 'B' < 'Z'\n---\nTRUE\nTRUE\nFALSE\nTRUE\n\n# Unicode strings.\n> '😀' < '🙁' \n> '😀' < '😀' \n> '🙁' < '😀' \n---\nTRUE\nFALSE\nFALSE\n\n# NULLs.\n> TRUE < NULL\n> NULL < TRUE\n> 1 < NULL\n> NULL < 1\n> 3.14 < NULL\n> NULL < 3.14\n> '' < NULl\n> NULL < ''\n> NULL < NULL\n> NULL < NAN\n---\nNULL\nNULL\nNULL\nNULL\nNULL\nNULL\nNULL\nNULL\nNULL\nNULL\n\n# Type conflicts.\n!> TRUE < 1\n!> TRUE < ''\n!> '' < 1\n---\nError: invalid input: can't compare TRUE and 1\nError: invalid input: can't compare TRUE and ''\nError: invalid input: can't compare '' and 1\n"
  },
  {
    "path": "src/sql/testscripts/expressions/op_compare_lesser_equal",
    "content": "# Tests the <= less than or equal operator.\n\n# This is implemented as < OR =, just verify this for a few basic cases.\n\n[expr]> 1 <= 0\n[expr]> 0 <= 0\n[expr]> -1 <= 0\n---\nFALSE ← Or(LessThan(Constant(Integer(1)), Constant(Integer(0))), Equal(Constant(Integer(1)), Constant(Integer(0))))\nTRUE ← Or(LessThan(Constant(Integer(0)), Constant(Integer(0))), Equal(Constant(Integer(0)), Constant(Integer(0))))\nTRUE ← Or(LessThan(Negate(Constant(Integer(1))), Constant(Integer(0))), Equal(Negate(Constant(Integer(1))), Constant(Integer(0))))\n\n[expr]> 0.0 <= -0.0\n[expr]> INFINITY <= INFINITY\n[expr]> NAN <= NAN\n---\nTRUE ← Or(LessThan(Constant(Float(0.0)), Negate(Constant(Float(0.0)))), Equal(Constant(Float(0.0)), Negate(Constant(Float(0.0)))))\nTRUE ← Or(LessThan(Constant(Float(inf)), Constant(Float(inf))), Equal(Constant(Float(inf)), Constant(Float(inf))))\nFALSE ← Or(LessThan(Constant(Float(NaN)), Constant(Float(NaN))), Equal(Constant(Float(NaN)), Constant(Float(NaN))))\n\n[expr]> NULL <= 1\n[expr]> NULL <= NAN\n[expr]> NULL <= NULL\n---\nNULL ← Or(LessThan(Constant(Null), Constant(Integer(1))), Equal(Constant(Null), Constant(Integer(1))))\nNULL ← Or(LessThan(Constant(Null), Constant(Float(NaN))), Equal(Constant(Null), Constant(Float(NaN))))\nNULL ← Or(LessThan(Constant(Null), Constant(Null)), Equal(Constant(Null), Constant(Null)))\n"
  },
  {
    "path": "src/sql/testscripts/expressions/op_compare_not_equal",
    "content": "# Tests the != inequality operator.\n\n# != is a combination of NOT and =, just verify that for a few basic cases.\n\n[expr]> 1 != 1\n[expr]> 1 != 3\n[expr]> 1 != NULL\n---\nFALSE ← Not(Equal(Constant(Integer(1)), Constant(Integer(1))))\nTRUE ← Not(Equal(Constant(Integer(1)), Constant(Integer(3))))\nNULL ← Not(Equal(Constant(Integer(1)), Constant(Null)))\n\n[expr]> 3.0 != 3\n[expr]> 0.0 != -0.0\n---\nFALSE ← Not(Equal(Constant(Float(3.0)), Constant(Integer(3))))\nFALSE ← Not(Equal(Constant(Float(0.0)), Negate(Constant(Float(0.0)))))\n\n[expr]> NAN != NAN\n[expr]> INFINITY != INFINITY\n[expr]> NULL != NULL\n---\nTRUE ← Not(Equal(Constant(Float(NaN)), Constant(Float(NaN))))\nFALSE ← Not(Equal(Constant(Float(inf)), Constant(Float(inf))))\nNULL ← Not(Equal(Constant(Null), Constant(Null)))\n"
  },
  {
    "path": "src/sql/testscripts/expressions/op_logic_and",
    "content": "# Tests the AND logical operator.\n\n# Basic truth table.\n> TRUE AND TRUE\n> TRUE AND FALSE\n> FALSE AND TRUE\n> FALSE AND FALSE\n---\nTRUE\nFALSE\nFALSE\nFALSE\n\n# Trinary logic.\n> TRUE AND NULL\n> NULL AND TRUE\n> FALSE AND NULL\n> NULL AND FALSE\n> NULL AND NULL\n---\nNULL\nNULL\nFALSE\nFALSE\nNULL\n\n# Non-booleans.\n!> 1 AND TRUE\n!> TRUE AND 1\n!> 1 AND 1\n!> 3.14 AND TRUE\n!> TRUE AND 3.14\n!> 3.14 AND 3.14\n!> 'true' AND TRUE\n!> TRUE AND 'true'\n!> 'true' AND 'true'\n---\nError: invalid input: can't AND 1 and TRUE\nError: invalid input: can't AND TRUE and 1\nError: invalid input: can't AND 1 and 1\nError: invalid input: can't AND 3.14 and TRUE\nError: invalid input: can't AND TRUE and 3.14\nError: invalid input: can't AND 3.14 and 3.14\nError: invalid input: can't AND 'true' and TRUE\nError: invalid input: can't AND TRUE and 'true'\nError: invalid input: can't AND 'true' and 'true'\n"
  },
  {
    "path": "src/sql/testscripts/expressions/op_logic_not",
    "content": "# Tests the NOT logical operator.\n\n> NOT TRUE\n> NOT FALSE\n> NOT NULL\n---\nFALSE\nTRUE\nNULL\n\n# Non-booleans.\n!> NOT 1\n!> NOT 3.14\n!> NOT 'true'\n---\nError: invalid input: can't NOT 1\nError: invalid input: can't NOT 3.14\nError: invalid input: can't NOT 'true'\n"
  },
  {
    "path": "src/sql/testscripts/expressions/op_logic_or",
    "content": "# Tests the OR logical operator.\n\n# Basic truth table.\n> TRUE OR TRUE\n> TRUE OR FALSE\n> FALSE OR TRUE\n> FALSE OR FALSE\n---\nTRUE\nTRUE\nTRUE\nFALSE\n\n# Trinary logic.\n> TRUE OR NULL\n> NULL OR TRUE\n> FALSE OR NULL\n> NULL OR FALSE\n> NULL OR NULL\n---\nTRUE\nTRUE\nNULL\nNULL\nNULL\n\n# Non-booleans.\n!> 1 OR TRUE\n!> TRUE OR 1\n!> 1 OR 1\n!> 3.14 OR TRUE\n!> TRUE OR 3.14\n!> 3.14 OR 3.14\n!> 'true' OR TRUE\n!> TRUE OR 'true'\n!> 'true' OR 'true'\n---\nError: invalid input: can't OR 1 and TRUE\nError: invalid input: can't OR TRUE and 1\nError: invalid input: can't OR 1 and 1\nError: invalid input: can't OR 3.14 and TRUE\nError: invalid input: can't OR TRUE and 3.14\nError: invalid input: can't OR 3.14 and 3.14\nError: invalid input: can't OR 'true' and TRUE\nError: invalid input: can't OR TRUE and 'true'\nError: invalid input: can't OR 'true' and 'true'\n"
  },
  {
    "path": "src/sql/testscripts/expressions/op_math_add",
    "content": "# Tests the + addition operator.\n\n# Simple integer addition.\n[expr]> 1 + 2\n[expr]> 1 + -3\n[expr]> 1 + -2 + 3\n---\n3 ← Add(Constant(Integer(1)), Constant(Integer(2)))\n-2 ← Add(Constant(Integer(1)), Negate(Constant(Integer(3))))\n2 ← Add(Add(Constant(Integer(1)), Negate(Constant(Integer(2)))), Constant(Integer(3)))\n\n# Simple float addition.\n[expr]> 3.1 + 2.71\n[expr]> 3.1 + -2.71\n---\n5.8100000000000005 ← Add(Constant(Float(3.1)), Constant(Float(2.71)))\n0.3900000000000001 ← Add(Constant(Float(3.1)), Negate(Constant(Float(2.71))))\n\n# Combined int/float addition yields floats.\n> 3.72 + 1\n> 1 + 3.72\n> 1 + 3.0\n> -1 + 3.72\n---\n4.720000000000001\n4.720000000000001\n4.0\n2.72\n\n# Addition with nulls yields null.\n> 1 + NULL\n> NULL + 3.14\n> NULL + NULL\n---\nNULL\nNULL\nNULL\n\n# Addition with infinity and NaN.\n> 1 + INFINITY\n> 1 + -INFINITY\n> -1 + INFINITY\n> 1 + NAN\n> 3.14 + -NAN\n> INFINITY + NAN\n---\ninf\n-inf\ninf\nNaN\nNaN\nNaN\n\n# Overflow and underflow.\n!> 9223372036854775807 + 1\n!> -9223372036854775807 + -2\n> 9223372036854775807 + 1.0\n> 2e308 + 2e308\n---\nError: invalid input: integer overflow\nError: invalid input: integer overflow\n9.223372036854776e18\ninf\n\n# Bools and strings error.\n!> TRUE + FALSE\n!> 'a' + 'b'\n---\nError: invalid input: can't add TRUE and FALSE\nError: invalid input: can't add 'a' and 'b'\n"
  },
  {
    "path": "src/sql/testscripts/expressions/op_math_divide",
    "content": "# Tests the / division operator.\n\n# Integers.\n[expr]> 9 / 3\n[expr]> 8 / 3\n[expr]> 8 / -3\n---\n3 ← Divide(Constant(Integer(9)), Constant(Integer(3)))\n2 ← Divide(Constant(Integer(8)), Constant(Integer(3)))\n-2 ← Divide(Constant(Integer(8)), Negate(Constant(Integer(3))))\n\n# Floats.\n[expr]> 4.16 / 3.2\n[expr]> 4.16 / -3.2\n---\n1.3 ← Divide(Constant(Float(4.16)), Constant(Float(3.2)))\n-1.3 ← Divide(Constant(Float(4.16)), Negate(Constant(Float(3.2))))\n\n# Mixed always yields floats.\n> 3 / 1.2\n> 1.2 / 3\n> 9.0 / 3\n> 0.0 / 1\n---\n2.5\n0.39999999999999997\n3.0\n0.0\n\n# Division by zero errors for integers, yields infinity or nan for floats.\n!> 1 / 0\n!> 0 / 0\n!> -1 / 0\n> 1.0 / 0.0\n> 0.0 / 0.0\n> -1.0 / 0.0\n> 1.0 / -0.0\n---\nError: invalid input: can't divide by zero\nError: invalid input: can't divide by zero\nError: invalid input: can't divide by zero\ninf\nNaN\n-inf\n-inf\n\n# Division with NULL always yields NULL.\n> 1 / NULL\n> NULL / 1\n> 1.0 / NULL\n> NULL / 1.0\n> NULL / NULL\n> NULL / 0\n---\nNULL\nNULL\nNULL\nNULL\nNULL\nNULL\n\n# Division by infinity.\n> 3.14 / INFINITY\n> 3.14 / -INFINITY\n> -3.14 / INFINITY\n> INFINITY / 10\n> 0 / INFINITY\n> INFINITY / 0.0\n> INFINITY / INFINITY\n> -INFINITY / -INFINITY\n---\n0.0\n-0.0\n-0.0\ninf\n0.0\ninf\nNaN\nNaN\n\n# Division by NaN.\n> 1 / NAN\n> NAN / 1\n> NAN / NAN\n> NAN / 0\n---\nNaN\nNaN\nNaN\nNaN\n\n# Bools and strings error.\n!> TRUE / FALSE\n!> 'a' / 'b'\n---\nError: invalid input: can't divide TRUE and FALSE\nError: invalid input: can't divide 'a' and 'b'\n"
  },
  {
    "path": "src/sql/testscripts/expressions/op_math_exponentiate",
    "content": "# Tests the ^ exponentiation operator.\n\n# Integers.\n[expr]> 2 ^ 3\n[expr]> 2 ^ 0\n[expr]> 0 ^ 2\n[expr]> 9 ^ -3\n---\n8 ← Exponentiate(Constant(Integer(2)), Constant(Integer(3)))\n1 ← Exponentiate(Constant(Integer(2)), Constant(Integer(0)))\n0 ← Exponentiate(Constant(Integer(0)), Constant(Integer(2)))\n0.0013717421124828531 ← Exponentiate(Constant(Integer(9)), Negate(Constant(Integer(3))))\n\n# Floats.\n[expr]> 6.25 ^ 0.5\n[expr]> 6.25 ^ 3.14\n---\n2.5 ← Exponentiate(Constant(Float(6.25)), Constant(Float(0.5)))\n315.5464179407336 ← Exponentiate(Constant(Float(6.25)), Constant(Float(3.14)))\n\n# Mixed.\n> 6.25 ^ 2\n> 9 ^ 0.5\n---\n39.0625\n3.0\n\n# Overflow and underflow.\n!> 2 ^ 10000000000\n!> 9223372036854775807 ^ 2\n> 10e200 ^ 2\n---\nError: invalid input: integer overflow\nError: invalid input: integer overflow\ninf\n\n# Nulls.\n> 1 ^ NULL\n> 3.14 ^ NULL\n> NULL ^ 2\n> NULL ^ 3.14\n> NULL ^ NULL\n---\nNULL\nNULL\nNULL\nNULL\nNULL\n\n# Infinity and NaN.\n> 2 ^ INFINITY\n> INFINITY ^ 2\n> INFINITY ^ INFINITY\n> 2 ^ -INFINITY\n> 2 ^ NAN\n> NAN ^ 2\n> NAN ^ NAN\n---\ninf\ninf\ninf\n0.0\nNaN\nNaN\nNaN\n\n# Bools and strings.\n!> TRUE ^ FALSE\n!> 'a' ^ 'b'\n---\nError: invalid input: can't exponentiate TRUE and FALSE\nError: invalid input: can't exponentiate 'a' and 'b'\n\n# Right-associativity.\n[expr]> 2 ^ 3 ^ 2\n[expr]> 2 ^ 1 ^ 2 ^ 3\n---\n512 ← Exponentiate(Constant(Integer(2)), Exponentiate(Constant(Integer(3)), Constant(Integer(2))))\n2 ← Exponentiate(Constant(Integer(2)), Exponentiate(Constant(Integer(1)), Exponentiate(Constant(Integer(2)), Constant(Integer(3)))))\n"
  },
  {
    "path": "src/sql/testscripts/expressions/op_math_factorial",
    "content": "# Tests the ! factorial suffix operator.\n\n# Integer works.\n[expr]> 3!\n---\n6 ← Factorial(Constant(Integer(3)))\n\n# But float, bool, and string fails.\n!> 3.14!\n!> 3.0!\n!> TRUE!\n!> 'a'!\n---\nError: invalid input: can't take factorial of 3.14\nError: invalid input: can't take factorial of 3.0\nError: invalid input: can't take factorial of TRUE\nError: invalid input: can't take factorial of 'a'\n\n# 0 factorial is 1, but negative factorial errors.\n> -0!\n!> -1!\n---\n1\nError: invalid input: can't take factorial of -1\n\n# NULL yields null, infinity and NaN error.\n> NULL!\n!> INFINITY!\n!> NAN!\n---\nNULL\nError: invalid input: can't take factorial of inf\nError: invalid input: can't take factorial of NaN\n\n# Multiple applications work.\n[expr]> 3!!\n[expr]> 2!!!!!!\n---\n720 ← Factorial(Factorial(Constant(Integer(3))))\n2 ← Factorial(Factorial(Factorial(Factorial(Factorial(Factorial(Constant(Integer(2))))))))\n\n# Overflow.\n[expr]!> 3!!!\n---\nError: invalid input: integer overflow\n"
  },
  {
    "path": "src/sql/testscripts/expressions/op_math_identity",
    "content": "# Tests the + identity prefix operator.\n\n# Integer and float works.\n[expr]> +1\n[expr]> +3.14\n---\n1 ← Identity(Constant(Integer(1)))\n3.14 ← Identity(Constant(Float(3.14)))\n\n# NULL, infinity and NaN.\n> +NULL\n> +INFINITY\n> +NAN\n---\nNULL\ninf\nNaN\n\n# Multiple applications work.\n[expr]> +++1\n---\n1 ← Identity(Identity(Identity(Constant(Integer(1)))))\n\n# Bool and string fails.\n!> +TRUE\n!> +'a'\n---\nError: invalid input: can't take the identity of TRUE\nError: invalid input: can't take the identity of 'a'\n"
  },
  {
    "path": "src/sql/testscripts/expressions/op_math_multiply",
    "content": "# Tests the * multiplication operator.\n\n# Integers.\n[expr]> 2 * 3\n[expr]> 2 * -3\n---\n6 ← Multiply(Constant(Integer(2)), Constant(Integer(3)))\n-6 ← Multiply(Constant(Integer(2)), Negate(Constant(Integer(3))))\n\n# Float.\n[expr]> 3.14 * 2.71\n[expr]> 3.14 * -2.71\n---\n8.5094 ← Multiply(Constant(Float(3.14)), Constant(Float(2.71)))\n-8.5094 ← Multiply(Constant(Float(3.14)), Negate(Constant(Float(2.71))))\n\n# Mixed.\n> 3.14 * 2\n> -2 * 3.14\n---\n6.28\n-6.28\n\n# Integer and float overflow, underflow, and precision loss.\n!> 9223372036854775807 * 2\n!> 9223372036854775807 * -2\n> 2e308 * 2\n> 9223372036854775807 * 2.0\n---\nError: invalid input: integer overflow\nError: invalid input: integer overflow\ninf\n1.8446744073709552e19\n\n\n# NULLs always yield NULL.\n> 1 * NULL\n> NULL * 3.14\n> NULL * NULL\n---\nNULL\nNULL\nNULL\n\n# Infinity.\n> 2 * INFINITY\n> -2 * INFINITY\n> 3.14 * -INFINITY\n> INFINITY * INFINITY\n> INFINITY * -INFINITY\n---\ninf\n-inf\n-inf\ninf\n-inf\n\n# NaN.\n> 2 * NAN\n> -3.14 * NAN\n> INFINITY * NAN\n> NAN * NAN\n---\nNaN\nNaN\nNaN\nNaN\n\n# Bools and strings.\n!> TRUE * FALSE\n!> 'a' * 'b'\n---\nError: invalid input: can't multiply TRUE and FALSE\nError: invalid input: can't multiply 'a' and 'b'\n"
  },
  {
    "path": "src/sql/testscripts/expressions/op_math_negate",
    "content": "# Tests the - negation prefix operator.\n\n# Integer and float works.\n[expr]> -1\n[expr]> -3.14\n---\n-1 ← Negate(Constant(Integer(1)))\n-3.14 ← Negate(Constant(Float(3.14)))\n\n# NULL, infinity and NaN.\n> -NULL\n> -INFINITY\n> -NAN\n---\nNULL\n-inf\nNaN\n\n# Multiple applications work.\n[expr]> ---1\n[expr]> ----1\n---\n-1 ← Negate(Negate(Negate(Constant(Integer(1)))))\n1 ← Negate(Negate(Negate(Negate(Constant(Integer(1))))))\n\n# Bool and string fails.\n!> -TRUE\n!> -'a'\n---\nError: invalid input: can't negate TRUE\nError: invalid input: can't negate 'a'\n"
  },
  {
    "path": "src/sql/testscripts/expressions/op_math_remainder",
    "content": "# Tests the % remainder operator.\n#\n# Note that remainder is not the same as modulo: the former has the sign of the\n# dividend, while the latter always has a positive value.\n\n# Integers.\n[expr]> 5 % 3\n[expr]> -5 % 3\n[expr]> 5 % -3\n---\n2 ← Remainder(Constant(Integer(5)), Constant(Integer(3)))\n-2 ← Remainder(Negate(Constant(Integer(5))), Constant(Integer(3)))\n2 ← Remainder(Constant(Integer(5)), Negate(Constant(Integer(3))))\n\n# Floats.\n[expr]> 6.28 % 2.2\n[expr]> 6.28 % -2.2\n---\n1.88 ← Remainder(Constant(Float(6.28)), Constant(Float(2.2)))\n1.88 ← Remainder(Constant(Float(6.28)), Negate(Constant(Float(2.2))))\n\n# Mixed.\n> 3.15 % 2\n> 6 % 3.15\n> 3.15 % -2\n---\n1.15\n2.85\n1.15\n\n# Division by zero.\n!> 7 % 0\n> 6.28 % 0.0\n---\nError: invalid input: can't divide by zero\nNaN\n\n# NULLs.\n> 1 % NULL\n> NULL % 3\n> 3.14 % NULL\n> NULL % NULL\n---\nNULL\nNULL\nNULL\nNULL\n\n# Infinity and NaN.\n> INFINITY % 7\n> 7 % INFINITY\n> 7 % -INFINITY\n> INFINITY % INFINITY\n> 7 % NAN\n> NAN % 7\n> NAN % NAN\n---\nNaN\n7.0\n7.0\nNaN\nNaN\nNaN\nNaN\n\n# Bools and strings.\n!> TRUE % FALSE\n!> 'a' % 'b'\n---\nError: invalid input: can't take remainder of TRUE and FALSE\nError: invalid input: can't take remainder of 'a' and 'b'\n"
  },
  {
    "path": "src/sql/testscripts/expressions/op_math_subtract",
    "content": "# Tests the - subtraction operator.\n\n# Simple integer subtraction.\n[expr]> 2 - 1\n[expr]> 2 - 3\n[expr]> 1 - -3 - 2\n---\n1 ← Subtract(Constant(Integer(2)), Constant(Integer(1)))\n-1 ← Subtract(Constant(Integer(2)), Constant(Integer(3)))\n2 ← Subtract(Subtract(Constant(Integer(1)), Negate(Constant(Integer(3)))), Constant(Integer(2)))\n\n# Simple float subtraction.\n[expr]> 3.1 - 2.71\n[expr]> 3.1 - -2.71\n---\n0.3900000000000001 ← Subtract(Constant(Float(3.1)), Constant(Float(2.71)))\n5.8100000000000005 ← Subtract(Constant(Float(3.1)), Negate(Constant(Float(2.71))))\n\n# Combined int/float subtraction yields floats.\n> 3.72 - 1\n> 1 - 3.72\n> 1 - 3.0\n> -1 - 3.72\n---\n2.72\n-2.72\n-2.0\n-4.720000000000001\n\n# Subtraction with nulls yields null.\n> 1 - NULL\n> NULL - 3.14\n> NULL - NULL\n---\nNULL\nNULL\nNULL\n\n# Subtraction with infinity and NaN.\n> 1 - INFINITY\n> -1 - INFINITY\n> -1 - -INFINITY\n> 1 - NAN\n> 3.14 - -NAN\n> INFINITY - NAN\n---\n-inf\n-inf\ninf\nNaN\nNaN\nNaN\n\n# Overflow and underflow.\n!> 9223372036854775807 - -1\n!> -9223372036854775807 - 2\n> 9223372036854775807 - -1.0\n> -2e308 - 2e308\n---\nError: invalid input: integer overflow\nError: invalid input: integer overflow\n9.223372036854776e18\n-inf\n\n# Bools and strings error.\n!> TRUE - FALSE\n!> 'a' - 'b'\n---\nError: invalid input: can't subtract TRUE and FALSE\nError: invalid input: can't subtract 'a' and 'b'\n\n# Left-associativity.\n> 5 - 3 - 1\n---\n1\n"
  },
  {
    "path": "src/sql/testscripts/expressions/op_precedence",
    "content": "# Tests operator precedence. Test each precedence level against the operators\n# beside and immediately below it, in order. The levels are:\n#\n# 10: prefix +, -\n# 9: postfix !\n# 8: ^ (left-associative)\n# 7: *, /, %\n# 6: +, -\n# 5: >, >=, <, <=\n# 4: =, !=, LIKE, IS\n# 3: NOT\n# 2: AND\n# 1: OR\n#\n# Only ^ is left-associative (and postfix operators by definition).\n\n# Parenthesis can boost a low precedence operator (e.g. addition) above the\n# highest precedence (e.g. prefix/postfix and ^).\n> 1 + 2 ^ 2\n> (1 + 2) ^ 2\n> -1 + 2\n> -(1 + 2)\n> 2 + 3!\n> (2 + 3)!\n---\n5\n9\n1\n-3\n8\n120\n\n# Prefix -.\n> -3 ^ 2\n> -(3 ^ 2)\n---\n9\n-9\n\n# Postfix !.\n> 2 ^ 3!\n> (2 ^ 3)!\n---\n64\n40320\n\n# ^, which is also left-associative.\n> 2 ^ 3 ^ 2\n> (2 ^ 3) ^ 2\n> 2 ^ 3 * 4\n> 2 ^ (3 * 4)\n> 2 ^ 4 / 2\n> 2 ^ (4 / 2)\n> 2 ^ 5 % 2\n> 2 ^ (5 % 2)\n---\n512\n64\n32\n4096\n8\n4\n0\n2\n\n# *\n> 3 * 4 / 2\n> 3 * (4 / 2)\n> 3 * 4 % 3\n> 3 * (4 % 3)\n> 1 + 2 * 3\n> (1 + 2) * 3\n> 1 - 2 * 3\n> (1 - 2) * 3\n---\n6\n6\n0\n3\n7\n9\n-5\n-3\n\n# /\n> 4 / 2 * 3\n> 4 / (2 * 3)\n> 8 / 4 % 3\n> 8 / (4 % 3)\n> 2 + 4 / 2\n> (2 + 4) / 2\n> 4 - 2 / 2\n> (4 - 2) / 2\n---\n6\n0\n2\n8\n4\n3\n3\n1\n\n# %\n> 4 % 3 * 3\n> 4 % (3 * 3)\n> 8 % 3 / 2\n> 8 % (3 / 2)\n> 2 + 4 % 3\n> (2 + 4) % 3\n> 8 - 5 % 3\n> (8 - 5) % 3\n---\n3\n4\n1\n0\n3\n0\n6\n0\n\n# +\n> 1 + 2 - 3\n> 1 + (2 - 3)\n> 1 + 2 > 2\n!> 1 + (2 > 2)\n> 1 + 2 >= 2\n!> 1 + (2 >= 2)\n> 1 + 2 < 2\n!> 1 + (2 < 2)\n> 1 + 2 <= 2\n!> 1 + (2 <= 2)\n---\n0\n0\nTRUE\nError: invalid input: can't add 1 and FALSE\nTRUE\nError: invalid input: can't add 1 and TRUE\nFALSE\nError: invalid input: can't add 1 and FALSE\nFALSE\nError: invalid input: can't add 1 and TRUE\n\n# -\n> 3 - 2 + 1\n> 3 - (2 + 1)\n> 2 - 1 > 2\n!> 2 - (1 > 2)\n> 2 - 1 >= 2\n!> 2 - (1 >= 2)\n> 2 - 1 < 2\n!> 2 - (1 < 2)\n> 2 - 1 <= 2\n!> 2 - (1 <= 2)\n---\n2\n0\nFALSE\nError: invalid input: can't subtract 2 and FALSE\nFALSE\nError: invalid input: can't subtract 2 and FALSE\nTRUE\nError: invalid input: can't subtract 2 and TRUE\nTRUE\nError: invalid input: can't subtract 2 and TRUE\n\n# >\n> 5 > 3 < TRUE\n!> 5 > (3 < TRUE)\n> 5 > 3 <= TRUE\n!> 5 > (3 <= TRUE)\n> 5 > 3 > TRUE\n!> 5 > (3 > TRUE)\n> 5 > 3 >= TRUE\n!> 5 > (3 >= TRUE)\n> 5 > 3 = TRUE\n!> 5 > (3 = TRUE)\n> 5 > 3 != TRUE\n!> 5 > (3 != TRUE)\n!> 5 > 3 LIKE 'abc'\n!> 5 > (3 LIKE 'abc')\n> 5 > 3 IS NULL\n!> 5 > (3 IS NULL)\n---\nFALSE\nError: invalid input: can't compare 3 and TRUE\nTRUE\nError: invalid input: can't compare 3 and TRUE\nFALSE\nError: invalid input: can't compare 3 and TRUE\nTRUE\nError: invalid input: can't compare 3 and TRUE\nTRUE\nError: invalid input: can't compare 3 and TRUE\nFALSE\nError: invalid input: can't compare 3 and TRUE\nError: invalid input: can't LIKE TRUE and 'abc'\nError: invalid input: can't LIKE 3 and 'abc'\nFALSE\nError: invalid input: can't compare 5 and FALSE\n\n# >=\n> 5 >= 3 < TRUE\n!> 5 >= (3 < TRUE)\n> 5 >= 3 <= TRUE\n!> 5 >= (3 <= TRUE)\n> 5 >= 3 > TRUE\n!> 5 >= (3 > TRUE)\n> 5 >= 3 >= TRUE\n!> 5 >= (3 >= TRUE)\n> 5 >= 3 = TRUE\n!> 5 >= (3 = TRUE)\n> 5 >= 3 != TRUE\n!> 5 >= (3 != TRUE)\n!> 5 >= 3 LIKE 'abc'\n!> 5 >= (3 LIKE 'abc')\n> 5 >= 3 IS NULL\n!> 5 >= (3 IS NULL)\n---\nFALSE\nError: invalid input: can't compare 3 and TRUE\nTRUE\nError: invalid input: can't compare 3 and TRUE\nFALSE\nError: invalid input: can't compare 3 and TRUE\nTRUE\nError: invalid input: can't compare 3 and TRUE\nTRUE\nError: invalid input: can't compare 3 and TRUE\nFALSE\nError: invalid input: can't compare 3 and TRUE\nError: invalid input: can't LIKE TRUE and 'abc'\nError: invalid input: can't LIKE 3 and 'abc'\nFALSE\nError: invalid input: can't compare 5 and FALSE\n\n# <\n> 5 < 3 < TRUE\n!> 5 < (3 < TRUE)\n> 5 < 3 <= TRUE\n!> 5 < (3 <= TRUE)\n> 5 < 3 > TRUE\n!> 5 < (3 > TRUE)\n> 5 < 3 >= TRUE\n!> 5 < (3 >= TRUE)\n> 5 < 3 = TRUE\n!> 5 < (3 = TRUE)\n> 5 < 3 != TRUE\n!> 5 < (3 != TRUE)\n!> 5 < 3 LIKE 'abc'\n!> 5 < (3 LIKE 'abc')\n> 5 < 3 IS NULL\n!> 5 < (3 IS NULL)\n---\nTRUE\nError: invalid input: can't compare 3 and TRUE\nTRUE\nError: invalid input: can't compare 3 and TRUE\nFALSE\nError: invalid input: can't compare 3 and TRUE\nFALSE\nError: invalid input: can't compare 3 and TRUE\nFALSE\nError: invalid input: can't compare 3 and TRUE\nTRUE\nError: invalid input: can't compare 3 and TRUE\nError: invalid input: can't LIKE FALSE and 'abc'\nError: invalid input: can't LIKE 3 and 'abc'\nFALSE\nError: invalid input: can't compare 5 and FALSE\n\n# <=\n> 5 <= 3 < TRUE\n!> 5 <= (3 < TRUE)\n> 5 <= 3 <= TRUE\n!> 5 <= (3 <= TRUE)\n> 5 <= 3 > TRUE\n!> 5 <= (3 > TRUE)\n> 5 <= 3 >= TRUE\n!> 5 <= (3 >= TRUE)\n> 5 <= 3 = TRUE\n!> 5 <= (3 = TRUE)\n> 5 <= 3 != TRUE\n!> 5 <= (3 != TRUE)\n!> 5 <= 3 LIKE 'abc'\n!> 5 <= (3 LIKE 'abc')\n> 5 <= 3 IS NULL\n!> 5 <= (3 IS NULL)\n---\nTRUE\nError: invalid input: can't compare 3 and TRUE\nTRUE\nError: invalid input: can't compare 3 and TRUE\nFALSE\nError: invalid input: can't compare 3 and TRUE\nFALSE\nError: invalid input: can't compare 3 and TRUE\nFALSE\nError: invalid input: can't compare 3 and TRUE\nTRUE\nError: invalid input: can't compare 3 and TRUE\nError: invalid input: can't LIKE FALSE and 'abc'\nError: invalid input: can't LIKE 3 and 'abc'\nFALSE\nError: invalid input: can't compare 5 and FALSE\n\n# =\n> 1 = 1 != FALSE\n!> 1 = (1 != FALSE)\n!> 1 = 1 LIKE 'abc'\n!> 1 = (1 LIKE 'abc')\n> 1 = NULL IS NULL\n!> 1 = (NULL IS NULL)\n> NOT 1 = 1\n!> (NOT 1) = 1\n---\nTRUE\nError: invalid input: can't compare 1 and FALSE\nError: invalid input: can't LIKE TRUE and 'abc'\nError: invalid input: can't LIKE 1 and 'abc'\nTRUE\nError: invalid input: can't compare 1 and TRUE\nFALSE\nError: invalid input: can't NOT 1\n\n# !=\n> 1 != 1 != FALSE\n!> 1 != (1 != FALSE)\n!> 1 != 1 LIKE 'abc'\n!> 1 != (1 LIKE 'abc')\n> 1 != NULL IS NULL\n!> 1 != (NULL IS NULL)\n> NOT 1 != 1\n!> (NOT 1) != 1\n---\nFALSE\nError: invalid input: can't compare 1 and FALSE\nError: invalid input: can't LIKE FALSE and 'abc'\nError: invalid input: can't LIKE 1 and 'abc'\nTRUE\nError: invalid input: can't compare 1 and TRUE\nTRUE\nError: invalid input: can't NOT 1\n\n# LIKE\n> 'abc' LIKE NULL IS NULL\n!> 'abc' LIKE (NULL IS NULL)\n> NOT 'abc' LIKE 'abc'\n!> (NOT 'abc') LIKE 'abc'\n---\nTRUE\nError: invalid input: can't LIKE 'abc' and TRUE\nFALSE\nError: invalid input: can't NOT 'abc'\n\n# IS NULL\n> NOT NULL IS NULL\n> (NOT NULL) IS NULL\n---\nFALSE\nTRUE\n\n# IS NOT NULL\n> NOT NULL IS NOT NULL\n> (NOT NULL) IS NOT NULL\n---\nTRUE\nFALSE\n\n# IS NAN\n> NOT NAN IS NAN\n!> (NOT NAN) IS NAN\n---\nFALSE\nError: invalid input: can't NOT NaN\n\n# IS NOT NAN\n> NOT NAN IS NOT NAN\n!> (NOT NAN) IS NOT NAN\n---\nTRUE\nError: invalid input: can't NOT NaN\n\n# NOT.\n> NOT TRUE AND FALSE\n> NOT (TRUE AND FALSE)\n---\nFALSE\nTRUE\n\n# AND\n> FALSE AND TRUE OR TRUE\n> FALSE AND (TRUE OR TRUE)\n> TRUE OR TRUE AND FALSE\n> (TRUE OR TRUE) AND FALSE\n---\nTRUE\nFALSE\nTRUE\nFALSE\n\n# OR has the lowest precedence, so nothing to test.\n"
  },
  {
    "path": "src/sql/testscripts/expressions/op_string_like",
    "content": "# Tests the LIKE string pattern matching operator.\n\n# Multi-character matches.\n> 'abcde' LIKE 'a%e'\n> 'abcde' LIKE 'abc%'\n> 'abcde' LIKE '%cde'\n> 'abcde' LIKE '%'\n---\nTRUE\nTRUE\nTRUE\nTRUE\n\n# Multi-character mismatches.\n> 'abcde' LIKE 'a%f'\n> 'abcde' LIKE 'b%e'\n> 'abcde' LIKE 'b%'\n> 'abcde' LIKE '%d'\n---\nFALSE\nFALSE\nFALSE\nFALSE\n\n# Multi-character wildcards match 0 characters.\n> 'abcde' LIKE 'abc%de'\n> 'abcde' LIKE '%abcde'\n> 'abcde' LIKE 'abcde%'\n> '' LIKE '%'\n---\nTRUE\nTRUE\nTRUE\nTRUE\n\n# Single-character matches.\n> 'abcde' LIKE 'ab_de'\n> 'abcde' LIKE '_bcde'\n> 'abcde' LIKE 'abcd_'\n---\nTRUE\nTRUE\nTRUE\n\n# Single-character mismatches.\n> 'abcde' LIKE 'ab_e'\n> 'abcde' LIKE 'abc_'\n> 'abcde' LIKE '_bcd'\n---\nFALSE\nFALSE\nFALSE\n\n# Single-character wildcards require at least one match.\n> 'abcde' LIKE 'abc_de'\n> 'abcde' LIKE '_abcde'\n> 'abcde' LIKE 'abcde_'\n> '' LIKE '_'\n---\nFALSE\nFALSE\nFALSE\nFALSE\n\n# Exact matches. Submatches are not sufficient.\n> 'abcde' LIKE 'abcde'\n> 'abcde' LIKE 'abc'\n> 'abcde' LIKE 'abcdef'\n---\nTRUE\nFALSE\nFALSE\n\n# Patterns are case-sensitive.\n> 'abcde' LIKE 'ABCDE'\n> 'abcde' LIKE 'A%'\n---\nFALSE\nFALSE\n\n# Wildcards can be mixed and used multiple times, and % can match nothing.\n> 'abcde' LIKE 'a%c%e'\n> 'abcde' LIKE '%%e'\n> 'abcde' LIKE '%%abcde'\n> 'abcde' LIKE 'a___e'\n> 'abcdefghijklmno' LIKE 'a_c%f%i_kl%m_o'\n---\nTRUE\nTRUE\nTRUE\nTRUE\nTRUE\n\n# NULLs.\n> NULL LIKE '%'\n> NULL LIKE '_'\n> 'abc' LIKE NULL\n> NULL LIKE NULL\n---\nNULL\nNULL\nNULL\nNULL\n\n# * and ? are not valid patterns.\n> 'abcde' LIKE 'a*e'\n> 'abcde' LIKE 'ab?de'\n---\nFALSE\nFALSE\n\n# Fails with non-strings.\n!> 'abc' LIKE 1\n!> 1 LIKE 'abc'\n!> 'abc' LIKE 3.14\n!> 3.14 LIKE 'abc'\n!> 'abc' LIKE TRUE\n!> TRUE LIKE 'abc'\n---\nError: invalid input: can't LIKE 'abc' and 1\nError: invalid input: can't LIKE 1 and 'abc'\nError: invalid input: can't LIKE 'abc' and 3.14\nError: invalid input: can't LIKE 3.14 and 'abc'\nError: invalid input: can't LIKE 'abc' and TRUE\nError: invalid input: can't LIKE TRUE and 'abc'\n"
  },
  {
    "path": "src/sql/testscripts/optimizers/constant_folder",
    "content": "# Tests the constant folding optimizer.\n\n> CREATE TABLE test (id INT PRIMARY KEY, value STRING)\n> INSERT INTO test VALUES (1, 'a'), (2, 'b'), (3, 'c')\n---\nok\n\n# Constant folding is applied in all places where expressions are used.\n[opt]> SELECT 1+1\n---\nInitial:\n   Projection: 1 + 1\n   └─ Values: blank row\nConstantFolding:\n   Projection: 2\n   └─ Values: blank row\n2\n\n[opt]> SELECT 1+1 FROM test\n---\nInitial:\n   Projection: 1 + 1\n   └─ Scan: test\nConstantFolding:\n   Projection: 2\n   └─ Scan: test\n2\n2\n2\n\n[opt]> SELECT * FROM test a JOIN test b ON 1+1 > 1\n---\nInitial:\n   NestedLoopJoin: inner on 1 + 1 > 1\n   ├─ Scan: test as a\n   └─ Scan: test as b\nConstantFolding:\n   NestedLoopJoin: inner on TRUE\n   ├─ Scan: test as a\n   └─ Scan: test as b\nFilterPushdown:\n   NestedLoopJoin: inner\n   ├─ Scan: test as a (TRUE)\n   └─ Scan: test as b (TRUE)\nShortCircuit:\n   NestedLoopJoin: inner\n   ├─ Scan: test as a\n   └─ Scan: test as b\n1, 'a', 1, 'a'\n1, 'a', 2, 'b'\n1, 'a', 3, 'c'\n2, 'b', 1, 'a'\n2, 'b', 2, 'b'\n2, 'b', 3, 'c'\n3, 'c', 1, 'a'\n3, 'c', 2, 'b'\n3, 'c', 3, 'c'\n\n[opt]> SELECT * FROM test WHERE 1+1 > 1\n---\nInitial:\n   Filter: 1 + 1 > 1\n   └─ Scan: test\nConstantFolding:\n   Filter: TRUE\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (TRUE)\nShortCircuit:\n   Scan: test\n1, 'a'\n2, 'b'\n3, 'c'\n\n[opt]> SELECT * FROM test ORDER BY 1+1\n---\nInitial:\n   Order: 1 + 1 asc\n   └─ Scan: test\nConstantFolding:\n   Order: 2 asc\n   └─ Scan: test\n1, 'a'\n2, 'b'\n3, 'c'\n\n[opt]> SELECT * FROM test LIMIT 1+1\n---\nInitial:\n   Limit: 2\n   └─ Scan: test\n1, 'a'\n2, 'b'\n\n[opt]> SELECT * FROM test OFFSET 1+1\n---\nInitial:\n   Offset: 2\n   └─ Scan: test\n3, 'c'\n\n# Constant folding folds the constant parts of a variable expression.\n# TODO: this should fold 4 - 6, but it needs to reorder operations.\n[opt]> SELECT 2 * 2 + id - 3 * 2 FROM test\n---\nInitial:\n   Projection: 2 * 2 + test.id - 3 * 2\n   └─ Scan: test\nConstantFolding:\n   Projection: 4 + test.id - 6\n   └─ Scan: test\n-1\n0\n1\n\n# Constant folding short-circuits variable logical operations.\n[opt]> SELECT * FROM test WHERE 1+1 > 1 OR id > 1\n---\nInitial:\n   Filter: 1 + 1 > 1 OR test.id > 1\n   └─ Scan: test\nConstantFolding:\n   Filter: TRUE\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (TRUE)\nShortCircuit:\n   Scan: test\n1, 'a'\n2, 'b'\n3, 'c'\n\n[opt]> SELECT * FROM test WHERE 1+1 < 1 OR id > 1\n---\nInitial:\n   Filter: 1 + 1 < 1 OR test.id > 1\n   └─ Scan: test\nConstantFolding:\n   Filter: test.id > 1\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (test.id > 1)\n2, 'b'\n3, 'c'\n\n[opt]> SELECT * FROM test WHERE 1+1 > 1 AND id > 1\n---\nInitial:\n   Filter: 1 + 1 > 1 AND test.id > 1\n   └─ Scan: test\nConstantFolding:\n   Filter: test.id > 1\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (test.id > 1)\n2, 'b'\n3, 'c'\n\n[opt]> SELECT * FROM test WHERE 1+1 < 1 AND id > 1\n---\nInitial:\n   Filter: 1 + 1 < 1 AND test.id > 1\n   └─ Scan: test\nConstantFolding:\n   Filter: FALSE\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (FALSE)\nShortCircuit:\n   Nothing\n"
  },
  {
    "path": "src/sql/testscripts/optimizers/filter_pushdown",
    "content": "# Tests filter pushdown.\n\n> CREATE TABLE test (id INT PRIMARY KEY, value STRING)\n> INSERT INTO test VALUES (1, 'a'), (2, 'b'), (3, 'c')\n---\nok\n\n# Pushes WHERE filters into Scan nodes.\n[opt]> SELECT * FROM test WHERE value = 'b'\n---\nInitial:\n   Filter: test.value = 'b'\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (test.value = 'b')\n2, 'b'\n\n# HAVING filters are not pushed past aggregate nodes, even when possible. The\n# optimizer could do this if it was smarter.\n[opt]> SELECT id, value FROM test GROUP BY id, value HAVING value = 'b'\n---\nInitial:\n   Filter: test.value = 'b'\n   └─ Projection: test.id, test.value\n      └─ Aggregate: test.id, test.value\n         └─ Scan: test\nShortCircuit:\n   Filter: test.value = 'b'\n   └─ Aggregate: test.id, test.value\n      └─ Scan: test\n2, 'b'\n\n# Pushes down independent predicates from JOIN nodes.\n[opt]> SELECT * FROM test a JOIN test b ON a.value = 'a' AND b.value = 'b'\n---\nInitial:\n   NestedLoopJoin: inner on a.value = 'a' AND b.value = 'b'\n   ├─ Scan: test as a\n   └─ Scan: test as b\nFilterPushdown:\n   NestedLoopJoin: inner\n   ├─ Scan: test as a (a.value = 'a')\n   └─ Scan: test as b (b.value = 'b')\n1, 'a', 2, 'b'\n\n# Pushes down independent predicates from JOIN nodes, even when there\n# are also dependent predicates.\n[opt]> SELECT * FROM test a JOIN test b ON a.id = b.id AND a.value = 'a' AND b.value = 'b'\n---\nInitial:\n   NestedLoopJoin: inner on a.id = b.id AND a.value = 'a' AND b.value = 'b'\n   ├─ Scan: test as a\n   └─ Scan: test as b\nFilterPushdown:\n   NestedLoopJoin: inner on a.id = b.id\n   ├─ Scan: test as a (a.value = 'a')\n   └─ Scan: test as b (b.value = 'b')\nHashJoin:\n   HashJoin: inner on a.id = b.id\n   ├─ Scan: test as a (a.value = 'a')\n   └─ Scan: test as b (b.value = 'b')\n\n# Does not push down JOIN node OR predicates.\n[opt]> SELECT * FROM test a JOIN test b ON a.value = 'a' OR b.value = 'b'\n---\nInitial:\n   NestedLoopJoin: inner on a.value = 'a' OR b.value = 'b'\n   ├─ Scan: test as a\n   └─ Scan: test as b\n1, 'a', 1, 'a'\n1, 'a', 2, 'b'\n1, 'a', 3, 'c'\n2, 'b', 2, 'b'\n3, 'c', 2, 'b'\n\n# Pushes WHERE predicates down into and past JOIN nodes.\n[opt]> SELECT * FROM test a JOIN test b ON a.id = b.id WHERE a.value = 'a' AND b.value = 'b'\n---\nInitial:\n   Filter: a.value = 'a' AND b.value = 'b'\n   └─ NestedLoopJoin: inner on a.id = b.id\n      ├─ Scan: test as a\n      └─ Scan: test as b\nFilterPushdown:\n   NestedLoopJoin: inner on a.id = b.id\n   ├─ Scan: test as a (a.value = 'a')\n   └─ Scan: test as b (b.value = 'b')\nHashJoin:\n   HashJoin: inner on a.id = b.id\n   ├─ Scan: test as a (a.value = 'a')\n   └─ Scan: test as b (b.value = 'b')\n\n# Pushes down the parts of predicates that can be pushed.\n[opt]> SELECT * FROM test a JOIN test b ON a.id = b.id WHERE a.value = 'a' AND b.value = 'b' AND (a.id > 0 OR b.id > 0)\n---\nInitial:\n   Filter: a.value = 'a' AND b.value = 'b' AND (a.id > 0 OR b.id > 0)\n   └─ NestedLoopJoin: inner on a.id = b.id\n      ├─ Scan: test as a\n      └─ Scan: test as b\nFilterPushdown:\n   NestedLoopJoin: inner on (a.id > 0 OR b.id > 0) AND a.id = b.id\n   ├─ Scan: test as a (a.value = 'a')\n   └─ Scan: test as b (b.value = 'b')\n\n# Equijoin pushdowns can transfer lookups from one relation to the other to make\n# use of indexes.\n[opt]> SELECT * FROM test a JOIN test b ON a.id = b.id WHERE (a.id = 1 OR a.id = 2)\n---\nInitial:\n   Filter: a.id = 1 OR a.id = 2\n   └─ NestedLoopJoin: inner on a.id = b.id\n      ├─ Scan: test as a\n      └─ Scan: test as b\nFilterPushdown:\n   NestedLoopJoin: inner on a.id = b.id\n   ├─ Scan: test as a (a.id = 1 OR a.id = 2)\n   └─ Scan: test as b (b.id = 1 OR b.id = 2)\nIndexLookup:\n   NestedLoopJoin: inner on a.id = b.id\n   ├─ KeyLookup: test as a (1, 2)\n   └─ KeyLookup: test as b (1, 2)\nHashJoin:\n   HashJoin: inner on a.id = b.id\n   ├─ KeyLookup: test as a (1, 2)\n   └─ KeyLookup: test as b (1, 2)\n1, 'a', 1, 'a'\n2, 'b', 2, 'b'\n\n[opt]> SELECT * FROM test a JOIN test b ON a.id = b.id WHERE (b.id = 1 OR b.id = 2)\n---\nInitial:\n   Filter: b.id = 1 OR b.id = 2\n   └─ NestedLoopJoin: inner on a.id = b.id\n      ├─ Scan: test as a\n      └─ Scan: test as b\nFilterPushdown:\n   NestedLoopJoin: inner on a.id = b.id\n   ├─ Scan: test as a (a.id = 1 OR a.id = 2)\n   └─ Scan: test as b (b.id = 1 OR b.id = 2)\nIndexLookup:\n   NestedLoopJoin: inner on a.id = b.id\n   ├─ KeyLookup: test as a (1, 2)\n   └─ KeyLookup: test as b (1, 2)\nHashJoin:\n   HashJoin: inner on a.id = b.id\n   ├─ KeyLookup: test as a (1, 2)\n   └─ KeyLookup: test as b (1, 2)\n1, 'a', 1, 'a'\n2, 'b', 2, 'b'\n\n# Pushdowns can propagate through multiple JOIN nodes.\n[opt]> SELECT * FROM \\\n    test a JOIN test b ON a.id = b.id JOIN test c ON b.id = c.id JOIN test d ON c.id = d.id \\\n    WHERE a.id > 0 AND b.id = 2 AND c.id < 3 AND (d.id = 2 OR d.id = 3)\n---\nInitial:\n   Filter: a.id > 0 AND b.id = 2 AND c.id < 3 AND (d.id = 2 OR d.id = 3)\n   └─ NestedLoopJoin: inner on c.id = d.id\n      ├─ NestedLoopJoin: inner on b.id = c.id\n      │  ├─ NestedLoopJoin: inner on a.id = b.id\n      │  │  ├─ Scan: test as a\n      │  │  └─ Scan: test as b\n      │  └─ Scan: test as c\n      └─ Scan: test as d\nFilterPushdown:\n   NestedLoopJoin: inner on c.id = d.id\n   ├─ NestedLoopJoin: inner on b.id = c.id\n   │  ├─ NestedLoopJoin: inner on a.id = b.id\n   │  │  ├─ Scan: test as a (a.id > 0 AND (a.id = 2 OR a.id = 3))\n   │  │  └─ Scan: test as b (b.id = 2 AND (b.id = 2 OR b.id = 3))\n   │  └─ Scan: test as c (c.id < 3 AND (c.id = 2 OR c.id = 3) AND c.id = 2)\n   └─ Scan: test as d (d.id = 2 OR d.id = 3)\nIndexLookup:\n   NestedLoopJoin: inner on c.id = d.id\n   ├─ NestedLoopJoin: inner on b.id = c.id\n   │  ├─ NestedLoopJoin: inner on a.id = b.id\n   │  │  ├─ Filter: a.id > 0\n   │  │  │  └─ KeyLookup: test as a (2, 3)\n   │  │  └─ Filter: b.id = 2 OR b.id = 3\n   │  │     └─ KeyLookup: test as b (2)\n   │  └─ Filter: c.id < 3 AND c.id = 2\n   │     └─ KeyLookup: test as c (2, 3)\n   └─ KeyLookup: test as d (2, 3)\nHashJoin:\n   HashJoin: inner on c.id = d.id\n   ├─ HashJoin: inner on b.id = c.id\n   │  ├─ HashJoin: inner on a.id = b.id\n   │  │  ├─ Filter: a.id > 0\n   │  │  │  └─ KeyLookup: test as a (2, 3)\n   │  │  └─ Filter: b.id = 2 OR b.id = 3\n   │  │     └─ KeyLookup: test as b (2)\n   │  └─ Filter: c.id < 3 AND c.id = 2\n   │     └─ KeyLookup: test as c (2, 3)\n   └─ KeyLookup: test as d (2, 3)\n2, 'b', 2, 'b', 2, 'b', 2, 'b'\n"
  },
  {
    "path": "src/sql/testscripts/optimizers/hash_join",
    "content": "# Tests the switch to hash joins where appropriate.\n\n> CREATE TABLE test (id INT PRIMARY KEY, value STRING)\n> INSERT INTO test VALUES (1, 'a'), (2, 'b'), (3, 'c')\n---\nok\n\n# Equijoins are converted to hash joins.\n[opt]> SELECT * FROM test a JOIN test b ON a.id = b.id\n---\nInitial:\n   NestedLoopJoin: inner on a.id = b.id\n   ├─ Scan: test as a\n   └─ Scan: test as b\nHashJoin:\n   HashJoin: inner on a.id = b.id\n   ├─ Scan: test as a\n   └─ Scan: test as b\n1, 'a', 1, 'a'\n2, 'b', 2, 'b'\n3, 'c', 3, 'c'\n\n# This also works for non-primary key columns.\n[opt]> SELECT * FROM test a JOIN test b ON a.value = b.value\n---\nInitial:\n   NestedLoopJoin: inner on a.value = b.value\n   ├─ Scan: test as a\n   └─ Scan: test as b\nHashJoin:\n   HashJoin: inner on a.value = b.value\n   ├─ Scan: test as a\n   └─ Scan: test as b\n1, 'a', 1, 'a'\n2, 'b', 2, 'b'\n3, 'c', 3, 'c'\n\n# However, it does not work with both.\n[opt]> SELECT * FROM test a JOIN test b ON a.id = b.id AND a.value = b.value \n---\nInitial:\n   NestedLoopJoin: inner on a.id = b.id AND a.value = b.value\n   ├─ Scan: test as a\n   └─ Scan: test as b\n1, 'a', 1, 'a'\n2, 'b', 2, 'b'\n3, 'c', 3, 'c'\n\n# It does not work with other predicates either. A smarter optimizer could\n# move the rest of the predicate into a new filter node.\n[opt]> SELECT * FROM test a JOIN test b ON a.id = b.id AND a.value >= b.value\n---\nInitial:\n   NestedLoopJoin: inner on a.id = b.id AND (a.value > b.value OR a.value = b.value)\n   ├─ Scan: test as a\n   └─ Scan: test as b\n1, 'a', 1, 'a'\n2, 'b', 2, 'b'\n3, 'c', 3, 'c'\n\n# However, the filter pushdown optimizer can save the day by pushing down\n# independent predicates into the scans.\n[opt]> SELECT * FROM test a JOIN test b ON a.id = b.id AND a.value = 'b' AND b.value = 'c'\n---\nInitial:\n   NestedLoopJoin: inner on a.id = b.id AND a.value = 'b' AND b.value = 'c'\n   ├─ Scan: test as a\n   └─ Scan: test as b\nFilterPushdown:\n   NestedLoopJoin: inner on a.id = b.id\n   ├─ Scan: test as a (a.value = 'b')\n   └─ Scan: test as b (b.value = 'c')\nHashJoin:\n   HashJoin: inner on a.id = b.id\n   ├─ Scan: test as a (a.value = 'b')\n   └─ Scan: test as b (b.value = 'c')\n"
  },
  {
    "path": "src/sql/testscripts/optimizers/index_lookup",
    "content": "# Tests the index_lookup optimizer.\n\n> CREATE TABLE test (id INT PRIMARY KEY, value STRING INDEX, \"float\" FLOAT INDEX)\n> INSERT INTO test VALUES (0, NULL), (1, 'a', 3.14), (2, 'b', NAN), (3, 'c', 0.0)\n> CREATE TABLE other (id INT PRIMARY KEY, test_id INT REFERENCES test)\n> INSERT INTO other VALUES (1, 1), (2, 2), (3, 3)\n---\nok\n\n# Primary key lookups.\n[opt]> SELECT * FROM test WHERE id = 2\n---\nInitial:\n   Filter: test.id = 2\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (test.id = 2)\nIndexLookup:\n   KeyLookup: test (2)\n2, 'b', NaN\n\n[opt]> SELECT * FROM test WHERE id = 1 OR id = 3\n---\nInitial:\n   Filter: test.id = 1 OR test.id = 3\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (test.id = 1 OR test.id = 3)\nIndexLookup:\n   KeyLookup: test (1, 3)\n1, 'a', 3.14\n3, 'c', 0.0\n\n# Can combine lookups with other predicates, but only AND.\n[opt]> SELECT * FROM test WHERE (id = 1 OR id = 3) AND value > 'a'\n---\nInitial:\n   Filter: (test.id = 1 OR test.id = 3) AND test.value > 'a'\n   └─ Scan: test\nFilterPushdown:\n   Scan: test ((test.id = 1 OR test.id = 3) AND test.value > 'a')\nIndexLookup:\n   Filter: test.value > 'a'\n   └─ KeyLookup: test (1, 3)\n3, 'c', 0.0\n\n[opt]> SELECT * FROM test WHERE id = 1 OR id = 3 OR value > 'a'\n---\nInitial:\n   Filter: test.id = 1 OR test.id = 3 OR test.value > 'a'\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (test.id = 1 OR test.id = 3 OR test.value > 'a')\n1, 'a', 3.14\n2, 'b', NaN\n3, 'c', 0.0\n\n# Same story with secondary indexes.\n[opt]> SELECT * FROM test WHERE value = 'b'\n---\nInitial:\n   Filter: test.value = 'b'\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (test.value = 'b')\nIndexLookup:\n   IndexLookup: test.value ('b')\n2, 'b', NaN\n\n[opt]> SELECT * FROM test WHERE value = 'a' OR value = 'c'\n---\nInitial:\n   Filter: test.value = 'a' OR test.value = 'c'\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (test.value = 'a' OR test.value = 'c')\nIndexLookup:\n   IndexLookup: test.value ('a', 'c')\n1, 'a', 3.14\n3, 'c', 0.0\n\n[opt]> SELECT * FROM test WHERE (value = 'a' OR value = 'c') AND id > 1\n---\nInitial:\n   Filter: (test.value = 'a' OR test.value = 'c') AND test.id > 1\n   └─ Scan: test\nFilterPushdown:\n   Scan: test ((test.value = 'a' OR test.value = 'c') AND test.id > 1)\nIndexLookup:\n   Filter: test.id > 1\n   └─ IndexLookup: test.value ('a', 'c')\n3, 'c', 0.0\n\n[opt]> SELECT * FROM test WHERE value = 'a' OR value = 'c' OR id > 1\n---\nInitial:\n   Filter: test.value = 'a' OR test.value = 'c' OR test.id > 1\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (test.value = 'a' OR test.value = 'c' OR test.id > 1)\n1, 'a', 3.14\n2, 'b', NaN\n3, 'c', 0.0\n\n# NULL lookups should match for IS NULL, but not for = NULL. IS NOT NULL\n# incurs a table scan.\n[opt]> SELECT * FROM test WHERE value IS NULL\n---\nInitial:\n   Filter: test.value IS NULL\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (test.value IS NULL)\nIndexLookup:\n   IndexLookup: test.value (NULL)\n0, NULL, NULL\n\n[opt]> SELECT * FROM test WHERE value = NULL\n---\nInitial:\n   Filter: test.value = NULL\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (test.value = NULL)\nIndexLookup:\n   IndexLookup: test.value (0 values)\nShortCircuit:\n   Nothing\n\n[opt]> SELECT * FROM test WHERE value != NULL\n---\nInitial:\n   Filter: NOT test.value = NULL\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (NOT test.value = NULL)\n\n[opt]> SELECT * FROM test WHERE value IS NOT NULL\n---\nInitial:\n   Filter: NOT test.value IS NULL\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (NOT test.value IS NULL)\n1, 'a', 3.14\n2, 'b', NaN\n3, 'c', 0.0\n\n# NAN lookups should be treated similarly to NULLs.\n[opt]> SELECT * FROM test WHERE \"float\" IS NAN\n---\nInitial:\n   Filter: test.float IS NAN\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (test.float IS NAN)\nIndexLookup:\n   IndexLookup: test.float (NaN)\n2, 'b', NaN\n\n[opt]> SELECT * FROM test WHERE \"float\" = NAN\n---\nInitial:\n   Filter: test.float = NaN\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (test.float = NaN)\nIndexLookup:\n   IndexLookup: test.float (0 values)\nShortCircuit:\n   Nothing\n\n[opt]> SELECT * FROM test WHERE \"float\" = -NAN\n---\nInitial:\n   Filter: test.float = -NaN\n   └─ Scan: test\nConstantFolding:\n   Filter: test.float = NaN\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (test.float = NaN)\nIndexLookup:\n   IndexLookup: test.float (0 values)\nShortCircuit:\n   Nothing\n\n# NB: NAN != NAN, so this should return row 2. This is unlike NULL, where NULL\n# != NULL yields NULL rather than true.\n[opt]> SELECT * FROM test WHERE \"float\" != NAN\n---\nInitial:\n   Filter: NOT test.float = NaN\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (NOT test.float = NaN)\n1, 'a', 3.14\n2, 'b', NaN\n3, 'c', 0.0\n\n[opt]> SELECT * FROM test WHERE \"float\" IS NOT NAN\n---\nInitial:\n   Filter: NOT test.float IS NAN\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (NOT test.float IS NAN)\n1, 'a', 3.14\n3, 'c', 0.0\n\n# Inner joins on foreign keys with index lookups are transferred across.\n[opt]> SELECT * FROM test JOIN other ON other.test_id = test.id WHERE test_id = 1 OR test_id = 3\n---\nInitial:\n   Filter: other.test_id = 1 OR other.test_id = 3\n   └─ NestedLoopJoin: inner on other.test_id = test.id\n      ├─ Scan: test\n      └─ Scan: other\nFilterPushdown:\n   NestedLoopJoin: inner on other.test_id = test.id\n   ├─ Scan: test (test.id = 1 OR test.id = 3)\n   └─ Scan: other (other.test_id = 1 OR other.test_id = 3)\nIndexLookup:\n   NestedLoopJoin: inner on other.test_id = test.id\n   ├─ KeyLookup: test (1, 3)\n   └─ IndexLookup: other.test_id (1, 3)\nHashJoin:\n   HashJoin: inner on test.id = other.test_id\n   ├─ KeyLookup: test (1, 3)\n   └─ IndexLookup: other.test_id (1, 3)\n1, 'a', 3.14, 1, 1\n3, 'c', 0.0, 3, 3\n\n# It's the same if the index lookups are given in the join predicate.\n[opt]> SELECT * FROM test JOIN other ON other.test_id = test.id AND (test_id = 1 OR test_id = 3)\n---\nInitial:\n   NestedLoopJoin: inner on other.test_id = test.id AND (other.test_id = 1 OR other.test_id = 3)\n   ├─ Scan: test\n   └─ Scan: other\nFilterPushdown:\n   NestedLoopJoin: inner on other.test_id = test.id\n   ├─ Scan: test (test.id = 1 OR test.id = 3)\n   └─ Scan: other (other.test_id = 1 OR other.test_id = 3)\nIndexLookup:\n   NestedLoopJoin: inner on other.test_id = test.id\n   ├─ KeyLookup: test (1, 3)\n   └─ IndexLookup: other.test_id (1, 3)\nHashJoin:\n   HashJoin: inner on test.id = other.test_id\n   ├─ KeyLookup: test (1, 3)\n   └─ IndexLookup: other.test_id (1, 3)\n1, 'a', 3.14, 1, 1\n3, 'c', 0.0, 3, 3\n\n# It also works with three tables.\n[opt]> SELECT * FROM test \\\n    JOIN other a ON a.test_id = test.id AND a.test_id = 2 \\\n    JOIN other b ON b.test_id = test.id AND b.test_id = 1 OR b.test_id = 3\n---\nInitial:\n   NestedLoopJoin: inner on b.test_id = test.id AND b.test_id = 1 OR b.test_id = 3\n   ├─ NestedLoopJoin: inner on a.test_id = test.id AND a.test_id = 2\n   │  ├─ Scan: test\n   │  └─ Scan: other as a\n   └─ Scan: other as b\nFilterPushdown:\n   NestedLoopJoin: inner on b.test_id = test.id OR b.test_id = 3\n   ├─ NestedLoopJoin: inner on a.test_id = test.id\n   │  ├─ Scan: test (test.id = 2)\n   │  └─ Scan: other as a (a.test_id = 2)\n   └─ Scan: other as b (b.test_id = 1 OR b.test_id = 3)\nIndexLookup:\n   NestedLoopJoin: inner on b.test_id = test.id OR b.test_id = 3\n   ├─ NestedLoopJoin: inner on a.test_id = test.id\n   │  ├─ KeyLookup: test (2)\n   │  └─ IndexLookup: other.test_id as a.test_id (2)\n   └─ IndexLookup: other.test_id as b.test_id (1, 3)\nHashJoin:\n   NestedLoopJoin: inner on b.test_id = test.id OR b.test_id = 3\n   ├─ HashJoin: inner on test.id = a.test_id\n   │  ├─ KeyLookup: test (2)\n   │  └─ IndexLookup: other.test_id as a.test_id (2)\n   └─ IndexLookup: other.test_id as b.test_id (1, 3)\n2, 'b', NaN, 2, 2, 3, 3\n"
  },
  {
    "path": "src/sql/testscripts/optimizers/short_circuit",
    "content": "# Tests the short circuiting optimizer.\n\n> CREATE TABLE test (id INT PRIMARY KEY, value STRING)\n> INSERT INTO test VALUES (1, 'a'), (2, 'b'), (3, 'c')\n> CREATE TABLE ref (id INT PRIMARY KEY, test_id INT REFERENCES test)\n> INSERT INTO ref VALUES (1, 1), (2, 2), (3, 3)\n---\nok\n\n# TRUE predicates are removed.\n[opt]> SELECT * FROM test WHERE TRUE\n---\nInitial:\n   Filter: TRUE\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (TRUE)\nShortCircuit:\n   Scan: test\n1, 'a'\n2, 'b'\n3, 'c'\n\n[opt]> SELECT 1, 2, 3 WHERE TRUE\n---\nInitial:\n   Projection: 1, 2, 3\n   └─ Filter: TRUE\n      └─ Values: blank row\nShortCircuit:\n   Projection: 1, 2, 3\n   └─ Values: blank row\n1, 2, 3\n\n[opt]> SELECT * FROM test JOIN ref ON TRUE\n---\nInitial:\n   NestedLoopJoin: inner on TRUE\n   ├─ Scan: test\n   └─ Scan: ref\nFilterPushdown:\n   NestedLoopJoin: inner\n   ├─ Scan: test (TRUE)\n   └─ Scan: ref (TRUE)\nShortCircuit:\n   NestedLoopJoin: inner\n   ├─ Scan: test\n   └─ Scan: ref\n1, 'a', 1, 1\n1, 'a', 2, 2\n1, 'a', 3, 3\n2, 'b', 1, 1\n2, 'b', 2, 2\n2, 'b', 3, 3\n3, 'c', 1, 1\n3, 'c', 2, 2\n3, 'c', 3, 3\n\n# FALSE predicates → Nothing (but retains column headers)\n[opt,header]> SELECT * FROM test WHERE FALSE\n---\nInitial:\n   Filter: FALSE\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (FALSE)\nShortCircuit:\n   Nothing\ntest.id, test.value\n\n[opt,header]> SELECT 1, 2, 3 WHERE FALSE\n---\nInitial:\n   Projection: 1, 2, 3\n   └─ Filter: FALSE\n      └─ Values: blank row\nShortCircuit:\n   Nothing\n, , \n\n[opt,header]> SELECT * FROM test JOIN ref ON ref.test_id = test.id AND FALSE\n---\nInitial:\n   NestedLoopJoin: inner on ref.test_id = test.id AND FALSE\n   ├─ Scan: test\n   └─ Scan: ref\nConstantFolding:\n   NestedLoopJoin: inner on FALSE\n   ├─ Scan: test\n   └─ Scan: ref\nFilterPushdown:\n   NestedLoopJoin: inner\n   ├─ Scan: test (FALSE)\n   └─ Scan: ref (FALSE)\nShortCircuit:\n   Nothing\ntest.id, test.value, ref.id, ref.test_id\n\n# NULL predicates → Nothing\n[opt,header]> SELECT * FROM test WHERE NULL\n---\nInitial:\n   Filter: NULL\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (NULL)\nShortCircuit:\n   Nothing\ntest.id, test.value\n\n[opt,header]> SELECT 1, 2, 3 WHERE NULL\n---\nInitial:\n   Projection: 1, 2, 3\n   └─ Filter: NULL\n      └─ Values: blank row\nShortCircuit:\n   Nothing\n, , \n\n[opt,header]> SELECT * FROM test JOIN ref ON ref.test_id = test.id AND NULL\n---\nInitial:\n   NestedLoopJoin: inner on ref.test_id = test.id AND NULL\n   ├─ Scan: test\n   └─ Scan: ref\nFilterPushdown:\n   NestedLoopJoin: inner on ref.test_id = test.id\n   ├─ Scan: test (NULL)\n   └─ Scan: ref (NULL)\nHashJoin:\n   HashJoin: inner on test.id = ref.test_id\n   ├─ Scan: test (NULL)\n   └─ Scan: ref (NULL)\nShortCircuit:\n   Nothing\ntest.id, test.value, ref.id, ref.test_id\n\n# Empty key/index lookups → Nothing\n[opt,header]> SELECT * FROM test WHERE id = NULL\n---\nInitial:\n   Filter: test.id = NULL\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (test.id = NULL)\nIndexLookup:\n   KeyLookup: test (0 keys)\nShortCircuit:\n   Nothing\ntest.id, test.value\n\n[opt,header]> SELECT * FROM ref WHERE test_id = NULL\n---\nInitial:\n   Filter: ref.test_id = NULL\n   └─ Scan: ref\nFilterPushdown:\n   Scan: ref (ref.test_id = NULL)\nIndexLookup:\n   IndexLookup: ref.test_id (0 values)\nShortCircuit:\n   Nothing\nref.id, ref.test_id\n\n# LIMIT 0 → Nothing\n[opt,header]> SELECT * FROM test LIMIT 0\n---\nInitial:\n   Limit: 0\n   └─ Scan: test\nShortCircuit:\n   Nothing\ntest.id, test.value\n\n# Remove projections that simply pass through source columns. Aliased\n# column names are retained.\n[opt]> SELECT id, value FROM test\n---\nInitial:\n   Projection: test.id, test.value\n   └─ Scan: test\nShortCircuit:\n   Scan: test\n1, 'a'\n2, 'b'\n3, 'c'\n\n[opt,header]> SELECT id AS foo, value AS bar FROM test\n---\nInitial:\n   Projection: test.id as foo, test.value as bar\n   └─ Scan: test\nfoo, bar\n1, 'a'\n2, 'b'\n3, 'c'\n\n[opt]> SELECT id, MIN(id), MAX(id) FROM test GROUP BY id\n---\nInitial:\n   Projection: test.id, #1, #2\n   └─ Aggregate: test.id, min(test.id), max(test.id)\n      └─ Scan: test\nShortCircuit:\n   Aggregate: test.id, min(test.id), max(test.id)\n   └─ Scan: test\n1, 1, 1\n2, 2, 2\n3, 3, 3\n\n# Constant folding happens before short-circuiting.\n[opt]> SELECT * FROM test WHERE 1 != 1 OR 0 > 3 AND NOT NULL\n---\nInitial:\n   Filter: NOT 1 = 1 OR 0 > 3 AND NOT NULL\n   └─ Scan: test\nConstantFolding:\n   Filter: FALSE\n   └─ Scan: test\nFilterPushdown:\n   Scan: test (FALSE)\nShortCircuit:\n   Nothing\n"
  },
  {
    "path": "src/sql/testscripts/queries/aggregate",
    "content": "# Tests aggregate functions.\n\n> CREATE TABLE test ( \\\n    id INT PRIMARY KEY, \\\n    \"bool\" BOOLEAN, \\\n    \"int\" INTEGER, \\\n    \"float\" FLOAT, \\\n    \"string\" STRING \\\n)\n> INSERT INTO test VALUES (0, NULL, NULL,   NULL,      NULL)\n> INSERT INTO test VALUES (1, TRUE,   -1,   3.14,      '')\n> INSERT INTO test VALUES (2, FALSE,  0,    2.718,     'abc')\n> INSERT INTO test VALUES (3, TRUE,   3,    -0.0,      'AB')\n> INSERT INTO test VALUES (4, NULL,   42,   INFINITY,  '👋')\n> INSERT INTO test VALUES (5, NULL,   NULL, NAN,       NULL)\n---\nok\n\n# COUNT(*) returns the row count.\n[plan]> SELECT COUNT(*) FROM test\n---\nAggregate: count(TRUE)\n└─ Scan: test\n6\n\n# COUNT works on constant values.\n[plan,header]> SELECT COUNT(NULL), COUNT(TRUE), COUNT(1), COUNT(3.14), COUNT(NAN), COUNT('')\n---\nAggregate: count(NULL), count(TRUE), count(1), count(3.14), count(NaN), count('')\n└─ Values: blank row\n, , , , , \n0, 1, 1, 1, 1, 1\n\n# COUNT works on no rows.\n[plan]> SELECT COUNT(id), COUNT(\"bool\"), COUNT(\"float\"), COUNT(\"string\") FROM test WHERE false\n---\nAggregate: count(test.id), count(test.bool), count(test.float), count(test.string)\n└─ Nothing\n0, 0, 0, 0\n\n# COUNT returns number of non-NULL values.\n[plan,header]> SELECT COUNT(id), COUNT(\"bool\"), COUNT(\"float\"), COUNT(\"string\") FROM test\n---\nAggregate: count(test.id), count(test.bool), count(test.float), count(test.string)\n└─ Scan: test\n, , , \n6, 3, 5, 4\n\n# MAX works on constant values.\n[plan]> SELECT MAX(NULL), MAX(TRUE), MAX(1), MAX(3.14), MAX(NAN), MAX('foo') FROM test\n---\nAggregate: max(NULL), max(TRUE), max(1), max(3.14), max(NaN), max('foo')\n└─ Scan: test\nNULL, TRUE, 1, 3.14, NaN, 'foo'\n\n# MAX works on no rows.\n[plan]> SELECT MAX(id), MAX(\"bool\"), MAX(\"float\"), MAX(\"string\") FROM test WHERE false\n---\nAggregate: max(test.id), max(test.bool), max(test.float), max(test.string)\n└─ Nothing\nNULL, NULL, NULL, NULL\n\n# MAX returns the max value, or NULL if any value is NULL.\n[plan]> SELECT MAX(id) FROM test\n---\nAggregate: max(test.id)\n└─ Scan: test\n5\n\n> SELECT MAX(\"bool\") FROM test\n---\nTRUE\n\n> SELECT MAX(\"int\") FROM test\n---\n42\n\n> SELECT MAX(\"float\") FROM test\n> SELECT MAX(\"float\") FROM test WHERE \"float\" IS NOT NAN\n---\nNaN\ninf\n\n> SELECT MAX(\"string\") FROM test\n---\n'👋'\n\n# MIN works on constant values.\n[plan]> SELECT MIN(NULL), MIN(TRUE), MIN(1), MIN(3.14), MIN(NAN), MIN('foo') FROM test\n---\nAggregate: min(NULL), min(TRUE), min(1), min(3.14), min(NaN), min('foo')\n└─ Scan: test\nNULL, TRUE, 1, 3.14, NaN, 'foo'\n\n# MIN works on no rows.\n[plan]> SELECT MIN(id), MIN(\"bool\"), MIN(\"float\"), MIN(\"string\") FROM test WHERE false\n---\nAggregate: min(test.id), min(test.bool), min(test.float), min(test.string)\n└─ Nothing\nNULL, NULL, NULL, NULL\n\n# MIN returns the min value, or NULL if any value is NULL.\n[plan]> SELECT MIN(id) FROM test\n---\nAggregate: min(test.id)\n└─ Scan: test\n0\n\n> SELECT MIN(\"bool\") FROM test\n---\nFALSE\n\n> SELECT MIN(\"int\") FROM test\n---\n-1\n\n> SELECT MIN(\"float\") FROM test\n---\n0.0\n\n> SELECT MIN(\"string\") FROM test\n---\n''\n\n# SUM works on constant values, but only numbers.\n[plan]> SELECT SUM(NULL), SUM(1), SUM(3.14), SUM(NAN) FROM test\n---\nAggregate: sum(NULL), sum(1), sum(3.14), sum(NaN)\n└─ Scan: test\nNULL, 6, 18.84, NaN\n\n!> SELECT SUM(TRUE)\n!> SELECT SUM('foo')\n---\nError: invalid input: can't add 0 and TRUE\nError: invalid input: can't add 0 and 'foo'\n\n# SUM works on no rows.\n[plan]> SELECT SUM(id), SUM(\"bool\"), SUM(\"float\"), SUM(\"string\") FROM test WHERE false\n---\nAggregate: sum(test.id), sum(test.bool), sum(test.float), sum(test.string)\n└─ Nothing\nNULL, NULL, NULL, NULL\n\n# SUM returns the sum, or NULL if any value is NULL. Errors\n# on booleans or strings.\n[plan]> SELECT SUM(id) FROM test\n---\nAggregate: sum(test.id)\n└─ Scan: test\n15\n\n!> SELECT SUM(\"bool\") FROM test\n---\nError: invalid input: can't add 0 and TRUE\n\n> SELECT SUM(\"int\") FROM test\n---\n44\n\n> SELECT SUM(\"float\") FROM test\n> SELECT SUM(\"float\") FROM test WHERE \"float\" IS NOT NAN\n---\nNaN\ninf\n\n!> SELECT SUM(\"string\") FROM test\n---\nError: invalid input: can't add 0 and ''\n\n# AVG works on constant values, but only numbers.\n[plan]> SELECT AVG(NULL), AVG(1), AVG(3.14), AVG(NAN) FROM test\n---\nAggregate: avg(NULL), avg(1), avg(3.14), avg(NaN)\n└─ Scan: test\nNULL, 1, 3.14, NaN\n\n!> SELECT AVG(TRUE)\n!> SELECT AVG('foo')\n---\nError: invalid input: can't add 0 and TRUE\nError: invalid input: can't add 0 and 'foo'\n\n# AVG works on no rows.\n[plan]> SELECT AVG(id), AVG(\"bool\"), AVG(\"float\"), AVG(\"string\") FROM test WHERE false\n---\nAggregate: avg(test.id), avg(test.bool), avg(test.float), avg(test.string)\n└─ Nothing\nNULL, NULL, NULL, NULL\n\n# AVG returns the average, or NULL if any value is NULL. Errors\n# on booleans or strings.\n[plan]> SELECT AVG(id) FROM test\n---\nAggregate: avg(test.id)\n└─ Scan: test\n2\n\n!> SELECT AVG(\"bool\") FROM test\n---\nError: invalid input: can't add 0 and TRUE\n\n> SELECT AVG(\"int\") FROM test\n---\n11\n\n> SELECT AVG(\"float\") FROM test\n> SELECT AVG(\"float\") FROM test WHERE \"float\" IS NOT NAN\n---\nNaN\ninf\n\n!> SELECT AVG(\"string\") FROM test\n---\nError: invalid input: can't add 0 and ''\n\n# Constant aggregates can be used with rows.\n[plan]> SELECT COUNT(1), MIN(1), MAX(1), SUM(1), AVG(1) FROM test\n---\nAggregate: count(1), min(1), max(1), sum(1), avg(1)\n└─ Scan: test\n6, 1, 1, 6, 1\n\n# Constant aggregates can't be used with value rows.\n!> SELECT *, COUNT(1), MIN(1), MAX(1), SUM(1), AVG(1) FROM test\n!> SELECT id, COUNT(1), MIN(1), MAX(1), SUM(1), AVG(1) FROM test\n---\nError: invalid input: column test.id must be used in an aggregate or GROUP BY expression\nError: invalid input: column id must be used in an aggregate or GROUP BY expression\n\n# Repeated aggregates work.\n[plan]> SELECT MAX(\"int\"), MAX(\"int\"), MAX(\"int\") FROM test\n---\nProjection: #0, #0, #0\n└─ Aggregate: max(test.int)\n   └─ Scan: test\n42, 42, 42\n\n# Aggregate can be expression, both inside and outside the aggregate.\n[plan]> SELECT SUM(\"int\" * 10) / COUNT(\"int\") + 7 FROM test WHERE \"int\" IS NOT NULL\n---\nProjection: #0 / #1 + 7\n└─ Aggregate: sum(test.int * 10), count(test.int)\n   └─ Scan: test (NOT test.int IS NULL)\n117\n\n# Aggregate functions can't be nested.\n!> SELECT MIN(MAX(\"int\")) FROM test\n---\nError: invalid input: aggregate functions can't be nested\n\n# Can't mix aggregate and non-aggregate expressions.\n!> SELECT MAX(\"int\") - \"int\" FROM test\n---\nError: invalid input: column int must be used in an aggregate or GROUP BY expression\n"
  },
  {
    "path": "src/sql/testscripts/queries/clauses",
    "content": "# Tests the ordering of SELECT clauses.\n\n> CREATE TABLE test ( \\\n    id INT PRIMARY KEY, \\\n    \"bool\" BOOLEAN, \\\n    \"float\" FLOAT, \\\n    \"int\" INT, \\\n    \"string\" STRING \\\n)\n> INSERT INTO test VALUES (1, true, 3.14, 7, 'foo')\n> INSERT INTO test VALUES (2, false, 2.718, 1, '👍')\n> INSERT INTO test VALUES (3, NULL, NULL, NULL, NULL)\n---\nok\n\n# This is the only valid order of all clauses:\n> SELECT COUNT(*) FROM test WHERE TRUE GROUP BY TRUE HAVING TRUE ORDER BY TRUE LIMIT 1 OFFSET 1\n---\nok\n\n# All clauses except SELECT are optional.\n> SELECT COUNT(*) WHERE TRUE GROUP BY TRUE HAVING TRUE ORDER BY TRUE LIMIT 1 OFFSET 1\n---\nok\n\n> SELECT COUNT(*) FROM test GROUP BY TRUE HAVING TRUE ORDER BY TRUE LIMIT 1 OFFSET 1\n---\nok\n\n> SELECT COUNT(*) FROM test WHERE TRUE HAVING TRUE ORDER BY TRUE LIMIT 1 OFFSET 1\n---\nok\n\n> SELECT COUNT(*) FROM test WHERE TRUE GROUP BY TRUE ORDER BY TRUE LIMIT 1 OFFSET 1\n---\nok\n\n> SELECT COUNT(*) FROM test WHERE TRUE GROUP BY TRUE HAVING TRUE LIMIT 1 OFFSET 1\n---\nok\n\n> SELECT COUNT(*) FROM test WHERE TRUE GROUP BY TRUE HAVING TRUE ORDER BY TRUE OFFSET 1\n---\nok\n\n> SELECT COUNT(*) FROM test WHERE TRUE GROUP BY TRUE HAVING TRUE ORDER BY TRUE LIMIT 1\n---\n3\n\n# The clause order is required. Moving any clause to the next position errors.\n!> FROM test SELECT COUNT(*) WHERE TRUE GROUP BY TRUE HAVING TRUE ORDER BY TRUE LIMIT 1 OFFSET 1\n---\nError: invalid input: unexpected token FROM\n\n!> SELECT COUNT(*) FROM test GROUP BY TRUE WHERE TRUE HAVING TRUE ORDER BY TRUE LIMIT 1 OFFSET 1\n---\nError: invalid input: unexpected token WHERE\n\n!> SELECT COUNT(*) FROM test WHERE TRUE HAVING TRUE GROUP BY TRUE ORDER BY TRUE LIMIT 1 OFFSET 1\n---\nError: invalid input: unexpected token GROUP\n\n!> SELECT COUNT(*) FROM test WHERE TRUE ORDER BY TRUE GROUP BY TRUE HAVING TRUE LIMIT 1 OFFSET 1\n---\nError: invalid input: unexpected token GROUP\n\n!> SELECT COUNT(*) FROM test WHERE TRUE GROUP BY TRUE HAVING TRUE LIMIT 1 ORDER BY TRUE OFFSET 1\n---\nError: invalid input: unexpected token ORDER\n\n!> SELECT COUNT(*) FROM test WHERE TRUE GROUP BY TRUE HAVING TRUE ORDER BY TRUE OFFSET 1 LIMIT 1 \n---\nError: invalid input: unexpected token LIMIT\n"
  },
  {
    "path": "src/sql/testscripts/queries/group_by",
    "content": "# Tests GROUP BY clauses. See \"aggregate\" for aggregate function tests.\n\n> CREATE TABLE test ( \\\n    id INT PRIMARY KEY, \\\n    \"group\" STRING, \\\n    \"bool\" BOOLEAN, \\\n    \"int\" INTEGER, \\\n    \"float\" FLOAT, \\\n    \"string\" STRING \\\n)\n> INSERT INTO test VALUES (0, NULL,   NULL, NULL,   NULL,      NULL)\n> INSERT INTO test VALUES (1, 'a',    TRUE,   -1,   3.14,      '')\n> INSERT INTO test VALUES (2, 'b',    FALSE,  0,    NAN,       'abc')\n> INSERT INTO test VALUES (3, 'a',    TRUE,   3,    -0.0,      'AB')\n> INSERT INTO test VALUES (4, 'b',    TRUE,   42,   INFINITY,  '👋')\n> INSERT INTO test VALUES (5, 'a',    FALSE,  7,    NAN,       '')\n> INSERT INTO test VALUES (6, 'b',    FALSE,  -1,   0.0,       'abc')\n\n> CREATE TABLE other (id INT PRIMARY KEY, value STRING)\n> INSERT INTO other VALUES (1, 'a'), (2, 'b')\n---\nok\n\n# Grouping with no input rows yields empty result.\n[plan]> SELECT COUNT(id), MIN(id), MAX(id), SUM(id), AVG(id) FROM test WHERE FALSE GROUP BY id\n---\nProjection: #1, #2, #3, #4, #5\n└─ Aggregate: test.id, count(test.id), min(test.id), max(test.id), sum(test.id), avg(test.id)\n   └─ Nothing\n\n# Simple GROUP BY, including NULL group.\n[plan]> SELECT \"group\", COUNT(*) FROM test GROUP BY \"group\"\n---\nAggregate: test.group, count(TRUE)\n└─ Scan: test\nNULL, 1\n'a', 3\n'b', 3\n\n[plan]> SELECT \"group\", COUNT(*), MIN(\"bool\"), MAX(\"string\"), SUM(\"int\"), AVG(\"float\") \\\n    FROM test GROUP BY \"group\"\n---\nAggregate: test.group, count(TRUE), min(test.bool), max(test.string), sum(test.int), avg(test.float)\n└─ Scan: test\nNULL, 1, NULL, NULL, NULL, NULL\n'a', 3, FALSE, 'AB', 9, NaN\n'b', 3, FALSE, '👋', 41, NaN\n\n# GROUP BY works on booleans.\n[plan]> SELECT \"bool\", COUNT(*) FROM test GROUP BY \"bool\"\n---\nAggregate: test.bool, count(TRUE)\n└─ Scan: test\nNULL, 1\nFALSE, 3\nTRUE, 3\n\n# GROUP BY works on integers.\n[plan]> SELECT \"int\", COUNT(*) FROM test GROUP BY \"int\"\n---\nAggregate: test.int, count(TRUE)\n└─ Scan: test\nNULL, 1\n-1, 2\n0, 1\n3, 1\n7, 1\n42, 1\n\n# GROUP BY works with floats, including a NAN group and -0.0 and 0.0 being equal.\n[plan]> SELECT \"float\", COUNT(*) FROM test GROUP BY \"float\"\n---\nAggregate: test.float, count(TRUE)\n└─ Scan: test\nNULL, 1\n0.0, 2\n3.14, 1\ninf, 1\nNaN, 2\n\n# GROUP BY works on strings.\n[plan]> SELECT \"string\", COUNT(*) FROM test GROUP BY \"string\"\n---\nAggregate: test.string, count(TRUE)\n└─ Scan: test\nNULL, 1\n'', 2\n'AB', 1\n'abc', 2\n'👋', 1\n\n# GROUP BY works even if the group column isn't in the result.\n[plan]> SELECT COUNT(*) FROM test GROUP BY \"group\"\n---\nProjection: #1\n└─ Aggregate: test.group, count(TRUE)\n   └─ Scan: test\n1\n3\n3\n\n# GROUP BY works when there is no aggregate function.\n[plan]> SELECT \"group\" FROM test GROUP BY \"group\"\n---\nAggregate: test.group\n└─ Scan: test\nNULL\n'a'\n'b'\n\n# GROUP BY does not work with SELECT aliases (also the case in e.g. SQL server).\n!> SELECT \"group\" AS g, COUNT(*) FROM test GROUP BY g\n---\nError: invalid input: unknown column g\n\n[plan]> SELECT \"group\", COUNT(*) FROM test AS t GROUP BY t.\"group\"\n---\nAggregate: t.group, count(TRUE)\n└─ Scan: test as t\nNULL, 1\n'a', 3\n'b', 3\n\n!> SELECT \"group\", COUNT(*) FROM test AS t GROUP BY test.\"group\"\n---\nError: invalid input: unknown table test\n\n# It errors when there is a non-group column.\n!> SELECT \"group\", id FROM test GROUP BY \"group\"\n---\nError: invalid input: column id must be used in an aggregate or GROUP BY expression\n\n# It errors on unknown tables and columns.\n!> SELECT COUNT(*) FROM test GROUP BY unknown\n!> SELECT COUNT(*) FROM test GROUP BY unknown.id\n---\nError: invalid input: unknown column unknown\nError: invalid input: unknown table unknown\n\n# GROUP BY can be arbitrary expressions.\n[plan]> SELECT COUNT(*) FROM test GROUP BY 1\n---\nProjection: #1\n└─ Aggregate: 1, count(TRUE)\n   └─ Scan: test\n7\n\n[plan]> SELECT COUNT(*) FROM test GROUP BY id % 2\n---\nProjection: #1\n└─ Aggregate: test.id % 2, count(TRUE)\n   └─ Scan: test\n4\n3\n\n# GROUP BY can use an expression also used in the SELECT.\n[plan]> SELECT id % 2, COUNT(*) FROM test GROUP BY id % 2\n---\nAggregate: test.id % 2, count(TRUE)\n└─ Scan: test\n0, 4\n1, 3\n\n# Can mix GROUP BY and aggregate expressions in SELECT.\n[plan]> SELECT MAX(\"int\") + id % 2 FROM test GROUP BY id\n---\nProjection: #1 + test.id % 2\n└─ Aggregate: test.id, max(test.int)\n   └─ Scan: test\nNULL\n0\n0\n4\n42\n8\n-1\n\n# GROUP BY can't use an aliased expression.\n!> SELECT id % 2 AS mod, COUNT(*) FROM test GROUP BY mod\n---\nError: invalid input: unknown column mod\n\n# GROUP BY can't use aggregate functions.\n!> SELECT COUNT(*) FROM test GROUP BY MIN(id)\n---\nError: invalid input: unknown function min with 1 arguments\n\n# GROUP BY works with multiple groups.\n[plan]> SELECT \"group\", \"bool\", COUNT(*) FROM test GROUP BY \"group\", \"bool\"\n---\nAggregate: test.group, test.bool, count(TRUE)\n└─ Scan: test\nNULL, NULL, 1\n'a', FALSE, 1\n'a', TRUE, 2\n'b', FALSE, 2\n'b', TRUE, 1\n\n# Repeated GROUP BY column works.\n[plan]> SELECT \"group\", \"group\", \"group\", COUNT(*) FROM test GROUP BY \"group\", \"group\"\n---\nProjection: test.group, test.group, test.group, #1\n└─ Aggregate: test.group, count(TRUE)\n   └─ Scan: test\nNULL, NULL, NULL, 1\n'a', 'a', 'a', 3\n'b', 'b', 'b', 3\n\n# GROUP BY works with joins.\n[plan]> SELECT t.id % 2, COUNT(*) FROM test t JOIN other o ON t.id % 2 = o.id GROUP BY t.id % 2\n---\nAggregate: t.id % 2, count(TRUE)\n└─ NestedLoopJoin: inner on t.id % 2 = o.id\n   ├─ Scan: test as t\n   └─ Scan: other as o\n1, 3\n\n# SELECT * requires all columns to be in GROUP BY.\n!> SELECT * FROM test GROUP BY id\n---\nError: invalid input: column test.group must be used in an aggregate or GROUP BY expression\n\n[plan]> SELECT * FROM test GROUP BY id, \"group\", \"bool\", \"int\", \"float\", \"string\"\n---\nAggregate: test.id, test.group, test.bool, test.int, test.float, test.string\n└─ Scan: test\n0, NULL, NULL, NULL, NULL, NULL\n1, 'a', TRUE, -1, 3.14, ''\n2, 'b', FALSE, 0, NaN, 'abc'\n3, 'a', TRUE, 3, 0.0, 'AB'\n4, 'b', TRUE, 42, inf, '👋'\n5, 'a', FALSE, 7, NaN, ''\n6, 'b', FALSE, -1, 0.0, 'abc'\n\n[plan]> SELECT * FROM test GROUP BY \"bool\", \"int\", \"float\", \"string\", \"group\", id\n---\nProjection: test.id, test.group, test.bool, test.int, test.float, test.string\n└─ Aggregate: test.bool, test.int, test.float, test.string, test.group, test.id\n   └─ Scan: test\n0, NULL, NULL, NULL, NULL, NULL\n6, 'b', FALSE, -1, 0.0, 'abc'\n2, 'b', FALSE, 0, NaN, 'abc'\n5, 'a', FALSE, 7, NaN, ''\n1, 'a', TRUE, -1, 3.14, ''\n3, 'a', TRUE, 3, 0.0, 'AB'\n4, 'b', TRUE, 42, inf, '👋'\n"
  },
  {
    "path": "src/sql/testscripts/queries/having",
    "content": "# Tests HAVING clauses. See \"aggregate\" and \"group_by\" for related tests.\n\n> CREATE TABLE test ( \\\n    id INT PRIMARY KEY, \\\n    \"group\" STRING, \\\n    \"bool\" BOOLEAN, \\\n    \"int\" INTEGER, \\\n    \"float\" FLOAT, \\\n    \"string\" STRING \\\n)\n> INSERT INTO test VALUES (0, NULL,   NULL, NULL,   NULL,      NULL)\n> INSERT INTO test VALUES (1, 'a',    TRUE,   -1,   3.14,      '')\n> INSERT INTO test VALUES (2, 'b',    FALSE,  0,    NAN,       'abc')\n> INSERT INTO test VALUES (3, 'a',    TRUE,   3,    -0.0,      'AB')\n> INSERT INTO test VALUES (4, 'b',    TRUE,   42,   INFINITY,  '👋')\n> INSERT INTO test VALUES (5, 'a',    FALSE,  7,    NAN,       '')\n> INSERT INTO test VALUES (6, 'b',    FALSE,  -1,   0.0,       'abc')\n---\nok\n\n# Having requires an aggregate function or GROUP BY clause.\n!> SELECT * FROM test HAVING id > 3\n---\nError: invalid input: HAVING requires GROUP BY or aggregate function\n\n> SELECT COUNT(*) FROM test HAVING COUNT(*) > 0\n---\n7\n\n> SELECT TRUE FROM test GROUP BY id HAVING id > 0\n---\nTRUE\nTRUE\nTRUE\nTRUE\nTRUE\nTRUE\n\n# Having works with an aggregate function, even if it's not in SELECT.\n[plan]> SELECT \"group\", MAX(\"int\") FROM test GROUP BY \"group\" HAVING MAX(\"int\") > 10\n---\nFilter: #1 > 10\n└─ Aggregate: test.group, max(test.int)\n   └─ Scan: test\n'b', 42\n\n[plan]> SELECT \"group\" FROM test GROUP BY \"group\" HAVING MAX(\"int\") > 10\n---\nRemap: test.group (dropped: #1)\n└─ Filter: #1 > 10\n   └─ Aggregate: test.group, max(test.int)\n      └─ Scan: test\n'b'\n\n[plan]> SELECT \"group\", MAX(\"int\") FROM test GROUP BY \"group\" HAVING MAX(\"int\") - MIN(\"int\") > 10\n---\nRemap: test.group, #1 (dropped: #2)\n└─ Filter: #1 - #2 > 10\n   └─ Aggregate: test.group, max(test.int), min(test.int)\n      └─ Scan: test\n'b', 42\n\n# Having works with SELECT aliases.\n[plan]> SELECT \"group\", MAX(\"int\") AS m FROM test GROUP BY \"group\" HAVING m > 10\n---\nFilter: m > 10\n└─ Projection: test.group, #1 as m\n   └─ Aggregate: test.group, max(test.int)\n      └─ Scan: test\n'b', 42\n\n# Having works with an aggregate function not in the SELECT clause.\n[plan]> SELECT \"group\", COUNT(*) FROM test GROUP BY \"group\" HAVING MAX(\"int\") > 10\n---\nRemap: test.group, #1 (dropped: #2)\n└─ Filter: #2 > 10\n   └─ Aggregate: test.group, count(TRUE), max(test.int)\n      └─ Scan: test\n'b', 3\n\n# Having works with compound expressions.\n[plan]> SELECT \"group\", COUNT(*) FROM test GROUP BY \"group\" HAVING MAX(\"int\") / COUNT(*) > 3\n---\nRemap: test.group, #1 (dropped: #2)\n└─ Filter: #2 / #1 > 3\n   └─ Aggregate: test.group, count(TRUE), max(test.int)\n      └─ Scan: test\n'b', 3\n\n# Having works with compound expressions using complex GROUP BY expressions\n# that are not on the SELECT clause.\n[plan]> SELECT COUNT(*) FROM test GROUP BY id % 2 HAVING 2 - id % 2 + 1 > 1\n---\nRemap: #0 (dropped: #1)\n└─ Filter: 2 - #1 + 1 > 1\n   └─ Projection: #1, #0\n      └─ Aggregate: test.id % 2, count(TRUE)\n         └─ Scan: test\n4\n3\n\n# Having can use (un)qualified expressions for an (un)qualified GROUP BY.\n[plan]> SELECT COUNT(*) FROM test GROUP BY \"group\" HAVING test.\"group\" = 'a'\n---\nRemap: #0 (dropped: test.group)\n└─ Filter: test.group = 'a'\n   └─ Projection: #1, test.group\n      └─ Aggregate: test.group, count(TRUE)\n         └─ Scan: test\n3\n\n[plan]> SELECT COUNT(*) FROM test GROUP BY test.\"group\" HAVING \"group\" = 'a'\n---\nRemap: #0 (dropped: test.group)\n└─ Filter: test.group = 'a'\n   └─ Projection: #1, test.group\n      └─ Aggregate: test.group, count(TRUE)\n         └─ Scan: test\n3\n\n# Having errors on nested aggregate functions.\n!> SELECT \"group\", COUNT(*) FROM test GROUP BY \"group\" HAVING MAX(MIN(\"int\")) > 0\n---\nError: invalid input: aggregate functions can't be nested\n\n# Having errors on columns not in the SELECT or GROUP BY clauses.\n!> SELECT \"group\", COUNT(*) FROM test GROUP BY \"group\" HAVING id > 3\n---\nError: invalid input: column id must be used in an aggregate or GROUP BY expression\n"
  },
  {
    "path": "src/sql/testscripts/queries/join_cross",
    "content": "# Tests cross joins.\n\n# Set up a movies dataset.\n> CREATE TABLE countries ( \\\n    id STRING PRIMARY KEY, \\\n    name STRING NOT NULL \\\n)\n> INSERT INTO countries VALUES \\\n    ('fr', 'France'), \\\n    ('ru', 'Russia'), \\\n    ('us', 'United States of America')\n>CREATE TABLE genres ( \\\n    id INTEGER PRIMARY KEY, \\\n    name STRING NOT NULL \\\n)\n> INSERT INTO genres VALUES \\\n    (1, 'Science Fiction'), \\\n    (2, 'Action'), \\\n    (3, 'Comedy')\n> CREATE TABLE studios ( \\\n    id INTEGER PRIMARY KEY, \\\n    name STRING NOT NULL, \\\n    country_id STRING INDEX REFERENCES countries \\\n)\n> INSERT INTO studios VALUES \\\n    (1, 'Mosfilm', 'ru'), \\\n    (2, 'Lionsgate', 'us'), \\\n    (3, 'StudioCanal', 'fr'), \\\n    (4, 'Warner Bros', 'us')\n> CREATE TABLE movies ( \\\n    id INTEGER PRIMARY KEY, \\\n    title STRING NOT NULL, \\\n    studio_id INTEGER NOT NULL INDEX REFERENCES studios, \\\n    genre_id INTEGER NOT NULL INDEX REFERENCES genres, \\\n    released INTEGER NOT NULL, \\\n    rating FLOAT, \\\n    ultrahd BOOLEAN \\\n)\n> INSERT INTO movies VALUES \\\n    (1, 'Stalker', 1, 1, 1979, 8.2, NULL), \\\n    (2, 'Sicario', 2, 2, 2015, 7.6, TRUE), \\\n    (3, 'Primer', 3, 1, 2004, 6.9, NULL), \\\n    (4, 'Heat', 4, 2, 1995, 8.2, TRUE), \\\n    (5, 'The Fountain', 4, 1, 2006, 7.2, FALSE), \\\n    (6, 'Solaris', 1, 1, 1972, 8.1, NULL), \\\n    (7, 'Gravity', 4, 1, 2013, 7.7, TRUE), \\\n    (8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE), \\\n    (9, 'Birdman', 4, 3, 2014, 7.7, TRUE), \\\n    (10, 'Inception', 4, 1, 2010, 8.8, TRUE)\n---\nok\n\n# Explicit cross join.\n[plan,header]> SELECT * FROM movies CROSS JOIN genres\n---\nNestedLoopJoin: inner\n├─ Scan: movies\n└─ Scan: genres\nmovies.id, movies.title, movies.studio_id, movies.genre_id, movies.released, movies.rating, movies.ultrahd, genres.id, genres.name\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 2, 'Action'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 3, 'Comedy'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 1, 'Science Fiction'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 3, 'Comedy'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 1, 'Science Fiction'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 2, 'Action'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 3, 'Comedy'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 1, 'Science Fiction'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 2, 'Action'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 3, 'Comedy'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 1, 'Science Fiction'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 2, 'Action'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 3, 'Comedy'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 1, 'Science Fiction'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 2, 'Action'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 3, 'Comedy'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 1, 'Science Fiction'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 2, 'Action'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 3, 'Comedy'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 1, 'Science Fiction'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 2, 'Action'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 3, 'Comedy'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 1, 'Science Fiction'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 2, 'Action'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 3, 'Comedy'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 1, 'Science Fiction'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 2, 'Action'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 3, 'Comedy'\n\n# Explicit triple cross join.\n[plan,header]> SELECT * FROM movies CROSS JOIN genres CROSS JOIN studios\n---\nNestedLoopJoin: inner\n├─ NestedLoopJoin: inner\n│  ├─ Scan: movies\n│  └─ Scan: genres\n└─ Scan: studios\nmovies.id, movies.title, movies.studio_id, movies.genre_id, movies.released, movies.rating, movies.ultrahd, genres.id, genres.name, studios.id, studios.name, studios.country_id\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 2, 'Action', 1, 'Mosfilm', 'ru'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 2, 'Action', 2, 'Lionsgate', 'us'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 2, 'Action', 3, 'StudioCanal', 'fr'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 2, 'Action', 4, 'Warner Bros', 'us'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 3, 'Comedy', 2, 'Lionsgate', 'us'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 3, 'Comedy', 4, 'Warner Bros', 'us'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action', 1, 'Mosfilm', 'ru'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action', 2, 'Lionsgate', 'us'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action', 3, 'StudioCanal', 'fr'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action', 4, 'Warner Bros', 'us'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 3, 'Comedy', 2, 'Lionsgate', 'us'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 3, 'Comedy', 4, 'Warner Bros', 'us'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 2, 'Action', 1, 'Mosfilm', 'ru'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 2, 'Action', 2, 'Lionsgate', 'us'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 2, 'Action', 3, 'StudioCanal', 'fr'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 2, 'Action', 4, 'Warner Bros', 'us'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 3, 'Comedy', 2, 'Lionsgate', 'us'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 3, 'Comedy', 4, 'Warner Bros', 'us'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 2, 'Action', 1, 'Mosfilm', 'ru'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 2, 'Action', 2, 'Lionsgate', 'us'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 2, 'Action', 3, 'StudioCanal', 'fr'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 2, 'Action', 4, 'Warner Bros', 'us'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 3, 'Comedy', 2, 'Lionsgate', 'us'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 3, 'Comedy', 4, 'Warner Bros', 'us'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 2, 'Action', 1, 'Mosfilm', 'ru'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 2, 'Action', 2, 'Lionsgate', 'us'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 2, 'Action', 3, 'StudioCanal', 'fr'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 2, 'Action', 4, 'Warner Bros', 'us'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 3, 'Comedy', 2, 'Lionsgate', 'us'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 3, 'Comedy', 4, 'Warner Bros', 'us'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 2, 'Action', 1, 'Mosfilm', 'ru'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 2, 'Action', 2, 'Lionsgate', 'us'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 2, 'Action', 3, 'StudioCanal', 'fr'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 2, 'Action', 4, 'Warner Bros', 'us'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 3, 'Comedy', 2, 'Lionsgate', 'us'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 3, 'Comedy', 4, 'Warner Bros', 'us'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 2, 'Action', 1, 'Mosfilm', 'ru'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 2, 'Action', 2, 'Lionsgate', 'us'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 2, 'Action', 3, 'StudioCanal', 'fr'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 2, 'Action', 4, 'Warner Bros', 'us'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 3, 'Comedy', 2, 'Lionsgate', 'us'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 3, 'Comedy', 4, 'Warner Bros', 'us'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 2, 'Action', 1, 'Mosfilm', 'ru'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 2, 'Action', 2, 'Lionsgate', 'us'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 2, 'Action', 3, 'StudioCanal', 'fr'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 2, 'Action', 4, 'Warner Bros', 'us'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 3, 'Comedy', 2, 'Lionsgate', 'us'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 3, 'Comedy', 4, 'Warner Bros', 'us'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 2, 'Action', 1, 'Mosfilm', 'ru'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 2, 'Action', 2, 'Lionsgate', 'us'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 2, 'Action', 3, 'StudioCanal', 'fr'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 2, 'Action', 4, 'Warner Bros', 'us'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 3, 'Comedy', 2, 'Lionsgate', 'us'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 3, 'Comedy', 4, 'Warner Bros', 'us'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 2, 'Action', 1, 'Mosfilm', 'ru'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 2, 'Action', 2, 'Lionsgate', 'us'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 2, 'Action', 3, 'StudioCanal', 'fr'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 2, 'Action', 4, 'Warner Bros', 'us'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 3, 'Comedy', 2, 'Lionsgate', 'us'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 3, 'Comedy', 4, 'Warner Bros', 'us'\n\n# Explicit cross join with other order.\n[plan,header]> SELECT * FROM studios CROSS JOIN movies CROSS JOIN genres\n---\nNestedLoopJoin: inner\n├─ NestedLoopJoin: inner\n│  ├─ Scan: studios\n│  └─ Scan: movies\n└─ Scan: genres\nstudios.id, studios.name, studios.country_id, movies.id, movies.title, movies.studio_id, movies.genre_id, movies.released, movies.rating, movies.ultrahd, genres.id, genres.name\n1, 'Mosfilm', 'ru', 1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction'\n1, 'Mosfilm', 'ru', 1, 'Stalker', 1, 1, 1979, 8.2, NULL, 2, 'Action'\n1, 'Mosfilm', 'ru', 1, 'Stalker', 1, 1, 1979, 8.2, NULL, 3, 'Comedy'\n1, 'Mosfilm', 'ru', 2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 1, 'Science Fiction'\n1, 'Mosfilm', 'ru', 2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action'\n1, 'Mosfilm', 'ru', 2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 3, 'Comedy'\n1, 'Mosfilm', 'ru', 3, 'Primer', 3, 1, 2004, 6.9, NULL, 1, 'Science Fiction'\n1, 'Mosfilm', 'ru', 3, 'Primer', 3, 1, 2004, 6.9, NULL, 2, 'Action'\n1, 'Mosfilm', 'ru', 3, 'Primer', 3, 1, 2004, 6.9, NULL, 3, 'Comedy'\n1, 'Mosfilm', 'ru', 4, 'Heat', 4, 2, 1995, 8.2, TRUE, 1, 'Science Fiction'\n1, 'Mosfilm', 'ru', 4, 'Heat', 4, 2, 1995, 8.2, TRUE, 2, 'Action'\n1, 'Mosfilm', 'ru', 4, 'Heat', 4, 2, 1995, 8.2, TRUE, 3, 'Comedy'\n1, 'Mosfilm', 'ru', 5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 1, 'Science Fiction'\n1, 'Mosfilm', 'ru', 5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 2, 'Action'\n1, 'Mosfilm', 'ru', 5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 3, 'Comedy'\n1, 'Mosfilm', 'ru', 6, 'Solaris', 1, 1, 1972, 8.1, NULL, 1, 'Science Fiction'\n1, 'Mosfilm', 'ru', 6, 'Solaris', 1, 1, 1972, 8.1, NULL, 2, 'Action'\n1, 'Mosfilm', 'ru', 6, 'Solaris', 1, 1, 1972, 8.1, NULL, 3, 'Comedy'\n1, 'Mosfilm', 'ru', 7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 1, 'Science Fiction'\n1, 'Mosfilm', 'ru', 7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 2, 'Action'\n1, 'Mosfilm', 'ru', 7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 3, 'Comedy'\n1, 'Mosfilm', 'ru', 8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 1, 'Science Fiction'\n1, 'Mosfilm', 'ru', 8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 2, 'Action'\n1, 'Mosfilm', 'ru', 8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 3, 'Comedy'\n1, 'Mosfilm', 'ru', 9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 1, 'Science Fiction'\n1, 'Mosfilm', 'ru', 9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 2, 'Action'\n1, 'Mosfilm', 'ru', 9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 3, 'Comedy'\n1, 'Mosfilm', 'ru', 10, 'Inception', 4, 1, 2010, 8.8, TRUE, 1, 'Science Fiction'\n1, 'Mosfilm', 'ru', 10, 'Inception', 4, 1, 2010, 8.8, TRUE, 2, 'Action'\n1, 'Mosfilm', 'ru', 10, 'Inception', 4, 1, 2010, 8.8, TRUE, 3, 'Comedy'\n2, 'Lionsgate', 'us', 1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction'\n2, 'Lionsgate', 'us', 1, 'Stalker', 1, 1, 1979, 8.2, NULL, 2, 'Action'\n2, 'Lionsgate', 'us', 1, 'Stalker', 1, 1, 1979, 8.2, NULL, 3, 'Comedy'\n2, 'Lionsgate', 'us', 2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 1, 'Science Fiction'\n2, 'Lionsgate', 'us', 2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action'\n2, 'Lionsgate', 'us', 2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 3, 'Comedy'\n2, 'Lionsgate', 'us', 3, 'Primer', 3, 1, 2004, 6.9, NULL, 1, 'Science Fiction'\n2, 'Lionsgate', 'us', 3, 'Primer', 3, 1, 2004, 6.9, NULL, 2, 'Action'\n2, 'Lionsgate', 'us', 3, 'Primer', 3, 1, 2004, 6.9, NULL, 3, 'Comedy'\n2, 'Lionsgate', 'us', 4, 'Heat', 4, 2, 1995, 8.2, TRUE, 1, 'Science Fiction'\n2, 'Lionsgate', 'us', 4, 'Heat', 4, 2, 1995, 8.2, TRUE, 2, 'Action'\n2, 'Lionsgate', 'us', 4, 'Heat', 4, 2, 1995, 8.2, TRUE, 3, 'Comedy'\n2, 'Lionsgate', 'us', 5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 1, 'Science Fiction'\n2, 'Lionsgate', 'us', 5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 2, 'Action'\n2, 'Lionsgate', 'us', 5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 3, 'Comedy'\n2, 'Lionsgate', 'us', 6, 'Solaris', 1, 1, 1972, 8.1, NULL, 1, 'Science Fiction'\n2, 'Lionsgate', 'us', 6, 'Solaris', 1, 1, 1972, 8.1, NULL, 2, 'Action'\n2, 'Lionsgate', 'us', 6, 'Solaris', 1, 1, 1972, 8.1, NULL, 3, 'Comedy'\n2, 'Lionsgate', 'us', 7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 1, 'Science Fiction'\n2, 'Lionsgate', 'us', 7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 2, 'Action'\n2, 'Lionsgate', 'us', 7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 3, 'Comedy'\n2, 'Lionsgate', 'us', 8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 1, 'Science Fiction'\n2, 'Lionsgate', 'us', 8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 2, 'Action'\n2, 'Lionsgate', 'us', 8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 3, 'Comedy'\n2, 'Lionsgate', 'us', 9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 1, 'Science Fiction'\n2, 'Lionsgate', 'us', 9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 2, 'Action'\n2, 'Lionsgate', 'us', 9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 3, 'Comedy'\n2, 'Lionsgate', 'us', 10, 'Inception', 4, 1, 2010, 8.8, TRUE, 1, 'Science Fiction'\n2, 'Lionsgate', 'us', 10, 'Inception', 4, 1, 2010, 8.8, TRUE, 2, 'Action'\n2, 'Lionsgate', 'us', 10, 'Inception', 4, 1, 2010, 8.8, TRUE, 3, 'Comedy'\n3, 'StudioCanal', 'fr', 1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction'\n3, 'StudioCanal', 'fr', 1, 'Stalker', 1, 1, 1979, 8.2, NULL, 2, 'Action'\n3, 'StudioCanal', 'fr', 1, 'Stalker', 1, 1, 1979, 8.2, NULL, 3, 'Comedy'\n3, 'StudioCanal', 'fr', 2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 1, 'Science Fiction'\n3, 'StudioCanal', 'fr', 2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action'\n3, 'StudioCanal', 'fr', 2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 3, 'Comedy'\n3, 'StudioCanal', 'fr', 3, 'Primer', 3, 1, 2004, 6.9, NULL, 1, 'Science Fiction'\n3, 'StudioCanal', 'fr', 3, 'Primer', 3, 1, 2004, 6.9, NULL, 2, 'Action'\n3, 'StudioCanal', 'fr', 3, 'Primer', 3, 1, 2004, 6.9, NULL, 3, 'Comedy'\n3, 'StudioCanal', 'fr', 4, 'Heat', 4, 2, 1995, 8.2, TRUE, 1, 'Science Fiction'\n3, 'StudioCanal', 'fr', 4, 'Heat', 4, 2, 1995, 8.2, TRUE, 2, 'Action'\n3, 'StudioCanal', 'fr', 4, 'Heat', 4, 2, 1995, 8.2, TRUE, 3, 'Comedy'\n3, 'StudioCanal', 'fr', 5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 1, 'Science Fiction'\n3, 'StudioCanal', 'fr', 5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 2, 'Action'\n3, 'StudioCanal', 'fr', 5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 3, 'Comedy'\n3, 'StudioCanal', 'fr', 6, 'Solaris', 1, 1, 1972, 8.1, NULL, 1, 'Science Fiction'\n3, 'StudioCanal', 'fr', 6, 'Solaris', 1, 1, 1972, 8.1, NULL, 2, 'Action'\n3, 'StudioCanal', 'fr', 6, 'Solaris', 1, 1, 1972, 8.1, NULL, 3, 'Comedy'\n3, 'StudioCanal', 'fr', 7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 1, 'Science Fiction'\n3, 'StudioCanal', 'fr', 7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 2, 'Action'\n3, 'StudioCanal', 'fr', 7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 3, 'Comedy'\n3, 'StudioCanal', 'fr', 8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 1, 'Science Fiction'\n3, 'StudioCanal', 'fr', 8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 2, 'Action'\n3, 'StudioCanal', 'fr', 8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 3, 'Comedy'\n3, 'StudioCanal', 'fr', 9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 1, 'Science Fiction'\n3, 'StudioCanal', 'fr', 9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 2, 'Action'\n3, 'StudioCanal', 'fr', 9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 3, 'Comedy'\n3, 'StudioCanal', 'fr', 10, 'Inception', 4, 1, 2010, 8.8, TRUE, 1, 'Science Fiction'\n3, 'StudioCanal', 'fr', 10, 'Inception', 4, 1, 2010, 8.8, TRUE, 2, 'Action'\n3, 'StudioCanal', 'fr', 10, 'Inception', 4, 1, 2010, 8.8, TRUE, 3, 'Comedy'\n4, 'Warner Bros', 'us', 1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction'\n4, 'Warner Bros', 'us', 1, 'Stalker', 1, 1, 1979, 8.2, NULL, 2, 'Action'\n4, 'Warner Bros', 'us', 1, 'Stalker', 1, 1, 1979, 8.2, NULL, 3, 'Comedy'\n4, 'Warner Bros', 'us', 2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 1, 'Science Fiction'\n4, 'Warner Bros', 'us', 2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action'\n4, 'Warner Bros', 'us', 2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 3, 'Comedy'\n4, 'Warner Bros', 'us', 3, 'Primer', 3, 1, 2004, 6.9, NULL, 1, 'Science Fiction'\n4, 'Warner Bros', 'us', 3, 'Primer', 3, 1, 2004, 6.9, NULL, 2, 'Action'\n4, 'Warner Bros', 'us', 3, 'Primer', 3, 1, 2004, 6.9, NULL, 3, 'Comedy'\n4, 'Warner Bros', 'us', 4, 'Heat', 4, 2, 1995, 8.2, TRUE, 1, 'Science Fiction'\n4, 'Warner Bros', 'us', 4, 'Heat', 4, 2, 1995, 8.2, TRUE, 2, 'Action'\n4, 'Warner Bros', 'us', 4, 'Heat', 4, 2, 1995, 8.2, TRUE, 3, 'Comedy'\n4, 'Warner Bros', 'us', 5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 1, 'Science Fiction'\n4, 'Warner Bros', 'us', 5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 2, 'Action'\n4, 'Warner Bros', 'us', 5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 3, 'Comedy'\n4, 'Warner Bros', 'us', 6, 'Solaris', 1, 1, 1972, 8.1, NULL, 1, 'Science Fiction'\n4, 'Warner Bros', 'us', 6, 'Solaris', 1, 1, 1972, 8.1, NULL, 2, 'Action'\n4, 'Warner Bros', 'us', 6, 'Solaris', 1, 1, 1972, 8.1, NULL, 3, 'Comedy'\n4, 'Warner Bros', 'us', 7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 1, 'Science Fiction'\n4, 'Warner Bros', 'us', 7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 2, 'Action'\n4, 'Warner Bros', 'us', 7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 3, 'Comedy'\n4, 'Warner Bros', 'us', 8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 1, 'Science Fiction'\n4, 'Warner Bros', 'us', 8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 2, 'Action'\n4, 'Warner Bros', 'us', 8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 3, 'Comedy'\n4, 'Warner Bros', 'us', 9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 1, 'Science Fiction'\n4, 'Warner Bros', 'us', 9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 2, 'Action'\n4, 'Warner Bros', 'us', 9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 3, 'Comedy'\n4, 'Warner Bros', 'us', 10, 'Inception', 4, 1, 2010, 8.8, TRUE, 1, 'Science Fiction'\n4, 'Warner Bros', 'us', 10, 'Inception', 4, 1, 2010, 8.8, TRUE, 2, 'Action'\n4, 'Warner Bros', 'us', 10, 'Inception', 4, 1, 2010, 8.8, TRUE, 3, 'Comedy'\n\n# Implicit cross join.\n[plan,header]> SELECT * FROM movies, genres, studios\n---\nNestedLoopJoin: inner\n├─ NestedLoopJoin: inner\n│  ├─ Scan: movies\n│  └─ Scan: genres\n└─ Scan: studios\nmovies.id, movies.title, movies.studio_id, movies.genre_id, movies.released, movies.rating, movies.ultrahd, genres.id, genres.name, studios.id, studios.name, studios.country_id\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 2, 'Action', 1, 'Mosfilm', 'ru'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 2, 'Action', 2, 'Lionsgate', 'us'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 2, 'Action', 3, 'StudioCanal', 'fr'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 2, 'Action', 4, 'Warner Bros', 'us'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 3, 'Comedy', 2, 'Lionsgate', 'us'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 3, 'Comedy', 4, 'Warner Bros', 'us'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action', 1, 'Mosfilm', 'ru'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action', 2, 'Lionsgate', 'us'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action', 3, 'StudioCanal', 'fr'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action', 4, 'Warner Bros', 'us'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 3, 'Comedy', 2, 'Lionsgate', 'us'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 3, 'Comedy', 4, 'Warner Bros', 'us'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 2, 'Action', 1, 'Mosfilm', 'ru'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 2, 'Action', 2, 'Lionsgate', 'us'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 2, 'Action', 3, 'StudioCanal', 'fr'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 2, 'Action', 4, 'Warner Bros', 'us'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 3, 'Comedy', 2, 'Lionsgate', 'us'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 3, 'Comedy', 4, 'Warner Bros', 'us'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 2, 'Action', 1, 'Mosfilm', 'ru'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 2, 'Action', 2, 'Lionsgate', 'us'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 2, 'Action', 3, 'StudioCanal', 'fr'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 2, 'Action', 4, 'Warner Bros', 'us'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 3, 'Comedy', 2, 'Lionsgate', 'us'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 3, 'Comedy', 4, 'Warner Bros', 'us'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 2, 'Action', 1, 'Mosfilm', 'ru'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 2, 'Action', 2, 'Lionsgate', 'us'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 2, 'Action', 3, 'StudioCanal', 'fr'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 2, 'Action', 4, 'Warner Bros', 'us'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 3, 'Comedy', 2, 'Lionsgate', 'us'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 3, 'Comedy', 4, 'Warner Bros', 'us'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 2, 'Action', 1, 'Mosfilm', 'ru'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 2, 'Action', 2, 'Lionsgate', 'us'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 2, 'Action', 3, 'StudioCanal', 'fr'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 2, 'Action', 4, 'Warner Bros', 'us'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 3, 'Comedy', 2, 'Lionsgate', 'us'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 3, 'Comedy', 4, 'Warner Bros', 'us'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 2, 'Action', 1, 'Mosfilm', 'ru'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 2, 'Action', 2, 'Lionsgate', 'us'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 2, 'Action', 3, 'StudioCanal', 'fr'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 2, 'Action', 4, 'Warner Bros', 'us'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 3, 'Comedy', 2, 'Lionsgate', 'us'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 3, 'Comedy', 4, 'Warner Bros', 'us'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 2, 'Action', 1, 'Mosfilm', 'ru'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 2, 'Action', 2, 'Lionsgate', 'us'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 2, 'Action', 3, 'StudioCanal', 'fr'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 2, 'Action', 4, 'Warner Bros', 'us'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 3, 'Comedy', 2, 'Lionsgate', 'us'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 3, 'Comedy', 4, 'Warner Bros', 'us'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 2, 'Action', 1, 'Mosfilm', 'ru'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 2, 'Action', 2, 'Lionsgate', 'us'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 2, 'Action', 3, 'StudioCanal', 'fr'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 2, 'Action', 4, 'Warner Bros', 'us'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 3, 'Comedy', 2, 'Lionsgate', 'us'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 3, 'Comedy', 4, 'Warner Bros', 'us'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 2, 'Action', 1, 'Mosfilm', 'ru'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 2, 'Action', 2, 'Lionsgate', 'us'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 2, 'Action', 3, 'StudioCanal', 'fr'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 2, 'Action', 4, 'Warner Bros', 'us'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 3, 'Comedy', 2, 'Lionsgate', 'us'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 3, 'Comedy', 4, 'Warner Bros', 'us'\n\n# Aliased cross join.\n[plan,header]> SELECT * FROM movies m, genres AS g, studios s\n---\nNestedLoopJoin: inner\n├─ NestedLoopJoin: inner\n│  ├─ Scan: movies as m\n│  └─ Scan: genres as g\n└─ Scan: studios as s\nm.id, m.title, m.studio_id, m.genre_id, m.released, m.rating, m.ultrahd, g.id, g.name, s.id, s.name, s.country_id\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 2, 'Action', 1, 'Mosfilm', 'ru'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 2, 'Action', 2, 'Lionsgate', 'us'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 2, 'Action', 3, 'StudioCanal', 'fr'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 2, 'Action', 4, 'Warner Bros', 'us'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 3, 'Comedy', 2, 'Lionsgate', 'us'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 3, 'Comedy', 4, 'Warner Bros', 'us'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action', 1, 'Mosfilm', 'ru'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action', 2, 'Lionsgate', 'us'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action', 3, 'StudioCanal', 'fr'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action', 4, 'Warner Bros', 'us'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 3, 'Comedy', 2, 'Lionsgate', 'us'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 3, 'Comedy', 4, 'Warner Bros', 'us'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 2, 'Action', 1, 'Mosfilm', 'ru'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 2, 'Action', 2, 'Lionsgate', 'us'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 2, 'Action', 3, 'StudioCanal', 'fr'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 2, 'Action', 4, 'Warner Bros', 'us'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 3, 'Comedy', 2, 'Lionsgate', 'us'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 3, 'Comedy', 4, 'Warner Bros', 'us'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 2, 'Action', 1, 'Mosfilm', 'ru'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 2, 'Action', 2, 'Lionsgate', 'us'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 2, 'Action', 3, 'StudioCanal', 'fr'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 2, 'Action', 4, 'Warner Bros', 'us'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 3, 'Comedy', 2, 'Lionsgate', 'us'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 3, 'Comedy', 4, 'Warner Bros', 'us'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 2, 'Action', 1, 'Mosfilm', 'ru'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 2, 'Action', 2, 'Lionsgate', 'us'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 2, 'Action', 3, 'StudioCanal', 'fr'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 2, 'Action', 4, 'Warner Bros', 'us'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 3, 'Comedy', 2, 'Lionsgate', 'us'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 3, 'Comedy', 4, 'Warner Bros', 'us'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 2, 'Action', 1, 'Mosfilm', 'ru'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 2, 'Action', 2, 'Lionsgate', 'us'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 2, 'Action', 3, 'StudioCanal', 'fr'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 2, 'Action', 4, 'Warner Bros', 'us'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 3, 'Comedy', 2, 'Lionsgate', 'us'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 3, 'Comedy', 4, 'Warner Bros', 'us'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 2, 'Action', 1, 'Mosfilm', 'ru'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 2, 'Action', 2, 'Lionsgate', 'us'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 2, 'Action', 3, 'StudioCanal', 'fr'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 2, 'Action', 4, 'Warner Bros', 'us'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 3, 'Comedy', 2, 'Lionsgate', 'us'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 3, 'Comedy', 4, 'Warner Bros', 'us'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 2, 'Action', 1, 'Mosfilm', 'ru'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 2, 'Action', 2, 'Lionsgate', 'us'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 2, 'Action', 3, 'StudioCanal', 'fr'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 2, 'Action', 4, 'Warner Bros', 'us'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 3, 'Comedy', 2, 'Lionsgate', 'us'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 3, 'Comedy', 4, 'Warner Bros', 'us'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 2, 'Action', 1, 'Mosfilm', 'ru'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 2, 'Action', 2, 'Lionsgate', 'us'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 2, 'Action', 3, 'StudioCanal', 'fr'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 2, 'Action', 4, 'Warner Bros', 'us'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 3, 'Comedy', 2, 'Lionsgate', 'us'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 3, 'Comedy', 4, 'Warner Bros', 'us'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 1, 'Science Fiction', 2, 'Lionsgate', 'us'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 2, 'Action', 1, 'Mosfilm', 'ru'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 2, 'Action', 2, 'Lionsgate', 'us'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 2, 'Action', 3, 'StudioCanal', 'fr'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 2, 'Action', 4, 'Warner Bros', 'us'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 3, 'Comedy', 1, 'Mosfilm', 'ru'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 3, 'Comedy', 2, 'Lionsgate', 'us'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 3, 'Comedy', 3, 'StudioCanal', 'fr'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 3, 'Comedy', 4, 'Warner Bros', 'us'\n\n# Cross join with self errors.\n!> SELECT * FROM genres, genres\n---\nError: invalid input: duplicate table name genres\n\n# But it works when aliased.\n[plan,header]> SELECT * FROM genres AS a, genres AS b, genres AS c\n---\nNestedLoopJoin: inner\n├─ NestedLoopJoin: inner\n│  ├─ Scan: genres as a\n│  └─ Scan: genres as b\n└─ Scan: genres as c\na.id, a.name, b.id, b.name, c.id, c.name\n1, 'Science Fiction', 1, 'Science Fiction', 1, 'Science Fiction'\n1, 'Science Fiction', 1, 'Science Fiction', 2, 'Action'\n1, 'Science Fiction', 1, 'Science Fiction', 3, 'Comedy'\n1, 'Science Fiction', 2, 'Action', 1, 'Science Fiction'\n1, 'Science Fiction', 2, 'Action', 2, 'Action'\n1, 'Science Fiction', 2, 'Action', 3, 'Comedy'\n1, 'Science Fiction', 3, 'Comedy', 1, 'Science Fiction'\n1, 'Science Fiction', 3, 'Comedy', 2, 'Action'\n1, 'Science Fiction', 3, 'Comedy', 3, 'Comedy'\n2, 'Action', 1, 'Science Fiction', 1, 'Science Fiction'\n2, 'Action', 1, 'Science Fiction', 2, 'Action'\n2, 'Action', 1, 'Science Fiction', 3, 'Comedy'\n2, 'Action', 2, 'Action', 1, 'Science Fiction'\n2, 'Action', 2, 'Action', 2, 'Action'\n2, 'Action', 2, 'Action', 3, 'Comedy'\n2, 'Action', 3, 'Comedy', 1, 'Science Fiction'\n2, 'Action', 3, 'Comedy', 2, 'Action'\n2, 'Action', 3, 'Comedy', 3, 'Comedy'\n3, 'Comedy', 1, 'Science Fiction', 1, 'Science Fiction'\n3, 'Comedy', 1, 'Science Fiction', 2, 'Action'\n3, 'Comedy', 1, 'Science Fiction', 3, 'Comedy'\n3, 'Comedy', 2, 'Action', 1, 'Science Fiction'\n3, 'Comedy', 2, 'Action', 2, 'Action'\n3, 'Comedy', 2, 'Action', 3, 'Comedy'\n3, 'Comedy', 3, 'Comedy', 1, 'Science Fiction'\n3, 'Comedy', 3, 'Comedy', 2, 'Action'\n3, 'Comedy', 3, 'Comedy', 3, 'Comedy'\n\n# Duplicate aliases error.\n!> SELECT * FROM movies a, genres a\n!> SELECT * FROM movies a CROSS JOIN genres a\n---\nError: invalid input: duplicate table name a\nError: invalid input: duplicate table name a\n\n# An explicit CROSS JOIN with an ON predicate should error. It's not a cross join.\n!> SELECT * FROM movies CROSS JOIN genres ON movies.genre_id = genres.id\n---\nError: invalid input: unexpected token ON\n"
  },
  {
    "path": "src/sql/testscripts/queries/join_inner",
    "content": "# Tests inner joins.\n\n# Set up a movies dataset.\n> CREATE TABLE countries ( \\\n    id STRING PRIMARY KEY, \\\n    name STRING NOT NULL \\\n)\n> INSERT INTO countries VALUES \\\n    ('fr', 'France'), \\\n    ('ru', 'Russia'), \\\n    ('us', 'United States of America')\n>CREATE TABLE genres ( \\\n    id INTEGER PRIMARY KEY, \\\n    name STRING NOT NULL \\\n)\n> INSERT INTO genres VALUES \\\n    (1, 'Science Fiction'), \\\n    (2, 'Action'), \\\n    (3, 'Comedy')\n> CREATE TABLE studios ( \\\n    id INTEGER PRIMARY KEY, \\\n    name STRING NOT NULL, \\\n    country_id STRING INDEX REFERENCES countries \\\n)\n> INSERT INTO studios VALUES \\\n    (1, 'Mosfilm', 'ru'), \\\n    (2, 'Lionsgate', 'us'), \\\n    (3, 'StudioCanal', 'fr'), \\\n    (4, 'Warner Bros', 'us')\n> CREATE TABLE movies ( \\\n    id INTEGER PRIMARY KEY, \\\n    title STRING NOT NULL, \\\n    studio_id INTEGER NOT NULL INDEX REFERENCES studios, \\\n    genre_id INTEGER NOT NULL INDEX REFERENCES genres, \\\n    released INTEGER NOT NULL, \\\n    rating FLOAT, \\\n    ultrahd BOOLEAN \\\n)\n> INSERT INTO movies VALUES \\\n    (1, 'Stalker', 1, 1, 1979, 8.2, NULL), \\\n    (2, 'Sicario', 2, 2, 2015, 7.6, TRUE), \\\n    (3, 'Primer', 3, 1, 2004, 6.9, NULL), \\\n    (4, 'Heat', 4, 2, 1995, 8.2, TRUE), \\\n    (5, 'The Fountain', 4, 1, 2006, 7.2, FALSE), \\\n    (6, 'Solaris', 1, 1, 1972, 8.1, NULL), \\\n    (7, 'Gravity', 4, 1, 2013, 7.7, TRUE), \\\n    (8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE), \\\n    (9, 'Birdman', 4, 3, 2014, 7.7, TRUE), \\\n    (10, 'Inception', 4, 1, 2010, 8.8, TRUE)\n---\nok\n\n# Inner join on foreign key.\n[plan,header]> SELECT * FROM movies INNER JOIN genres ON movies.genre_id = genres.id\n---\nHashJoin: inner on movies.genre_id = genres.id\n├─ Scan: movies\n└─ Scan: genres\nmovies.id, movies.title, movies.studio_id, movies.genre_id, movies.released, movies.rating, movies.ultrahd, genres.id, genres.name\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 1, 'Science Fiction'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 2, 'Action'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 1, 'Science Fiction'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 1, 'Science Fiction'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 1, 'Science Fiction'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 3, 'Comedy'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 3, 'Comedy'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 1, 'Science Fiction'\n\n# Implicit inner join.\n[plan,header]> SELECT * FROM movies JOIN genres ON movies.genre_id = genres.id\n---\nHashJoin: inner on movies.genre_id = genres.id\n├─ Scan: movies\n└─ Scan: genres\nmovies.id, movies.title, movies.studio_id, movies.genre_id, movies.released, movies.rating, movies.ultrahd, genres.id, genres.name\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 1, 'Science Fiction'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 2, 'Action'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 1, 'Science Fiction'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 1, 'Science Fiction'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 1, 'Science Fiction'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 3, 'Comedy'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 3, 'Comedy'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 1, 'Science Fiction'\n\n# Implicit inner join with cross join and WHERE.\n[plan,header]> SELECT * FROM movies, genres WHERE movies.genre_id = genres.id\n---\nHashJoin: inner on movies.genre_id = genres.id\n├─ Scan: movies\n└─ Scan: genres\nmovies.id, movies.title, movies.studio_id, movies.genre_id, movies.released, movies.rating, movies.ultrahd, genres.id, genres.name\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 1, 'Science Fiction'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 2, 'Action'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 1, 'Science Fiction'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 1, 'Science Fiction'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 1, 'Science Fiction'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 3, 'Comedy'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 3, 'Comedy'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 1, 'Science Fiction'\n\n# Three-way inner join.\n[plan,header]> SELECT * FROM movies \\\n    INNER JOIN genres ON movies.genre_id = genres.id \\\n    INNER JOIN studios ON movies.studio_id = studios.id\n---\nHashJoin: inner on movies.studio_id = studios.id\n├─ HashJoin: inner on movies.genre_id = genres.id\n│  ├─ Scan: movies\n│  └─ Scan: genres\n└─ Scan: studios\nmovies.id, movies.title, movies.studio_id, movies.genre_id, movies.released, movies.rating, movies.ultrahd, genres.id, genres.name, studios.id, studios.name, studios.country_id\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action', 2, 'Lionsgate', 'us'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 2, 'Action', 4, 'Warner Bros', 'us'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 3, 'Comedy', 2, 'Lionsgate', 'us'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 3, 'Comedy', 4, 'Warner Bros', 'us'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n\n# Inner join on TRUE and FALSE.\n[plan]> SELECT * FROM movies INNER JOIN genres ON TRUE\n---\nNestedLoopJoin: inner\n├─ Scan: movies\n└─ Scan: genres\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 2, 'Action'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 3, 'Comedy'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 1, 'Science Fiction'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 3, 'Comedy'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 1, 'Science Fiction'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 2, 'Action'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 3, 'Comedy'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 1, 'Science Fiction'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 2, 'Action'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 3, 'Comedy'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 1, 'Science Fiction'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 2, 'Action'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 3, 'Comedy'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 1, 'Science Fiction'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 2, 'Action'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 3, 'Comedy'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 1, 'Science Fiction'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 2, 'Action'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 3, 'Comedy'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 1, 'Science Fiction'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 2, 'Action'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 3, 'Comedy'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 1, 'Science Fiction'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 2, 'Action'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 3, 'Comedy'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 1, 'Science Fiction'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 2, 'Action'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 3, 'Comedy'\n\n[plan]> SELECT * FROM movies INNER JOIN genres ON FALSE\n---\nNothing\n\n# Inner join on multiple predicates.\n[plan]> SELECT * FROM movies INNER JOIN genres ON movies.genre_id = genres.id AND movies.id = genres.id\n---\nNestedLoopJoin: inner on movies.genre_id = genres.id AND movies.id = genres.id\n├─ Scan: movies\n└─ Scan: genres\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action'\n\n# Aliased inner join.\n[plan]> SELECT * FROM movies m INNER JOIN genres g ON m.genre_id = g.id INNER JOIN studios AS s ON m.studio_id = s.id\n---\nHashJoin: inner on m.studio_id = s.id\n├─ HashJoin: inner on m.genre_id = g.id\n│  ├─ Scan: movies as m\n│  └─ Scan: genres as g\n└─ Scan: studios as s\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action', 2, 'Lionsgate', 'us'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 1, 'Science Fiction', 3, 'StudioCanal', 'fr'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, 2, 'Action', 4, 'Warner Bros', 'us'\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, 1, 'Science Fiction', 1, 'Mosfilm', 'ru'\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, 3, 'Comedy', 2, 'Lionsgate', 'us'\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, 3, 'Comedy', 4, 'Warner Bros', 'us'\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, 1, 'Science Fiction', 4, 'Warner Bros', 'us'\n\n# Inner join with missing ON errors.\n!> SELECT * FROM movies INNER JOIN genres\n---\nError: invalid input: unexpected end of input\n\n# Inner join on WHERE errors.\n!> SELECT * FROM movies INNER JOIN genres WHERE movies.genre_id = genres.id\n---\nError: invalid input: expected token ON, found WHERE\n\n# Errors on missing table or column.\n!> SELECT * FROM movies INNER JOIN unknown on movies.id = unknown.id\n!> SELECT * FROM movies INNER JOIN genres on movies.unknown_id = genres.id\n---\nError: invalid input: table unknown does not exist\nError: invalid input: unknown column movies.unknown_id\n\n# Hash joins with multiple matches work, on either side of the join.\n[plan]> SELECT movies.title, genres.name FROM movies JOIN genres ON movies.genre_id = genres.id\n---\nProjection: movies.title, genres.name\n└─ HashJoin: inner on movies.genre_id = genres.id\n   ├─ Scan: movies\n   └─ Scan: genres\n'Stalker', 'Science Fiction'\n'Sicario', 'Action'\n'Primer', 'Science Fiction'\n'Heat', 'Action'\n'The Fountain', 'Science Fiction'\n'Solaris', 'Science Fiction'\n'Gravity', 'Science Fiction'\n'Blindspotting', 'Comedy'\n'Birdman', 'Comedy'\n'Inception', 'Science Fiction'\n\n[plan]> SELECT movies.title, genres.name FROM genres JOIN movies ON genres.id = movies.genre_id\n---\nProjection: movies.title, genres.name\n└─ HashJoin: inner on genres.id = movies.genre_id\n   ├─ Scan: genres\n   └─ Scan: movies\n'Stalker', 'Science Fiction'\n'Primer', 'Science Fiction'\n'The Fountain', 'Science Fiction'\n'Solaris', 'Science Fiction'\n'Gravity', 'Science Fiction'\n'Inception', 'Science Fiction'\n'Sicario', 'Action'\n'Heat', 'Action'\n'Blindspotting', 'Comedy'\n'Birdman', 'Comedy'\n\n# Also try multi-match self hash joins joins on ultrahd, where both sides have\n# multiple matches. Note that NULL matches are ignored.\n[plan]> SELECT a.title, b.title FROM movies a JOIN movies b ON a.ultrahd = b.ultrahd\n---\nProjection: a.title, b.title\n└─ HashJoin: inner on a.ultrahd = b.ultrahd\n   ├─ Scan: movies as a\n   └─ Scan: movies as b\n'Sicario', 'Sicario'\n'Sicario', 'Heat'\n'Sicario', 'Gravity'\n'Sicario', 'Blindspotting'\n'Sicario', 'Birdman'\n'Sicario', 'Inception'\n'Heat', 'Sicario'\n'Heat', 'Heat'\n'Heat', 'Gravity'\n'Heat', 'Blindspotting'\n'Heat', 'Birdman'\n'Heat', 'Inception'\n'The Fountain', 'The Fountain'\n'Gravity', 'Sicario'\n'Gravity', 'Heat'\n'Gravity', 'Gravity'\n'Gravity', 'Blindspotting'\n'Gravity', 'Birdman'\n'Gravity', 'Inception'\n'Blindspotting', 'Sicario'\n'Blindspotting', 'Heat'\n'Blindspotting', 'Gravity'\n'Blindspotting', 'Blindspotting'\n'Blindspotting', 'Birdman'\n'Blindspotting', 'Inception'\n'Birdman', 'Sicario'\n'Birdman', 'Heat'\n'Birdman', 'Gravity'\n'Birdman', 'Blindspotting'\n'Birdman', 'Birdman'\n'Birdman', 'Inception'\n'Inception', 'Sicario'\n'Inception', 'Heat'\n'Inception', 'Gravity'\n'Inception', 'Blindspotting'\n'Inception', 'Birdman'\n'Inception', 'Inception'\n\n# Try a complex multi-way join with multiple joins of the same table. Uses GROUP\n# BY to discard duplicates from the cross join. The query finds all movies\n# belonging to a studio that's released at least one movies rated 8 or higher.\n[plan]> SELECT m.id, m.title, g.name AS genre, s.name AS studio, m.rating \\\n  FROM movies m JOIN genres g ON m.genre_id = g.id, \\\n    studios s JOIN movies good ON good.studio_id = s.id AND good.rating >= 8 \\\n  WHERE m.studio_id = s.id \\\n  GROUP BY m.id, m.title, g.name, s.name, m.rating, m.released \\\n  ORDER BY m.rating DESC, m.released ASC, m.id ASC\n---\nRemap: m.id, m.title, genre, studio, m.rating (dropped: m.released)\n└─ Order: m.rating desc, m.released asc, m.id asc\n   └─ Projection: m.id, m.title, g.name as genre, s.name as studio, m.rating, m.released\n      └─ Aggregate: m.id, m.title, g.name, s.name, m.rating, m.released\n         └─ HashJoin: inner on m.studio_id = s.id\n            ├─ HashJoin: inner on m.genre_id = g.id\n            │  ├─ Scan: movies as m\n            │  └─ Scan: genres as g\n            └─ HashJoin: inner on s.id = good.studio_id\n               ├─ Scan: studios as s\n               └─ Scan: movies as good (good.rating > 8 OR good.rating = 8)\n10, 'Inception', 'Science Fiction', 'Warner Bros', 8.8\n1, 'Stalker', 'Science Fiction', 'Mosfilm', 8.2\n4, 'Heat', 'Action', 'Warner Bros', 8.2\n6, 'Solaris', 'Science Fiction', 'Mosfilm', 8.1\n7, 'Gravity', 'Science Fiction', 'Warner Bros', 7.7\n9, 'Birdman', 'Comedy', 'Warner Bros', 7.7\n5, 'The Fountain', 'Science Fiction', 'Warner Bros', 7.2\n"
  },
  {
    "path": "src/sql/testscripts/queries/join_outer",
    "content": "# Tests left/right outer joins.\n\n# Set up a movies dataset.\n> CREATE TABLE countries ( \\\n    id STRING PRIMARY KEY, \\\n    name STRING NOT NULL \\\n)\n> INSERT INTO countries VALUES \\\n    ('fr', 'France'), \\\n    ('ru', 'Russia'), \\\n    ('us', 'United States of America')\n>CREATE TABLE genres ( \\\n    id INTEGER PRIMARY KEY, \\\n    name STRING NOT NULL \\\n)\n> INSERT INTO genres VALUES \\\n    (1, 'Science Fiction'), \\\n    (2, 'Action'), \\\n    (3, 'Comedy')\n> CREATE TABLE studios ( \\\n    id INTEGER PRIMARY KEY, \\\n    name STRING NOT NULL, \\\n    country_id STRING INDEX REFERENCES countries \\\n)\n> INSERT INTO studios VALUES \\\n    (1, 'Mosfilm', 'ru'), \\\n    (2, 'Lionsgate', 'us'), \\\n    (3, 'StudioCanal', 'fr'), \\\n    (4, 'Warner Bros', 'us')\n> CREATE TABLE movies ( \\\n    id INTEGER PRIMARY KEY, \\\n    title STRING NOT NULL, \\\n    studio_id INTEGER NOT NULL INDEX REFERENCES studios, \\\n    genre_id INTEGER NOT NULL INDEX REFERENCES genres, \\\n    released INTEGER NOT NULL, \\\n    rating FLOAT, \\\n    ultrahd BOOLEAN \\\n)\n> INSERT INTO movies VALUES \\\n    (1, 'Stalker', 1, 1, 1979, 8.2, NULL), \\\n    (2, 'Sicario', 2, 2, 2015, 7.6, TRUE), \\\n    (3, 'Primer', 3, 1, 2004, 6.9, NULL), \\\n    (4, 'Heat', 4, 2, 1995, 8.2, TRUE), \\\n    (5, 'The Fountain', 4, 1, 2006, 7.2, FALSE), \\\n    (6, 'Solaris', 1, 1, 1972, 8.1, NULL), \\\n    (7, 'Gravity', 4, 1, 2013, 7.7, TRUE), \\\n    (8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE), \\\n    (9, 'Birdman', 4, 3, 2014, 7.7, TRUE), \\\n    (10, 'Inception', 4, 1, 2010, 8.8, TRUE)\n---\nok\n\n# Left join.\n[plan]> SELECT * FROM movies LEFT JOIN genres ON movies.id = genres.id\n---\nHashJoin: outer on movies.id = genres.id\n├─ Scan: movies\n└─ Scan: genres\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 3, 'Comedy'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, NULL, NULL\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, NULL, NULL\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, NULL, NULL\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, NULL, NULL\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, NULL, NULL\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, NULL, NULL\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, NULL, NULL\n\n# Right join.\n[plan]> SELECT * FROM genres RIGHT JOIN movies ON movies.id = genres.id\n---\nRemap: genres.id, genres.name, movies.id, movies.title, movies.studio_id, movies.genre_id, movies.released, movies.rating, movies.ultrahd\n└─ HashJoin: outer on movies.id = genres.id\n   ├─ Scan: movies\n   └─ Scan: genres\n1, 'Science Fiction', 1, 'Stalker', 1, 1, 1979, 8.2, NULL\n2, 'Action', 2, 'Sicario', 2, 2, 2015, 7.6, TRUE\n3, 'Comedy', 3, 'Primer', 3, 1, 2004, 6.9, NULL\nNULL, NULL, 4, 'Heat', 4, 2, 1995, 8.2, TRUE\nNULL, NULL, 5, 'The Fountain', 4, 1, 2006, 7.2, FALSE\nNULL, NULL, 6, 'Solaris', 1, 1, 1972, 8.1, NULL\nNULL, NULL, 7, 'Gravity', 4, 1, 2013, 7.7, TRUE\nNULL, NULL, 8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE\nNULL, NULL, 9, 'Birdman', 4, 3, 2014, 7.7, TRUE\nNULL, NULL, 10, 'Inception', 4, 1, 2010, 8.8, TRUE\n\n# Optional OUTER keyword.\n[plan]> SELECT * FROM movies LEFT OUTER JOIN genres ON movies.id = genres.id\n---\nHashJoin: outer on movies.id = genres.id\n├─ Scan: movies\n└─ Scan: genres\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 3, 'Comedy'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, NULL, NULL\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, NULL, NULL\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, NULL, NULL\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, NULL, NULL\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, NULL, NULL\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, NULL, NULL\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, NULL, NULL\n\n[plan]> SELECT * FROM genres RIGHT OUTER JOIN movies ON movies.id = genres.id\n---\nRemap: genres.id, genres.name, movies.id, movies.title, movies.studio_id, movies.genre_id, movies.released, movies.rating, movies.ultrahd\n└─ HashJoin: outer on movies.id = genres.id\n   ├─ Scan: movies\n   └─ Scan: genres\n1, 'Science Fiction', 1, 'Stalker', 1, 1, 1979, 8.2, NULL\n2, 'Action', 2, 'Sicario', 2, 2, 2015, 7.6, TRUE\n3, 'Comedy', 3, 'Primer', 3, 1, 2004, 6.9, NULL\nNULL, NULL, 4, 'Heat', 4, 2, 1995, 8.2, TRUE\nNULL, NULL, 5, 'The Fountain', 4, 1, 2006, 7.2, FALSE\nNULL, NULL, 6, 'Solaris', 1, 1, 1972, 8.1, NULL\nNULL, NULL, 7, 'Gravity', 4, 1, 2013, 7.7, TRUE\nNULL, NULL, 8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE\nNULL, NULL, 9, 'Birdman', 4, 3, 2014, 7.7, TRUE\nNULL, NULL, 10, 'Inception', 4, 1, 2010, 8.8, TRUE\n\n# Truncates when the inner side is shorter.\n[plan]> SELECT * FROM genres LEFT JOIN movies ON movies.id = genres.id\n---\nHashJoin: outer on genres.id = movies.id\n├─ Scan: genres\n└─ Scan: movies\n1, 'Science Fiction', 1, 'Stalker', 1, 1, 1979, 8.2, NULL\n2, 'Action', 2, 'Sicario', 2, 2, 2015, 7.6, TRUE\n3, 'Comedy', 3, 'Primer', 3, 1, 2004, 6.9, NULL\n\n[plan]> SELECT * FROM movies RIGHT JOIN genres ON movies.id = genres.id\n---\nRemap: movies.id, movies.title, movies.studio_id, movies.genre_id, movies.released, movies.rating, movies.ultrahd, genres.id, genres.name\n└─ HashJoin: outer on genres.id = movies.id\n   ├─ Scan: genres\n   └─ Scan: movies\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 3, 'Comedy'\n\n# Arbitrary predicate.\n[plan]> SELECT * FROM movies LEFT JOIN genres ON genres.id >= movies.id\n---\nNestedLoopJoin: outer on genres.id > movies.id OR genres.id = movies.id\n├─ Scan: movies\n└─ Scan: genres\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 2, 'Action'\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 3, 'Comedy'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 3, 'Comedy'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 3, 'Comedy'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, NULL, NULL\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, NULL, NULL\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, NULL, NULL\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, NULL, NULL\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, NULL, NULL\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, NULL, NULL\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, NULL, NULL\n\n# Three-way join.\n[plan]> SELECT * FROM studios \\\n    LEFT JOIN genres ON studios.id = genres.id \\\n    RIGHT JOIN movies ON movies.id = studios.id\n---\nRemap: studios.id, studios.name, studios.country_id, genres.id, genres.name, movies.id, movies.title, movies.studio_id, movies.genre_id, movies.released, movies.rating, movies.ultrahd\n└─ HashJoin: outer on movies.id = studios.id\n   ├─ Scan: movies\n   └─ HashJoin: outer on studios.id = genres.id\n      ├─ Scan: studios\n      └─ Scan: genres\n1, 'Mosfilm', 'ru', 1, 'Science Fiction', 1, 'Stalker', 1, 1, 1979, 8.2, NULL\n2, 'Lionsgate', 'us', 2, 'Action', 2, 'Sicario', 2, 2, 2015, 7.6, TRUE\n3, 'StudioCanal', 'fr', 3, 'Comedy', 3, 'Primer', 3, 1, 2004, 6.9, NULL\n4, 'Warner Bros', 'us', NULL, NULL, 4, 'Heat', 4, 2, 1995, 8.2, TRUE\nNULL, NULL, NULL, NULL, NULL, 5, 'The Fountain', 4, 1, 2006, 7.2, FALSE\nNULL, NULL, NULL, NULL, NULL, 6, 'Solaris', 1, 1, 1972, 8.1, NULL\nNULL, NULL, NULL, NULL, NULL, 7, 'Gravity', 4, 1, 2013, 7.7, TRUE\nNULL, NULL, NULL, NULL, NULL, 8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE\nNULL, NULL, NULL, NULL, NULL, 9, 'Birdman', 4, 3, 2014, 7.7, TRUE\nNULL, NULL, NULL, NULL, NULL, 10, 'Inception', 4, 1, 2010, 8.8, TRUE\n\n# Aliased tables.\n[plan]> SELECT * FROM movies m LEFT JOIN genres AS g on m.id = g.id\n---\nHashJoin: outer on m.id = g.id\n├─ Scan: movies as m\n└─ Scan: genres as g\n1, 'Stalker', 1, 1, 1979, 8.2, NULL, 1, 'Science Fiction'\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE, 2, 'Action'\n3, 'Primer', 3, 1, 2004, 6.9, NULL, 3, 'Comedy'\n4, 'Heat', 4, 2, 1995, 8.2, TRUE, NULL, NULL\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE, NULL, NULL\n6, 'Solaris', 1, 1, 1972, 8.1, NULL, NULL, NULL\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE, NULL, NULL\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE, NULL, NULL\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE, NULL, NULL\n10, 'Inception', 4, 1, 2010, 8.8, TRUE, NULL, NULL\n\n# Outer joins without ON errors.\n!> SELECT * FROM movies LEFT JOIN genres\n!> SELECT * FROM movies RIGHT JOIN genres\n---\nError: invalid input: unexpected end of input\nError: invalid input: unexpected end of input\n"
  },
  {
    "path": "src/sql/testscripts/queries/limit",
    "content": "# Tests LIMIT clauses.\n\n> CREATE TABLE test (id INT PRIMARY KEY, value STRING)\n> INSERT INTO test VALUES (1, 'a'), (2, 'b'), (3, 'c')\n---\nok\n\n# Test all limits from 4 to 0.\n[plan]> SELECT * FROM test LIMIT 4\n---\nLimit: 4\n└─ Scan: test\n1, 'a'\n2, 'b'\n3, 'c'\n\n> SELECT * FROM test LIMIT 3\n---\n1, 'a'\n2, 'b'\n3, 'c'\n\n> SELECT * FROM test LIMIT 2\n---\n1, 'a'\n2, 'b'\n\n> SELECT * FROM test LIMIT 1\n---\n1, 'a'\n\n[plan]> SELECT * FROM test LIMIT 0\n---\nNothing\n\n# A max i64 limit works.\n> SELECT * FROM test LIMIT 9223372036854775807\n---\n1, 'a'\n2, 'b'\n3, 'c'\n\n# Limits can also be used with constant values.\n[plan]> SELECT 1, 2, 3 LIMIT 1\n---\nLimit: 1\n└─ Projection: 1, 2, 3\n   └─ Values: blank row\n1, 2, 3\n\n[plan]> SELECT 1, 2, 3 LIMIT 0\n---\nNothing\n\n# Limits can be expressions, but only constant ones.\n[plan]> SELECT * FROM test LIMIT 1 + 1\n---\nLimit: 2\n└─ Scan: test\n1, 'a'\n2, 'b'\n\n!> SELECT * FROM test LIMIT id\n---\nError: invalid input: expression must be constant, found column id\n\n# Negative and NULL limits error.\n!> SELECT * FROM test LIMIT -1\n!> SELECT * FROM test LIMIT NULL\n---\nError: invalid input: invalid limit -1\nError: invalid input: invalid limit NULL\n\n# Non-integer limits error.\n!> SELECT * FROM test LIMIT FALSE\n!> SELECT * FROM test LIMIT 1.0\n!> SELECT * FROM test LIMIT '1'\n---\nError: invalid input: invalid limit FALSE\nError: invalid input: invalid limit 1.0\nError: invalid input: invalid limit '1'\n\n# Multiple limits error.\n!> SELECT * FROM test LIMIT 1 2\n!> SELECT * FROM test LIMIT 1,2\n---\nError: invalid input: unexpected token 2\nError: invalid input: unexpected token ,\n"
  },
  {
    "path": "src/sql/testscripts/queries/offset",
    "content": "# Tests OFFSET clauses.\n\n> CREATE TABLE test (id INT PRIMARY KEY, value STRING)\n> INSERT INTO test VALUES (1, 'a'), (2, 'b'), (3, 'c')\n---\nok\n\n# Test all offsets from 4 to 0.\n[plan]> SELECT * FROM test OFFSET 4\n---\nOffset: 4\n└─ Scan: test\n\n> SELECT * FROM test OFFSET 3\n---\nok\n\n> SELECT * FROM test OFFSET 2\n---\n3, 'c'\n\n> SELECT * FROM test OFFSET 1\n---\n2, 'b'\n3, 'c'\n\n[plan]> SELECT * FROM test OFFSET 0\n---\nOffset: 0\n└─ Scan: test\n1, 'a'\n2, 'b'\n3, 'c'\n\n# A max i64 offset works.\n> SELECT * FROM test OFFSET 9223372036854775807\n---\nok\n\n# Offsets can also be used with constant values.\n[plan]> SELECT 1, 2, 3 OFFSET 1\n---\nOffset: 1\n└─ Projection: 1, 2, 3\n   └─ Values: blank row\n\n[plan]> SELECT 1, 2, 3 OFFSET 0\n---\nOffset: 0\n└─ Projection: 1, 2, 3\n   └─ Values: blank row\n1, 2, 3\n\n# Offsets can be expressions, but only constant ones.\n[plan]> SELECT * FROM test OFFSET 2 - 1\n---\nOffset: 1\n└─ Scan: test\n2, 'b'\n3, 'c'\n\n!> SELECT * FROM test OFFSET id\n---\nError: invalid input: expression must be constant, found column id\n\n# Negative and NULL offset error.\n!> SELECT * FROM test OFFSET -1\n!> SELECT * FROM test OFFSET NULL\n---\nError: invalid input: invalid offset -1\nError: invalid input: invalid offset NULL\n\n# Non-integer offsets error.\n!> SELECT * FROM test OFFSET FALSE\n!> SELECT * FROM test OFFSET 1.0\n!> SELECT * FROM test OFFSET '1'\n---\nError: invalid input: invalid offset FALSE\nError: invalid input: invalid offset 1.0\nError: invalid input: invalid offset '1'\n\n# Multiple offsets error.\n!> SELECT * FROM test OFFSET 1 2\n!> SELECT * FROM test OFFSET 1,2\n---\nError: invalid input: unexpected token 2\nError: invalid input: unexpected token ,\n"
  },
  {
    "path": "src/sql/testscripts/queries/order",
    "content": "# Tests ORDER BY clauses.\n\n# Create a table with representative values of all types.\n> CREATE TABLE test ( \\\n    id INT PRIMARY KEY, \\\n    \"bool\" BOOLEAN, \\\n    \"int\" INTEGER, \\\n    \"float\" FLOAT, \\\n    \"string\" STRING, \\\n    static INT \\\n)\n> INSERT INTO test VALUES (0, NULL,  NULL,  NULL,      NULL,  1)\n> INSERT INTO test VALUES (1, TRUE,  0,     3.14,      'a',   1)\n> INSERT INTO test VALUES (2, FALSE, -1,    -2.718,    'ab',  1)\n> INSERT INTO test VALUES (3, NULL,  1,     0.0,       'aaa', 1)\n> INSERT INTO test VALUES (4, NULL,  1000,  -0.0,      'A',   1)\n> INSERT INTO test VALUES (5, NULL,  -1000, INFINITY,  '',    1)\n> INSERT INTO test VALUES (6, NULL,  7,     -INFINITY, 'åa',  1)\n> INSERT INTO test VALUES (7, NULL,  -9,    NAN,       'Åa',  1)\n> INSERT INTO test VALUES (8, NULL,  NULL,  NULL,      'B',   1)\n> INSERT INTO test VALUES (9, NULL,  NULL,  NULL,      '👍',  1)\n\n> CREATE TABLE other (id INT PRIMARY KEY, value STRING)\n> INSERT INTO other VALUES (1, 'a'), (2, 'b')\n---\nok\n\n# Order by constant values. There's only one row, but it should be valid.\n[plan]> SELECT 1 AS value ORDER BY value ASC\n---\nOrder: value asc\n└─ Projection: 1 as value\n   └─ Values: blank row\n1\n\n[plan]> SELECT 1 AS value ORDER BY value DESC\n---\nOrder: value desc\n└─ Projection: 1 as value\n   └─ Values: blank row\n1\n\n# Order by primary key.\n[plan]> SELECT * FROM test ORDER BY id ASC\n---\nOrder: test.id asc\n└─ Scan: test\n0, NULL, NULL, NULL, NULL, 1\n1, TRUE, 0, 3.14, 'a', 1\n2, FALSE, -1, -2.718, 'ab', 1\n3, NULL, 1, 0.0, 'aaa', 1\n4, NULL, 1000, 0.0, 'A', 1\n5, NULL, -1000, inf, '', 1\n6, NULL, 7, -inf, 'åa', 1\n7, NULL, -9, NaN, 'Åa', 1\n8, NULL, NULL, NULL, 'B', 1\n9, NULL, NULL, NULL, '👍', 1\n\n[plan]> SELECT * FROM test ORDER BY id DESC\n---\nOrder: test.id desc\n└─ Scan: test\n9, NULL, NULL, NULL, '👍', 1\n8, NULL, NULL, NULL, 'B', 1\n7, NULL, -9, NaN, 'Åa', 1\n6, NULL, 7, -inf, 'åa', 1\n5, NULL, -1000, inf, '', 1\n4, NULL, 1000, 0.0, 'A', 1\n3, NULL, 1, 0.0, 'aaa', 1\n2, FALSE, -1, -2.718, 'ab', 1\n1, TRUE, 0, 3.14, 'a', 1\n0, NULL, NULL, NULL, NULL, 1\n\n# Booleans.\n> SELECT id, \"bool\" FROM test ORDER BY \"bool\" ASC\n---\n0, NULL\n3, NULL\n4, NULL\n5, NULL\n6, NULL\n7, NULL\n8, NULL\n9, NULL\n2, FALSE\n1, TRUE\n\n> SELECT id, \"bool\" FROM test ORDER BY \"bool\" DESC\n---\n1, TRUE\n2, FALSE\n0, NULL\n3, NULL\n4, NULL\n5, NULL\n6, NULL\n7, NULL\n8, NULL\n9, NULL\n\n# Integers.\n> SELECT id, \"int\" FROM test ORDER BY \"int\" ASC\n---\n0, NULL\n8, NULL\n9, NULL\n5, -1000\n7, -9\n2, -1\n1, 0\n3, 1\n6, 7\n4, 1000\n\n> SELECT id, \"int\" FROM test ORDER BY \"int\" DESC\n---\n4, 1000\n6, 7\n3, 1\n1, 0\n2, -1\n7, -9\n5, -1000\n0, NULL\n8, NULL\n9, NULL\n\n# Floats.\n> SELECT id, \"float\" FROM test ORDER BY \"float\" ASC\n---\n0, NULL\n8, NULL\n9, NULL\n6, -inf\n2, -2.718\n3, 0.0\n4, 0.0\n1, 3.14\n5, inf\n7, NaN\n\n> SELECT id, \"float\" FROM test ORDER BY \"float\" DESC\n---\n7, NaN\n5, inf\n1, 3.14\n3, 0.0\n4, 0.0\n2, -2.718\n6, -inf\n0, NULL\n8, NULL\n9, NULL\n\n# Strings.\n> SELECT id, \"string\" FROM test ORDER BY \"string\" ASC\n---\n0, NULL\n5, ''\n4, 'A'\n8, 'B'\n1, 'a'\n3, 'aaa'\n2, 'ab'\n7, 'Åa'\n6, 'åa'\n9, '👍'\n\n> SELECT id, \"string\" FROM test ORDER BY \"string\" DESC\n---\n9, '👍'\n6, 'åa'\n7, 'Åa'\n2, 'ab'\n3, 'aaa'\n1, 'a'\n8, 'B'\n4, 'A'\n5, ''\n0, NULL\n\n# When all values are equal, they are stably ordered by the primary key in\n# ascending order (the scan order).\n> SELECT id, static FROM test ORDER BY static ASC\n---\n0, 1\n1, 1\n2, 1\n3, 1\n4, 1\n5, 1\n6, 1\n7, 1\n8, 1\n9, 1\n\n\n> SELECT id, static FROM test ORDER BY static DESC\n---\n0, 1\n1, 1\n2, 1\n3, 1\n4, 1\n5, 1\n6, 1\n7, 1\n8, 1\n9, 1\n\n# Order by multiple columns. Again, the ascending primary key is tiebreaker.\n> SELECT id, static, \"bool\", \"int\", \"string\" FROM test \\\n  ORDER BY static ASC, \"bool\" DESC, \"int\" ASC, \"string\" DESC\n---\n1, 1, TRUE, 0, 'a'\n2, 1, FALSE, -1, 'ab'\n9, 1, NULL, NULL, '👍'\n8, 1, NULL, NULL, 'B'\n0, 1, NULL, NULL, NULL\n5, 1, NULL, -1000, ''\n7, 1, NULL, -9, 'Åa'\n3, 1, NULL, 1, 'aaa'\n6, 1, NULL, 7, 'åa'\n4, 1, NULL, 1000, 'A'\n\n> SELECT id, static, \"bool\", \"int\", \"string\" FROM test \\\n  ORDER BY static DESC, \"bool\" ASC, \"int\" DESC, \"string\" ASC\n---\n4, 1, NULL, 1000, 'A'\n6, 1, NULL, 7, 'åa'\n3, 1, NULL, 1, 'aaa'\n7, 1, NULL, -9, 'Åa'\n5, 1, NULL, -1000, ''\n0, 1, NULL, NULL, NULL\n8, 1, NULL, NULL, 'B'\n9, 1, NULL, NULL, '👍'\n2, 1, FALSE, -1, 'ab'\n1, 1, TRUE, 0, 'a'\n\n# Can order by expressions.\n[plan]> SELECT id, \"float\" FROM test ORDER BY \"float\" ^ 2\n---\nOrder: test.float ^ 2 asc\n└─ Projection: test.id, test.float\n   └─ Scan: test\n0, NULL\n8, NULL\n9, NULL\n3, 0.0\n4, 0.0\n2, -2.718\n1, 3.14\n5, inf\n6, -inf\n7, NaN\n\n# Can order by columns not in the result. Multiple references to the same column\n# only result in one hidden column.\n[plan]> SELECT id, \"int\" FROM test ORDER BY \"bool\" DESC\n---\nRemap: test.id, test.int (dropped: test.bool)\n└─ Order: test.bool desc\n   └─ Projection: test.id, test.int, test.bool\n      └─ Scan: test\n1, 0\n2, -1\n0, NULL\n3, 1\n4, 1000\n5, -1000\n6, 7\n7, -9\n8, NULL\n9, NULL\n\n[plan]> SELECT id, \"int\" FROM test ORDER BY \"bool\" DESC, \"bool\" ASC\n---\nRemap: test.id, test.int (dropped: test.bool)\n└─ Order: test.bool desc, test.bool asc\n   └─ Projection: test.id, test.int, test.bool\n      └─ Scan: test\n1, 0\n2, -1\n0, NULL\n3, 1\n4, 1000\n5, -1000\n6, 7\n7, -9\n8, NULL\n9, NULL\n\n# Can order on expressions on columns not in the result.\n[plan]> SELECT id FROM test ORDER BY \"float\" ^ 2 - \"int\" ^ 2 DESC\n---\nRemap: test.id (dropped: test.float, test.int)\n└─ Order: test.float ^ 2 - test.int ^ 2 desc\n   └─ Projection: test.id, test.float, test.int\n      └─ Scan: test\n7\n5\n6\n1\n2\n3\n4\n0\n8\n9\n\n# Order by aliased table or column.\n> SELECT id, \"int\" AS foo FROM test ORDER BY foo\n---\n0, NULL\n8, NULL\n9, NULL\n5, -1000\n7, -9\n2, -1\n1, 0\n3, 1\n6, 7\n4, 1000\n\n> SELECT id, \"int\" FROM test AS t ORDER BY t.\"int\"\n---\n0, NULL\n8, NULL\n9, NULL\n5, -1000\n7, -9\n2, -1\n1, 0\n3, 1\n6, 7\n4, 1000\n\n# Order by an aliased expression.\n> SELECT id, \"int\" ^ 2 AS square FROM test ORDER BY square ASC\n---\n0, NULL\n8, NULL\n9, NULL\n1, 0\n2, 1\n3, 1\n6, 49\n7, 81\n4, 1000000\n5, 1000000\n\n# Errors if the column is ambiguous.\n!> SELECT id, \"int\" ^ 2 AS foo, \"int\" AS foo FROM test ORDER BY foo ASC\n---\nError: invalid input: ambiguous column foo\n\n# Prefers alias over table column if ambiguous, but not if fully qualified.\n[plan]> SELECT id AS \"int\" FROM test ORDER BY \"int\" DESC\n---\nOrder: int desc\n└─ Projection: test.id as int\n   └─ Scan: test\n9\n8\n7\n6\n5\n4\n3\n2\n1\n0\n\n[plan]> SELECT id AS \"int\" FROM test ORDER BY test.\"int\" DESC\n---\nRemap: int (dropped: test.int)\n└─ Order: test.int desc\n   └─ Projection: test.id as int, test.int\n      └─ Scan: test\n4\n6\n3\n1\n2\n7\n5\n0\n8\n9\n\n# Errors on unknown table or column, even the original table name when aliased.\n!> SELECT * FROM test ORDER BY unknown\n!> SELECT * FROM test ORDER BY test.unknown\n!> SELECT * FROM test ORDER BY unknown.id\n!> SELECT * FROM test AS t ORDER BY test.\"int\"\n---\nError: invalid input: unknown column unknown\nError: invalid input: unknown column test.unknown\nError: invalid input: unknown table unknown\nError: invalid input: unknown table test\n\n# Errors on unknown direction.\n!> SELECT * FROM test ORDER BY id UNKNOWN\n---\nError: invalid input: unexpected token unknown\n\n# Errors on trailing comma.\n!> SELECT * FROM test ORDER BY id,\n---\nError: invalid input: unexpected end of input\n\n# Errors on ambiguous columns.\n!> SELECT * FROM test, other ORDER BY id DESC\n---\nError: invalid input: ambiguous column id\n\n# Works with qualified columns, even when aliased.\n[plan]> SELECT * FROM test, other ORDER BY other.id DESC\n---\nOrder: other.id desc\n└─ NestedLoopJoin: inner\n   ├─ Scan: test\n   └─ Scan: other\n0, NULL, NULL, NULL, NULL, 1, 2, 'b'\n1, TRUE, 0, 3.14, 'a', 1, 2, 'b'\n2, FALSE, -1, -2.718, 'ab', 1, 2, 'b'\n3, NULL, 1, 0.0, 'aaa', 1, 2, 'b'\n4, NULL, 1000, 0.0, 'A', 1, 2, 'b'\n5, NULL, -1000, inf, '', 1, 2, 'b'\n6, NULL, 7, -inf, 'åa', 1, 2, 'b'\n7, NULL, -9, NaN, 'Åa', 1, 2, 'b'\n8, NULL, NULL, NULL, 'B', 1, 2, 'b'\n9, NULL, NULL, NULL, '👍', 1, 2, 'b'\n0, NULL, NULL, NULL, NULL, 1, 1, 'a'\n1, TRUE, 0, 3.14, 'a', 1, 1, 'a'\n2, FALSE, -1, -2.718, 'ab', 1, 1, 'a'\n3, NULL, 1, 0.0, 'aaa', 1, 1, 'a'\n4, NULL, 1000, 0.0, 'A', 1, 1, 'a'\n5, NULL, -1000, inf, '', 1, 1, 'a'\n6, NULL, 7, -inf, 'åa', 1, 1, 'a'\n7, NULL, -9, NaN, 'Åa', 1, 1, 'a'\n8, NULL, NULL, NULL, 'B', 1, 1, 'a'\n9, NULL, NULL, NULL, '👍', 1, 1, 'a'\n\n[plan]> SELECT * FROM test t, other o ORDER BY o.id DESC, t.id ASC\n---\nOrder: o.id desc, t.id asc\n└─ NestedLoopJoin: inner\n   ├─ Scan: test as t\n   └─ Scan: other as o\n0, NULL, NULL, NULL, NULL, 1, 2, 'b'\n1, TRUE, 0, 3.14, 'a', 1, 2, 'b'\n2, FALSE, -1, -2.718, 'ab', 1, 2, 'b'\n3, NULL, 1, 0.0, 'aaa', 1, 2, 'b'\n4, NULL, 1000, 0.0, 'A', 1, 2, 'b'\n5, NULL, -1000, inf, '', 1, 2, 'b'\n6, NULL, 7, -inf, 'åa', 1, 2, 'b'\n7, NULL, -9, NaN, 'Åa', 1, 2, 'b'\n8, NULL, NULL, NULL, 'B', 1, 2, 'b'\n9, NULL, NULL, NULL, '👍', 1, 2, 'b'\n0, NULL, NULL, NULL, NULL, 1, 1, 'a'\n1, TRUE, 0, 3.14, 'a', 1, 1, 'a'\n2, FALSE, -1, -2.718, 'ab', 1, 1, 'a'\n3, NULL, 1, 0.0, 'aaa', 1, 1, 'a'\n4, NULL, 1000, 0.0, 'A', 1, 1, 'a'\n5, NULL, -1000, inf, '', 1, 1, 'a'\n6, NULL, 7, -inf, 'åa', 1, 1, 'a'\n7, NULL, -9, NaN, 'Åa', 1, 1, 'a'\n8, NULL, NULL, NULL, 'B', 1, 1, 'a'\n9, NULL, NULL, NULL, '👍', 1, 1, 'a'\n\n# Order by aggregates, both when in SELECT and otherwise.\n[plan]> SELECT \"bool\", MAX(\"int\") FROM test GROUP BY \"bool\" ORDER BY MAX(\"int\") DESC\n---\nOrder: #1 desc\n└─ Aggregate: test.bool, max(test.int)\n   └─ Scan: test\nNULL, 1000\nTRUE, 0\nFALSE, -1\n\n[plan]> SELECT \"bool\" FROM test GROUP BY \"bool\" ORDER BY MAX(\"int\") DESC\n---\nRemap: test.bool (dropped: #1)\n└─ Order: #1 desc\n   └─ Aggregate: test.bool, max(test.int)\n      └─ Scan: test\nNULL\nTRUE\nFALSE\n\n[plan]> SELECT \"bool\", MAX(\"int\") FROM test GROUP BY \"bool\" ORDER BY MAX(\"int\") - MIN(\"int\") DESC\n---\nRemap: test.bool, #1 (dropped: #2)\n└─ Order: #1 - #2 desc\n   └─ Aggregate: test.bool, max(test.int), min(test.int)\n      └─ Scan: test\nNULL, 1000\nFALSE, -1\nTRUE, 0\n\n# ORDER BY works with compound expressions using complex GROUP BY expressions\n# that are not on the SELECT clause.\n[plan]> SELECT COUNT(*) FROM test GROUP BY id % 2 ORDER BY 2 - id % 2 + 1 > 1\n---\nRemap: #0 (dropped: #1)\n└─ Order: 2 - #1 + 1 > 1 asc\n   └─ Projection: #1, #0\n      └─ Aggregate: test.id % 2, count(TRUE)\n         └─ Scan: test\n5\n5\n\n# ORDER BY can use (un)qualified expressions for an (un)qualified GROUP BY.\n[plan]> SELECT COUNT(*) FROM test GROUP BY \"bool\" ORDER BY test.\"bool\"\n---\nRemap: #0 (dropped: test.bool)\n└─ Order: test.bool asc\n   └─ Projection: #1, test.bool\n      └─ Aggregate: test.bool, count(TRUE)\n         └─ Scan: test\n8\n1\n1\n\n[plan]> SELECT COUNT(*) FROM test GROUP BY test.\"bool\" ORDER BY \"bool\"\n---\nRemap: #0 (dropped: test.bool)\n└─ Order: test.bool asc\n   └─ Projection: #1, test.bool\n      └─ Aggregate: test.bool, count(TRUE)\n         └─ Scan: test\n8\n1\n1\n\n# ORDER BY errors on columns not in the SELECT or GROUP BY clauses.\n!> SELECT \"bool\", COUNT(*) FROM test GROUP BY \"bool\" ORDER BY id\n---\nError: invalid input: column id must be used in an aggregate or GROUP BY expression\n"
  },
  {
    "path": "src/sql/testscripts/queries/select",
    "content": "# Tests the SELECT part of queries.\n\n# Create a basic test table, and a secondary table for join column lookups.\n> CREATE TABLE test ( \\\n    id INT PRIMARY KEY, \\\n    \"bool\" BOOLEAN, \\\n    \"float\" FLOAT, \\\n    \"int\" INT, \\\n    \"string\" STRING \\\n)\n> INSERT INTO test VALUES (1, true, 3.14, 7, 'foo')\n> INSERT INTO test VALUES (2, false, 2.718, 1, '👍')\n> INSERT INTO test VALUES (3, NULL, NULL, NULL, NULL)\n\n> CREATE TABLE other (id INT PRIMARY KEY, value STRING)\n> INSERT INTO other VALUES (1, 'a'), (2, 'b')\n---\nok\n\n# Select constant values.\n[plan]> select 1\n---\nProjection: 1\n└─ Values: blank row\n1\n\n[plan]> SELECT NULL, NOT FALSE, 2^2+1, 3.14*2, 'Hi 👋'\n---\nProjection: NULL, TRUE, 5, 6.28, 'Hi 👋'\n└─ Values: blank row\nNULL, TRUE, 5, 6.28, 'Hi 👋'\n\n# Bare select errors, as does trailing comma and identifier without a table.\n!> SELECT\n!> SELECT 1,\n!> SELECT foo\n---\nError: invalid input: unexpected end of input\nError: invalid input: unexpected end of input\nError: invalid input: expression must be constant, found column foo\n\n# Select from a table.\n[plan,header]> SELECT * FROM test\n---\nScan: test\ntest.id, test.bool, test.float, test.int, test.string\n1, TRUE, 3.14, 7, 'foo'\n2, FALSE, 2.718, 1, '👍'\n3, NULL, NULL, NULL, NULL\n\n[plan,header]> SELECT \"bool\" FROM test\n---\nProjection: test.bool\n└─ Scan: test\ntest.bool\nTRUE\nFALSE\nNULL\n\n# * can't be used with table names, for simplicity.\n!> SELECT test.* FROM test\n---\nError: invalid input: expected identifier, got *\n\n# A SELECT * without a table errors, as does a bare FROM.\n!> SELECT *\n!> SELECT * FROM\n---\nError: invalid input: SELECT * requires a FROM clause\nError: invalid input: unexpected end of input\n\n# A * errors in expressions. For simplicity, expressions only support scalars.\n!> SELECT 1 + * FROM test\n!> SELECT sqrt(*) FROM test\n!> SELECT max(*) FROM test\n---\nError: invalid input: unsupported use of *\nError: invalid input: unsupported use of *\nError: invalid input: unsupported use of *\n\n# A * can be used multiple times.\n[plan,header]> SELECT *, *, * FROM test\n---\nProjection: test.id, test.bool, test.float, test.int, test.string, test.id, test.bool, test.float, test.int, test.string, test.id, test.bool, test.float, test.int, test.string\n└─ Scan: test\ntest.id, test.bool, test.float, test.int, test.string, test.id, test.bool, test.float, test.int, test.string, test.id, test.bool, test.float, test.int, test.string\n1, TRUE, 3.14, 7, 'foo', 1, TRUE, 3.14, 7, 'foo', 1, TRUE, 3.14, 7, 'foo'\n2, FALSE, 2.718, 1, '👍', 2, FALSE, 2.718, 1, '👍', 2, FALSE, 2.718, 1, '👍'\n3, NULL, NULL, NULL, NULL, 3, NULL, NULL, NULL, NULL, 3, NULL, NULL, NULL, NULL\n\n# Mix *, columns, column expressions, and constant expressions.\n[plan,header]> SELECT id, 7-4, *, \"float\"^2 FROM test\n---\nProjection: test.id, 3, test.id, test.bool, test.float, test.int, test.string, test.float ^ 2\n└─ Scan: test\ntest.id, , test.id, test.bool, test.float, test.int, test.string, \n1, 3, 1, TRUE, 3.14, 7, 'foo', 9.8596\n2, 3, 2, FALSE, 2.718, 1, '👍', 7.387524\n3, 3, 3, NULL, NULL, NULL, NULL, NULL\n\n# Column names may be qualified or unqualified.\n[header]> SELECT id, test.\"bool\" FROM test\n---\ntest.id, test.bool\n1, TRUE\n2, FALSE\n3, NULL\n\n# The table may be aliased, and qualified using the alias. The AS alias keyword\n# is optional.\n[header,plan]> SELECT id, t.\"bool\" FROM test AS t\n---\nProjection: t.id, t.bool\n└─ Scan: test as t\nt.id, t.bool\n1, TRUE\n2, FALSE\n3, NULL\n\n[header]> SELECT id, t.\"bool\" FROM test t\n---\nt.id, t.bool\n1, TRUE\n2, FALSE\n3, NULL\n\n# Unknown tables or columns error. Including the original table when aliased.\n!> SELECT * FROM unknown\n!> SELECT unknown FROM test\n!> SELECT test.unknown FROM test\n!> SELECT test.id.unknown FROM test\n!> SELECT unknown.id FROM test\n!> SELECT test.id FROM test AS t\n---\nError: invalid input: table unknown does not exist\nError: invalid input: unknown column unknown\nError: invalid input: unknown column test.unknown\nError: invalid input: unexpected token .\nError: invalid input: unknown table unknown\nError: invalid input: unknown table test\n\n# Columns, both constant and from tables, can be aliased.\n# The AS keyword is optional.\n[header,plan]> SELECT 1 AS one, test.\"int\" value FROM test\n---\nProjection: 1 as one, test.int as value\n└─ Scan: test\none, value\n1, 7\n1, 1\n1, NULL\n\n# Aliases can have special characters and keywords if quoted.\n[header]> SELECT 1 AS \"integer\", 2 AS \"hi 👋\"\n---\ninteger, hi 👋\n1, 2\n\n# Expressions can't reference aliases.\n!> SELECT 1 AS one, one + 1\n!> SELECT id AS alias, alias + 1 FROM test\n---\nError: invalid input: expression must be constant, found column one\nError: invalid input: unknown column alias\n\n# Aliases can have the same name as table columns, but won't shadow them.\n[header]> SELECT 'foo' AS id, id, id + 3 FROM test\n---\nid, test.id, \n'foo', 1, 4\n'foo', 2, 5\n'foo', 3, 6\n\n# Multiple aliases can have the same name.\n[header]> SELECT 1 AS id, id, \"float\" AS id FROM test\n---\nid, test.id, id\n1, 1, 3.14\n1, 2, 2.718\n1, 3, NULL\n\n# Aliases can't be qualified.\n!> SELECT 1 AS foo.bar\n---\nError: invalid input: unexpected token .\n\n# Bare and * aliases error.\n!> SELECT 1 AS\n!> SELECT * AS all FROM test\n---\nError: invalid input: unexpected end of input\nError: invalid input: can't alias *\n\n# Ambiguous columns error.\n!> SELECT id FROM test, other\n---\nError: invalid input: ambiguous column id\n\n# Unambiguous columns don't, resulting in a cross join.\n> SELECT \"bool\", value FROM test, other\n---\nTRUE, 'a'\nTRUE, 'b'\nFALSE, 'a'\nFALSE, 'b'\nNULL, 'a'\nNULL, 'b'\n\n# Qualified columns work, also when aliased.\n> SELECT test.id, other.id FROM test, other\n---\n1, 1\n1, 2\n2, 1\n2, 2\n3, 1\n3, 2\n\n> SELECT t.id, o.id FROM test t, other o\n---\n1, 1\n1, 2\n2, 1\n2, 2\n3, 1\n3, 2\n\n# A select with no rows optimized to a Nothing node still emits headers.\n[plan,header]> SELECT * FROM test WHERE FALSE\n---\nNothing\ntest.id, test.bool, test.float, test.int, test.string\n"
  },
  {
    "path": "src/sql/testscripts/queries/where_",
    "content": "# Tests basic WHERE clauses.\n\n> CREATE TABLE test (id INT PRIMARY KEY, value STRING)\n> INSERT INTO test VALUES (1, 'a'), (2, 'b'), (3, 'c')\n\n> CREATE TABLE other (id INT PRIMARY KEY, \"bool\" BOOLEAN)\n> INSERT INTO other VALUES (1, FALSE), (2, TRUE)\n---\nok\n\n# Constant TRUE and FALSE filters work as expected.\n[plan]> SELECT * FROM test WHERE TRUE\n---\nScan: test\n1, 'a'\n2, 'b'\n3, 'c'\n\n[plan]> SELECT * FROM test WHERE FALSE\n---\nNothing\n\n# NULL is treated as FALSE.\n[plan]> SELECT * FROM test WHERE NULL\n---\nNothing\n\n# Field predicate expressions work as expected.\n[plan]> SELECT * FROM test WHERE id > 1\n---\nScan: test (test.id > 1)\n2, 'b'\n3, 'c'\n\n[plan]> SELECT * FROM test WHERE id > 1 AND value < 'c'\n---\nScan: test (test.id > 1 AND test.value < 'c')\n2, 'b'\n\n# Errors on non-boolean type.\n!> SELECT * FROM test WHERE 1\n!> SELECT * FROM test WHERE 1.0\n!> SELECT * FROM test WHERE ''\n---\nError: invalid input: filter returned 1, expected boolean\nError: invalid input: filter returned 1.0, expected boolean\nError: invalid input: filter returned '', expected boolean\n\n# Errors on bare WHERE clause or multiple predicates.\n!> SELECT * FROM test WHERE\n!> SELECT * FROM test WHERE TRUE, TRUE\n---\nError: invalid input: unexpected end of input\nError: invalid input: unexpected token ,\n\n# Errors on unknown tables and columns.\n!> SELECT * FROM test WHERE unknown > 0\n!> SELECT * FROM test WHERE unknown.id > 0\n---\nError: invalid input: unknown column unknown\nError: invalid input: unknown table unknown\n\n# Qualified names are valid.\n> SELECT * FROM test WHERE test.value = 'b'\n---\n2, 'b'\n\n# Expression and column aliases aren't visible.\n!> SELECT value AS v FROM test WHERE v = 'b'\n!> SELECT 1 + 1 AS two WHERE two = 2\n---\nError: invalid input: unknown column v\nError: invalid input: expression must be constant, found column two\n\n# Table aliases are visible.\n> SELECT * FROM test AS t WHERE t.id = 2\n---\n2, 'b'\n\n# Ambiguous columns error.\n!> SELECT * FROM test, other WHERE id > 1\n---\nError: invalid input: ambiguous column id\n\n# Unambiguous columns work.\n> SELECT * FROM test, other WHERE value = 'b'\n---\n2, 'b', 1, FALSE\n2, 'b', 2, TRUE\n\n\n# Qualified columns work, also when aliased.\n> SELECT * FROM test, other WHERE test.id = 2 AND other.id = 2\n---\n2, 'b', 2, TRUE\n\n> SELECT * FROM test t, other o WHERE t.id = 2 AND o.id = 2\n---\n2, 'b', 2, TRUE\n\n# WHERE can be combined with joins, even when aliased.\n[plan]> SELECT * FROM test JOIN other ON test.id = other.id WHERE test.id > 1\n---\nHashJoin: inner on test.id = other.id\n├─ Scan: test (test.id > 1)\n└─ Scan: other\n2, 'b', 2, TRUE\n\n[plan]> SELECT * FROM test t JOIN other o ON t.id = o.id WHERE t.id > 1\n---\nHashJoin: inner on t.id = o.id\n├─ Scan: test as t (t.id > 1)\n└─ Scan: other as o\n2, 'b', 2, TRUE\n"
  },
  {
    "path": "src/sql/testscripts/queries/where_index",
    "content": "# Tests WHERE index lookups.\n\n# Create a table with representative values of all types.\n> CREATE TABLE test ( \\\n    id INT PRIMARY KEY, \\\n    \"bool\" BOOLEAN INDEX, \\\n    \"int\" INTEGER INDEX, \\\n    \"float\" FLOAT INDEX, \\\n    \"string\" STRING INDEX \\\n)\n> INSERT INTO test VALUES (0, NULL,  NULL,  NULL,      NULL)\n> INSERT INTO test VALUES (1, TRUE,  0,     3.14,      'abc')\n> INSERT INTO test VALUES (2, FALSE, -1,    -2.718,    'a')\n> INSERT INTO test VALUES (3, TRUE,  1,     0.0,       'ABC')\n> INSERT INTO test VALUES (4, NULL,  1,     -0.0,      '👍')\n> INSERT INTO test VALUES (5, NULL,  NULL,  INFINITY,  'å')\n> INSERT INTO test VALUES (6, NULL,  NULL,  NAN,       '')\n---\nok\n\n# Boolean lookups.\n[plan]> SELECT * FROM test WHERE \"bool\" = TRUE\n---\nIndexLookup: test.bool (TRUE)\n1, TRUE, 0, 3.14, 'abc'\n3, TRUE, 1, 0.0, 'ABC'\n\n[plan]> SELECT * FROM test WHERE \"bool\" = FALSE\n---\nIndexLookup: test.bool (FALSE)\n2, FALSE, -1, -2.718, 'a'\n\n# Integer lookups, including multiple matches and missing values.\n[plan]> SELECT * FROM test WHERE \"int\" = -1\n---\nIndexLookup: test.int (-1)\n2, FALSE, -1, -2.718, 'a'\n\n[plan]> SELECT * FROM test WHERE \"int\" = 0\n---\nIndexLookup: test.int (0)\n1, TRUE, 0, 3.14, 'abc'\n\n[plan]> SELECT * FROM test WHERE \"int\" = 1\n---\nIndexLookup: test.int (1)\n3, TRUE, 1, 0.0, 'ABC'\n4, NULL, 1, 0.0, '👍'\n\n[plan]> SELECT * FROM test WHERE \"int\" = 7\n---\nIndexLookup: test.int (7)\n\n# Floats. 0.0 and -0.0 should be equal. NAN should be unequal,\n# but IS NAN should yield lookups.\n[plan]> SELECT * FROM test WHERE \"float\" = -2.718\n---\nIndexLookup: test.float (-2.718)\n2, FALSE, -1, -2.718, 'a'\n\n[plan]> SELECT * FROM test WHERE \"float\" = -0.0\n---\nIndexLookup: test.float (-0.0)\n3, TRUE, 1, 0.0, 'ABC'\n4, NULL, 1, 0.0, '👍'\n\n[plan]> SELECT * FROM test WHERE \"float\" = 0.0\n---\nIndexLookup: test.float (0.0)\n3, TRUE, 1, 0.0, 'ABC'\n4, NULL, 1, 0.0, '👍'\n\n[plan]> SELECT * FROM test WHERE \"float\" = 3.14\n---\nIndexLookup: test.float (3.14)\n1, TRUE, 0, 3.14, 'abc'\n\n[plan]> SELECT * FROM test WHERE \"float\" = INFINITY\n---\nIndexLookup: test.float (inf)\n5, NULL, NULL, inf, 'å'\n\n[plan]> SELECT * FROM test WHERE \"float\" = NAN\n---\nNothing\n\n[plan]> SELECT * FROM test WHERE \"float\" = -NAN\n---\nNothing\n\n[plan]> SELECT * FROM test WHERE \"float\" IS NAN\n---\nIndexLookup: test.float (NaN)\n6, NULL, NULL, NaN, ''\n\n# Strings. Should be case-insensitive.\n[plan]> SELECT * FROM test WHERE \"string\" = 'abc'\n---\nIndexLookup: test.string ('abc')\n1, TRUE, 0, 3.14, 'abc'\n\n[plan]> SELECT * FROM test WHERE \"string\" = 'a'\n---\nIndexLookup: test.string ('a')\n2, FALSE, -1, -2.718, 'a'\n\n[plan]> SELECT * FROM test WHERE \"string\" = 'å'\n---\nIndexLookup: test.string ('å')\n5, NULL, NULL, inf, 'å'\n\n[plan]> SELECT * FROM test WHERE \"string\" = '👍'\n---\nIndexLookup: test.string ('👍')\n4, NULL, 1, 0.0, '👍'\n\n[plan]> SELECT * FROM test WHERE \"string\" = ''\n---\nIndexLookup: test.string ('')\n6, NULL, NULL, NaN, ''\n\n# LIKE does not use an index.\n[plan]> SELECT * FROM test WHERE \"string\" LIKE 'a%'\n---\nScan: test (test.string LIKE 'a%')\n1, TRUE, 0, 3.14, 'abc'\n2, FALSE, -1, -2.718, 'a'\n\n# IS NULL lookups should use an index. = NULL should give no matches.\n[plan]> SELECT * FROM test WHERE \"int\" IS NULL\n---\nIndexLookup: test.int (NULL)\n0, NULL, NULL, NULL, NULL\n5, NULL, NULL, inf, 'å'\n6, NULL, NULL, NaN, ''\n\n[plan]> SELECT * FROM test WHERE \"int\" = NULL\n---\nNothing\n\n# Multiple lookups work and use the index.\n[plan]> SELECT * FROM test WHERE \"int\" = -1 OR \"int\" = 0 OR \"int\" = 1 OR \"int\" = 7\n---\nIndexLookup: test.int (-1, 0, 1, 7)\n1, TRUE, 0, 3.14, 'abc'\n2, FALSE, -1, -2.718, 'a'\n3, TRUE, 1, 0.0, 'ABC'\n4, NULL, 1, 0.0, '👍'\n\n# > or < predicates don't use an index.\n[plan]> SELECT * FROM test WHERE \"int\" < 1\n---\nScan: test (test.int < 1)\n1, TRUE, 0, 3.14, 'abc'\n2, FALSE, -1, -2.718, 'a'\n\n[plan]> SELECT * FROM test WHERE \"int\" > -1\n---\nScan: test (test.int > -1)\n1, TRUE, 0, 3.14, 'abc'\n3, TRUE, 1, 0.0, 'ABC'\n4, NULL, 1, 0.0, '👍'\n"
  },
  {
    "path": "src/sql/testscripts/queries/where_primary_key",
    "content": "# Tests WHERE index lookups.\n\n# Boolean lookups.\n> CREATE TABLE \"bool\" (id BOOL PRIMARY KEY)\n> INSERT INTO \"bool\" VALUES (TRUE), (FALSE)\n---\nok\n\n[plan]> SELECT * FROM \"bool\" WHERE id = TRUE\n---\nKeyLookup: bool (TRUE)\nTRUE\n\n[plan]> SELECT * FROM \"bool\" WHERE id = FALSE\n---\nKeyLookup: bool (FALSE)\nFALSE\n\n# Integer lookups, including a missing value.\n> CREATE TABLE \"int\" (id INT PRIMARY KEY)\n> INSERT INTO \"int\" VALUES (-1), (0), (1)\n---\nok\n\n[plan]> SELECT * FROM \"int\" WHERE id = -1\n---\nKeyLookup: int (-1)\n-1\n\n[plan]> SELECT * FROM \"int\" WHERE id = 0\n---\nKeyLookup: int (0)\n0\n\n[plan]> SELECT * FROM \"int\" WHERE id = 1\n---\nKeyLookup: int (1)\n1\n\n[plan]> SELECT * FROM \"int\" WHERE id = 7\n---\nKeyLookup: int (7)\n\n# Floats. NAN matches fail (and aren't valid primary keys anyway).\n# 0.0 and -0.0 should be considered equal.\n> CREATE TABLE \"float\" (id FLOAT PRIMARY KEY)\n> INSERT INTO \"float\" VALUES (-2.718), (-0.0), (3.14), (INFINITY)\n\n[plan]> SELECT * FROM \"float\" WHERE id = -2.718\n---\nKeyLookup: float (-2.718)\n-2.718\n\n[plan]> SELECT * FROM \"float\" WHERE id = -0.0\n---\nKeyLookup: float (-0.0)\n0.0\n\n[plan]> SELECT * FROM \"float\" WHERE id = 0.0\n---\nKeyLookup: float (0.0)\n0.0\n\n[plan]> SELECT * FROM \"float\" WHERE id = 3.14\n---\nKeyLookup: float (3.14)\n3.14\n\n[plan]> SELECT * FROM \"float\" WHERE id = INFINITY\n---\nKeyLookup: float (inf)\ninf\n\n[plan]> SELECT * FROM \"float\" WHERE id = NAN\n---\nNothing\n\n[plan]> SELECT * FROM \"float\" WHERE id IS NAN\n---\nKeyLookup: float (NaN)\n\n# Strings. Should be case-insensitive.\n> CREATE TABLE \"string\" (id STRING PRIMARY KEY)\n> INSERT INTO \"string\" VALUES (''), ('a'), ('å'), ('abc'), ('ABC'), ('👍')\n\n[plan]> SELECT * FROM \"string\" WHERE id = ''\n---\nKeyLookup: string ('')\n''\n\n[plan]> SELECT * FROM \"string\" WHERE id = 'a'\n---\nKeyLookup: string ('a')\n'a'\n\n[plan]> SELECT * FROM \"string\" WHERE id = 'å'\n---\nKeyLookup: string ('å')\n'å'\n\n[plan]> SELECT * FROM \"string\" WHERE id = 'abc'\n---\nKeyLookup: string ('abc')\n'abc'\n\n[plan]> SELECT * FROM \"string\" WHERE id = '👍'\n---\nKeyLookup: string ('👍')\n'👍'\n\n# LIKE does not use an index.\n[plan]> SELECT * FROM \"string\" WHERE id LIKE 'a%'\n---\nScan: string (string.id LIKE 'a%')\n'a'\n'abc'\n\n# NULL lookups should be legal but give no matches.\n[plan]> SELECT * FROM \"int\" WHERE id = NULL\n---\nNothing\n\n[plan]> SELECT * FROM \"int\" WHERE id IS NULL\n---\nKeyLookup: int (NULL)\n\n# Multiple lookups work.\n[plan]> SELECT * FROM \"int\" WHERE id = -1 OR id = 0 OR id = 1 OR id = 7\n---\nKeyLookup: int (-1, 0, 1, 7)\n-1\n0\n1\n\n# > or < predicates don't use an index.\n[plan]> SELECT * FROM \"int\" WHERE id < 1\n---\nScan: int (int.id < 1)\n-1\n0\n\n[plan]> SELECT * FROM \"int\" WHERE id > -1\n---\nScan: int (int.id > -1)\n0\n1\n"
  },
  {
    "path": "src/sql/testscripts/schema/create_table",
    "content": "# Tests basic CREATE TABLE functionality.\n\n# The result contains the table name. The table is written to storage. Also\n# output the plan, which is trivial.\n[plan,result,ops]> CREATE TABLE test (id INTEGER PRIMARY KEY)\n---\nCreateTable: test\nset mvcc:NextVersion → 2 [\"\\x00\" → \"\\x02\"]\nset mvcc:TxnActive(1) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\"]\nset mvcc:TxnWrite(1, sql:Table(test)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\xfftest\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Table(test), 1) → CREATE TABLE test ( id INTEGER PRIMARY KEY ) [\"\\x04\\x00\\xfftest\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x10\\x04test\\x00\\x01\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(1, sql:Table(test)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\xfftest\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnActive(1) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\"]\nCreateTable { name: \"test\" }\n\ndump\n---\nmvcc:NextVersion → 2 [\"\\x00\" → \"\\x02\"]\nmvcc:Version(sql:Table(test), 1) → CREATE TABLE test ( id INTEGER PRIMARY KEY ) [\"\\x04\\x00\\xfftest\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x10\\x04test\\x00\\x01\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\"]\n\n# Errors if table already exists.\n!> CREATE TABLE test (id INTEGER PRIMARY KEY)\n---\nError: invalid input: table test already exists\n\n# No table name or columns errors.\n!> CREATE TABLE\n!> CREATE TABLE name\n!> CREATE TABLE name ()\n---\nError: invalid input: unexpected end of input\nError: invalid input: unexpected end of input\nError: invalid input: expected identifier, got )\n\n# Missing table or column names error.\n!> CREATE TABLE (id INTEGER PRIMARY KEY)\n!> CREATE TABLE name (INTEGER PRIMARY KEY)\n---\nError: invalid input: expected identifier, got (\nError: invalid input: expected identifier, got INTEGER\n\n# Unterminated identifier errors.\n!> CREATE TABLE \"name (id INTEGER PRIMARY KEY)\n---\nError: invalid input: unexpected end of quoted identifier\n"
  },
  {
    "path": "src/sql/testscripts/schema/create_table_datatypes",
    "content": "# Tests CREATE TABLE datatypes.\n\n# Create columns with all datatypes.\n> CREATE TABLE datatypes ( \\\n    id INTEGER PRIMARY KEY, \\\n    \"bool\" BOOL, \\\n    \"boolean\" BOOLEAN, \\\n    \"double\" DOUBLE, \\\n    \"float\" FLOAT, \\\n    \"int\" INT, \\\n    \"integer\" INTEGER, \\\n    \"string\" STRING, \\\n    \"text\" TEXT, \\\n    \"varchar\" VARCHAR \\\n)\nschema\n---\nCREATE TABLE datatypes (\n  id INTEGER PRIMARY KEY,\n  \"bool\" BOOLEAN DEFAULT NULL,\n  \"boolean\" BOOLEAN DEFAULT NULL,\n  \"double\" FLOAT DEFAULT NULL,\n  \"float\" FLOAT DEFAULT NULL,\n  \"int\" INTEGER DEFAULT NULL,\n  \"integer\" INTEGER DEFAULT NULL,\n  \"string\" STRING DEFAULT NULL,\n  \"text\" STRING DEFAULT NULL,\n  \"varchar\" STRING DEFAULT NULL\n)\n\n# Missing or unknown datatype errors.\n!> CREATE TABLE test (id INTEGER PRIMARY KEY, value)\n!> CREATE TABLE test (id INTEGER PRIMARY KEY, value FOO)\n!> CREATE TABLE test (id INTEGER PRIMARY KEY, value INDEX)\n---\nError: invalid input: unexpected token )\nError: invalid input: unexpected token foo\nError: invalid input: unexpected token INDEX\n"
  },
  {
    "path": "src/sql/testscripts/schema/create_table_default",
    "content": "# Tests column defaults.\n\n# All datatypes.\n> CREATE TABLE datatypes ( \\\n    id INT PRIMARY KEY, \\\n    \"bool\" BOOLEAN DEFAULT true, \\\n    \"float\" FLOAT DEFAULT 3.14, \\\n    \"int\" INTEGER DEFAULT 7, \\\n    \"string\" STRING DEFAULT 'foo' \\\n)\nschema datatypes\n---\nCREATE TABLE datatypes (\n  id INTEGER PRIMARY KEY,\n  \"bool\" BOOLEAN DEFAULT TRUE,\n  \"float\" FLOAT DEFAULT 3.14,\n  \"int\" INTEGER DEFAULT 7,\n  \"string\" STRING DEFAULT 'foo'\n)\n\n# Default datatypes must match column. This includes float/integer types.\n!> CREATE TABLE name (id INT PRIMARY KEY, value STRING DEFAULT 7)\n!> CREATE TABLE name (id INT PRIMARY KEY, value INTEGER DEFAULT 3.14)\n!> CREATE TABLE name (id INT PRIMARY KEY, value FLOAT DEFAULT 7)\n---\nError: invalid input: invalid default type INTEGER for STRING column value\nError: invalid input: invalid default type FLOAT for INTEGER column value\nError: invalid input: invalid default type INTEGER for FLOAT column value\n\n# Default values can be expressions.\n> CREATE TABLE expr (id INT PRIMARY KEY, value INT DEFAULT 7 + 3 * 2)\nschema expr\n---\nCREATE TABLE expr (\n  id INTEGER PRIMARY KEY,\n  value INTEGER DEFAULT 13\n)\n\n# NULL is a value default for a nullable column (and is the implicit default).\n> CREATE TABLE \"nullable\" (id INT PRIMARY KEY, value STRING DEFAULT NULL, implicit STRING)\nschema nullable\n---\nCREATE TABLE nullable (\n  id INTEGER PRIMARY KEY,\n  value STRING DEFAULT NULL,\n  implicit STRING DEFAULT NULL\n)\n\n# A NULL default errors for a non-nullable column, including primary keys.\n!> CREATE TABLE name (id INT PRIMARY KEY DEFAULT NULL)\n!> CREATE TABLE name (id INT PRIMARY KEY, value STRING NOT NULL DEFAULT NULL)\n---\nError: invalid input: invalid NULL default for non-nullable column id\nError: invalid input: invalid NULL default for non-nullable column value\n"
  },
  {
    "path": "src/sql/testscripts/schema/create_table_index",
    "content": "# Creating a table with an index only results in a single schema entry (no\n# separate index).\n[ops]> CREATE TABLE indexed (id INTEGER PRIMARY KEY, \"index\" INTEGER INDEX)\n---\nset mvcc:NextVersion → 2 [\"\\x00\" → \"\\x02\"]\nset mvcc:TxnActive(1) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\"]\nset mvcc:TxnWrite(1, sql:Table(indexed)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\xffindexed\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Table(indexed), 1) → CREATE TABLE indexed ( id INTEGER PRIMARY KEY, \"index\" INTEGER DEFAULT NULL INDEX ) [\"\\x04\\x00\\xffindexed\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01 \\x07indexed\\x00\\x02\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x05index\\x01\\x01\\x01\\x00\\x00\\x01\\x00\"]\ndelete mvcc:TxnWrite(1, sql:Table(indexed)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\xffindexed\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnActive(1) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\"]\n\nschema\n---\nCREATE TABLE indexed (\n  id INTEGER PRIMARY KEY,\n  \"index\" INTEGER DEFAULT NULL INDEX\n)\n\n# Explicit indexes can be given for primary keys, foreign keys,\n# and unique columns.\n> CREATE TABLE explicit ( \\\n    id INTEGER PRIMARY KEY INDEX, \\\n    \"unique\" INTEGER UNIQUE INDEX, \\\n    \"reference\" INTEGER REFERENCES indexed INDEX \\\n)\nschema explicit\n---\nCREATE TABLE explicit (\n  id INTEGER PRIMARY KEY,\n  \"unique\" INTEGER DEFAULT NULL UNIQUE INDEX,\n  reference INTEGER DEFAULT NULL INDEX REFERENCES indexed\n)\n"
  },
  {
    "path": "src/sql/testscripts/schema/create_table_names",
    "content": "# Tests CREATE TABLE table and column name validation.\n\n# A couple of valid names.\n> CREATE TABLE a_123 (a_123 INTEGER PRIMARY KEY)\n> CREATE TABLE 表 (身元 INTEGER PRIMARY KEY, 名前 STRING)\nschema\n---\nCREATE TABLE a_123 (\n  a_123 INTEGER PRIMARY KEY\n)\nCREATE TABLE 表 (\n  身元 INTEGER PRIMARY KEY,\n  名前 STRING DEFAULT NULL\n)\n\n# Mixed case is valid, but interpreted as lower case. Quoted identifiers retain\n# their case.\n> CREATE TABLE mIxEd_cAsE (ÄÅÆ STRING PRIMARY KEY)\n> CREATE TABLE \"mIxEd_cAsE\" (\"ÄÅÆ\" STRING PRIMARY KEY)\nschema mixed_case\nschema mIxEd_cAsE\n---\nCREATE TABLE mixed_case (\n  äåæ STRING PRIMARY KEY\n)\nCREATE TABLE mIxEd_cAsE (\n  ÄÅÆ STRING PRIMARY KEY\n)\n\n# Unquoted _, number, keyword, and emoji errors.\n!> CREATE TABLE _name (id INTEGER PRIMARY KEY)\n!> CREATE TABLE 123 (1 INTEGER PRIMARY KEY)\n!> CREATE TABLE table (primary INTEGER PRIMARY KEY)\n!> CREATE TABLE 👋 (🆔 INTEGER PRIMARY KEY)\n---\nError: invalid input: unexpected character _\nError: invalid input: expected identifier, got 123\nError: invalid input: expected identifier, got TABLE\nError: invalid input: unexpected character 👋\n\n# Double quotes allow them.\n> CREATE TABLE \"_name\" (id INTEGER PRIMARY KEY)\n> CREATE TABLE \"123\" (\"1\" INTEGER PRIMARY KEY)\n> CREATE TABLE \"table\" (\"primary\" INTEGER PRIMARY KEY)\n> CREATE TABLE \"👋\" (\"🆔\" INTEGER PRIMARY KEY)\nschema _name 123 table \"👋\"\n---\nCREATE TABLE \"_name\" (\n  id INTEGER PRIMARY KEY\n)\nCREATE TABLE \"123\" (\n  \"1\" INTEGER PRIMARY KEY\n)\nCREATE TABLE \"table\" (\n  \"primary\" INTEGER PRIMARY KEY\n)\nCREATE TABLE \"👋\" (\n  \"🆔\" INTEGER PRIMARY KEY\n)\n\n# \"\" escapes \" in identifiers.\n> CREATE TABLE \"name with \"\"quotes\"\"\" (id INTEGER PRIMARY KEY);\nschema 'name with \"quotes\"'\n---\nCREATE TABLE \"name with \"\"quotes\"\"\" (\n  id INTEGER PRIMARY KEY\n)\n\n# ' are for string literals, not identifiers.\n!> CREATE TABLE 'name' (id INTEGER PRIMARY KEY)\n---\nError: invalid input: expected identifier, got name\n"
  },
  {
    "path": "src/sql/testscripts/schema/create_table_null",
    "content": "# Tests column nullability.\n\n# All datatypes can be nullable. Their default value is NULL.\n> CREATE TABLE datatypes ( \\\n    id INTEGER PRIMARY KEY, \\\n    \"bool\" BOOLEAN NULL, \\\n    \"float\" FLOAT NULL, \\\n    \"int\" INTEGER NULL, \\\n    \"string\" STRING NULL \\\n)\nschema datatypes\n---\nCREATE TABLE datatypes (\n  id INTEGER PRIMARY KEY,\n  \"bool\" BOOLEAN DEFAULT NULL,\n  \"float\" FLOAT DEFAULT NULL,\n  \"int\" INTEGER DEFAULT NULL,\n  \"string\" STRING DEFAULT NULL\n)\n\n# Column can be made explicitly non-nullable.\n> CREATE TABLE non_null (id INTEGER PRIMARY KEY, value STRING NOT NULL)\nschema non_null\n---\nCREATE TABLE non_null (\n  id INTEGER PRIMARY KEY,\n  value STRING NOT NULL\n)\n\n# Column can't be both nullable and non-nullable.\n!> CREATE TABLE test (id INTEGER PRIMARY KEY, value STRING NULL NOT NULL)\n---\nError: invalid input: nullability already set for column value\n"
  },
  {
    "path": "src/sql/testscripts/schema/create_table_primary_key",
    "content": "# Tests primary keys.\n\n# There must be exactly one primary key.\n!> CREATE TABLE \"primary\" (id INTEGER)\n!> CREATE TABLE \"primary\" (id INTEGER PRIMARY KEY, name STRING PRIMARY KEY)\n> CREATE TABLE \"primary\" (id INTEGER PRIMARY KEY)\nschema primary\n---\nError: invalid input: no primary key for table primary\nError: invalid input: multiple primary keys for table primary\nCREATE TABLE \"primary\" (\n  id INTEGER PRIMARY KEY\n)\n\n# The primary key can't be nullable.\n!> CREATE TABLE \"null\" (id INTEGER PRIMARY KEY NULL)\n---\nError: invalid input: primary key id cannot be nullable\n\n# It can have a default value though.\n> CREATE TABLE \"default\" (id INTEGER PRIMARY KEY DEFAULT 1)\n---\nok\n\n# Primary keys can also take all datatypes.\n> CREATE TABLE \"bool\" (id BOOL PRIMARY KEY)\n> CREATE TABLE \"float\" (id FLOAT PRIMARY KEY)\n> CREATE TABLE \"string\" (id STRING PRIMARY KEY)\nschema bool float string\n---\nCREATE TABLE \"bool\" (\n  id BOOLEAN PRIMARY KEY\n)\nCREATE TABLE \"float\" (\n  id FLOAT PRIMARY KEY\n)\nCREATE TABLE \"string\" (\n  id STRING PRIMARY KEY\n)\n"
  },
  {
    "path": "src/sql/testscripts/schema/create_table_reference",
    "content": "# Tests foreign key references during CREATE TABLE.\n\n# Create two reference tables, with int/string primary keys.\n> CREATE TABLE \"ref\" (id INT PRIMARY KEY, value STRING NOT NULL)\n> INSERT INTO \"ref\" VALUES (1, 'a'), (2, 'b')\n---\nok\n\n> CREATE TABLE sref (id STRING PRIMARY KEY, value INTEGER NOT NULL)\n> INSERT INTO sref VALUES ('a', 1), ('b', 2)\n---\nok\n\n# Creating a table with references works. The reference columns get implicit\n# indexes, but only a single schema entity.\n[ops]> CREATE TABLE name (id INT PRIMARY KEY, ref_id INT REFERENCES \"ref\", sref_id STRING REFERENCES sref)\n---\nset mvcc:NextVersion → 6 [\"\\x00\" → \"\\x06\"]\nset mvcc:TxnActive(5) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\"]\nset mvcc:TxnWrite(5, sql:Table(name)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Table(name), 5) → CREATE TABLE name ( id INTEGER PRIMARY KEY, ref_id INTEGER DEFAULT NULL INDEX REFERENCES ref, sref_id STRING DEFAULT NULL INDEX REFERENCES sref ) [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x016\\x04name\\x00\\x03\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x06ref_id\\x01\\x01\\x01\\x00\\x00\\x01\\x01\\x03ref\\x07sref_id\\x03\\x01\\x01\\x00\\x00\\x01\\x01\\x04sref\"]\ndelete mvcc:TxnWrite(5, sql:Table(name)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnActive(5) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\"]\n\nschema name\n---\nCREATE TABLE name (\n  id INTEGER PRIMARY KEY,\n  ref_id INTEGER DEFAULT NULL INDEX REFERENCES ref,\n  sref_id STRING DEFAULT NULL INDEX REFERENCES sref\n)\n\ndump\n---\nmvcc:NextVersion → 6 [\"\\x00\" → \"\\x06\"]\nmvcc:Version(sql:Table(name), 5) → CREATE TABLE name ( id INTEGER PRIMARY KEY, ref_id INTEGER DEFAULT NULL INDEX REFERENCES ref, sref_id STRING DEFAULT NULL INDEX REFERENCES sref ) [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x016\\x04name\\x00\\x03\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x06ref_id\\x01\\x01\\x01\\x00\\x00\\x01\\x01\\x03ref\\x07sref_id\\x03\\x01\\x01\\x00\\x00\\x01\\x01\\x04sref\"]\nmvcc:Version(sql:Table(ref), 1) → CREATE TABLE ref ( id INTEGER PRIMARY KEY, value STRING NOT NULL ) [\"\\x04\\x00\\xffref\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x1b\\x03ref\\x00\\x02\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x05value\\x03\\x00\\x00\\x00\\x00\\x00\"]\nmvcc:Version(sql:Table(sref), 3) → CREATE TABLE sref ( id STRING PRIMARY KEY, value INTEGER NOT NULL ) [\"\\x04\\x00\\xffsref\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x1c\\x04sref\\x00\\x02\\x02id\\x03\\x00\\x00\\x01\\x00\\x00\\x05value\\x01\\x00\\x00\\x00\\x00\\x00\"]\nmvcc:Version(sql:Row(ref, 1), 2) → 1,'a' [\"\\x04\\x02ref\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x02\\x04\\x01a\"]\nmvcc:Version(sql:Row(ref, 2), 2) → 2,'b' [\"\\x04\\x02ref\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x04\\x04\\x01b\"]\nmvcc:Version(sql:Row(sref, 'a'), 4) → 'a',1 [\"\\x04\\x02sref\\x00\\xff\\x00\\xff\\x04a\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x06\\x02\\x04\\x01a\\x02\\x02\"]\nmvcc:Version(sql:Row(sref, 'b'), 4) → 'b',2 [\"\\x04\\x02sref\\x00\\xff\\x00\\xff\\x04b\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x06\\x02\\x04\\x01b\\x02\\x04\"]\n\n# Missing reference table errors.\n!> CREATE TABLE test (id INT PRIMARY KEY, \"ref\" INT REFERENCES missing)\n---\nError: invalid input: unknown table missing referenced by column ref\n\n# Reference type conflicts errors.\n!> CREATE TABLE test (id INT PRIMARY KEY, ref_id FLOAT REFERENCES \"ref\")\n!> CREATE TABLE test (id INT PRIMARY KEY, sref_id INT REFERENCES sref)\n---\nError: invalid input: can't reference INTEGER primary key of ref from FLOAT column ref_id\nError: invalid input: can't reference STRING primary key of sref from INTEGER column sref_id\n\n# Self-references work.\n> CREATE TABLE self (id INT PRIMARY KEY, self_id INT REFERENCES self)\nschema self\n---\nCREATE TABLE self (\n  id INTEGER PRIMARY KEY,\n  self_id INTEGER DEFAULT NULL INDEX REFERENCES self\n)\n"
  },
  {
    "path": "src/sql/testscripts/schema/create_table_transaction",
    "content": "# Tests that CREATE TABLE is transactional.\n\n> BEGIN\n[ops]> CREATE TABLE name (id INT PRIMARY KEY, value STRING)\n---\nset mvcc:TxnWrite(1, sql:Table(name)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Table(name), 1) → CREATE TABLE name ( id INTEGER PRIMARY KEY, value STRING DEFAULT NULL ) [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x1d\\x04name\\x00\\x02\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x05value\\x03\\x01\\x01\\x00\\x00\\x00\\x00\"]\n\nschema name\n---\nCREATE TABLE name (\n  id INTEGER PRIMARY KEY,\n  value STRING DEFAULT NULL\n)\n\ndump\n---\nmvcc:NextVersion → 2 [\"\\x00\" → \"\\x02\"]\nmvcc:TxnActive(1) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\"]\nmvcc:TxnWrite(1, sql:Table(name)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nmvcc:Version(sql:Table(name), 1) → CREATE TABLE name ( id INTEGER PRIMARY KEY, value STRING DEFAULT NULL ) [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x1d\\x04name\\x00\\x02\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x05value\\x03\\x01\\x01\\x00\\x00\\x00\\x00\"]\n\n# Rolling it back undoes it.\n[ops]> ROLLBACK\n---\ndelete mvcc:Version(sql:Table(name), 1) [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\"]\ndelete mvcc:TxnWrite(1, sql:Table(name)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnActive(1) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\"]\n\ndump\n---\nmvcc:NextVersion → 2 [\"\\x00\" → \"\\x02\"]\n\n# Committing a table also works.\n> BEGIN\n> CREATE TABLE name (id INT PRIMARY KEY, value STRING)\n[ops]> COMMIT\n---\ndelete mvcc:TxnWrite(2, sql:Table(name)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnActive(2) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\"]\n\ndump\n---\nmvcc:NextVersion → 3 [\"\\x00\" → \"\\x03\"]\nmvcc:Version(sql:Table(name), 2) → CREATE TABLE name ( id INTEGER PRIMARY KEY, value STRING DEFAULT NULL ) [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x1d\\x04name\\x00\\x02\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x05value\\x03\\x01\\x01\\x00\\x00\\x00\\x00\"]\n"
  },
  {
    "path": "src/sql/testscripts/schema/create_table_unique",
    "content": "# Creating a table with a unique index only results in a single schema entry (no\n# separate index).\n[ops]> CREATE TABLE indexed (id INTEGER PRIMARY KEY, \"index\" INTEGER UNIQUE)\n---\nset mvcc:NextVersion → 2 [\"\\x00\" → \"\\x02\"]\nset mvcc:TxnActive(1) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\"]\nset mvcc:TxnWrite(1, sql:Table(indexed)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\xffindexed\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Table(indexed), 1) → CREATE TABLE indexed ( id INTEGER PRIMARY KEY, \"index\" INTEGER DEFAULT NULL UNIQUE INDEX ) [\"\\x04\\x00\\xffindexed\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01 \\x07indexed\\x00\\x02\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x05index\\x01\\x01\\x01\\x00\\x01\\x01\\x00\"]\ndelete mvcc:TxnWrite(1, sql:Table(indexed)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\xffindexed\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnActive(1) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\"]\n\n# The column gets an implicit secondary index marker.\nschema\n---\nCREATE TABLE indexed (\n  id INTEGER PRIMARY KEY,\n  \"index\" INTEGER DEFAULT NULL UNIQUE INDEX\n)\n\n# Unique indexes work for primary key, foreign key, nullable, non-nullable, and\n# default columns.\n> CREATE TABLE \"unique\" ( \\\n  id INTEGER PRIMARY KEY UNIQUE, \\\n  ref INTEGER REFERENCES indexed UNIQUE, \\\n  nullable INTEGER NULL UNIQUE, \\\n  non_nullable INTEGER NOT NULL UNIQUE, \\\n  \"default\" INTEGER DEFAULT 7 UNIQUE \\\n)\nschema unique\n---\nCREATE TABLE \"unique\" (\n  id INTEGER PRIMARY KEY,\n  ref INTEGER DEFAULT NULL UNIQUE INDEX REFERENCES indexed,\n  nullable INTEGER DEFAULT NULL UNIQUE INDEX,\n  non_nullable INTEGER NOT NULL UNIQUE INDEX,\n  \"default\" INTEGER DEFAULT 7 UNIQUE INDEX\n)\n"
  },
  {
    "path": "src/sql/testscripts/schema/drop_table",
    "content": "# Basic DROP TABLE tests.\n\n> CREATE TABLE name (id INT PRIMARY KEY, value STRING NOT NULL)\n> INSERT INTO name VALUES (1, 'a'), (2, 'b')\n---\nok\n\n# Dropping a simple table works, and removes the schema entry and rows.\n# Also output the trivial plan and statement result.\n[plan,result,ops]> DROP TABLE name\n---\nDropTable: name\nset mvcc:NextVersion → 4 [\"\\x00\" → \"\\x04\"]\nset mvcc:TxnActive(3) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\"]\nset mvcc:TxnWrite(3, sql:Table(name)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Table(name), 3) → None [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x00\"]\nset mvcc:TxnWrite(3, sql:Row(name, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 1), 3) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x00\"]\nset mvcc:TxnWrite(3, sql:Row(name, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 2), 3) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x00\"]\ndelete mvcc:TxnWrite(3, sql:Table(name)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(3, sql:Row(name, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(3, sql:Row(name, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnActive(3) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\"]\nDropTable { name: \"name\", existed: true }\n\nschema\n---\nok\n\ndump\n---\nmvcc:NextVersion → 4 [\"\\x00\" → \"\\x04\"]\nmvcc:Version(sql:Table(name), 1) → CREATE TABLE name ( id INTEGER PRIMARY KEY, value STRING NOT NULL ) [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x1c\\x04name\\x00\\x02\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x05value\\x03\\x00\\x00\\x00\\x00\\x00\"]\nmvcc:Version(sql:Table(name), 3) → None [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x00\"]\nmvcc:Version(sql:Row(name, 1), 2) → 1,'a' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x02\\x04\\x01a\"]\nmvcc:Version(sql:Row(name, 1), 3) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x00\"]\nmvcc:Version(sql:Row(name, 2), 2) → 2,'b' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x04\\x04\\x01b\"]\nmvcc:Version(sql:Row(name, 2), 3) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x00\"]\n\n# Dropping a missing table errors, but not if IF EXISTS is given.\n!> DROP TABLE name\n---\nError: invalid input: table name does not exist\n\n[result,ops]> DROP TABLE IF EXISTS name\n---\nset mvcc:NextVersion → 6 [\"\\x00\" → \"\\x06\"]\nset mvcc:TxnActive(5) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\"]\ndelete mvcc:TxnActive(5) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\"]\nDropTable { name: \"name\", existed: false }\n\n# No table or multiple tables errors.\n!> DROP TABLE\n!> DROP TABLE a, b, c\n---\nError: invalid input: unexpected end of input\nError: invalid input: unexpected token ,\n"
  },
  {
    "path": "src/sql/testscripts/schema/drop_table_index",
    "content": "# Tests that DROP TABLE cleans up secondary indexes of all kinds.\n\n> CREATE TABLE \"ref\" (id INT PRIMARY KEY, value STRING NOT NULL)\n> INSERT INTO \"ref\" VALUES (1, 'a'), (2, 'b')\n---\nok\n\n> CREATE TABLE name ( \\\n  id INT PRIMARY KEY, \\\n  \"index\" STRING, \\\n  \"unique\" INT UNIQUE NOT NULL, \\\n  ref_id INT REFERENCES \"ref\" \\\n)\n> INSERT INTO name VALUES (1, 'foo', 1, 1)\n> INSERT INTO name VALUES (2, 'bar', 2, 2)\n> INSERT INTO name VALUES (3, 'foo', 3, NULL)\n> INSERT INTO name VALUES (4, NULL, 4, 2)\n---\nok\n\ndump\n---\nmvcc:NextVersion → 8 [\"\\x00\" → \"\\x08\"]\nmvcc:Version(sql:Table(name), 3) → CREATE TABLE name ( id INTEGER PRIMARY KEY, \"index\" STRING DEFAULT NULL, \"unique\" INTEGER NOT NULL UNIQUE INDEX, ref_id INTEGER DEFAULT NULL INDEX REFERENCES ref ) [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01<\\x04name\\x00\\x04\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x05index\\x03\\x01\\x01\\x00\\x00\\x00\\x00\\x06unique\\x01\\x00\\x00\\x01\\x01\\x00\\x06ref_id\\x01\\x01\\x01\\x00\\x00\\x01\\x01\\x03ref\"]\nmvcc:Version(sql:Table(ref), 1) → CREATE TABLE ref ( id INTEGER PRIMARY KEY, value STRING NOT NULL ) [\"\\x04\\x00\\xffref\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x1b\\x03ref\\x00\\x02\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x05value\\x03\\x00\\x00\\x00\\x00\\x00\"]\nmvcc:Version(sql:Index(name.ref_id, NULL), 6) → 3 [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\x03\\x01\\x02\\x06\"]\nmvcc:Version(sql:Index(name.ref_id, 1), 4) → 1 [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nmvcc:Version(sql:Index(name.ref_id, 2), 5) → 2 [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nmvcc:Version(sql:Index(name.ref_id, 2), 7) → 2,4 [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x05\\x02\\x02\\x04\\x02\\x08\"]\nmvcc:Version(sql:Index(name.unique, 1), 4) → 1 [\"\\x04\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nmvcc:Version(sql:Index(name.unique, 2), 5) → 2 [\"\\x04\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nmvcc:Version(sql:Index(name.unique, 3), 6) → 3 [\"\\x04\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\x03\\x01\\x02\\x06\"]\nmvcc:Version(sql:Index(name.unique, 4), 7) → 4 [\"\\x04\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x03\\x01\\x02\\x08\"]\nmvcc:Version(sql:Row(name, 1), 4) → 1,'foo',1,1 [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x0c\\x04\\x02\\x02\\x04\\x03foo\\x02\\x02\\x02\\x02\"]\nmvcc:Version(sql:Row(name, 2), 5) → 2,'bar',2,2 [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x01\\x0c\\x04\\x02\\x04\\x04\\x03bar\\x02\\x04\\x02\\x04\"]\nmvcc:Version(sql:Row(name, 3), 6) → 3,'foo',3,NULL [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\x0b\\x04\\x02\\x06\\x04\\x03foo\\x02\\x06\\x00\"]\nmvcc:Version(sql:Row(name, 4), 7) → 4,NULL,4,2 [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x08\\x04\\x02\\x08\\x00\\x02\\x08\\x02\\x04\"]\nmvcc:Version(sql:Row(ref, 1), 2) → 1,'a' [\"\\x04\\x02ref\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x02\\x04\\x01a\"]\nmvcc:Version(sql:Row(ref, 2), 2) → 2,'b' [\"\\x04\\x02ref\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x04\\x04\\x01b\"]\n\n# Dropping the table deletes all index entries.\n[ops]> DROP TABLE name\n> DROP TABLE ref\n---\nset mvcc:NextVersion → 9 [\"\\x00\" → \"\\t\"]\nset mvcc:TxnActive(8) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\"]\nset mvcc:TxnWrite(8, sql:Table(name)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Table(name), 8) → None [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nset mvcc:TxnWrite(8, sql:Row(name, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 1), 8) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nset mvcc:TxnWrite(8, sql:Row(name, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 2), 8) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nset mvcc:TxnWrite(8, sql:Row(name, 3)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 3), 8) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nset mvcc:TxnWrite(8, sql:Row(name, 4)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 4), 8) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nset mvcc:TxnWrite(8, sql:Index(name.unique, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.unique, 1), 8) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nset mvcc:TxnWrite(8, sql:Index(name.unique, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.unique, 2), 8) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nset mvcc:TxnWrite(8, sql:Index(name.unique, 3)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.unique, 3), 8) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nset mvcc:TxnWrite(8, sql:Index(name.unique, 4)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.unique, 4), 8) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nset mvcc:TxnWrite(8, sql:Index(name.ref_id, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.ref_id, NULL), 8) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nset mvcc:TxnWrite(8, sql:Index(name.ref_id, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.ref_id, 1), 8) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nset mvcc:TxnWrite(8, sql:Index(name.ref_id, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.ref_id, 2), 8) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Table(name)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Index(name.ref_id, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Index(name.ref_id, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Index(name.ref_id, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Index(name.unique, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Index(name.unique, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Index(name.unique, 3)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Index(name.unique, 4)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Row(name, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Row(name, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Row(name, 3)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Row(name, 4)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\"]\ndelete mvcc:TxnActive(8) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\"]\n\ndump\n---\nmvcc:NextVersion → 10 [\"\\x00\" → \"\\n\"]\nmvcc:Version(sql:Table(name), 3) → CREATE TABLE name ( id INTEGER PRIMARY KEY, \"index\" STRING DEFAULT NULL, \"unique\" INTEGER NOT NULL UNIQUE INDEX, ref_id INTEGER DEFAULT NULL INDEX REFERENCES ref ) [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01<\\x04name\\x00\\x04\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x05index\\x03\\x01\\x01\\x00\\x00\\x00\\x00\\x06unique\\x01\\x00\\x00\\x01\\x01\\x00\\x06ref_id\\x01\\x01\\x01\\x00\\x00\\x01\\x01\\x03ref\"]\nmvcc:Version(sql:Table(name), 8) → None [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nmvcc:Version(sql:Table(ref), 1) → CREATE TABLE ref ( id INTEGER PRIMARY KEY, value STRING NOT NULL ) [\"\\x04\\x00\\xffref\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x1b\\x03ref\\x00\\x02\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x05value\\x03\\x00\\x00\\x00\\x00\\x00\"]\nmvcc:Version(sql:Table(ref), 9) → None [\"\\x04\\x00\\xffref\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x00\"]\nmvcc:Version(sql:Index(name.ref_id, NULL), 6) → 3 [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\x03\\x01\\x02\\x06\"]\nmvcc:Version(sql:Index(name.ref_id, NULL), 8) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nmvcc:Version(sql:Index(name.ref_id, 1), 4) → 1 [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nmvcc:Version(sql:Index(name.ref_id, 1), 8) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nmvcc:Version(sql:Index(name.ref_id, 2), 5) → 2 [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nmvcc:Version(sql:Index(name.ref_id, 2), 7) → 2,4 [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x05\\x02\\x02\\x04\\x02\\x08\"]\nmvcc:Version(sql:Index(name.ref_id, 2), 8) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nmvcc:Version(sql:Index(name.unique, 1), 4) → 1 [\"\\x04\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nmvcc:Version(sql:Index(name.unique, 1), 8) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nmvcc:Version(sql:Index(name.unique, 2), 5) → 2 [\"\\x04\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nmvcc:Version(sql:Index(name.unique, 2), 8) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nmvcc:Version(sql:Index(name.unique, 3), 6) → 3 [\"\\x04\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\x03\\x01\\x02\\x06\"]\nmvcc:Version(sql:Index(name.unique, 3), 8) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nmvcc:Version(sql:Index(name.unique, 4), 7) → 4 [\"\\x04\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x03\\x01\\x02\\x08\"]\nmvcc:Version(sql:Index(name.unique, 4), 8) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nmvcc:Version(sql:Row(name, 1), 4) → 1,'foo',1,1 [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x0c\\x04\\x02\\x02\\x04\\x03foo\\x02\\x02\\x02\\x02\"]\nmvcc:Version(sql:Row(name, 1), 8) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nmvcc:Version(sql:Row(name, 2), 5) → 2,'bar',2,2 [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x01\\x0c\\x04\\x02\\x04\\x04\\x03bar\\x02\\x04\\x02\\x04\"]\nmvcc:Version(sql:Row(name, 2), 8) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nmvcc:Version(sql:Row(name, 3), 6) → 3,'foo',3,NULL [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\x0b\\x04\\x02\\x06\\x04\\x03foo\\x02\\x06\\x00\"]\nmvcc:Version(sql:Row(name, 3), 8) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nmvcc:Version(sql:Row(name, 4), 7) → 4,NULL,4,2 [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x08\\x04\\x02\\x08\\x00\\x02\\x08\\x02\\x04\"]\nmvcc:Version(sql:Row(name, 4), 8) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nmvcc:Version(sql:Row(ref, 1), 2) → 1,'a' [\"\\x04\\x02ref\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x02\\x04\\x01a\"]\nmvcc:Version(sql:Row(ref, 1), 9) → None [\"\\x04\\x02ref\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x00\"]\nmvcc:Version(sql:Row(ref, 2), 2) → 2,'b' [\"\\x04\\x02ref\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x04\\x04\\x01b\"]\nmvcc:Version(sql:Row(ref, 2), 9) → None [\"\\x04\\x02ref\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x00\"]\n"
  },
  {
    "path": "src/sql/testscripts/schema/drop_table_ref",
    "content": "# Tests DROP TABLE with references.\n\n# Create a reference table and foreign key table.\n> CREATE TABLE \"ref\" (id INT PRIMARY KEY)\n> CREATE TABLE name (id INT PRIMARY KEY, ref_if INT REFERENCES \"ref\")\n---\nok\n\n# Dropping a table with a foreign key reference to it errors.\n!> DROP TABLE \"ref\"\n---\nError: invalid input: table ref is referenced from name.ref_if\n\n# But it works if the source table is dropped first.\n> DROP TABLE name\n> DROP TABLE \"ref\"\n---\nok\n\n# Dropping a table with a self reference also works.\n> CREATE TABLE self (id INT PRIMARY KEY, self_id INT REFERENCES self)\n---\nok\n\n> DROP TABLE self\n---\nok\n\nschema\n---\nok\n"
  },
  {
    "path": "src/sql/testscripts/schema/drop_table_transaction",
    "content": "# Tests that DROP TABLE is transactional.\n\n> CREATE TABLE name (id INT PRIMARY KEY, value STRING)\n> INSERT INTO name VALUES (1, 'a'), (2, 'b')\n---\nok\n\ndump\n---\nmvcc:NextVersion → 3 [\"\\x00\" → \"\\x03\"]\nmvcc:Version(sql:Table(name), 1) → CREATE TABLE name ( id INTEGER PRIMARY KEY, value STRING DEFAULT NULL ) [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x1d\\x04name\\x00\\x02\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x05value\\x03\\x01\\x01\\x00\\x00\\x00\\x00\"]\nmvcc:Version(sql:Row(name, 1), 2) → 1,'a' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x02\\x04\\x01a\"]\nmvcc:Version(sql:Row(name, 2), 2) → 2,'b' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x04\\x04\\x01b\"]\n\n# Drop the table in a transaction.\n> BEGIN\n[ops]> DROP TABLE name\n---\nset mvcc:TxnWrite(3, sql:Table(name)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Table(name), 3) → None [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x00\"]\nset mvcc:TxnWrite(3, sql:Row(name, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 1), 3) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x00\"]\nset mvcc:TxnWrite(3, sql:Row(name, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 2), 3) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x00\"]\n\nschema\n---\nok\n\ndump\n---\nmvcc:NextVersion → 4 [\"\\x00\" → \"\\x04\"]\nmvcc:TxnActive(3) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\"]\nmvcc:TxnWrite(3, sql:Table(name)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nmvcc:TxnWrite(3, sql:Row(name, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nmvcc:TxnWrite(3, sql:Row(name, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nmvcc:Version(sql:Table(name), 1) → CREATE TABLE name ( id INTEGER PRIMARY KEY, value STRING DEFAULT NULL ) [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x1d\\x04name\\x00\\x02\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x05value\\x03\\x01\\x01\\x00\\x00\\x00\\x00\"]\nmvcc:Version(sql:Table(name), 3) → None [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x00\"]\nmvcc:Version(sql:Row(name, 1), 2) → 1,'a' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x02\\x04\\x01a\"]\nmvcc:Version(sql:Row(name, 1), 3) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x00\"]\nmvcc:Version(sql:Row(name, 2), 2) → 2,'b' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x04\\x04\\x01b\"]\nmvcc:Version(sql:Row(name, 2), 3) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x00\"]\n\n# Rolling it back undoes it.\n[ops]> ROLLBACK\n---\ndelete mvcc:Version(sql:Table(name), 3) [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\"]\ndelete mvcc:TxnWrite(3, sql:Table(name)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:Version(sql:Row(name, 1), 3) [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\"]\ndelete mvcc:TxnWrite(3, sql:Row(name, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:Version(sql:Row(name, 2), 3) [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\"]\ndelete mvcc:TxnWrite(3, sql:Row(name, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnActive(3) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\"]\n\ndump\n---\nmvcc:NextVersion → 4 [\"\\x00\" → \"\\x04\"]\nmvcc:Version(sql:Table(name), 1) → CREATE TABLE name ( id INTEGER PRIMARY KEY, value STRING DEFAULT NULL ) [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x1d\\x04name\\x00\\x02\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x05value\\x03\\x01\\x01\\x00\\x00\\x00\\x00\"]\nmvcc:Version(sql:Row(name, 1), 2) → 1,'a' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x02\\x04\\x01a\"]\nmvcc:Version(sql:Row(name, 2), 2) → 2,'b' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x04\\x04\\x01b\"]\n\n# Committing the drop also works.\n> BEGIN\n> DROP TABLE name\n[ops]> COMMIT\n---\ndelete mvcc:TxnWrite(4, sql:Table(name)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(4, sql:Row(name, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(4, sql:Row(name, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnActive(4) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\"]\n\ndump\n---\nmvcc:NextVersion → 5 [\"\\x00\" → \"\\x05\"]\nmvcc:Version(sql:Table(name), 1) → CREATE TABLE name ( id INTEGER PRIMARY KEY, value STRING DEFAULT NULL ) [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x1d\\x04name\\x00\\x02\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x05value\\x03\\x01\\x01\\x00\\x00\\x00\\x00\"]\nmvcc:Version(sql:Table(name), 4) → None [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x00\"]\nmvcc:Version(sql:Row(name, 1), 2) → 1,'a' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x02\\x04\\x01a\"]\nmvcc:Version(sql:Row(name, 1), 4) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x00\"]\nmvcc:Version(sql:Row(name, 2), 2) → 2,'b' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x04\\x04\\x01b\"]\nmvcc:Version(sql:Row(name, 2), 4) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x00\"]\n"
  },
  {
    "path": "src/sql/testscripts/transactions/anomaly_dirty_read",
    "content": "# A dirty read is when c2 can read an uncommitted value set by c1. Snapshot\n# isolation prevents this.\n\n> CREATE TABLE test (id INT PRIMARY KEY, value STRING)\n---\nok\n\nc1:> BEGIN\nc1:> INSERT INTO test VALUES (1, 'a')\n---\nok\n\nc2:> BEGIN\nc2:> SELECT * FROM test WHERE id = 1\n---\nok\n"
  },
  {
    "path": "src/sql/testscripts/transactions/anomaly_dirty_write",
    "content": "# A dirty write is when c2 overwrites an uncommitted value written by c1.\n# Snapshot isolation prevents this.\n\n> CREATE TABLE test (id INT PRIMARY KEY, value STRING)\n---\nok\n\nc1:> BEGIN\nc1:> INSERT INTO test VALUES (1, 'a')\n---\nok\n\nc2:> BEGIN\nc2:!> INSERT INTO test VALUES (1, 'a')\n---\nc2: Error: serialization failure, retry transaction\n"
  },
  {
    "path": "src/sql/testscripts/transactions/anomaly_fuzzy_read",
    "content": "# A fuzzy (or unrepeatable) read is when c2 sees a value change after c1\n# updates it. Snapshot isolation prevents this.\n\n> CREATE TABLE test (id INT PRIMARY KEY, value STRING)\n> INSERT INTO test VALUES (1, 'a')\n---\nok\n\nc1:> BEGIN\nc2:> BEGIN\n---\nok\n\nc2:> SELECT * FROM test WHERE id = 1\n---\nc2: 1, 'a'\n\nc1:> UPDATE test SET value = 'b' WHERE id = 1\nc1:> COMMIT\nc1:> SELECT * FROM test\n---\nc1: 1, 'b'\n\nc2:> SELECT * FROM test WHERE id = 1\n---\nc2: 1, 'a'\n"
  },
  {
    "path": "src/sql/testscripts/transactions/anomaly_lost_update",
    "content": "# A lost update is when c1 and c2 both read a value and update it, where\n# c2's update replaces c1. Snapshot isolation prevents this.\n\n> CREATE TABLE test (id INT PRIMARY KEY, value STRING)\n---\nok\n\n\nc1:> BEGIN\nc1:> SELECT * FROM test WHERE id = 1\n---\nok\n\nc2:> BEGIN\nc2:> SELECT * FROM test WHERE id = 1\n---\nok\n\nc1:> INSERT INTO test VALUES (1, 'a')\nc1:> COMMIT\n---\nok\n\nc2:!> INSERT INTO test VALUES (1, 'a')\n---\nc2: Error: serialization failure, retry transaction\n"
  },
  {
    "path": "src/sql/testscripts/transactions/anomaly_phantom_read",
    "content": "# A phantom read is when c1 reads entries matching some predicate, but a\n# modification by c2 changes which entries match the predicate such that a later\n# read by c1 returns them. Snapshot isolation prevents this.\n\n> CREATE TABLE test (id INT PRIMARY KEY, value STRING)\n> INSERT INTO test VALUES (1, 'a'), (2, 'b'), (3, 'c')\n---\nok\n\nc1:> BEGIN\nc2:> BEGIN\n---\nok\n\nc1:> SELECT * FROM test WHERE id > 1\n---\nc1: 2, 'b'\nc1: 3, 'c'\n\nc2:> DELETE FROM test WHERE id = 2\nc2:> INSERT INTO test VALUES (4, 'd')\nc2:> COMMIT\n---\nok\n\nc1:> SELECT * FROM test WHERE id > 1\n---\nc1: 2, 'b'\nc1: 3, 'c'\n"
  },
  {
    "path": "src/sql/testscripts/transactions/anomaly_read_skew",
    "content": "# Read skew is when c1 reads a and b, but c2 modifies b in between the\n# reads. Snapshot isolation prevents this.\n\n> CREATE TABLE test (id INT PRIMARY KEY, value STRING)\n> INSERT INTO test VALUES (1, 'a'), (2, 'b')\n---\nok\n\nc1:> BEGIN\nc2:> BEGIN\n---\nok\n\nc1:> SELECT * FROM test WHERE id = 1\n---\nc1: 1, 'a'\n\nc2:> UPDATE test SET value = 'b' WHERE id = 1\nc2:> UPDATE test SET value = 'a' WHERE id = 2\nc2:> COMMIT\n---\nok\n\nc1:> SELECT * FROM test WHERE id = 2\n---\nc1: 2, 'b'\n"
  },
  {
    "path": "src/sql/testscripts/transactions/anomaly_write_skew",
    "content": "# Write skew is when c1 reads a and writes it to b while c2 reads b and writes\n# it to a. Snapshot isolation does not prevent this, which is expected, so we\n# assert the anomalous behavior. Fixing this would require implementing\n# serializable snapshot isolation.\n\n> CREATE TABLE test (id INT PRIMARY KEY, value STRING)\n> INSERT INTO test VALUES (1, 'a'), (2, 'b')\n---\nok\n\nc1:> BEGIN\nc2:> BEGIN\n---\nok\n\nc1:> SELECT * FROM test WHERE id = 1\nc2:> SELECT * FROM test WHERE id = 2\n---\nc1: 1, 'a'\nc2: 2, 'b'\n\nc1:> UPDATE test SET value = 'a' WHERE id = 2\nc2:> UPDATE test SET value = 'b' WHERE id = 1\n---\nok\n\nc1:> COMMIT\nc2:> COMMIT\n---\nok\n\n> SELECT * FROM test\n---\n1, 'b'\n2, 'a'\n"
  },
  {
    "path": "src/sql/testscripts/transactions/begin",
    "content": "# Tests BEGIN.\n\n> CREATE TABLE test (id INT PRIMARY KEY, value STRING)\n> INSERT INTO test VALUES (0, '')\n---\nok\n\n# BEGIN starts a new transaction. It bumps NextVersion and writes a TxnActive\n# record for itself.\nc1:[result,ops]> BEGIN\n---\nc1: set mvcc:NextVersion → 4 [\"\\x00\" → \"\\x04\"]\nc1: set mvcc:TxnActive(3) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\"]\nc1: Begin(TransactionState { version: 3, read_only: false, active: {} })\n\n# Starting another transaction for c1 errors.\nc1:!> BEGIN\nc1:!> BEGIN READ ONLY\n---\nc1: Error: invalid input: already in a transaction\nc1: Error: invalid input: already in a transaction\n\n# Another client can begin a concurrent transaction, capturing c1's version in\n# its active set. The active snapshot is persisted to storage.\nc2:[result,ops]> BEGIN\n---\nc2: set mvcc:NextVersion → 5 [\"\\x00\" → \"\\x05\"]\nc2: set mvcc:TxnActiveSnapshot(4) → {3} [\"\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x03\"]\nc2: set mvcc:TxnActive(4) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\"]\nc2: Begin(TransactionState { version: 4, read_only: false, active: {3} })\n\n# A read-only transaction doesn't allocate a new version, and doesn't perform\n# any storage engine writes. It does capture an active set though, and it can't\n# perform any writes.\nc3:[result,ops]> BEGIN READ ONLY\nc3:!> INSERT INTO test VALUES (0, '')\nc3:> ROLLBACK\n---\nc3: Begin(TransactionState { version: 5, read_only: true, active: {3, 4} })\nc3: Error: invalid input: primary key 0 already exists\n\n# c1 writes a value and commits.\nc1:> INSERT INTO test VALUES (1, 'a')\nc1:> COMMIT\n---\nok\n\n# A transaction as of version 1 doesn't see anything, since the\n# table was created in this version.\nc3:[result,ops]> BEGIN READ ONLY AS OF SYSTEM TIME 1\nc3:!> SELECT * FROM test\nc3:> ROLLBACK\n---\nc3: Begin(TransactionState { version: 1, read_only: true, active: {} })\nc3: Error: invalid input: table test does not exist\n\n# It sees the table at version 2, but no rows. The row is visible\n# at version 3, but not c1's write which was committed at the end\n# of version 3.\nc3:[result,ops]> BEGIN READ ONLY AS OF SYSTEM TIME 2\nc3:> SELECT * FROM test\nc3:> ROLLBACK\n---\nc3: Begin(TransactionState { version: 2, read_only: true, active: {} })\n\nc3:[result,ops]> BEGIN READ ONLY AS OF SYSTEM TIME 3\nc3:> SELECT * FROM test\nc3:> ROLLBACK\n---\nc3: Begin(TransactionState { version: 3, read_only: true, active: {} })\nc3: 0, ''\n\n# At version 4, we inherit c2's active set which excludes c1, and still can't\n# see c1's write.\nc3:[result,ops]> BEGIN READ ONLY AS OF SYSTEM TIME 4\nc3:> SELECT * FROM test\nc3:> ROLLBACK\n---\nc3: Begin(TransactionState { version: 4, read_only: true, active: {3} })\nc3: 0, ''\n"
  },
  {
    "path": "src/sql/testscripts/transactions/commit",
    "content": "# Tests COMMIT.\n\n> CREATE TABLE test (id INT PRIMARY KEY, value STRING)\n---\nok\n\n# A commit removes the TxnActive record and its TxnWrite records.\n[ops,result]> BEGIN\n[ops,result]> INSERT INTO test VALUES (1, 'a'), (2, 'b')\n[ops,result]> COMMIT\n---\nset mvcc:NextVersion → 3 [\"\\x00\" → \"\\x03\"]\nset mvcc:TxnActive(2) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\"]\nBegin(TransactionState { version: 2, read_only: false, active: {} })\nset mvcc:TxnWrite(2, sql:Row(test, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x02test\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(test, 1), 2) → 1,'a' [\"\\x04\\x02test\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x02\\x04\\x01a\"]\nset mvcc:TxnWrite(2, sql:Row(test, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x02test\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(test, 2), 2) → 2,'b' [\"\\x04\\x02test\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x04\\x04\\x01b\"]\nInsert { count: 2 }\ndelete mvcc:TxnWrite(2, sql:Row(test, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x02test\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(2, sql:Row(test, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x02test\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnActive(2) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\"]\nCommit { version: 2 }\n\n# A later transaction can see its writes.\nc1:> SELECT * FROM test\n---\nc1: 1, 'a'\nc1: 2, 'b'\n\n# If there are concurrent transactions, it does not remove the TxnActiveSnapshot.\nc1:> BEGIN\n---\nok\n\nc2:[ops,result]> BEGIN\nc2:[ops,result]> COMMIT\n---\nc2: set mvcc:NextVersion → 5 [\"\\x00\" → \"\\x05\"]\nc2: set mvcc:TxnActiveSnapshot(4) → {3} [\"\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x03\"]\nc2: set mvcc:TxnActive(4) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\"]\nc2: Begin(TransactionState { version: 4, read_only: false, active: {3} })\nc2: delete mvcc:TxnActive(4) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\"]\nc2: Commit { version: 4 }\n\n# Commit errors when there's no open transaction.\n!> COMMIT\n---\nError: invalid input: not in a transaction\n"
  },
  {
    "path": "src/sql/testscripts/transactions/isolation",
    "content": "# Tests transaction isolation.\n#\n# Transactions are tested more thoroughly in the MVCC tests, this just does some\n# basic SQL-level testing.\n#\n# Sets up a sequence of transactions that each perform a write, and checks\n# what they can see.\n#\n# c1: past, committed before c4 began\n# c2: past, commits after c4 began\n# c3: past, uncommitted\n# c4: test transaction\n# c5: future, committed\n# c6: future, uncommitted\n# c7: future, AS OF version 4\n\n# c1: past, committed before c4 began\nc1:> BEGIN\nc1:> CREATE TABLE test (id INT PRIMARY KEY, value STRING)\nc1:> INSERT INTO test VALUES (1, 'a')\nc1:> COMMIT\n---\nok\n\n# c2: past, commits after c4 began\nc2:> BEGIN\nc2:> INSERT INTO test VALUES (2, 'b')\n---\nok\n\n# c3: past, uncommitted\nc3:> BEGIN\nc3:> INSERT INTO test VALUES (3, 'c')\n---\nok\n\n# c4: test transaction\nc4:[result]> BEGIN\nc4:> INSERT INTO test VALUES (4, 'd')\n---\nc4: Begin(TransactionState { version: 4, read_only: false, active: {2, 3} })\n\n# Commit c2.\nc2:> COMMIT\n---\nok\n\n# c5: future, committed\nc5:> BEGIN\nc5:> INSERT INTO test VALUES (5, 'e')\nc5:> COMMIT\n---\nok\n\n# c6: future, uncommitted\nc6:> BEGIN\nc6:> INSERT INTO test VALUES (6, 'f')\n---\nok\n\n# When c4 scans, it should only see the write of c1 and itself.\nc4:> SELECT * FROM test\n---\nc4: 1, 'a'\nc4: 4, 'd'\n\n# An AS OF transaction in version 4 should not see c4's uncomitted write.\nc7:> BEGIN READ ONLY AS OF SYSTEM TIME 4\nc7:> SELECT * FROM test\nc7:> ROLLBACK\n---\nc7: 1, 'a'\n\n# c4 can commit.\nc4:> COMMIT\n---\nok\n\n# An implicit transaction should see c1, c2, c4, c5:\n> SELECT * FROM test\n---\n1, 'a'\n2, 'b'\n4, 'd'\n5, 'e'\n\n# An AS OF transaction in version 4 should not see c4's write even after it\n# has committed, such that it's consistent with the previous AS OF 4. The\n# snapshot is taken out at the start of the version.\nc7:> BEGIN READ ONLY AS OF SYSTEM TIME 4\nc7:> SELECT * FROM test\nc7:> ROLLBACK\n---\nc7: 1, 'a'\n"
  },
  {
    "path": "src/sql/testscripts/transactions/rollback",
    "content": "# Tests ROLLBACK.\n\n> CREATE TABLE test (id INT PRIMARY KEY, value STRING)\n---\nok\n\n# A rollback removes the row, TxnActive record and its TxnWrite records.\n[ops,result]> BEGIN\n[ops,result]> INSERT INTO test VALUES (1, 'a'), (2, 'b')\n[ops,result]> ROLLBACK\n---\nset mvcc:NextVersion → 3 [\"\\x00\" → \"\\x03\"]\nset mvcc:TxnActive(2) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\"]\nBegin(TransactionState { version: 2, read_only: false, active: {} })\nset mvcc:TxnWrite(2, sql:Row(test, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x02test\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(test, 1), 2) → 1,'a' [\"\\x04\\x02test\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x02\\x04\\x01a\"]\nset mvcc:TxnWrite(2, sql:Row(test, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x02test\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(test, 2), 2) → 2,'b' [\"\\x04\\x02test\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x04\\x04\\x01b\"]\nInsert { count: 2 }\ndelete mvcc:Version(sql:Row(test, 1), 2) [\"\\x04\\x02test\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\"]\ndelete mvcc:TxnWrite(2, sql:Row(test, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x02test\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:Version(sql:Row(test, 2), 2) [\"\\x04\\x02test\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\"]\ndelete mvcc:TxnWrite(2, sql:Row(test, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x02test\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnActive(2) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\"]\nRollback { version: 2 }\n\n# A later transaction can't see its writes.\nc1:> SELECT * FROM test\n---\nok\n\n# If there are concurrent transactions, it does not remove the\n# TxnActiveSnapshot. This is needed for consistent AS OF queries.\nc1:> BEGIN\n---\nok\n\nc2:[ops,result]> BEGIN\nc2:[ops,result]> ROLLBACK\n---\nc2: set mvcc:NextVersion → 5 [\"\\x00\" → \"\\x05\"]\nc2: set mvcc:TxnActiveSnapshot(4) → {3} [\"\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x03\"]\nc2: set mvcc:TxnActive(4) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\"]\nc2: Begin(TransactionState { version: 4, read_only: false, active: {3} })\nc2: delete mvcc:TxnActive(4) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\"]\nc2: Rollback { version: 4 }\n\n# Rollback errors when there's no open transaction.\n!> ROLLBACK\n---\nError: invalid input: not in a transaction\n"
  },
  {
    "path": "src/sql/testscripts/transactions/schema",
    "content": "# Tests that schema changes are transactional.\n\nc1:> BEGIN\nc1:[ops]> CREATE TABLE test (id INT PRIMARY KEY, value STRING)\nc1:> SELECT * FROM test\n---\nc1: set mvcc:TxnWrite(1, sql:Table(test)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\xfftest\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nc1: set mvcc:Version(sql:Table(test), 1) → CREATE TABLE test ( id INTEGER PRIMARY KEY, value STRING DEFAULT NULL ) [\"\\x04\\x00\\xfftest\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x1d\\x04test\\x00\\x02\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x05value\\x03\\x01\\x01\\x00\\x00\\x00\\x00\"]\n\n# A concurrent transaction can't see the uncommitted table.\nc2:!> SELECT * FROM test\n---\nc2: Error: invalid input: table test does not exist\n\n# Rolling back the transaction removes the table.\nc1:[ops]> ROLLBACK\n---\nc1: delete mvcc:Version(sql:Table(test), 1) [\"\\x04\\x00\\xfftest\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\"]\nc1: delete mvcc:TxnWrite(1, sql:Table(test)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\xfftest\\x00\\xff\\x00\\xff\\x00\\x00\"]\nc1: delete mvcc:TxnActive(1) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\"]\n\nc1:!> SELECT * FROM test\nc2:!> SELECT * FROM test\n---\nc1: Error: invalid input: table test does not exist\nc2: Error: invalid input: table test does not exist\n\n# Committing a transaction does reveal the table.\nc1:> BEGIN\nc1:[ops]> CREATE TABLE test (id INT PRIMARY KEY, value STRING)\n---\nc1: set mvcc:TxnWrite(2, sql:Table(test)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\xfftest\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nc1: set mvcc:Version(sql:Table(test), 2) → CREATE TABLE test ( id INTEGER PRIMARY KEY, value STRING DEFAULT NULL ) [\"\\x04\\x00\\xfftest\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x1d\\x04test\\x00\\x02\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x05value\\x03\\x01\\x01\\x00\\x00\\x00\\x00\"]\n\nc2:!> SELECT * FROM test\n---\nc2: Error: invalid input: table test does not exist\n\nc1:[ops]> COMMIT\n---\nc1: delete mvcc:TxnWrite(2, sql:Table(test)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\xfftest\\x00\\xff\\x00\\xff\\x00\\x00\"]\nc1: delete mvcc:TxnActive(2) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\"]\n\nc2:> SELECT * FROM test\n---\nok\n"
  },
  {
    "path": "src/sql/testscripts/writes/delete",
    "content": "# Tests basic DELETE.\n\n# Insert some data into a table. We'll use transactions to avoid\n# \n> CREATE TABLE name (id INT PRIMARY KEY, value STRING)\n> INSERT INTO name VALUES (1, 'a'), (2, 'b'), (3, 'c')\n---\nok\n\n# Deleting from a table works. Use a transaction to keep the fixture.\n> BEGIN\n[plan,ops]> DELETE FROM name\n---\nDelete: name\n└─ Scan: name\nset mvcc:TxnWrite(3, sql:Row(name, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 1), 3) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x00\"]\nset mvcc:TxnWrite(3, sql:Row(name, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 2), 3) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x00\"]\nset mvcc:TxnWrite(3, sql:Row(name, 3)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 3), 3) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x00\"]\n\n> SELECT * FROM name\n> ROLLBACK\n---\nok\n\n# Deleting without a table, or with a missing or multiple tables errors.\n!> DELETE\n!> DELETE FROM\n!> DELETE FROM missing\n!> DELETE FROM name, foo\n---\nError: invalid input: unexpected end of input\nError: invalid input: unexpected end of input\nError: invalid input: table missing does not exist\nError: invalid input: unexpected token ,\n\n# Deleting in an implicit transaction works, and deletes.\n[ops]> DELETE FROM name\n---\nset mvcc:NextVersion → 6 [\"\\x00\" → \"\\x06\"]\nset mvcc:TxnActive(5) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\"]\nset mvcc:TxnWrite(5, sql:Row(name, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 1), 5) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x00\"]\nset mvcc:TxnWrite(5, sql:Row(name, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 2), 5) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x00\"]\nset mvcc:TxnWrite(5, sql:Row(name, 3)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 3), 5) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x00\"]\ndelete mvcc:TxnWrite(5, sql:Row(name, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(5, sql:Row(name, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnWrite(5, sql:Row(name, 3)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\"]\ndelete mvcc:TxnActive(5) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\"]\n\n> SELECT * FROM name\n---\nok\n\ndump\n---\nmvcc:NextVersion → 6 [\"\\x00\" → \"\\x06\"]\nmvcc:Version(sql:Table(name), 1) → CREATE TABLE name ( id INTEGER PRIMARY KEY, value STRING DEFAULT NULL ) [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x1d\\x04name\\x00\\x02\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x05value\\x03\\x01\\x01\\x00\\x00\\x00\\x00\"]\nmvcc:Version(sql:Row(name, 1), 2) → 1,'a' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x02\\x04\\x01a\"]\nmvcc:Version(sql:Row(name, 1), 5) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x00\"]\nmvcc:Version(sql:Row(name, 2), 2) → 2,'b' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x04\\x04\\x01b\"]\nmvcc:Version(sql:Row(name, 2), 5) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x00\"]\nmvcc:Version(sql:Row(name, 3), 2) → 3,'c' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x06\\x04\\x01c\"]\nmvcc:Version(sql:Row(name, 3), 5) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x00\"]\n\n# Bare DELETE errors.\n!> DELETE\n!> DELETE FROM\n---\nError: invalid input: unexpected end of input\nError: invalid input: unexpected end of input\n\n# Unknown table errors.\n!> DELETE FROM foo\n---\nError: invalid input: table foo does not exist\n\n# LIMIT and ORDER BY clauses error.\n!> DELETE FROM name LIMIT 2\n!> DELETE FROM name ORDER BY id\n---\nError: invalid input: unexpected token LIMIT\nError: invalid input: unexpected token ORDER\n"
  },
  {
    "path": "src/sql/testscripts/writes/delete_index",
    "content": "# Tests index updates during DELETE.\n\n# Create a table with a few indexes.\n> CREATE TABLE ref (id INT PRIMARY KEY, value STRING)\n> INSERT INTO ref VALUES (1, 'a'), (2, 'b')\n> CREATE TABLE name ( \\\n    id INT PRIMARY KEY, \\\n    \"index\" INT INDEX, \\\n    \"unique\" STRING UNIQUE, \\\n    ref_id INT REFERENCES ref \\\n)\n> INSERT INTO name VALUES (1, 2, 'foo', 1)\n> INSERT INTO name VALUES (2, 4, 'bar', 1)\n> INSERT INTO name VALUES (3, 6, NULL, 2)\n> INSERT INTO name VALUES (4, 8, 'baz', 2)\n> INSERT INTO name VALUES (5, 10, NULL, 1)\n---\nok\n\n# DELETE updates the secondary indexes.\n[ops]> DELETE FROM name WHERE id = 4\n---\nset mvcc:NextVersion → 10 [\"\\x00\" → \"\\n\"]\nset mvcc:TxnActive(9) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\"]\nset mvcc:TxnWrite(9, sql:Index(name.index, 8)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01name\\x00\\xff\\x00\\xffindex\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x08\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.index, 8), 9) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xffindex\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x08\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x00\"]\nset mvcc:TxnWrite(9, sql:Index(name.unique, 'baz')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x04baz\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.unique, 'baz'), 9) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x04baz\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x00\"]\nset mvcc:TxnWrite(9, sql:Index(name.ref_id, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.ref_id, 2), 9) → 3 [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x01\\x03\\x01\\x02\\x06\"]\nset mvcc:TxnWrite(9, sql:Row(name, 4)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 4), 9) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x00\"]\ndelete mvcc:TxnWrite(9, sql:Index(name.index, 8)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01name\\x00\\xff\\x00\\xffindex\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x08\\x00\\x00\"]\ndelete mvcc:TxnWrite(9, sql:Index(name.ref_id, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnWrite(9, sql:Index(name.unique, 'baz')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x04baz\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(9, sql:Row(name, 4)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\"]\ndelete mvcc:TxnActive(9) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\"]\n\n# Dump the final state.\n> SELECT * FROM name\n---\n1, 2, 'foo', 1\n2, 4, 'bar', 1\n3, 6, NULL, 2\n5, 10, NULL, 1\n\ndump\n---\nmvcc:NextVersion → 10 [\"\\x00\" → \"\\n\"]\nmvcc:Version(sql:Table(name), 3) → CREATE TABLE name ( id INTEGER PRIMARY KEY, \"index\" INTEGER DEFAULT NULL INDEX, \"unique\" STRING DEFAULT NULL UNIQUE INDEX, ref_id INTEGER DEFAULT NULL INDEX REFERENCES ref ) [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01=\\x04name\\x00\\x04\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x05index\\x01\\x01\\x01\\x00\\x00\\x01\\x00\\x06unique\\x03\\x01\\x01\\x00\\x01\\x01\\x00\\x06ref_id\\x01\\x01\\x01\\x00\\x00\\x01\\x01\\x03ref\"]\nmvcc:Version(sql:Table(ref), 1) → CREATE TABLE ref ( id INTEGER PRIMARY KEY, value STRING DEFAULT NULL ) [\"\\x04\\x00\\xffref\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x1c\\x03ref\\x00\\x02\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x05value\\x03\\x01\\x01\\x00\\x00\\x00\\x00\"]\nmvcc:Version(sql:Index(name.index, 2), 4) → 1 [\"\\x04\\x01name\\x00\\xff\\x00\\xffindex\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nmvcc:Version(sql:Index(name.index, 4), 5) → 2 [\"\\x04\\x01name\\x00\\xff\\x00\\xffindex\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nmvcc:Version(sql:Index(name.index, 6), 6) → 3 [\"\\x04\\x01name\\x00\\xff\\x00\\xffindex\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x06\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\x03\\x01\\x02\\x06\"]\nmvcc:Version(sql:Index(name.index, 8), 7) → 4 [\"\\x04\\x01name\\x00\\xff\\x00\\xffindex\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x08\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x03\\x01\\x02\\x08\"]\nmvcc:Version(sql:Index(name.index, 8), 9) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xffindex\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x08\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x00\"]\nmvcc:Version(sql:Index(name.index, 10), 8) → 5 [\"\\x04\\x01name\\x00\\xff\\x00\\xffindex\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\n\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x03\\x01\\x02\\n\"]\nmvcc:Version(sql:Index(name.ref_id, 1), 4) → 1 [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nmvcc:Version(sql:Index(name.ref_id, 1), 5) → 1,2 [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x01\\x05\\x02\\x02\\x02\\x02\\x04\"]\nmvcc:Version(sql:Index(name.ref_id, 1), 8) → 1,2,5 [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x07\\x03\\x02\\x02\\x02\\x04\\x02\\n\"]\nmvcc:Version(sql:Index(name.ref_id, 2), 6) → 3 [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\x03\\x01\\x02\\x06\"]\nmvcc:Version(sql:Index(name.ref_id, 2), 7) → 3,4 [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x05\\x02\\x02\\x06\\x02\\x08\"]\nmvcc:Version(sql:Index(name.ref_id, 2), 9) → 3 [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x01\\x03\\x01\\x02\\x06\"]\nmvcc:Version(sql:Index(name.unique, NULL), 6) → 3 [\"\\x04\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\x03\\x01\\x02\\x06\"]\nmvcc:Version(sql:Index(name.unique, NULL), 8) → 3,5 [\"\\x04\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x05\\x02\\x02\\x06\\x02\\n\"]\nmvcc:Version(sql:Index(name.unique, 'bar'), 5) → 2 [\"\\x04\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x04bar\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nmvcc:Version(sql:Index(name.unique, 'baz'), 7) → 4 [\"\\x04\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x04baz\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x03\\x01\\x02\\x08\"]\nmvcc:Version(sql:Index(name.unique, 'baz'), 9) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x04baz\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x00\"]\nmvcc:Version(sql:Index(name.unique, 'foo'), 4) → 1 [\"\\x04\\x01name\\x00\\xff\\x00\\xffunique\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nmvcc:Version(sql:Row(name, 1), 4) → 1,2,'foo',1 [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x0c\\x04\\x02\\x02\\x02\\x04\\x04\\x03foo\\x02\\x02\"]\nmvcc:Version(sql:Row(name, 2), 5) → 2,4,'bar',1 [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x01\\x0c\\x04\\x02\\x04\\x02\\x08\\x04\\x03bar\\x02\\x02\"]\nmvcc:Version(sql:Row(name, 3), 6) → 3,6,NULL,2 [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\x08\\x04\\x02\\x06\\x02\\x0c\\x00\\x02\\x04\"]\nmvcc:Version(sql:Row(name, 4), 7) → 4,8,'baz',2 [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x0c\\x04\\x02\\x08\\x02\\x10\\x04\\x03baz\\x02\\x04\"]\nmvcc:Version(sql:Row(name, 4), 9) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x00\"]\nmvcc:Version(sql:Row(name, 5), 8) → 5,10,NULL,1 [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x05\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x08\\x04\\x02\\n\\x02\\x14\\x00\\x02\\x02\"]\nmvcc:Version(sql:Row(ref, 1), 2) → 1,'a' [\"\\x04\\x02ref\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x02\\x04\\x01a\"]\nmvcc:Version(sql:Row(ref, 2), 2) → 2,'b' [\"\\x04\\x02ref\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x04\\x04\\x01b\"]\n"
  },
  {
    "path": "src/sql/testscripts/writes/delete_reference",
    "content": "# Tests DELETE with foreign key constraints.\n\n# Create a few reference tables with data.\n> CREATE TABLE ref (id INT PRIMARY KEY, value STRING)\n> CREATE TABLE sref (id STRING PRIMARY KEY)\n> INSERT INTO ref VALUES (1, 'a'), (2, 'b'), (3, 'c')\n> INSERT INTO sref VALUES ('a'), ('b')\n\n> CREATE TABLE name ( \\\n    id INT PRIMARY KEY, \\\n    ref_id INT REFERENCES ref, \\\n    sref_id STRING NOT NULL REFERENCES sref \\\n)\n> INSERT INTO name VALUES (1, 1, 'a')\n> INSERT INTO name VALUES (2, NULL, 'b')\n> INSERT INTO name VALUES (3, 2, 'b')\n> INSERT INTO name VALUES (4, 2, 'a')\n> INSERT INTO name VALUES (5, 1, 'a')\n---\nok\n\n# DELETE with a reference errors. It does not remove rows that could be removed\n# in isolation.\n!> DELETE FROM ref\n!> DELETE FROM ref WHERE id = 1\n---\nError: invalid input: row referenced by name.id=1\nError: invalid input: row referenced by name.id=1\n\n> SELECT * FROM ref\n---\n1, 'a'\n2, 'b'\n3, 'c'\n\n# DELETE of an unreferenced row succeeds.\n[ops]> DELETE FROM ref WHERE id = 3\n---\nset mvcc:NextVersion → 14 [\"\\x00\" → \"\\x0e\"]\nset mvcc:TxnActive(13) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\" → \"\"]\nset mvcc:TxnWrite(13, sql:Row(ref, 3)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x02ref\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(ref, 3), 13) → None [\"\\x04\\x02ref\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\" → \"\\x00\"]\ndelete mvcc:TxnWrite(13, sql:Row(ref, 3)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x02ref\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\"]\ndelete mvcc:TxnActive(13) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\"]\n\n> SELECT * FROM ref\n---\n1, 'a'\n2, 'b'\n\n# DELETE in the source table succeeds. It also removes the index entries.\n[ops]> DELETE FROM name WHERE id = 2 OR id = 3\n---\nset mvcc:NextVersion → 15 [\"\\x00\" → \"\\x0f\"]\nset mvcc:TxnActive(14) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\" → \"\"]\nset mvcc:TxnWrite(14, sql:Index(name.ref_id, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.ref_id, NULL), 14) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\" → \"\\x00\"]\nset mvcc:TxnWrite(14, sql:Index(name.sref_id, 'b')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x01name\\x00\\xff\\x00\\xffsref_id\\x00\\xff\\x00\\xff\\x04b\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.sref_id, 'b'), 14) → 3 [\"\\x04\\x01name\\x00\\xff\\x00\\xffsref_id\\x00\\xff\\x00\\xff\\x04b\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\" → \"\\x01\\x03\\x01\\x02\\x06\"]\nset mvcc:TxnWrite(14, sql:Row(name, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 2), 14) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\" → \"\\x00\"]\nset mvcc:TxnWrite(14, sql:Index(name.ref_id, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.ref_id, 2), 14) → 4 [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\" → \"\\x01\\x03\\x01\\x02\\x08\"]\nset mvcc:TxnWrite(14, sql:Index(name.sref_id, 'b')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x01name\\x00\\xff\\x00\\xffsref_id\\x00\\xff\\x00\\xff\\x04b\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.sref_id, 'b'), 14) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xffsref_id\\x00\\xff\\x00\\xff\\x04b\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\" → \"\\x00\"]\nset mvcc:TxnWrite(14, sql:Row(name, 3)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 3), 14) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\" → \"\\x00\"]\ndelete mvcc:TxnWrite(14, sql:Index(name.ref_id, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(14, sql:Index(name.ref_id, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnWrite(14, sql:Index(name.sref_id, 'b')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x01name\\x00\\xff\\x00\\xffsref_id\\x00\\xff\\x00\\xff\\x04b\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(14, sql:Row(name, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnWrite(14, sql:Row(name, 3)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\"]\ndelete mvcc:TxnActive(14) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\"]\n\n# DELETE of a no-longer-referenced row succeeds.\n> DELETE FROM sref WHERE id = 'b'\n---\nok\n\n# Test self-references.\n> CREATE TABLE self (id INT PRIMARY KEY, self_id INT REFERENCES self)\n> INSERT INTO self VALUES (1, 1)\n> INSERT INTO self VALUES (2, 2)\n> INSERT INTO self VALUES (3, 2)\n> INSERT INTO self VALUES (4, 2)\n---\nok\n\n# Deleting all self-ref rows always works.\n> BEGIN\n> DELETE FROM self\n> SELECT * FROM self\n> ROLLBACK\n---\nok\n\n# Deleting a referenced row errors.\n!> DELETE FROM self WHERE id = 2\n---\nError: invalid input: row referenced by self.id=3\n\n# Deleting an unreferenced row works.\n> DELETE FROM self WHERE id = 4\n---\nok\n\n# Deleting a row only referencing itself works.\n> DELETE FROM self WHERE id = 1\n---\nok\n\n> SELECT * FROM self\n---\n2, 2\n3, 2\n\n# Dump the raw dataset.\ndump\n---\nmvcc:NextVersion → 25 [\"\\x00\" → \"\\x19\"]\nmvcc:Version(sql:Table(name), 5) → CREATE TABLE name ( id INTEGER PRIMARY KEY, ref_id INTEGER DEFAULT NULL INDEX REFERENCES ref, sref_id STRING NOT NULL INDEX REFERENCES sref ) [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x015\\x04name\\x00\\x03\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x06ref_id\\x01\\x01\\x01\\x00\\x00\\x01\\x01\\x03ref\\x07sref_id\\x03\\x00\\x00\\x00\\x01\\x01\\x04sref\"]\nmvcc:Version(sql:Table(ref), 1) → CREATE TABLE ref ( id INTEGER PRIMARY KEY, value STRING DEFAULT NULL ) [\"\\x04\\x00\\xffref\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x1c\\x03ref\\x00\\x02\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x05value\\x03\\x01\\x01\\x00\\x00\\x00\\x00\"]\nmvcc:Version(sql:Table(self), 16) → CREATE TABLE self ( id INTEGER PRIMARY KEY, self_id INTEGER DEFAULT NULL INDEX REFERENCES self ) [\"\\x04\\x00\\xffself\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x10\" → \"\\x01$\\x04self\\x00\\x02\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x07self_id\\x01\\x01\\x01\\x00\\x00\\x01\\x01\\x04self\"]\nmvcc:Version(sql:Table(sref), 2) → CREATE TABLE sref ( id STRING PRIMARY KEY ) [\"\\x04\\x00\\xffsref\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x10\\x04sref\\x00\\x01\\x02id\\x03\\x00\\x00\\x01\\x00\\x00\"]\nmvcc:Version(sql:Index(name.ref_id, NULL), 7) → 2 [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nmvcc:Version(sql:Index(name.ref_id, NULL), 14) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\" → \"\\x00\"]\nmvcc:Version(sql:Index(name.ref_id, 1), 6) → 1 [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nmvcc:Version(sql:Index(name.ref_id, 1), 10) → 1,5 [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\\x01\\x05\\x02\\x02\\x02\\x02\\n\"]\nmvcc:Version(sql:Index(name.ref_id, 2), 8) → 3 [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x03\\x01\\x02\\x06\"]\nmvcc:Version(sql:Index(name.ref_id, 2), 9) → 3,4 [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x01\\x05\\x02\\x02\\x06\\x02\\x08\"]\nmvcc:Version(sql:Index(name.ref_id, 2), 14) → 4 [\"\\x04\\x01name\\x00\\xff\\x00\\xffref_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\" → \"\\x01\\x03\\x01\\x02\\x08\"]\nmvcc:Version(sql:Index(name.sref_id, 'a'), 6) → 1 [\"\\x04\\x01name\\x00\\xff\\x00\\xffsref_id\\x00\\xff\\x00\\xff\\x04a\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nmvcc:Version(sql:Index(name.sref_id, 'a'), 9) → 1,4 [\"\\x04\\x01name\\x00\\xff\\x00\\xffsref_id\\x00\\xff\\x00\\xff\\x04a\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x01\\x05\\x02\\x02\\x02\\x02\\x08\"]\nmvcc:Version(sql:Index(name.sref_id, 'a'), 10) → 1,4,5 [\"\\x04\\x01name\\x00\\xff\\x00\\xffsref_id\\x00\\xff\\x00\\xff\\x04a\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\\x01\\x07\\x03\\x02\\x02\\x02\\x08\\x02\\n\"]\nmvcc:Version(sql:Index(name.sref_id, 'b'), 7) → 2 [\"\\x04\\x01name\\x00\\xff\\x00\\xffsref_id\\x00\\xff\\x00\\xff\\x04b\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nmvcc:Version(sql:Index(name.sref_id, 'b'), 8) → 2,3 [\"\\x04\\x01name\\x00\\xff\\x00\\xffsref_id\\x00\\xff\\x00\\xff\\x04b\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x05\\x02\\x02\\x04\\x02\\x06\"]\nmvcc:Version(sql:Index(name.sref_id, 'b'), 14) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xffsref_id\\x00\\xff\\x00\\xff\\x04b\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\" → \"\\x00\"]\nmvcc:Version(sql:Index(self.self_id, 1), 17) → 1 [\"\\x04\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x11\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nmvcc:Version(sql:Index(self.self_id, 1), 24) → None [\"\\x04\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x18\" → \"\\x00\"]\nmvcc:Version(sql:Index(self.self_id, 2), 18) → 2 [\"\\x04\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nmvcc:Version(sql:Index(self.self_id, 2), 19) → 2,3 [\"\\x04\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x13\" → \"\\x01\\x05\\x02\\x02\\x04\\x02\\x06\"]\nmvcc:Version(sql:Index(self.self_id, 2), 20) → 2,3,4 [\"\\x04\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x14\" → \"\\x01\\x07\\x03\\x02\\x04\\x02\\x06\\x02\\x08\"]\nmvcc:Version(sql:Index(self.self_id, 2), 23) → 2,3 [\"\\x04\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x17\" → \"\\x01\\x05\\x02\\x02\\x04\\x02\\x06\"]\nmvcc:Version(sql:Row(name, 1), 6) → 1,1,'a' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\x08\\x03\\x02\\x02\\x02\\x02\\x04\\x01a\"]\nmvcc:Version(sql:Row(name, 2), 7) → 2,NULL,'b' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x07\\x03\\x02\\x04\\x00\\x04\\x01b\"]\nmvcc:Version(sql:Row(name, 2), 14) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\" → \"\\x00\"]\nmvcc:Version(sql:Row(name, 3), 8) → 3,2,'b' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x08\\x03\\x02\\x06\\x02\\x04\\x04\\x01b\"]\nmvcc:Version(sql:Row(name, 3), 14) → None [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\" → \"\\x00\"]\nmvcc:Version(sql:Row(name, 4), 9) → 4,2,'a' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x01\\x08\\x03\\x02\\x08\\x02\\x04\\x04\\x01a\"]\nmvcc:Version(sql:Row(name, 5), 10) → 5,1,'a' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x05\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\\x01\\x08\\x03\\x02\\n\\x02\\x02\\x04\\x01a\"]\nmvcc:Version(sql:Row(ref, 1), 3) → 1,'a' [\"\\x04\\x02ref\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x06\\x02\\x02\\x02\\x04\\x01a\"]\nmvcc:Version(sql:Row(ref, 2), 3) → 2,'b' [\"\\x04\\x02ref\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x06\\x02\\x02\\x04\\x04\\x01b\"]\nmvcc:Version(sql:Row(ref, 3), 3) → 3,'c' [\"\\x04\\x02ref\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x06\\x02\\x02\\x06\\x04\\x01c\"]\nmvcc:Version(sql:Row(ref, 3), 13) → None [\"\\x04\\x02ref\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\" → \"\\x00\"]\nmvcc:Version(sql:Row(self, 1), 17) → 1,1 [\"\\x04\\x02self\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x11\" → \"\\x01\\x05\\x02\\x02\\x02\\x02\\x02\"]\nmvcc:Version(sql:Row(self, 1), 24) → None [\"\\x04\\x02self\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x18\" → \"\\x00\"]\nmvcc:Version(sql:Row(self, 2), 18) → 2,2 [\"\\x04\\x02self\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\" → \"\\x01\\x05\\x02\\x02\\x04\\x02\\x04\"]\nmvcc:Version(sql:Row(self, 3), 19) → 3,2 [\"\\x04\\x02self\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x13\" → \"\\x01\\x05\\x02\\x02\\x06\\x02\\x04\"]\nmvcc:Version(sql:Row(self, 4), 20) → 4,2 [\"\\x04\\x02self\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x14\" → \"\\x01\\x05\\x02\\x02\\x08\\x02\\x04\"]\nmvcc:Version(sql:Row(self, 4), 23) → None [\"\\x04\\x02self\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x17\" → \"\\x00\"]\nmvcc:Version(sql:Row(sref, 'a'), 4) → 'a' [\"\\x04\\x02sref\\x00\\xff\\x00\\xff\\x04a\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x04\\x01\\x04\\x01a\"]\nmvcc:Version(sql:Row(sref, 'b'), 4) → 'b' [\"\\x04\\x02sref\\x00\\xff\\x00\\xff\\x04b\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x04\\x01\\x04\\x01b\"]\nmvcc:Version(sql:Row(sref, 'b'), 15) → None [\"\\x04\\x02sref\\x00\\xff\\x00\\xff\\x04b\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\" → \"\\x00\"]\n"
  },
  {
    "path": "src/sql/testscripts/writes/delete_where",
    "content": "# Tests filtered DELETE statements.\n\n# Create a table with some data.\n> CREATE TABLE name (id INT PRIMARY KEY, value STRING, \"index\" INT INDEX)\n> INSERT INTO name VALUES (1, 'a', 1), (2, 'b', 2), (3, 'c', 3), (0, NULL, NULL)\n---\nok\n\n# Boolean filters work, and are trivial.\n> BEGIN\n[plan]> DELETE FROM name WHERE true\n> SELECT * FROM name\n> ROLLBACK\n---\nDelete: name\n└─ Scan: name\n\n[plan]> DELETE FROM name WHERE false\n> SELECT * FROM name\n---\nDelete: name\n└─ Nothing\n0, NULL, NULL\n1, 'a', 1\n2, 'b', 2\n3, 'c', 3\n\n# Deleting by primary key lookup works.\n> BEGIN\n[plan]> DELETE FROM name WHERE id = 1 OR id = 3\n> SELECT * FROM name\n> ROLLBACK\n---\nDelete: name\n└─ KeyLookup: name (1, 3)\n0, NULL, NULL\n2, 'b', 2\n\n# Deleting by secondary index lookup works.\n> BEGIN\n[plan]> DELETE FROM name WHERE \"index\" = 3\n> SELECT * FROM name\n> ROLLBACK\n---\nDelete: name\n└─ IndexLookup: name.index (3)\n0, NULL, NULL\n1, 'a', 1\n2, 'b', 2\n\n# Including IS NULL predicates.\n> BEGIN\n[plan]> DELETE FROM name WHERE \"index\" IS NULL\n> SELECT * FROM name\n> ROLLBACK\n---\nDelete: name\n└─ IndexLookup: name.index (NULL)\n1, 'a', 1\n2, 'b', 2\n3, 'c', 3\n\n# Deleting by arbitrary predicate works.\n> BEGIN\n[plan]> DELETE FROM name WHERE id >= 5 - 2 OR (value LIKE 'a') IS NULL\n> SELECT * FROM name\n> ROLLBACK\n---\nDelete: name\n└─ Scan: name (name.id > 3 OR name.id = 3 OR name.value LIKE 'a' IS NULL)\n1, 'a', 1\n2, 'b', 2\n\n# Other types error, except NULL which is equivalent to false.\n!> DELETE FROM name WHERE 0\n!> DELETE FROM name WHERE 1\n!> DELETE FROM name WHERE 3.14\n!> DELETE FROM name WHERE NaN\n!> DELETE FROM name WHERE ''\n!> DELETE FROM name WHERE 'true'\n---\nError: invalid input: filter returned 0, expected boolean\nError: invalid input: filter returned 1, expected boolean\nError: invalid input: filter returned 3.14, expected boolean\nError: invalid input: filter returned NaN, expected boolean\nError: invalid input: filter returned '', expected boolean\nError: invalid input: filter returned 'true', expected boolean\n\n> DELETE FROM name WHERE NULL\n> SELECT * FROM name\n---\n0, NULL, NULL\n1, 'a', 1\n2, 'b', 2\n3, 'c', 3\n\n# Bare WHERE errors.\n!> DELETE FROM name WHERE\n---\nError: invalid input: unexpected end of input\n\n# Missing column errors.\n!> DELETE FROM name WHERE missing = 'foo'\n---\nError: invalid input: unknown column missing\n"
  },
  {
    "path": "src/sql/testscripts/writes/insert",
    "content": "# Tests basic INSERT functionality.\n\n> CREATE TABLE name (id INT PRIMARY KEY, value STRING)\n---\nok\n\n# INSERT writes a row to the table, and returns the number of rows.\n[plan,result,ops]> INSERT INTO name VALUES (1, 'a')\n---\nInsert: name\n└─ Values: 1, 'a'\nset mvcc:NextVersion → 3 [\"\\x00\" → \"\\x03\"]\nset mvcc:TxnActive(2) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\"]\nset mvcc:TxnWrite(2, sql:Row(name, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 1), 2) → 1,'a' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x02\\x04\\x01a\"]\ndelete mvcc:TxnWrite(2, sql:Row(name, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnActive(2) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\"]\nInsert { count: 1 }\n\n# It can also write multiple rows.\n[plan,result,ops]> INSERT INTO name VALUES (2, 'b'), (3, 'c'), (4, 'd')\n---\nInsert: name\n└─ Values: 3 rows\nset mvcc:NextVersion → 4 [\"\\x00\" → \"\\x04\"]\nset mvcc:TxnActive(3) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\"]\nset mvcc:TxnWrite(3, sql:Row(name, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 2), 3) → 2,'b' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x06\\x02\\x02\\x04\\x04\\x01b\"]\nset mvcc:TxnWrite(3, sql:Row(name, 3)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 3), 3) → 3,'c' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x06\\x02\\x02\\x06\\x04\\x01c\"]\nset mvcc:TxnWrite(3, sql:Row(name, 4)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 4), 3) → 4,'d' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x06\\x02\\x02\\x08\\x04\\x01d\"]\ndelete mvcc:TxnWrite(3, sql:Row(name, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnWrite(3, sql:Row(name, 3)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\"]\ndelete mvcc:TxnWrite(3, sql:Row(name, 4)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\"]\ndelete mvcc:TxnActive(3) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\"]\nInsert { count: 3 }\n\n> SELECT * FROM name\n---\n1, 'a'\n2, 'b'\n3, 'c'\n4, 'd'\n\ndump\n---\nmvcc:NextVersion → 4 [\"\\x00\" → \"\\x04\"]\nmvcc:Version(sql:Table(name), 1) → CREATE TABLE name ( id INTEGER PRIMARY KEY, value STRING DEFAULT NULL ) [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x1d\\x04name\\x00\\x02\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x05value\\x03\\x01\\x01\\x00\\x00\\x00\\x00\"]\nmvcc:Version(sql:Row(name, 1), 2) → 1,'a' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x02\\x04\\x01a\"]\nmvcc:Version(sql:Row(name, 2), 3) → 2,'b' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x06\\x02\\x02\\x04\\x04\\x01b\"]\nmvcc:Version(sql:Row(name, 3), 3) → 3,'c' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x06\\x02\\x02\\x06\\x04\\x01c\"]\nmvcc:Version(sql:Row(name, 4), 3) → 4,'d' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x06\\x02\\x02\\x08\\x04\\x01d\"]\n\n# INSERTs can use expressions, but only constant ones.\n> INSERT INTO name VALUES (2^2+1, 'abc')\n> SELECT * FROM name\n---\n1, 'a'\n2, 'b'\n3, 'c'\n4, 'd'\n5, 'abc'\n\n!> INSERT INTO name VALUES (id + 2, 'abc')\n---\nError: invalid input: expression must be constant, found column id\n\n# INSERTs with too many columns errors. Fewer are tested by insert_default.\n!> INSERT INTO name VALUES (6, 'e', NULL)\n---\nError: invalid input: too many values for table name\n\n# Bare insert errors, as does no values.\n!> INSERT\n!> INSERT INTO\n!> INSERT INTO name\n---\nError: invalid input: unexpected end of input\nError: invalid input: unexpected end of input\nError: invalid input: unexpected end of input\n\n# Unknown table or column errors.\n!> INSERT INTO foo VALUES (1)\n!> INSERT INTO name (id, foo) VALUES (1, 'bar')\n---\nError: invalid input: table foo does not exist\nError: invalid input: unknown column foo in table name\n\n# Multiple tables errors.\n!> INSERT INTO name, other VALUES (1)\n---\nError: invalid input: expected token VALUES, found ,\n\n# Specifying the same column multiple times errors.\n!> INSERT INTO name (id, value, value) VALUES (6, 'e', 'f')\n---\nError: invalid input: column value given multiple times\n"
  },
  {
    "path": "src/sql/testscripts/writes/insert_datatypes",
    "content": "# Tests INSERT of all datatypes.\n\n# Create columns with all datatypes.\n> CREATE TABLE datatypes ( \\\n    id INTEGER PRIMARY KEY, \\\n    \"bool\" BOOL, \\\n    \"int\" INT, \\\n    \"float\" FLOAT, \\\n    \"string\" STRING \\\n)\n---\nok\n\n# Booleans.\n> BEGIN\n> INSERT INTO datatypes (id, \"bool\") VALUES (1, true)\n> INSERT INTO datatypes (id, \"bool\") VALUES (2, false)\n> INSERT INTO datatypes (id, \"bool\") VALUES (3, NULL)\n---\nok\n\n> SELECT * FROM datatypes\n> ROLLBACK\n---\n1, TRUE, NULL, NULL, NULL\n2, FALSE, NULL, NULL, NULL\n3, NULL, NULL, NULL, NULL\n\n!> INSERT INTO datatypes (id, \"bool\") VALUES (0, 1)\n!> INSERT INTO datatypes (id, \"bool\") VALUES (0, 3.14)\n!> INSERT INTO datatypes (id, \"bool\") VALUES (0, 'false')\n---\nError: invalid input: invalid datatype INTEGER for BOOLEAN column bool\nError: invalid input: invalid datatype FLOAT for BOOLEAN column bool\nError: invalid input: invalid datatype STRING for BOOLEAN column bool\n\n# Integers.\n> BEGIN\n> INSERT INTO datatypes (id, \"int\") VALUES (1, 1)\n> INSERT INTO datatypes (id, \"int\") VALUES (2, 0)\n> INSERT INTO datatypes (id, \"int\") VALUES (3, -1)\n> INSERT INTO datatypes (id, \"int\") VALUES (4, 9223372036854775807)\n> INSERT INTO datatypes (id, \"int\") VALUES (5, -9223372036854775807)\n> INSERT INTO datatypes (id, \"int\") VALUES (6, NULL)\n---\nok\n\n> SELECT * FROM datatypes\n> ROLLBACK\n---\n1, NULL, 1, NULL, NULL\n2, NULL, 0, NULL, NULL\n3, NULL, -1, NULL, NULL\n4, NULL, 9223372036854775807, NULL, NULL\n5, NULL, -9223372036854775807, NULL, NULL\n6, NULL, NULL, NULL, NULL\n\n!> INSERT INTO datatypes (id, \"int\") VALUES (0, false)\n!> INSERT INTO datatypes (id, \"int\") VALUES (0, 3.0)\n!> INSERT INTO datatypes (id, \"int\") VALUES (0, '0')\n---\nError: invalid input: invalid datatype BOOLEAN for INTEGER column int\nError: invalid input: invalid datatype FLOAT for INTEGER column int\nError: invalid input: invalid datatype STRING for INTEGER column int\n\n# Floats. -0.0 and -NaN is normalized as 0.0 and NaN.\n> BEGIN\n> INSERT INTO datatypes (id, \"float\") VALUES (1, 3.14)\n> INSERT INTO datatypes (id, \"float\") VALUES (2, -3.14)\n> INSERT INTO datatypes (id, \"float\") VALUES (3, 0.0)\n> INSERT INTO datatypes (id, \"float\") VALUES (4, -0.0)\n> INSERT INTO datatypes (id, \"float\") VALUES (5, 1.23456789012345e308)\n> INSERT INTO datatypes (id, \"float\") VALUES (6, -1.23456789012345e308)\n> INSERT INTO datatypes (id, \"float\") VALUES (7, INFINITY)\n> INSERT INTO datatypes (id, \"float\") VALUES (8, -INFINITY)\n> INSERT INTO datatypes (id, \"float\") VALUES (9, -NAN)\n> INSERT INTO datatypes (id, \"float\") VALUES (10, NAN)\n> INSERT INTO datatypes (id, \"float\") VALUES (11, NULL)\n---\nok\n\n> SELECT * FROM datatypes\n> ROLLBACK\n---\n1, NULL, NULL, 3.14, NULL\n2, NULL, NULL, -3.14, NULL\n3, NULL, NULL, 0.0, NULL\n4, NULL, NULL, 0.0, NULL\n5, NULL, NULL, 1.23456789012345e308, NULL\n6, NULL, NULL, -1.23456789012345e308, NULL\n7, NULL, NULL, inf, NULL\n8, NULL, NULL, -inf, NULL\n9, NULL, NULL, NaN, NULL\n10, NULL, NULL, NaN, NULL\n11, NULL, NULL, NULL, NULL\n\n!> INSERT INTO datatypes (id, \"float\") VALUES (0, false)\n!> INSERT INTO datatypes (id, \"float\") VALUES (0, 3)\n!> INSERT INTO datatypes (id, \"float\") VALUES (0, '0')\n---\nError: invalid input: invalid datatype BOOLEAN for FLOAT column float\nError: invalid input: invalid datatype INTEGER for FLOAT column float\nError: invalid input: invalid datatype STRING for FLOAT column float\n\n# Strings.\n> BEGIN\n> INSERT INTO datatypes (id, \"string\") VALUES (1, '')\n> INSERT INTO datatypes (id, \"string\") VALUES (2, '  ')\n> INSERT INTO datatypes (id, \"string\") VALUES (3, 'abc')\n> INSERT INTO datatypes (id, \"string\") VALUES (4, 'Hi! 👋')\n> INSERT INTO datatypes (id, \"string\") VALUES (5, NULL)\n---\nok\n\n> SELECT * FROM datatypes\n> ROLLBACK\n---\n1, NULL, NULL, NULL, ''\n2, NULL, NULL, NULL, '  '\n3, NULL, NULL, NULL, 'abc'\n4, NULL, NULL, NULL, 'Hi! 👋'\n5, NULL, NULL, NULL, NULL\n\n!> INSERT INTO datatypes (id, \"string\") VALUES (0, false)\n!> INSERT INTO datatypes (id, \"string\") VALUES (0, 3)\n!> INSERT INTO datatypes (id, \"string\") VALUES (0, 3.14)\n---\nError: invalid input: invalid datatype BOOLEAN for STRING column string\nError: invalid input: invalid datatype INTEGER for STRING column string\nError: invalid input: invalid datatype FLOAT for STRING column string\n"
  },
  {
    "path": "src/sql/testscripts/writes/insert_default",
    "content": "# Tests INSERT handling of DEFAULT values.\n\n> CREATE TABLE defaults ( \\\n    id INTEGER PRIMARY KEY, \\\n    required BOOLEAN NOT NULL, \\\n    \"null\" BOOLEAN, \\\n    \"boolean\" BOOLEAN DEFAULT TRUE, \\\n    \"float\" FLOAT DEFAULT 3.14, \\\n    \"integer\" INTEGER DEFAULT 7, \\\n    \"string\" STRING DEFAULT 'foo' \\\n)\n---\nok\n\n# INSERT without specifying default columns fills in defaults.\n> INSERT INTO defaults (id, required) VALUES (1, true)\n> INSERT INTO defaults VALUES (2, false)\n---\nok\n\n> SELECT * FROM defaults\n---\n1, TRUE, NULL, TRUE, 3.14, 7, 'foo'\n2, FALSE, NULL, TRUE, 3.14, 7, 'foo'\n\n# INSERT only specifying some default columns fills in rest.\n> INSERT INTO defaults (\"integer\", id, \"null\", required) VALUES (9, 3, NULL, false)\n---\nok\n\n> SELECT * FROM defaults\n---\n1, TRUE, NULL, TRUE, 3.14, 7, 'foo'\n2, FALSE, NULL, TRUE, 3.14, 7, 'foo'\n3, FALSE, NULL, TRUE, 3.14, 9, 'foo'\n\n# Using a variable number of values works.\n> INSERT INTO defaults VALUES (4, false, NULL, false), (5, true), (6, true, false, true, 3.14, 9, 'bar')\n---\nok\n\n> SELECT * FROM defaults\n---\n1, TRUE, NULL, TRUE, 3.14, 7, 'foo'\n2, FALSE, NULL, TRUE, 3.14, 7, 'foo'\n3, FALSE, NULL, TRUE, 3.14, 9, 'foo'\n4, FALSE, NULL, FALSE, 3.14, 7, 'foo'\n5, TRUE, NULL, TRUE, 3.14, 7, 'foo'\n6, TRUE, FALSE, TRUE, 3.14, 9, 'bar'\n\n# INSERT with all NULLs does not yield default values.\n> INSERT INTO defaults VALUES (7, false, NULL, NULL, NULL, NULL, NULL)\n---\nok\n\n> SELECT * FROM defaults\n---\n1, TRUE, NULL, TRUE, 3.14, 7, 'foo'\n2, FALSE, NULL, TRUE, 3.14, 7, 'foo'\n3, FALSE, NULL, TRUE, 3.14, 9, 'foo'\n4, FALSE, NULL, FALSE, 3.14, 7, 'foo'\n5, TRUE, NULL, TRUE, 3.14, 7, 'foo'\n6, TRUE, FALSE, TRUE, 3.14, 9, 'bar'\n7, FALSE, NULL, NULL, NULL, NULL, NULL\n\n# Errors if required column isn't given.\n!> INSERT INTO defaults VALUES (8)\n---\nError: invalid input: no value given for column required with no default\n"
  },
  {
    "path": "src/sql/testscripts/writes/insert_index",
    "content": "# Tests INSERT index writes.\n\n> CREATE TABLE \"index\" ( \\\n    id INT PRIMARY KEY, \\\n    \"bool\" BOOL INDEX, \\\n    \"int\" INT INDEX, \\\n    \"float\" FLOAT INDEX, \\\n    \"string\" STRING INDEX \\\n)\n---\nok\n\n# An INSERT writes to all indexes.\n[ops]> INSERT INTO \"index\" VALUES (1, TRUE, 7, 3.14, 'foo')\n---\nset mvcc:NextVersion → 3 [\"\\x00\" → \"\\x03\"]\nset mvcc:TxnActive(2) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\"]\nset mvcc:TxnWrite(2, sql:Row(index, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(index, 1), 2) → 1,TRUE,7,3.14,'foo' [\"\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x15\\x05\\x02\\x02\\x01\\x01\\x02\\x0e\\x03\\x1f\\x85\\xebQ\\xb8\\x1e\\t@\\x04\\x03foo\"]\nset mvcc:TxnWrite(2, sql:Index(index.bool, TRUE)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, TRUE), 2) → 1 [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(2, sql:Index(index.int, 7)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, 7), 2) → 1 [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(2, sql:Index(index.float, 3.14)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, 3.14), 2) → 1 [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(2, sql:Index(index.string, 'foo')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, 'foo'), 2) → 1 [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x03\\x01\\x02\\x02\"]\ndelete mvcc:TxnWrite(2, sql:Index(index.bool, TRUE)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(2, sql:Index(index.float, 3.14)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\"]\ndelete mvcc:TxnWrite(2, sql:Index(index.int, 7)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\"]\ndelete mvcc:TxnWrite(2, sql:Index(index.string, 'foo')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(2, sql:Row(index, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnActive(2) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\"]\n\n# Another insert with the same values adds to the index entries.\n[ops]> INSERT INTO \"index\" VALUES (2, TRUE, 7, 3.14, 'foo')\n---\nset mvcc:NextVersion → 4 [\"\\x00\" → \"\\x04\"]\nset mvcc:TxnActive(3) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\"]\nset mvcc:TxnWrite(3, sql:Row(index, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(index, 2), 3) → 2,TRUE,7,3.14,'foo' [\"\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x15\\x05\\x02\\x04\\x01\\x01\\x02\\x0e\\x03\\x1f\\x85\\xebQ\\xb8\\x1e\\t@\\x04\\x03foo\"]\nset mvcc:TxnWrite(3, sql:Index(index.bool, TRUE)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, TRUE), 3) → 1,2 [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x05\\x02\\x02\\x02\\x02\\x04\"]\nset mvcc:TxnWrite(3, sql:Index(index.int, 7)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, 7), 3) → 1,2 [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x05\\x02\\x02\\x02\\x02\\x04\"]\nset mvcc:TxnWrite(3, sql:Index(index.float, 3.14)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, 3.14), 3) → 1,2 [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x05\\x02\\x02\\x02\\x02\\x04\"]\nset mvcc:TxnWrite(3, sql:Index(index.string, 'foo')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, 'foo'), 3) → 1,2 [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x05\\x02\\x02\\x02\\x02\\x04\"]\ndelete mvcc:TxnWrite(3, sql:Index(index.bool, TRUE)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(3, sql:Index(index.float, 3.14)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\"]\ndelete mvcc:TxnWrite(3, sql:Index(index.int, 7)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\"]\ndelete mvcc:TxnWrite(3, sql:Index(index.string, 'foo')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(3, sql:Row(index, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnActive(3) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\"]\n\n# An insert with different values writes new index entries.\n[ops]> INSERT INTO \"index\" VALUES (3, FALSE, 0, 2.718, '')\n---\nset mvcc:NextVersion → 5 [\"\\x00\" → \"\\x05\"]\nset mvcc:TxnActive(4) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\"]\nset mvcc:TxnWrite(4, sql:Row(index, 3)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(index, 3), 4) → 3,FALSE,0,2.718,'' [\"\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x12\\x05\\x02\\x06\\x01\\x00\\x02\\x00\\x03X9\\xb4\\xc8v\\xbe\\x05@\\x04\\x00\"]\nset mvcc:TxnWrite(4, sql:Index(index.bool, FALSE)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, FALSE), 4) → 3 [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x03\\x01\\x02\\x06\"]\nset mvcc:TxnWrite(4, sql:Index(index.int, 0)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, 0), 4) → 3 [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x03\\x01\\x02\\x06\"]\nset mvcc:TxnWrite(4, sql:Index(index.float, 2.718)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\x05\\xbev\\xc8\\xb49X\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, 2.718), 4) → 3 [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\x05\\xbev\\xc8\\xb49X\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x03\\x01\\x02\\x06\"]\nset mvcc:TxnWrite(4, sql:Index(index.string, '')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, ''), 4) → 3 [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x03\\x01\\x02\\x06\"]\ndelete mvcc:TxnWrite(4, sql:Index(index.bool, FALSE)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(4, sql:Index(index.float, 2.718)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\x05\\xbev\\xc8\\xb49X\\x00\\x00\"]\ndelete mvcc:TxnWrite(4, sql:Index(index.int, 0)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(4, sql:Index(index.string, '')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(4, sql:Row(index, 3)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\"]\ndelete mvcc:TxnActive(4) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\"]\n\n# Inserts with NULLS adds NULL entries. These are used for IS NULL queries.\n[ops]> INSERT INTO \"index\" VALUES (4), (5)\n---\nset mvcc:NextVersion → 6 [\"\\x00\" → \"\\x06\"]\nset mvcc:TxnActive(5) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\"]\nset mvcc:TxnWrite(5, sql:Row(index, 4)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(index, 4), 5) → 4,NULL,NULL,NULL,NULL [\"\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x01\\x07\\x05\\x02\\x08\\x00\\x00\\x00\\x00\"]\nset mvcc:TxnWrite(5, sql:Index(index.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, NULL), 5) → 4 [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x01\\x03\\x01\\x02\\x08\"]\nset mvcc:TxnWrite(5, sql:Index(index.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, NULL), 5) → 4 [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x01\\x03\\x01\\x02\\x08\"]\nset mvcc:TxnWrite(5, sql:Index(index.float, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, NULL), 5) → 4 [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x01\\x03\\x01\\x02\\x08\"]\nset mvcc:TxnWrite(5, sql:Index(index.string, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, NULL), 5) → 4 [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x01\\x03\\x01\\x02\\x08\"]\nset mvcc:TxnWrite(5, sql:Row(index, 5)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x05\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(index, 5), 5) → 5,NULL,NULL,NULL,NULL [\"\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x05\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x01\\x07\\x05\\x02\\n\\x00\\x00\\x00\\x00\"]\nset mvcc:TxnWrite(5, sql:Index(index.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, NULL), 5) → 4,5 [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x01\\x05\\x02\\x02\\x08\\x02\\n\"]\nset mvcc:TxnWrite(5, sql:Index(index.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, NULL), 5) → 4,5 [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x01\\x05\\x02\\x02\\x08\\x02\\n\"]\nset mvcc:TxnWrite(5, sql:Index(index.float, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, NULL), 5) → 4,5 [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x01\\x05\\x02\\x02\\x08\\x02\\n\"]\nset mvcc:TxnWrite(5, sql:Index(index.string, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, NULL), 5) → 4,5 [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\" → \"\\x01\\x05\\x02\\x02\\x08\\x02\\n\"]\ndelete mvcc:TxnWrite(5, sql:Index(index.bool, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(5, sql:Index(index.float, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(5, sql:Index(index.int, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(5, sql:Index(index.string, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(5, sql:Row(index, 4)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\"]\ndelete mvcc:TxnWrite(5, sql:Row(index, 5)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x05\\x00\\x00\"]\ndelete mvcc:TxnActive(5) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\"]\n\n# Float NaNs are considered equal and indexed.\n[ops]> INSERT INTO \"index\" (id, \"float\") VALUES (6, NAN), (7, NAN)\n---\nset mvcc:NextVersion → 7 [\"\\x00\" → \"\\x07\"]\nset mvcc:TxnActive(6) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\"]\nset mvcc:TxnWrite(6, sql:Row(index, 6)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x06\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(index, 6), 6) → 6,NULL,NULL,NaN,NULL [\"\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x06\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\x0f\\x05\\x02\\x0c\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\xf8\\x7f\\x00\"]\nset mvcc:TxnWrite(6, sql:Index(index.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, NULL), 6) → 4,5,6 [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\x07\\x03\\x02\\x08\\x02\\n\\x02\\x0c\"]\nset mvcc:TxnWrite(6, sql:Index(index.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, NULL), 6) → 4,5,6 [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\x07\\x03\\x02\\x08\\x02\\n\\x02\\x0c\"]\nset mvcc:TxnWrite(6, sql:Index(index.float, NaN)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf8\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, NaN), 6) → 6 [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf8\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\x03\\x01\\x02\\x0c\"]\nset mvcc:TxnWrite(6, sql:Index(index.string, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, NULL), 6) → 4,5,6 [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\x07\\x03\\x02\\x08\\x02\\n\\x02\\x0c\"]\nset mvcc:TxnWrite(6, sql:Row(index, 7)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(index, 7), 6) → 7,NULL,NULL,NaN,NULL [\"\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\x0f\\x05\\x02\\x0e\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\xf8\\x7f\\x00\"]\nset mvcc:TxnWrite(6, sql:Index(index.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, NULL), 6) → 4,5,6,7 [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\t\\x04\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\"]\nset mvcc:TxnWrite(6, sql:Index(index.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, NULL), 6) → 4,5,6,7 [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\t\\x04\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\"]\nset mvcc:TxnWrite(6, sql:Index(index.float, NaN)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf8\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, NaN), 6) → 6,7 [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf8\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\x05\\x02\\x02\\x0c\\x02\\x0e\"]\nset mvcc:TxnWrite(6, sql:Index(index.string, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, NULL), 6) → 4,5,6,7 [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\t\\x04\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\"]\ndelete mvcc:TxnWrite(6, sql:Index(index.bool, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(6, sql:Index(index.float, NaN)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf8\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(6, sql:Index(index.int, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(6, sql:Index(index.string, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(6, sql:Row(index, 6)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x06\\x00\\x00\"]\ndelete mvcc:TxnWrite(6, sql:Row(index, 7)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\"]\ndelete mvcc:TxnActive(6) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\"]\n\n# Float 0.0 and -0.0 are normalized as 0.0 and indexed as such.\n[ops]> INSERT INTO \"index\" (id, \"float\") VALUES (8, -0.0), (9, 0.0)\n---\nset mvcc:NextVersion → 8 [\"\\x00\" → \"\\x08\"]\nset mvcc:TxnActive(7) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\"]\nset mvcc:TxnWrite(7, sql:Row(index, 8)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x08\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(index, 8), 7) → 8,NULL,NULL,0.0,NULL [\"\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x08\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x0f\\x05\\x02\\x10\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\"]\nset mvcc:TxnWrite(7, sql:Index(index.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, NULL), 7) → 4,5,6,7,8 [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x0b\\x05\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\"]\nset mvcc:TxnWrite(7, sql:Index(index.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, NULL), 7) → 4,5,6,7,8 [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x0b\\x05\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\"]\nset mvcc:TxnWrite(7, sql:Index(index.float, 0.0)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, 0.0), 7) → 8 [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x03\\x01\\x02\\x10\"]\nset mvcc:TxnWrite(7, sql:Index(index.string, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, NULL), 7) → 4,5,6,7,8 [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x0b\\x05\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\"]\nset mvcc:TxnWrite(7, sql:Row(index, 9)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\t\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(index, 9), 7) → 9,NULL,NULL,0.0,NULL [\"\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\t\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x0f\\x05\\x02\\x12\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\"]\nset mvcc:TxnWrite(7, sql:Index(index.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, NULL), 7) → 4,5,6,7,8,9 [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\r\\x06\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x12\"]\nset mvcc:TxnWrite(7, sql:Index(index.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, NULL), 7) → 4,5,6,7,8,9 [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\r\\x06\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x12\"]\nset mvcc:TxnWrite(7, sql:Index(index.float, 0.0)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, 0.0), 7) → 8,9 [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x05\\x02\\x02\\x10\\x02\\x12\"]\nset mvcc:TxnWrite(7, sql:Index(index.string, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, NULL), 7) → 4,5,6,7,8,9 [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\r\\x06\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x12\"]\ndelete mvcc:TxnWrite(7, sql:Index(index.bool, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(7, sql:Index(index.float, 0.0)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(7, sql:Index(index.int, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(7, sql:Index(index.string, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(7, sql:Row(index, 8)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x08\\x00\\x00\"]\ndelete mvcc:TxnWrite(7, sql:Row(index, 9)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\t\\x00\\x00\"]\ndelete mvcc:TxnActive(7) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\"]\n\n# Float INFINITY is also indexed.\n[ops]> INSERT INTO \"index\" (id, \"float\") VALUES (10, -INFINITY), (11, INFINITY)\n---\nset mvcc:NextVersion → 9 [\"\\x00\" → \"\\t\"]\nset mvcc:TxnActive(8) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\"]\nset mvcc:TxnWrite(8, sql:Row(index, 10)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\n\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(index, 10), 8) → 10,NULL,NULL,-inf,NULL [\"\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\n\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x0f\\x05\\x02\\x14\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\xf0\\xff\\x00\"]\nset mvcc:TxnWrite(8, sql:Index(index.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, NULL), 8) → 4,5,6,7,8,9,10 [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x0f\\x07\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x12\\x02\\x14\"]\nset mvcc:TxnWrite(8, sql:Index(index.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, NULL), 8) → 4,5,6,7,8,9,10 [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x0f\\x07\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x12\\x02\\x14\"]\nset mvcc:TxnWrite(8, sql:Index(index.float, -inf)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x00\\xff\\x0f\\xff\\xff\\xff\\xff\\xff\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, -inf), 8) → 10 [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x00\\xff\\x0f\\xff\\xff\\xff\\xff\\xff\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x03\\x01\\x02\\x14\"]\nset mvcc:TxnWrite(8, sql:Index(index.string, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, NULL), 8) → 4,5,6,7,8,9,10 [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x0f\\x07\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x12\\x02\\x14\"]\nset mvcc:TxnWrite(8, sql:Row(index, 11)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x0b\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(index, 11), 8) → 11,NULL,NULL,inf,NULL [\"\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x0b\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x0f\\x05\\x02\\x16\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\xf0\\x7f\\x00\"]\nset mvcc:TxnWrite(8, sql:Index(index.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, NULL), 8) → 4,5,6,7,8,9,10,11 [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x11\\x08\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x12\\x02\\x14\\x02\\x16\"]\nset mvcc:TxnWrite(8, sql:Index(index.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, NULL), 8) → 4,5,6,7,8,9,10,11 [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x11\\x08\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x12\\x02\\x14\\x02\\x16\"]\nset mvcc:TxnWrite(8, sql:Index(index.float, inf)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf0\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, inf), 8) → 11 [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf0\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x03\\x01\\x02\\x16\"]\nset mvcc:TxnWrite(8, sql:Index(index.string, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, NULL), 8) → 4,5,6,7,8,9,10,11 [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x11\\x08\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x12\\x02\\x14\\x02\\x16\"]\ndelete mvcc:TxnWrite(8, sql:Index(index.bool, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Index(index.float, -inf)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x00\\xff\\x0f\\xff\\xff\\xff\\xff\\xff\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Index(index.float, inf)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf0\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Index(index.int, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Index(index.string, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Row(index, 10)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\n\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Row(index, 11)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x0b\\x00\\x00\"]\ndelete mvcc:TxnActive(8) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\"]\n\n[ops]> INSERT INTO \"index\" (id, \"float\") VALUES (12, -INFINITY), (13, INFINITY)\n---\nset mvcc:NextVersion → 10 [\"\\x00\" → \"\\n\"]\nset mvcc:TxnActive(9) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\"]\nset mvcc:TxnWrite(9, sql:Row(index, 12)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x0c\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(index, 12), 9) → 12,NULL,NULL,-inf,NULL [\"\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x0c\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x01\\x0f\\x05\\x02\\x18\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\xf0\\xff\\x00\"]\nset mvcc:TxnWrite(9, sql:Index(index.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, NULL), 9) → 4,5,6,7,8,9,10,11,12 [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x01\\x13\\t\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x12\\x02\\x14\\x02\\x16\\x02\\x18\"]\nset mvcc:TxnWrite(9, sql:Index(index.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, NULL), 9) → 4,5,6,7,8,9,10,11,12 [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x01\\x13\\t\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x12\\x02\\x14\\x02\\x16\\x02\\x18\"]\nset mvcc:TxnWrite(9, sql:Index(index.float, -inf)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x00\\xff\\x0f\\xff\\xff\\xff\\xff\\xff\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, -inf), 9) → 10,12 [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x00\\xff\\x0f\\xff\\xff\\xff\\xff\\xff\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x01\\x05\\x02\\x02\\x14\\x02\\x18\"]\nset mvcc:TxnWrite(9, sql:Index(index.string, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, NULL), 9) → 4,5,6,7,8,9,10,11,12 [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x01\\x13\\t\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x12\\x02\\x14\\x02\\x16\\x02\\x18\"]\nset mvcc:TxnWrite(9, sql:Row(index, 13)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\r\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(index, 13), 9) → 13,NULL,NULL,inf,NULL [\"\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\r\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x01\\x0f\\x05\\x02\\x1a\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\xf0\\x7f\\x00\"]\nset mvcc:TxnWrite(9, sql:Index(index.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, NULL), 9) → 4,5,6,7,8,9,10,11,12,13 [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x01\\x15\\n\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x12\\x02\\x14\\x02\\x16\\x02\\x18\\x02\\x1a\"]\nset mvcc:TxnWrite(9, sql:Index(index.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, NULL), 9) → 4,5,6,7,8,9,10,11,12,13 [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x01\\x15\\n\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x12\\x02\\x14\\x02\\x16\\x02\\x18\\x02\\x1a\"]\nset mvcc:TxnWrite(9, sql:Index(index.float, inf)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf0\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, inf), 9) → 11,13 [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf0\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x01\\x05\\x02\\x02\\x16\\x02\\x1a\"]\nset mvcc:TxnWrite(9, sql:Index(index.string, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, NULL), 9) → 4,5,6,7,8,9,10,11,12,13 [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x01\\x15\\n\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x12\\x02\\x14\\x02\\x16\\x02\\x18\\x02\\x1a\"]\ndelete mvcc:TxnWrite(9, sql:Index(index.bool, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(9, sql:Index(index.float, -inf)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x00\\xff\\x0f\\xff\\xff\\xff\\xff\\xff\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(9, sql:Index(index.float, inf)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf0\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(9, sql:Index(index.int, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(9, sql:Index(index.string, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(9, sql:Row(index, 12)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x0c\\x00\\x00\"]\ndelete mvcc:TxnWrite(9, sql:Row(index, 13)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\r\\x00\\x00\"]\ndelete mvcc:TxnActive(9) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\"]\n\n# Empty strings are considered equal.\n[ops]> INSERT INTO \"index\" (id, \"string\") VALUES (14, ''), (15, '')\n---\nset mvcc:NextVersion → 11 [\"\\x00\" → \"\\x0b\"]\nset mvcc:TxnActive(10) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\"]\nset mvcc:TxnWrite(10, sql:Row(index, 14)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x0e\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(index, 14), 10) → 14,NULL,NULL,NULL,'' [\"\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x0e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\\x01\\x08\\x05\\x02\\x1c\\x00\\x00\\x00\\x04\\x00\"]\nset mvcc:TxnWrite(10, sql:Index(index.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, NULL), 10) → 4,5,6,7,8,9,10,11,12,13,14 [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\\x01\\x17\\x0b\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x12\\x02\\x14\\x02\\x16\\x02\\x18\\x02\\x1a\\x02\\x1c\"]\nset mvcc:TxnWrite(10, sql:Index(index.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, NULL), 10) → 4,5,6,7,8,9,10,11,12,13,14 [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\\x01\\x17\\x0b\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x12\\x02\\x14\\x02\\x16\\x02\\x18\\x02\\x1a\\x02\\x1c\"]\nset mvcc:TxnWrite(10, sql:Index(index.float, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, NULL), 10) → 4,5,14 [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\\x01\\x07\\x03\\x02\\x08\\x02\\n\\x02\\x1c\"]\nset mvcc:TxnWrite(10, sql:Index(index.string, '')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, ''), 10) → 3,14 [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\\x01\\x05\\x02\\x02\\x06\\x02\\x1c\"]\nset mvcc:TxnWrite(10, sql:Row(index, 15)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x0f\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(index, 15), 10) → 15,NULL,NULL,NULL,'' [\"\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x0f\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\\x01\\x08\\x05\\x02\\x1e\\x00\\x00\\x00\\x04\\x00\"]\nset mvcc:TxnWrite(10, sql:Index(index.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, NULL), 10) → 4,5,6,7,8,9,10,11,12,13,14,15 [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\\x01\\x19\\x0c\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x12\\x02\\x14\\x02\\x16\\x02\\x18\\x02\\x1a\\x02\\x1c\\x02\\x1e\"]\nset mvcc:TxnWrite(10, sql:Index(index.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, NULL), 10) → 4,5,6,7,8,9,10,11,12,13,14,15 [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\\x01\\x19\\x0c\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x12\\x02\\x14\\x02\\x16\\x02\\x18\\x02\\x1a\\x02\\x1c\\x02\\x1e\"]\nset mvcc:TxnWrite(10, sql:Index(index.float, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, NULL), 10) → 4,5,14,15 [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\\x01\\t\\x04\\x02\\x08\\x02\\n\\x02\\x1c\\x02\\x1e\"]\nset mvcc:TxnWrite(10, sql:Index(index.string, '')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, ''), 10) → 3,14,15 [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\\x01\\x07\\x03\\x02\\x06\\x02\\x1c\\x02\\x1e\"]\ndelete mvcc:TxnWrite(10, sql:Index(index.bool, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(10, sql:Index(index.float, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(10, sql:Index(index.int, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(10, sql:Index(index.string, '')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(10, sql:Row(index, 14)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x0e\\x00\\x00\"]\ndelete mvcc:TxnWrite(10, sql:Row(index, 15)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x0f\\x00\\x00\"]\ndelete mvcc:TxnActive(10) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\"]\n\n# Case differences are not considered equal\n[ops]> INSERT INTO \"index\" (id, \"string\") VALUES (16, 'case'), (17, 'CaSe')\n---\nset mvcc:NextVersion → 12 [\"\\x00\" → \"\\x0c\"]\nset mvcc:TxnActive(11) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\"]\nset mvcc:TxnWrite(11, sql:Row(index, 16)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x10\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(index, 16), 11) → 16,NULL,NULL,NULL,'case' [\"\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x10\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\\x01\\x0c\\x05\\x02 \\x00\\x00\\x00\\x04\\x04case\"]\nset mvcc:TxnWrite(11, sql:Index(index.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, NULL), 11) → 4,5,6,7,8,9,10,11,12,13,14,15,16 [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\\x01\\x1b\\r\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x12\\x02\\x14\\x02\\x16\\x02\\x18\\x02\\x1a\\x02\\x1c\\x02\\x1e\\x02 \"]\nset mvcc:TxnWrite(11, sql:Index(index.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, NULL), 11) → 4,5,6,7,8,9,10,11,12,13,14,15,16 [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\\x01\\x1b\\r\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x12\\x02\\x14\\x02\\x16\\x02\\x18\\x02\\x1a\\x02\\x1c\\x02\\x1e\\x02 \"]\nset mvcc:TxnWrite(11, sql:Index(index.float, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, NULL), 11) → 4,5,14,15,16 [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\\x01\\x0b\\x05\\x02\\x08\\x02\\n\\x02\\x1c\\x02\\x1e\\x02 \"]\nset mvcc:TxnWrite(11, sql:Index(index.string, 'case')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04case\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, 'case'), 11) → 16 [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04case\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\\x01\\x03\\x01\\x02 \"]\nset mvcc:TxnWrite(11, sql:Row(index, 17)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x11\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(index, 17), 11) → 17,NULL,NULL,NULL,'CaSe' [\"\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x11\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\\x01\\x0c\\x05\\x02\\\"\\x00\\x00\\x00\\x04\\x04CaSe\"]\nset mvcc:TxnWrite(11, sql:Index(index.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, NULL), 11) → 4,5,6,7,8,9,10,11,12,13,14,15,16,17 [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\\x01\\x1d\\x0e\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x12\\x02\\x14\\x02\\x16\\x02\\x18\\x02\\x1a\\x02\\x1c\\x02\\x1e\\x02 \\x02\\\"\"]\nset mvcc:TxnWrite(11, sql:Index(index.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, NULL), 11) → 4,5,6,7,8,9,10,11,12,13,14,15,16,17 [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\\x01\\x1d\\x0e\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x12\\x02\\x14\\x02\\x16\\x02\\x18\\x02\\x1a\\x02\\x1c\\x02\\x1e\\x02 \\x02\\\"\"]\nset mvcc:TxnWrite(11, sql:Index(index.float, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, NULL), 11) → 4,5,14,15,16,17 [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\\x01\\r\\x06\\x02\\x08\\x02\\n\\x02\\x1c\\x02\\x1e\\x02 \\x02\\\"\"]\nset mvcc:TxnWrite(11, sql:Index(index.string, 'CaSe')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04CaSe\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, 'CaSe'), 11) → 17 [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04CaSe\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\\x01\\x03\\x01\\x02\\\"\"]\ndelete mvcc:TxnWrite(11, sql:Index(index.bool, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(11, sql:Index(index.float, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(11, sql:Index(index.int, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(11, sql:Index(index.string, 'CaSe')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04CaSe\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(11, sql:Index(index.string, 'case')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04case\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(11, sql:Row(index, 16)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x10\\x00\\x00\"]\ndelete mvcc:TxnWrite(11, sql:Row(index, 17)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x11\\x00\\x00\"]\ndelete mvcc:TxnActive(11) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\"]\n"
  },
  {
    "path": "src/sql/testscripts/writes/insert_null",
    "content": "# Tests nullability handling of INSERT.\n\n# Create a table with NULL constraints.\n> CREATE TABLE name ( \\\n    id INT PRIMARY KEY, \\\n    \"null\" STRING NULL, \\\n    not_null STRING NOT NULL \\\n)\n---\nok\n\n# INSERT with NULL works.\n> INSERT INTO name VALUES (1, NULL, 'foo')\n---\nok\n\n> SELECT * FROM name\n---\n1, NULL, 'foo'\n\n# INSERT with NULL into non-NULL columns errors.\n!> INSERT INTO name VALUES (NULL, 'foo', 'bar')\n!> INSERT INTO name VALUES (2, 'foo', NULL)\n---\nError: invalid input: invalid primary key NULL\nError: invalid input: NULL value not allowed for column not_null\n\n# Omitting a NULLable column works.\n> INSERT INTO name (id, not_null) VALUES (2, 'foo')\n---\nok\n\n> SELECT * FROM name\n---\n1, NULL, 'foo'\n2, NULL, 'foo'\n"
  },
  {
    "path": "src/sql/testscripts/writes/insert_primary_key",
    "content": "# Tests INSERT primary key handling.\n\n# Boolean.\n> CREATE TABLE \"bool\" (id BOOLEAN PRIMARY KEY)\n> INSERT INTO \"bool\" VALUES (true)\n> INSERT INTO \"bool\" VALUES (false)\n> SELECT * FROM \"bool\";\n---\nFALSE\nTRUE\n\n!> INSERT INTO \"bool\" VALUES (true)\n!> INSERT INTO \"bool\" VALUES (false)\n!> INSERT INTO \"bool\" VALUES (NULL)\n---\nError: invalid input: primary key TRUE already exists\nError: invalid input: primary key FALSE already exists\nError: invalid input: invalid primary key NULL\n\n# Integer.\n> CREATE TABLE \"int\" (id INT PRIMARY KEY)\n> INSERT INTO \"int\" VALUES (1)\n> INSERT INTO \"int\" VALUES (0)\n> INSERT INTO \"int\" VALUES (-1)\n> INSERT INTO \"int\" VALUES (9223372036854775807)\n> INSERT INTO \"int\" VALUES (-9223372036854775807)\n> SELECT * FROM \"int\";\n---\n-9223372036854775807\n-1\n0\n1\n9223372036854775807\n\n!> INSERT INTO \"int\" VALUES (1)\n!> INSERT INTO \"int\" VALUES (0)\n!> INSERT INTO \"int\" VALUES (-1)\n!> INSERT INTO \"int\" VALUES (9223372036854775807)\n!> INSERT INTO \"int\" VALUES (-9223372036854775807)\n!> INSERT INTO \"int\" VALUES (NULL)\n---\nError: invalid input: primary key 1 already exists\nError: invalid input: primary key 0 already exists\nError: invalid input: primary key -1 already exists\nError: invalid input: primary key 9223372036854775807 already exists\nError: invalid input: primary key -9223372036854775807 already exists\nError: invalid input: invalid primary key NULL\n\n# Float. -0.0 is normalized as 0.0.\n> CREATE TABLE \"float\" (id FLOAT PRIMARY KEY)\n> INSERT INTO \"float\" VALUES (3.14)\n> INSERT INTO \"float\" VALUES (-3.14)\n> INSERT INTO \"float\" VALUES (-0.0)\n> INSERT INTO \"float\" VALUES (1.23456789012345e308)\n> INSERT INTO \"float\" VALUES (-1.23456789012345e308)\n> INSERT INTO \"float\" VALUES (INFINITY)\n> INSERT INTO \"float\" VALUES (-INFINITY)\n> SELECT * FROM \"float\";\n---\n-inf\n-1.23456789012345e308\n-3.14\n0.0\n3.14\n1.23456789012345e308\ninf\n\n!> INSERT INTO \"float\" VALUES (3.14)\n!> INSERT INTO \"float\" VALUES (-3.14)\n!> INSERT INTO \"float\" VALUES (0.0)\n!> INSERT INTO \"float\" VALUES (-0.0)\n!> INSERT INTO \"float\" VALUES (1.23456789012345e308)\n!> INSERT INTO \"float\" VALUES (-1.23456789012345e308)\n!> INSERT INTO \"float\" VALUES (INFINITY)\n!> INSERT INTO \"float\" VALUES (-INFINITY)\n!> INSERT INTO \"float\" VALUES (NAN)\n!> INSERT INTO \"float\" VALUES (NULL)\n---\nError: invalid input: primary key 3.14 already exists\nError: invalid input: primary key -3.14 already exists\nError: invalid input: primary key 0.0 already exists\nError: invalid input: primary key -0.0 already exists\nError: invalid input: primary key 1.23456789012345e308 already exists\nError: invalid input: primary key -1.23456789012345e308 already exists\nError: invalid input: primary key inf already exists\nError: invalid input: primary key -inf already exists\nError: invalid input: invalid primary key NaN\nError: invalid input: invalid primary key NULL\n\n# String.\n> CREATE TABLE \"string\" (id STRING PRIMARY KEY)\n> INSERT INTO \"string\" VALUES ('')\n> INSERT INTO \"string\" VALUES ('  ')\n> INSERT INTO \"string\" VALUES ('abc')\n> INSERT INTO \"string\" VALUES ('ABC')\n> INSERT INTO \"string\" VALUES ('Hi! 👋')\n> SELECT * FROM \"string\";\n---\n''\n'  '\n'ABC'\n'Hi! 👋'\n'abc'\n\n!> INSERT INTO \"string\" VALUES ('')\n!> INSERT INTO \"string\" VALUES ('  ')\n!> INSERT INTO \"string\" VALUES ('abc')\n!> INSERT INTO \"string\" VALUES ('ABC')\n!> INSERT INTO \"string\" VALUES ('Hi! 👋')\n!> INSERT INTO \"string\" VALUES (NULL)\n---\nError: invalid input: primary key '' already exists\nError: invalid input: primary key '  ' already exists\nError: invalid input: primary key 'abc' already exists\nError: invalid input: primary key 'ABC' already exists\nError: invalid input: primary key 'Hi! 👋' already exists\nError: invalid input: invalid primary key NULL\n"
  },
  {
    "path": "src/sql/testscripts/writes/insert_reference",
    "content": "# Tests INSERT foreign key references.\n\n# Create reference tables for all datatypes.\n> CREATE TABLE \"bool\" (id BOOL PRIMARY KEY)\n> INSERT INTO \"bool\" VALUES (true)\n\n> CREATE TABLE \"int\" (id INT PRIMARY KEY)\n> INSERT INTO \"int\" VALUES (-1), (0), (1)\n\n> CREATE TABLE \"float\" (id FLOAT PRIMARY KEY)\n> INSERT INTO \"float\" VALUES (3.14), (0.0), (INFINITY)\n\n> CREATE TABLE \"string\" (id STRING PRIMARY KEY)\n> INSERT INTO \"string\" VALUES (''), ('foo')\n\n> CREATE TABLE name ( \\\n    id INT PRIMARY KEY, \\\n    \"bool\" BOOL REFERENCES \"bool\", \\\n    \"int\" INT REFERENCES \"int\", \\\n    \"float\" FLOAT REFERENCES \"float\", \\\n    \"string\" STRING REFERENCES \"string\" \\\n)\n---\nok\n\n# INSERTs with existing references work, and update the index entries.\n[ops]> INSERT INTO name VALUES (1, true, 1, 3.14, 'foo')\n---\nset mvcc:NextVersion → 11 [\"\\x00\" → \"\\x0b\"]\nset mvcc:TxnActive(10) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\"]\nset mvcc:TxnWrite(10, sql:Row(name, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 1), 10) → 1,TRUE,1,3.14,'foo' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\\x01\\x15\\x05\\x02\\x02\\x01\\x01\\x02\\x02\\x03\\x1f\\x85\\xebQ\\xb8\\x1e\\t@\\x04\\x03foo\"]\nset mvcc:TxnWrite(10, sql:Index(name.bool, TRUE)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01name\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.bool, TRUE), 10) → 1 [\"\\x04\\x01name\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(10, sql:Index(name.int, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01name\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.int, 1), 10) → 1 [\"\\x04\\x01name\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(10, sql:Index(name.float, 3.14)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01name\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.float, 3.14), 10) → 1 [\"\\x04\\x01name\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(10, sql:Index(name.string, 'foo')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01name\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.string, 'foo'), 10) → 1 [\"\\x04\\x01name\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\\x01\\x03\\x01\\x02\\x02\"]\ndelete mvcc:TxnWrite(10, sql:Index(name.bool, TRUE)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01name\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(10, sql:Index(name.float, 3.14)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01name\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\"]\ndelete mvcc:TxnWrite(10, sql:Index(name.int, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01name\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(10, sql:Index(name.string, 'foo')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01name\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(10, sql:Row(name, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnActive(10) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\"]\n\n# INSERTs error on missing references.\n!> INSERT INTO name (id, \"bool\") VALUES (2, FALSE)\n!> INSERT INTO name (id, \"int\") VALUES (2, 7)\n!> INSERT INTO name (id, \"float\") VALUES (2, 2.718)\n!> INSERT INTO name (id, \"string\") VALUES (2, 'bar')\n---\nError: invalid input: reference FALSE not in table bool\nError: invalid input: reference 7 not in table int\nError: invalid input: reference 2.718 not in table float\nError: invalid input: reference 'bar' not in table string\n\n# -0.0 is equivalent to 0.0.\n[ops]> INSERT INTO name (id, \"float\") VALUES (2, -0.0)\n---\nset mvcc:NextVersion → 16 [\"\\x00\" → \"\\x10\"]\nset mvcc:TxnActive(15) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\" → \"\"]\nset mvcc:TxnWrite(15, sql:Row(name, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 2), 15) → 2,NULL,NULL,0.0,NULL [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\" → \"\\x01\\x0f\\x05\\x02\\x04\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\"]\nset mvcc:TxnWrite(15, sql:Index(name.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\\x01name\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.bool, NULL), 15) → 2 [\"\\x04\\x01name\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nset mvcc:TxnWrite(15, sql:Index(name.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\\x01name\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.int, NULL), 15) → 2 [\"\\x04\\x01name\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nset mvcc:TxnWrite(15, sql:Index(name.float, 0.0)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\\x01name\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.float, 0.0), 15) → 2 [\"\\x04\\x01name\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nset mvcc:TxnWrite(15, sql:Index(name.string, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\\x01name\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.string, NULL), 15) → 2 [\"\\x04\\x01name\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\" → \"\\x01\\x03\\x01\\x02\\x04\"]\ndelete mvcc:TxnWrite(15, sql:Index(name.bool, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\\x01name\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(15, sql:Index(name.float, 0.0)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\\x01name\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(15, sql:Index(name.int, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\\x01name\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(15, sql:Index(name.string, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\\x01name\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(15, sql:Row(name, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnActive(15) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\"]\n\n# NaN is not valid as a missing reference marker.\n!> INSERT INTO name (id, \"float\") VALUES (3, NAN)\n---\nError: invalid input: reference NaN not in table float\n\n# INFINITY is a valid reference.\n> INSERT INTO name (id, \"float\") VALUES (3, INFINITY)\n---\nok\n\n# References are case sensitive.\n!> INSERT INTO name (id, \"string\") VALUES (4, 'FOO')\n---\nError: invalid input: reference 'FOO' not in table string\n\n# Empty strings are valid references.\n> INSERT INTO name (id, \"string\") VALUES (5, '')\n---\nok\n\n# NULLs are valid.\n> INSERT INTO name (id) VALUES (6)\n---\nok\n\n> SELECT * FROM name\n---\n1, TRUE, 1, 3.14, 'foo'\n2, NULL, NULL, 0.0, NULL\n3, NULL, NULL, inf, NULL\n5, NULL, NULL, NULL, ''\n6, NULL, NULL, NULL, NULL\n\n# Self references are fine.\n> CREATE TABLE self (id INT PRIMARY KEY, self_id INT REFERENCES self)\n---\nok\n\n[ops]> INSERT INTO self VALUES (1, 1)\n---\nset mvcc:NextVersion → 23 [\"\\x00\" → \"\\x17\"]\nset mvcc:TxnActive(22) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x16\" → \"\"]\nset mvcc:TxnWrite(22, sql:Row(self, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x16\\x02self\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(self, 1), 22) → 1,1 [\"\\x04\\x02self\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x16\" → \"\\x01\\x05\\x02\\x02\\x02\\x02\\x02\"]\nset mvcc:TxnWrite(22, sql:Index(self.self_id, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x16\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(self.self_id, 1), 22) → 1 [\"\\x04\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x16\" → \"\\x01\\x03\\x01\\x02\\x02\"]\ndelete mvcc:TxnWrite(22, sql:Index(self.self_id, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x16\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(22, sql:Row(self, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x16\\x02self\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnActive(22) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x16\"]\n\n[ops]> INSERT INTO self VALUES (2, 1)\n---\nset mvcc:NextVersion → 24 [\"\\x00\" → \"\\x18\"]\nset mvcc:TxnActive(23) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x17\" → \"\"]\nset mvcc:TxnWrite(23, sql:Row(self, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x17\\x02self\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(self, 2), 23) → 2,1 [\"\\x04\\x02self\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x17\" → \"\\x01\\x05\\x02\\x02\\x04\\x02\\x02\"]\nset mvcc:TxnWrite(23, sql:Index(self.self_id, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x17\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(self.self_id, 1), 23) → 1,2 [\"\\x04\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x17\" → \"\\x01\\x05\\x02\\x02\\x02\\x02\\x04\"]\ndelete mvcc:TxnWrite(23, sql:Index(self.self_id, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x17\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(23, sql:Row(self, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x17\\x02self\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnActive(23) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x17\"]\n\n[ops]> INSERT INTO self VALUES (3, NULL)\n---\nset mvcc:NextVersion → 25 [\"\\x00\" → \"\\x19\"]\nset mvcc:TxnActive(24) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x18\" → \"\"]\nset mvcc:TxnWrite(24, sql:Row(self, 3)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x18\\x02self\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(self, 3), 24) → 3,NULL [\"\\x04\\x02self\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x18\" → \"\\x01\\x04\\x02\\x02\\x06\\x00\"]\nset mvcc:TxnWrite(24, sql:Index(self.self_id, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x18\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(self.self_id, NULL), 24) → 3 [\"\\x04\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x18\" → \"\\x01\\x03\\x01\\x02\\x06\"]\ndelete mvcc:TxnWrite(24, sql:Index(self.self_id, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x18\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(24, sql:Row(self, 3)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x18\\x02self\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\"]\ndelete mvcc:TxnActive(24) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x18\"]\n\n!> INSERT INTO self VALUES (4, 9)\n---\nError: invalid input: reference 9 not in table self\n"
  },
  {
    "path": "src/sql/testscripts/writes/insert_unique",
    "content": "# Tests INSERT index writes.\n\n> CREATE TABLE \"unique\" ( \\\n    id INT PRIMARY KEY, \\\n    \"bool\" BOOL UNIQUE, \\\n    \"int\" INT UNIQUE, \\\n    \"float\" FLOAT UNIQUE, \\\n    \"string\" STRING UNIQUE \\\n)\n---\nok\n\n# An INSERT writes to all indexes.\n[ops]> INSERT INTO \"unique\" VALUES (1, TRUE, 7, 3.14, 'foo')\n---\nset mvcc:NextVersion → 3 [\"\\x00\" → \"\\x03\"]\nset mvcc:TxnActive(2) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\"]\nset mvcc:TxnWrite(2, sql:Row(unique, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(unique, 1), 2) → 1,TRUE,7,3.14,'foo' [\"\\x04\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x15\\x05\\x02\\x02\\x01\\x01\\x02\\x0e\\x03\\x1f\\x85\\xebQ\\xb8\\x1e\\t@\\x04\\x03foo\"]\nset mvcc:TxnWrite(2, sql:Index(unique.bool, TRUE)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.bool, TRUE), 2) → 1 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(2, sql:Index(unique.int, 7)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.int, 7), 2) → 1 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(2, sql:Index(unique.float, 3.14)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, 3.14), 2) → 1 [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(2, sql:Index(unique.string, 'foo')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.string, 'foo'), 2) → 1 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x03\\x01\\x02\\x02\"]\ndelete mvcc:TxnWrite(2, sql:Index(unique.bool, TRUE)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(2, sql:Index(unique.float, 3.14)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\"]\ndelete mvcc:TxnWrite(2, sql:Index(unique.int, 7)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\"]\ndelete mvcc:TxnWrite(2, sql:Index(unique.string, 'foo')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(2, sql:Row(unique, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnActive(2) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\"]\n\n# Another insert with the same values errors for all indexes.\n!> INSERT INTO \"unique\" (id, \"bool\") VALUES (2, TRUE)\n!> INSERT INTO \"unique\" (id, \"int\") VALUES (2, 7)\n!> INSERT INTO \"unique\" (id, \"float\") VALUES (2, 3.14)\n!> INSERT INTO \"unique\" (id, \"string\") VALUES (2, 'foo')\n---\nError: invalid input: value TRUE already in unique column bool\nError: invalid input: value 7 already in unique column int\nError: invalid input: value 3.14 already in unique column float\nError: invalid input: value 'foo' already in unique column string\n\n# An insert with different values writes new index entries.\n[ops]> INSERT INTO \"unique\" VALUES (3, FALSE, 0, 2.718, 'bar')\n---\nset mvcc:NextVersion → 8 [\"\\x00\" → \"\\x08\"]\nset mvcc:TxnActive(7) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\"]\nset mvcc:TxnWrite(7, sql:Row(unique, 3)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(unique, 3), 7) → 3,FALSE,0,2.718,'bar' [\"\\x04\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x15\\x05\\x02\\x06\\x01\\x00\\x02\\x00\\x03X9\\xb4\\xc8v\\xbe\\x05@\\x04\\x03bar\"]\nset mvcc:TxnWrite(7, sql:Index(unique.bool, FALSE)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.bool, FALSE), 7) → 3 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x03\\x01\\x02\\x06\"]\nset mvcc:TxnWrite(7, sql:Index(unique.int, 0)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.int, 0), 7) → 3 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x03\\x01\\x02\\x06\"]\nset mvcc:TxnWrite(7, sql:Index(unique.float, 2.718)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\x05\\xbev\\xc8\\xb49X\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, 2.718), 7) → 3 [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\x05\\xbev\\xc8\\xb49X\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x03\\x01\\x02\\x06\"]\nset mvcc:TxnWrite(7, sql:Index(unique.string, 'bar')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04bar\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.string, 'bar'), 7) → 3 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04bar\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x03\\x01\\x02\\x06\"]\ndelete mvcc:TxnWrite(7, sql:Index(unique.bool, FALSE)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(7, sql:Index(unique.float, 2.718)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\x05\\xbev\\xc8\\xb49X\\x00\\x00\"]\ndelete mvcc:TxnWrite(7, sql:Index(unique.int, 0)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(7, sql:Index(unique.string, 'bar')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04bar\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(7, sql:Row(unique, 3)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\"]\ndelete mvcc:TxnActive(7) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\"]\n\n# Inserts with NULLS adds NULL entries. Duplicates are allowed\n[ops]> INSERT INTO \"unique\" VALUES (4)\n---\nset mvcc:NextVersion → 9 [\"\\x00\" → \"\\t\"]\nset mvcc:TxnActive(8) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\"]\nset mvcc:TxnWrite(8, sql:Row(unique, 4)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(unique, 4), 8) → 4,NULL,NULL,NULL,NULL [\"\\x04\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x07\\x05\\x02\\x08\\x00\\x00\\x00\\x00\"]\nset mvcc:TxnWrite(8, sql:Index(unique.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.bool, NULL), 8) → 4 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x03\\x01\\x02\\x08\"]\nset mvcc:TxnWrite(8, sql:Index(unique.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.int, NULL), 8) → 4 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x03\\x01\\x02\\x08\"]\nset mvcc:TxnWrite(8, sql:Index(unique.float, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, NULL), 8) → 4 [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x03\\x01\\x02\\x08\"]\nset mvcc:TxnWrite(8, sql:Index(unique.string, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.string, NULL), 8) → 4 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x03\\x01\\x02\\x08\"]\ndelete mvcc:TxnWrite(8, sql:Index(unique.bool, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Index(unique.float, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Index(unique.int, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Index(unique.string, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Row(unique, 4)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x04\\x00\\x00\"]\ndelete mvcc:TxnActive(8) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\"]\n\n[ops]> INSERT INTO \"unique\" VALUES (5)\n---\nset mvcc:NextVersion → 10 [\"\\x00\" → \"\\n\"]\nset mvcc:TxnActive(9) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\"]\nset mvcc:TxnWrite(9, sql:Row(unique, 5)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x05\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(unique, 5), 9) → 5,NULL,NULL,NULL,NULL [\"\\x04\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x05\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x01\\x07\\x05\\x02\\n\\x00\\x00\\x00\\x00\"]\nset mvcc:TxnWrite(9, sql:Index(unique.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.bool, NULL), 9) → 4,5 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x01\\x05\\x02\\x02\\x08\\x02\\n\"]\nset mvcc:TxnWrite(9, sql:Index(unique.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.int, NULL), 9) → 4,5 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x01\\x05\\x02\\x02\\x08\\x02\\n\"]\nset mvcc:TxnWrite(9, sql:Index(unique.float, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, NULL), 9) → 4,5 [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x01\\x05\\x02\\x02\\x08\\x02\\n\"]\nset mvcc:TxnWrite(9, sql:Index(unique.string, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.string, NULL), 9) → 4,5 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\" → \"\\x01\\x05\\x02\\x02\\x08\\x02\\n\"]\ndelete mvcc:TxnWrite(9, sql:Index(unique.bool, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(9, sql:Index(unique.float, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(9, sql:Index(unique.int, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(9, sql:Index(unique.string, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(9, sql:Row(unique, 5)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x05\\x00\\x00\"]\ndelete mvcc:TxnActive(9) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\"]\n\n# Float NaNs are considered different and allowed.\n[ops]> INSERT INTO \"unique\" (id, \"float\") VALUES (6, NAN)\n---\nset mvcc:NextVersion → 11 [\"\\x00\" → \"\\x0b\"]\nset mvcc:TxnActive(10) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\"]\nset mvcc:TxnWrite(10, sql:Row(unique, 6)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x06\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(unique, 6), 10) → 6,NULL,NULL,NaN,NULL [\"\\x04\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x06\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\\x01\\x0f\\x05\\x02\\x0c\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\xf8\\x7f\\x00\"]\nset mvcc:TxnWrite(10, sql:Index(unique.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.bool, NULL), 10) → 4,5,6 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\\x01\\x07\\x03\\x02\\x08\\x02\\n\\x02\\x0c\"]\nset mvcc:TxnWrite(10, sql:Index(unique.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.int, NULL), 10) → 4,5,6 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\\x01\\x07\\x03\\x02\\x08\\x02\\n\\x02\\x0c\"]\nset mvcc:TxnWrite(10, sql:Index(unique.float, NaN)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf8\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, NaN), 10) → 6 [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf8\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\\x01\\x03\\x01\\x02\\x0c\"]\nset mvcc:TxnWrite(10, sql:Index(unique.string, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.string, NULL), 10) → 4,5,6 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\" → \"\\x01\\x07\\x03\\x02\\x08\\x02\\n\\x02\\x0c\"]\ndelete mvcc:TxnWrite(10, sql:Index(unique.bool, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(10, sql:Index(unique.float, NaN)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf8\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(10, sql:Index(unique.int, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(10, sql:Index(unique.string, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(10, sql:Row(unique, 6)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x06\\x00\\x00\"]\ndelete mvcc:TxnActive(10) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\"]\n\n[ops]> INSERT INTO \"unique\" (id, \"float\") VALUES (7, NAN)\n---\nset mvcc:NextVersion → 12 [\"\\x00\" → \"\\x0c\"]\nset mvcc:TxnActive(11) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\"]\nset mvcc:TxnWrite(11, sql:Row(unique, 7)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(unique, 7), 11) → 7,NULL,NULL,NaN,NULL [\"\\x04\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\\x01\\x0f\\x05\\x02\\x0e\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\xf8\\x7f\\x00\"]\nset mvcc:TxnWrite(11, sql:Index(unique.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.bool, NULL), 11) → 4,5,6,7 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\\x01\\t\\x04\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\"]\nset mvcc:TxnWrite(11, sql:Index(unique.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.int, NULL), 11) → 4,5,6,7 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\\x01\\t\\x04\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\"]\nset mvcc:TxnWrite(11, sql:Index(unique.float, NaN)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf8\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, NaN), 11) → 6,7 [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf8\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\\x01\\x05\\x02\\x02\\x0c\\x02\\x0e\"]\nset mvcc:TxnWrite(11, sql:Index(unique.string, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.string, NULL), 11) → 4,5,6,7 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\\x01\\t\\x04\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\"]\ndelete mvcc:TxnWrite(11, sql:Index(unique.bool, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(11, sql:Index(unique.float, NaN)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf8\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(11, sql:Index(unique.int, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(11, sql:Index(unique.string, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(11, sql:Row(unique, 7)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\"]\ndelete mvcc:TxnActive(11) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\"]\n\n# Float 0.0 and -0.0 are considered equal.\n[ops]> INSERT INTO \"unique\" (id, \"float\") VALUES (8, -0.0)\n---\nset mvcc:NextVersion → 13 [\"\\x00\" → \"\\r\"]\nset mvcc:TxnActive(12) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\" → \"\"]\nset mvcc:TxnWrite(12, sql:Row(unique, 8)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x08\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(unique, 8), 12) → 8,NULL,NULL,0.0,NULL [\"\\x04\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x08\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\" → \"\\x01\\x0f\\x05\\x02\\x10\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\"]\nset mvcc:TxnWrite(12, sql:Index(unique.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.bool, NULL), 12) → 4,5,6,7,8 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\" → \"\\x01\\x0b\\x05\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\"]\nset mvcc:TxnWrite(12, sql:Index(unique.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.int, NULL), 12) → 4,5,6,7,8 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\" → \"\\x01\\x0b\\x05\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\"]\nset mvcc:TxnWrite(12, sql:Index(unique.float, 0.0)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, 0.0), 12) → 8 [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\" → \"\\x01\\x03\\x01\\x02\\x10\"]\nset mvcc:TxnWrite(12, sql:Index(unique.string, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.string, NULL), 12) → 4,5,6,7,8 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\" → \"\\x01\\x0b\\x05\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\"]\ndelete mvcc:TxnWrite(12, sql:Index(unique.bool, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(12, sql:Index(unique.float, 0.0)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(12, sql:Index(unique.int, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(12, sql:Index(unique.string, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(12, sql:Row(unique, 8)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x08\\x00\\x00\"]\ndelete mvcc:TxnActive(12) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\"]\n\n!> INSERT INTO \"unique\" (id, \"float\") VALUES (9, 0.0)\n---\nError: invalid input: value 0.0 already in unique column float\n\n# Float INFINITY is also unique.\n[ops]> INSERT INTO \"unique\" (id, \"float\") VALUES (10, INFINITY)\n---\nset mvcc:NextVersion → 15 [\"\\x00\" → \"\\x0f\"]\nset mvcc:TxnActive(14) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\" → \"\"]\nset mvcc:TxnWrite(14, sql:Row(unique, 10)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\n\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(unique, 10), 14) → 10,NULL,NULL,inf,NULL [\"\\x04\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\n\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\" → \"\\x01\\x0f\\x05\\x02\\x14\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\xf0\\x7f\\x00\"]\nset mvcc:TxnWrite(14, sql:Index(unique.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.bool, NULL), 14) → 4,5,6,7,8,10 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\" → \"\\x01\\r\\x06\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x14\"]\nset mvcc:TxnWrite(14, sql:Index(unique.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.int, NULL), 14) → 4,5,6,7,8,10 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\" → \"\\x01\\r\\x06\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x14\"]\nset mvcc:TxnWrite(14, sql:Index(unique.float, inf)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf0\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, inf), 14) → 10 [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf0\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\" → \"\\x01\\x03\\x01\\x02\\x14\"]\nset mvcc:TxnWrite(14, sql:Index(unique.string, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.string, NULL), 14) → 4,5,6,7,8,10 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\" → \"\\x01\\r\\x06\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x14\"]\ndelete mvcc:TxnWrite(14, sql:Index(unique.bool, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(14, sql:Index(unique.float, inf)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf0\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(14, sql:Index(unique.int, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(14, sql:Index(unique.string, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(14, sql:Row(unique, 10)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\n\\x00\\x00\"]\ndelete mvcc:TxnActive(14) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\"]\n\n!> INSERT INTO \"unique\" (id, \"float\") VALUES (11, INFINITY)\n---\nError: invalid input: value inf already in unique column float\n\n# Empty strings are considered equal.\n[ops]> INSERT INTO \"unique\" (id, \"string\") VALUES (11, '')\n---\nset mvcc:NextVersion → 17 [\"\\x00\" → \"\\x11\"]\nset mvcc:TxnActive(16) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x10\" → \"\"]\nset mvcc:TxnWrite(16, sql:Row(unique, 11)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x10\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x0b\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(unique, 11), 16) → 11,NULL,NULL,NULL,'' [\"\\x04\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x0b\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x10\" → \"\\x01\\x08\\x05\\x02\\x16\\x00\\x00\\x00\\x04\\x00\"]\nset mvcc:TxnWrite(16, sql:Index(unique.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x10\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.bool, NULL), 16) → 4,5,6,7,8,10,11 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x10\" → \"\\x01\\x0f\\x07\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x14\\x02\\x16\"]\nset mvcc:TxnWrite(16, sql:Index(unique.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x10\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.int, NULL), 16) → 4,5,6,7,8,10,11 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x10\" → \"\\x01\\x0f\\x07\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x14\\x02\\x16\"]\nset mvcc:TxnWrite(16, sql:Index(unique.float, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x10\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, NULL), 16) → 4,5,11 [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x10\" → \"\\x01\\x07\\x03\\x02\\x08\\x02\\n\\x02\\x16\"]\nset mvcc:TxnWrite(16, sql:Index(unique.string, '')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x10\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.string, ''), 16) → 11 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x10\" → \"\\x01\\x03\\x01\\x02\\x16\"]\ndelete mvcc:TxnWrite(16, sql:Index(unique.bool, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x10\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(16, sql:Index(unique.float, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x10\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(16, sql:Index(unique.int, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x10\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(16, sql:Index(unique.string, '')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x10\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(16, sql:Row(unique, 11)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x10\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x0b\\x00\\x00\"]\ndelete mvcc:TxnActive(16) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x10\"]\n\n!> INSERT INTO \"unique\" (id, \"string\") VALUES (12, '')\n---\nError: invalid input: value '' already in unique column string\n\n# Case differences are not considered equal.\n[ops]> INSERT INTO \"unique\" (id, \"string\") VALUES (12, 'case')\n---\nset mvcc:NextVersion → 19 [\"\\x00\" → \"\\x13\"]\nset mvcc:TxnActive(18) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\" → \"\"]\nset mvcc:TxnWrite(18, sql:Row(unique, 12)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x0c\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(unique, 12), 18) → 12,NULL,NULL,NULL,'case' [\"\\x04\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x0c\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\" → \"\\x01\\x0c\\x05\\x02\\x18\\x00\\x00\\x00\\x04\\x04case\"]\nset mvcc:TxnWrite(18, sql:Index(unique.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.bool, NULL), 18) → 4,5,6,7,8,10,11,12 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\" → \"\\x01\\x11\\x08\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x14\\x02\\x16\\x02\\x18\"]\nset mvcc:TxnWrite(18, sql:Index(unique.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.int, NULL), 18) → 4,5,6,7,8,10,11,12 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\" → \"\\x01\\x11\\x08\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x14\\x02\\x16\\x02\\x18\"]\nset mvcc:TxnWrite(18, sql:Index(unique.float, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, NULL), 18) → 4,5,11,12 [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\" → \"\\x01\\t\\x04\\x02\\x08\\x02\\n\\x02\\x16\\x02\\x18\"]\nset mvcc:TxnWrite(18, sql:Index(unique.string, 'case')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04case\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.string, 'case'), 18) → 12 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04case\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\" → \"\\x01\\x03\\x01\\x02\\x18\"]\ndelete mvcc:TxnWrite(18, sql:Index(unique.bool, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(18, sql:Index(unique.float, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(18, sql:Index(unique.int, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(18, sql:Index(unique.string, 'case')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04case\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(18, sql:Row(unique, 12)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x0c\\x00\\x00\"]\ndelete mvcc:TxnActive(18) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\"]\n\n[ops]> INSERT INTO \"unique\" (id, \"string\") VALUES (13, 'CaSe')\n---\nset mvcc:NextVersion → 20 [\"\\x00\" → \"\\x14\"]\nset mvcc:TxnActive(19) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x13\" → \"\"]\nset mvcc:TxnWrite(19, sql:Row(unique, 13)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x13\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\r\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(unique, 13), 19) → 13,NULL,NULL,NULL,'CaSe' [\"\\x04\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\r\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x13\" → \"\\x01\\x0c\\x05\\x02\\x1a\\x00\\x00\\x00\\x04\\x04CaSe\"]\nset mvcc:TxnWrite(19, sql:Index(unique.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x13\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.bool, NULL), 19) → 4,5,6,7,8,10,11,12,13 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x13\" → \"\\x01\\x13\\t\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x14\\x02\\x16\\x02\\x18\\x02\\x1a\"]\nset mvcc:TxnWrite(19, sql:Index(unique.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x13\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.int, NULL), 19) → 4,5,6,7,8,10,11,12,13 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x13\" → \"\\x01\\x13\\t\\x02\\x08\\x02\\n\\x02\\x0c\\x02\\x0e\\x02\\x10\\x02\\x14\\x02\\x16\\x02\\x18\\x02\\x1a\"]\nset mvcc:TxnWrite(19, sql:Index(unique.float, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x13\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, NULL), 19) → 4,5,11,12,13 [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x13\" → \"\\x01\\x0b\\x05\\x02\\x08\\x02\\n\\x02\\x16\\x02\\x18\\x02\\x1a\"]\nset mvcc:TxnWrite(19, sql:Index(unique.string, 'CaSe')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x13\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04CaSe\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.string, 'CaSe'), 19) → 13 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04CaSe\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x13\" → \"\\x01\\x03\\x01\\x02\\x1a\"]\ndelete mvcc:TxnWrite(19, sql:Index(unique.bool, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x13\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(19, sql:Index(unique.float, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x13\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(19, sql:Index(unique.int, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x13\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(19, sql:Index(unique.string, 'CaSe')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x13\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04CaSe\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(19, sql:Row(unique, 13)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x13\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\r\\x00\\x00\"]\ndelete mvcc:TxnActive(19) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x13\"]\n"
  },
  {
    "path": "src/sql/testscripts/writes/update",
    "content": "# Tests basic UPDATE functionality.\n\n> CREATE TABLE name (id INT PRIMARY KEY, value STRING)\n> INSERT INTO name VALUES (1, 'a'), (2, 'b')\n---\nok\n\n# UPDATE updates rows, and returns the number of rows.\n[plan,result,ops]> UPDATE name SET value = 'foo'\n---\nUpdate: name (value='foo')\n└─ Scan: name\nset mvcc:NextVersion → 4 [\"\\x00\" → \"\\x04\"]\nset mvcc:TxnActive(3) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\"]\nset mvcc:TxnWrite(3, sql:Row(name, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 1), 3) → 1,'foo' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x08\\x02\\x02\\x02\\x04\\x03foo\"]\nset mvcc:TxnWrite(3, sql:Row(name, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 2), 3) → 2,'foo' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x08\\x02\\x02\\x04\\x04\\x03foo\"]\ndelete mvcc:TxnWrite(3, sql:Row(name, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(3, sql:Row(name, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnActive(3) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\"]\nUpdate { count: 2 }\n\n> SELECT * FROM name\n---\n1, 'foo'\n2, 'foo'\n\ndump\n---\nmvcc:NextVersion → 4 [\"\\x00\" → \"\\x04\"]\nmvcc:Version(sql:Table(name), 1) → CREATE TABLE name ( id INTEGER PRIMARY KEY, value STRING DEFAULT NULL ) [\"\\x04\\x00\\xffname\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x1d\\x04name\\x00\\x02\\x02id\\x01\\x00\\x00\\x01\\x00\\x00\\x05value\\x03\\x01\\x01\\x00\\x00\\x00\\x00\"]\nmvcc:Version(sql:Row(name, 1), 2) → 1,'a' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x02\\x04\\x01a\"]\nmvcc:Version(sql:Row(name, 1), 3) → 1,'foo' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x08\\x02\\x02\\x02\\x04\\x03foo\"]\nmvcc:Version(sql:Row(name, 2), 2) → 2,'b' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x06\\x02\\x02\\x04\\x04\\x01b\"]\nmvcc:Version(sql:Row(name, 2), 3) → 2,'foo' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x08\\x02\\x02\\x04\\x04\\x03foo\"]\n\n# Bare UPDATE errors.\n!> UPDATE\n!> UPDATE name\n!> UPDATE name SET\n!> UPDATE name SET value\n---\nError: invalid input: unexpected end of input\nError: invalid input: unexpected end of input\nError: invalid input: unexpected end of input\nError: invalid input: unexpected end of input\n\n# Unknown table or column errors.\n!> UPDATE foo SET value = 'bar'\n!> UPDATE name SET foo = 'bar'\n---\nError: invalid input: table foo does not exist\nError: invalid input: unknown column foo\n\n# Specifying the same column multiple times errors.\n!> UPDATE name SET value = 'e', value = 'f'\n---\nError: invalid input: column value set multiple times\n"
  },
  {
    "path": "src/sql/testscripts/writes/update_datatypes",
    "content": "# Tests UPDATE of all datatypes.\n\n# Create columns and a row with all datatypes.\n> CREATE TABLE datatypes ( \\\n    id INTEGER PRIMARY KEY, \\\n    \"bool\" BOOL, \\\n    \"int\" INT, \\\n    \"float\" FLOAT, \\\n    \"string\" STRING \\\n)\n> INSERT INTO datatypes VALUES (1)\n---\nok\n\n# Booleans.\n> UPDATE datatypes SET \"bool\" = NULL\n> UPDATE datatypes SET \"bool\" = FALSE\n> UPDATE datatypes SET \"bool\" = TRUE\n> SELECT * FROM datatypes\n---\n1, TRUE, NULL, NULL, NULL\n\n!> UPDATE datatypes SET \"bool\" = 1\n!> UPDATE datatypes SET \"bool\" = 3.14\n!> UPDATE datatypes SET \"bool\" = 'false'\n---\nError: invalid input: invalid datatype INTEGER for BOOLEAN column bool\nError: invalid input: invalid datatype FLOAT for BOOLEAN column bool\nError: invalid input: invalid datatype STRING for BOOLEAN column bool\n\n# Integers.\n> UPDATE datatypes SET \"int\" = NULL\n> UPDATE datatypes SET \"int\" = 1\n> UPDATE datatypes SET \"int\" = 0\n> UPDATE datatypes SET \"int\" = -1\n> UPDATE datatypes SET \"int\" = 9223372036854775807\n> UPDATE datatypes SET \"int\" = -9223372036854775807\n> SELECT * FROM datatypes\n---\n1, TRUE, -9223372036854775807, NULL, NULL\n\n!> UPDATE datatypes SET \"int\" = false\n!> UPDATE datatypes SET \"int\" = 3.0\n!> UPDATE datatypes SET \"int\" = '0'\n---\nError: invalid input: invalid datatype BOOLEAN for INTEGER column int\nError: invalid input: invalid datatype FLOAT for INTEGER column int\nError: invalid input: invalid datatype STRING for INTEGER column int\n\n# Floats.\n> UPDATE datatypes SET \"float\" = NULL\n> UPDATE datatypes SET \"float\" = 3.14\n> UPDATE datatypes SET \"float\" = -3.14\n> UPDATE datatypes SET \"float\" = 1.23456789012345e308\n> UPDATE datatypes SET \"float\" = -1.23456789012345e308\n> UPDATE datatypes SET \"float\" = INFINITY\n> UPDATE datatypes SET \"float\" = -INFINITY\n> SELECT * FROM datatypes\n---\n1, TRUE, -9223372036854775807, -inf, NULL\n\n> UPDATE datatypes SET \"float\" = NAN\n> SELECT \"float\" FROM datatypes\n> UPDATE datatypes SET \"float\" = -NAN\n> SELECT \"float\" FROM datatypes\n> UPDATE datatypes SET \"float\" = 0.0\n> SELECT \"float\" FROM datatypes\n> UPDATE datatypes SET \"float\" = -0.0\n> SELECT \"float\" FROM datatypes\n---\nNaN\nNaN\n0.0\n0.0\n\n# Strings.\n> UPDATE datatypes SET \"string\" = NULL\n> UPDATE datatypes SET \"string\" = ''\n> UPDATE datatypes SET \"string\" = '  '\n> UPDATE datatypes SET \"string\" = 'abc'\n> UPDATE datatypes SET \"string\" = 'Hi! 👋'\n> SELECT * FROM datatypes\n---\n1, TRUE, -9223372036854775807, 0.0, 'Hi! 👋'\n\n!> UPDATE datatypes SET \"string\" = false\n!> UPDATE datatypes SET \"string\" = 3\n!> UPDATE datatypes SET \"string\" = 3.14\n---\nError: invalid input: invalid datatype BOOLEAN for STRING column string\nError: invalid input: invalid datatype INTEGER for STRING column string\nError: invalid input: invalid datatype FLOAT for STRING column string\n"
  },
  {
    "path": "src/sql/testscripts/writes/update_default",
    "content": "# UPDATE can set default values.\n\n> CREATE TABLE defaults ( \\\n    id INTEGER PRIMARY KEY, \\\n    required BOOLEAN NOT NULL, \\\n    \"null\" BOOLEAN, \\\n    \"boolean\" BOOLEAN DEFAULT TRUE, \\\n    \"float\" FLOAT DEFAULT 3.14, \\\n    \"integer\" INTEGER DEFAULT 7, \\\n    \"string\" STRING DEFAULT 'foo' \\\n)\n> INSERT INTO defaults VALUES (1, true, NULL, NULL, NULL, NULL, NULL)\n---\nok\n\n> UPDATE defaults SET \"null\" = DEFAULT, \"boolean\" = DEFAULT, \"float\" = DEFAULT, \"integer\" = DEFAULT, \"string\" = DEFAULT\n> SELECT * FROM defaults\n---\n1, TRUE, NULL, TRUE, 3.14, 7, 'foo'\n\n# Errors on columns with no default.\n!> UPDATE defaults SET required = DEFAULT\n---\nError: invalid input: column required has no default value\n"
  },
  {
    "path": "src/sql/testscripts/writes/update_expression",
    "content": "# Tests UPDATE expression evaluation.\n\n> CREATE TABLE test (id INT PRIMARY KEY, value INT, quantity INT NOT NULL)\n> INSERT INTO test VALUES (0, NULL, 0), (1, 1, 0), (2, 2, 0)\n---\nok\n\n# UPDATE can evaluate constant expressions.\n> UPDATE test SET value = 2 * 2 + 3\n> SELECT * FROM test\n---\n0, 7, 0\n1, 7, 0\n2, 7, 0\n\n# UPDATE can evaluate variable expressions.\n> UPDATE test SET value = id + 10 - quantity\n> SELECT * FROM test\n---\n0, 10, 0\n1, 11, 0\n2, 12, 0\n\n# UPDATE evaluation uses the old values.\n> UPDATE test SET value = id + 1, quantity = value\n> SELECT * FROM test\n---\n0, 1, 10\n1, 2, 11\n2, 3, 12\n\n# This is also true with primary key updates.\n> UPDATE test SET id = id - 1, value = id, quantity = value\n> SELECT * FROM test\n---\n-1, 0, 1\n0, 1, 2\n1, 2, 3\n\n# UPDATE expressions respect constraints.\n> UPDATE test SET value = NULL WHERE id = 0\n> SELECT * FROM test\n!> UPDATE test SET quantity = value\n---\n-1, 0, 1\n0, NULL, 2\n1, 2, 3\nError: invalid input: NULL value not allowed for column quantity\n"
  },
  {
    "path": "src/sql/testscripts/writes/update_index",
    "content": "# Tests UPDATE index writes.\n\n> CREATE TABLE \"index\" ( \\\n    id INT PRIMARY KEY, \\\n    \"bool\" BOOL INDEX, \\\n    \"int\" INT INDEX, \\\n    \"float\" FLOAT INDEX, \\\n    \"string\" STRING INDEX \\\n)\n> INSERT INTO \"index\" VALUES (1, TRUE, 7, 3.14, 'foo')\n---\nok\n\n# An UPDATE writes to all indexes.\n[ops]> UPDATE \"index\" SET \"bool\" = FALSE, \"int\" = 1, \"float\" = 2.718, \"string\" = 'bar'\n---\nset mvcc:NextVersion → 4 [\"\\x00\" → \"\\x04\"]\nset mvcc:TxnActive(3) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\"]\nset mvcc:TxnWrite(3, sql:Index(index.bool, TRUE)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, TRUE), 3) → None [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x00\"]\nset mvcc:TxnWrite(3, sql:Index(index.bool, FALSE)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, FALSE), 3) → 1 [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(3, sql:Index(index.int, 7)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, 7), 3) → None [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x00\"]\nset mvcc:TxnWrite(3, sql:Index(index.int, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, 1), 3) → 1 [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(3, sql:Index(index.float, 3.14)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, 3.14), 3) → None [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x00\"]\nset mvcc:TxnWrite(3, sql:Index(index.float, 2.718)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\x05\\xbev\\xc8\\xb49X\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, 2.718), 3) → 1 [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\x05\\xbev\\xc8\\xb49X\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(3, sql:Index(index.string, 'foo')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, 'foo'), 3) → None [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x00\"]\nset mvcc:TxnWrite(3, sql:Index(index.string, 'bar')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04bar\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, 'bar'), 3) → 1 [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04bar\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(3, sql:Row(index, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(index, 1), 3) → 1,FALSE,1,2.718,'bar' [\"\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x15\\x05\\x02\\x02\\x01\\x00\\x02\\x02\\x03X9\\xb4\\xc8v\\xbe\\x05@\\x04\\x03bar\"]\ndelete mvcc:TxnWrite(3, sql:Index(index.bool, FALSE)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(3, sql:Index(index.bool, TRUE)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(3, sql:Index(index.float, 2.718)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\x05\\xbev\\xc8\\xb49X\\x00\\x00\"]\ndelete mvcc:TxnWrite(3, sql:Index(index.float, 3.14)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\"]\ndelete mvcc:TxnWrite(3, sql:Index(index.int, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(3, sql:Index(index.int, 7)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\"]\ndelete mvcc:TxnWrite(3, sql:Index(index.string, 'bar')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04bar\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(3, sql:Index(index.string, 'foo')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(3, sql:Row(index, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnActive(3) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\"]\n\n# A single-column update only updates the relevant index.\n[ops]> UPDATE \"index\" SET \"bool\" = TRUE\n---\nset mvcc:NextVersion → 5 [\"\\x00\" → \"\\x05\"]\nset mvcc:TxnActive(4) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\"]\nset mvcc:TxnWrite(4, sql:Index(index.bool, FALSE)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, FALSE), 4) → None [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x00\"]\nset mvcc:TxnWrite(4, sql:Index(index.bool, TRUE)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, TRUE), 4) → 1 [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(4, sql:Row(index, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(index, 1), 4) → 1,TRUE,1,2.718,'bar' [\"\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x15\\x05\\x02\\x02\\x01\\x01\\x02\\x02\\x03X9\\xb4\\xc8v\\xbe\\x05@\\x04\\x03bar\"]\ndelete mvcc:TxnWrite(4, sql:Index(index.bool, FALSE)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(4, sql:Index(index.bool, TRUE)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(4, sql:Row(index, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnActive(4) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\"]\n\n# An update with different values writes new index entries.\n> INSERT INTO \"index\" VALUES (2, NULL, NULL, NULL, NULL)\n[ops]> UPDATE \"index\" SET \"bool\" = FALSE, \"int\" = 7, \"float\" = 3.14, \"string\" = 'abc' WHERE id = 2\n---\nset mvcc:NextVersion → 7 [\"\\x00\" → \"\\x07\"]\nset mvcc:TxnActive(6) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\"]\nset mvcc:TxnWrite(6, sql:Index(index.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, NULL), 6) → None [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x00\"]\nset mvcc:TxnWrite(6, sql:Index(index.bool, FALSE)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, FALSE), 6) → 2 [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nset mvcc:TxnWrite(6, sql:Index(index.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, NULL), 6) → None [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x00\"]\nset mvcc:TxnWrite(6, sql:Index(index.int, 7)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, 7), 6) → 2 [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nset mvcc:TxnWrite(6, sql:Index(index.float, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, NULL), 6) → None [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x00\"]\nset mvcc:TxnWrite(6, sql:Index(index.float, 3.14)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, 3.14), 6) → 2 [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nset mvcc:TxnWrite(6, sql:Index(index.string, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, NULL), 6) → None [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x00\"]\nset mvcc:TxnWrite(6, sql:Index(index.string, 'abc')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04abc\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, 'abc'), 6) → 2 [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04abc\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nset mvcc:TxnWrite(6, sql:Row(index, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(index, 2), 6) → 2,FALSE,7,3.14,'abc' [\"\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\" → \"\\x01\\x15\\x05\\x02\\x04\\x01\\x00\\x02\\x0e\\x03\\x1f\\x85\\xebQ\\xb8\\x1e\\t@\\x04\\x03abc\"]\ndelete mvcc:TxnWrite(6, sql:Index(index.bool, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(6, sql:Index(index.bool, FALSE)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(6, sql:Index(index.float, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(6, sql:Index(index.float, 3.14)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\"]\ndelete mvcc:TxnWrite(6, sql:Index(index.int, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(6, sql:Index(index.int, 7)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\"]\ndelete mvcc:TxnWrite(6, sql:Index(index.string, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(6, sql:Index(index.string, 'abc')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04abc\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(6, sql:Row(index, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnActive(6) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\"]\n\n> SELECT * FROM \"index\"\n---\n1, TRUE, 1, 2.718, 'bar'\n2, FALSE, 7, 3.14, 'abc'\n\n# Updates with same values merges the index entries.\n[ops]> UPDATE \"index\" SET \"bool\" = TRUE, \"int\" = 7, \"float\" = 3.14, \"string\" = 'foo'\n---\nset mvcc:NextVersion → 8 [\"\\x00\" → \"\\x08\"]\nset mvcc:TxnActive(7) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\"]\nset mvcc:TxnWrite(7, sql:Index(index.int, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, 1), 7) → None [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x00\"]\nset mvcc:TxnWrite(7, sql:Index(index.int, 7)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, 7), 7) → 1,2 [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x05\\x02\\x02\\x02\\x02\\x04\"]\nset mvcc:TxnWrite(7, sql:Index(index.float, 2.718)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\x05\\xbev\\xc8\\xb49X\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, 2.718), 7) → None [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\x05\\xbev\\xc8\\xb49X\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x00\"]\nset mvcc:TxnWrite(7, sql:Index(index.float, 3.14)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, 3.14), 7) → 1,2 [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x05\\x02\\x02\\x02\\x02\\x04\"]\nset mvcc:TxnWrite(7, sql:Index(index.string, 'bar')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04bar\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, 'bar'), 7) → None [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04bar\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x00\"]\nset mvcc:TxnWrite(7, sql:Index(index.string, 'foo')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, 'foo'), 7) → 1 [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(7, sql:Row(index, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(index, 1), 7) → 1,TRUE,7,3.14,'foo' [\"\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x15\\x05\\x02\\x02\\x01\\x01\\x02\\x0e\\x03\\x1f\\x85\\xebQ\\xb8\\x1e\\t@\\x04\\x03foo\"]\nset mvcc:TxnWrite(7, sql:Index(index.bool, FALSE)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, FALSE), 7) → None [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x00\"]\nset mvcc:TxnWrite(7, sql:Index(index.bool, TRUE)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, TRUE), 7) → 1,2 [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x05\\x02\\x02\\x02\\x02\\x04\"]\nset mvcc:TxnWrite(7, sql:Index(index.string, 'abc')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04abc\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, 'abc'), 7) → None [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04abc\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x00\"]\nset mvcc:TxnWrite(7, sql:Index(index.string, 'foo')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, 'foo'), 7) → 1,2 [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x05\\x02\\x02\\x02\\x02\\x04\"]\nset mvcc:TxnWrite(7, sql:Row(index, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(index, 2), 7) → 2,TRUE,7,3.14,'foo' [\"\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\" → \"\\x01\\x15\\x05\\x02\\x04\\x01\\x01\\x02\\x0e\\x03\\x1f\\x85\\xebQ\\xb8\\x1e\\t@\\x04\\x03foo\"]\ndelete mvcc:TxnWrite(7, sql:Index(index.bool, FALSE)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(7, sql:Index(index.bool, TRUE)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(7, sql:Index(index.float, 2.718)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\x05\\xbev\\xc8\\xb49X\\x00\\x00\"]\ndelete mvcc:TxnWrite(7, sql:Index(index.float, 3.14)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\"]\ndelete mvcc:TxnWrite(7, sql:Index(index.int, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(7, sql:Index(index.int, 7)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\"]\ndelete mvcc:TxnWrite(7, sql:Index(index.string, 'abc')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04abc\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(7, sql:Index(index.string, 'bar')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04bar\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(7, sql:Index(index.string, 'foo')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(7, sql:Row(index, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(7, sql:Row(index, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnActive(7) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\"]\n\n> SELECT * FROM \"index\"\n---\n1, TRUE, 7, 3.14, 'foo'\n2, TRUE, 7, 3.14, 'foo'\n\n# Updates with all NULLs work and get indexed.\n[ops]> UPDATE \"index\" SET \"bool\" = NULL, \"int\" = NULL, \"float\" = NULL, \"string\" = NULL\n---\nset mvcc:NextVersion → 9 [\"\\x00\" → \"\\t\"]\nset mvcc:TxnActive(8) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\"]\nset mvcc:TxnWrite(8, sql:Index(index.bool, TRUE)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, TRUE), 8) → 2 [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nset mvcc:TxnWrite(8, sql:Index(index.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, NULL), 8) → 1 [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(8, sql:Index(index.int, 7)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, 7), 8) → 2 [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nset mvcc:TxnWrite(8, sql:Index(index.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, NULL), 8) → 1 [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(8, sql:Index(index.float, 3.14)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, 3.14), 8) → 2 [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nset mvcc:TxnWrite(8, sql:Index(index.float, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, NULL), 8) → 1 [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(8, sql:Index(index.string, 'foo')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, 'foo'), 8) → 2 [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nset mvcc:TxnWrite(8, sql:Index(index.string, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, NULL), 8) → 1 [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(8, sql:Row(index, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(index, 1), 8) → 1,NULL,NULL,NULL,NULL [\"\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x07\\x05\\x02\\x02\\x00\\x00\\x00\\x00\"]\nset mvcc:TxnWrite(8, sql:Index(index.bool, TRUE)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, TRUE), 8) → None [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nset mvcc:TxnWrite(8, sql:Index(index.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.bool, NULL), 8) → 1,2 [\"\\x04\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x05\\x02\\x02\\x02\\x02\\x04\"]\nset mvcc:TxnWrite(8, sql:Index(index.int, 7)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, 7), 8) → None [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nset mvcc:TxnWrite(8, sql:Index(index.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.int, NULL), 8) → 1,2 [\"\\x04\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x05\\x02\\x02\\x02\\x02\\x04\"]\nset mvcc:TxnWrite(8, sql:Index(index.float, 3.14)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, 3.14), 8) → None [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nset mvcc:TxnWrite(8, sql:Index(index.float, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.float, NULL), 8) → 1,2 [\"\\x04\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x05\\x02\\x02\\x02\\x02\\x04\"]\nset mvcc:TxnWrite(8, sql:Index(index.string, 'foo')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, 'foo'), 8) → None [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x00\"]\nset mvcc:TxnWrite(8, sql:Index(index.string, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(index.string, NULL), 8) → 1,2 [\"\\x04\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x05\\x02\\x02\\x02\\x02\\x04\"]\nset mvcc:TxnWrite(8, sql:Row(index, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(index, 2), 8) → 2,NULL,NULL,NULL,NULL [\"\\x04\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\" → \"\\x01\\x07\\x05\\x02\\x04\\x00\\x00\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Index(index.bool, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Index(index.bool, TRUE)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Index(index.float, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Index(index.float, 3.14)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Index(index.int, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Index(index.int, 7)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x07\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Index(index.string, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Index(index.string, 'foo')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x01index\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Row(index, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(8, sql:Row(index, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x02index\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnActive(8) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\"]\n\n> SELECT * FROM \"index\"\n---\n1, NULL, NULL, NULL, NULL\n2, NULL, NULL, NULL, NULL\n"
  },
  {
    "path": "src/sql/testscripts/writes/update_null",
    "content": "# Tests nullability handling of UPSERT.\n\n# Create a table with NULL constraints and a row.\n> CREATE TABLE name ( \\\n    id INT PRIMARY KEY, \\\n    \"null\" STRING NULL, \\\n    not_null STRING NOT NULL \\\n)\n> INSERT INTO name VALUES (1, 'foo', 'bar')\n---\nok\n\n# UPDATE with NULL works.\n> UPDATE name SET \"null\" = NULL\n> SELECT * FROM name\n---\n1, NULL, 'bar'\n\n# UPDATE with NULL in non-NULL column errors.\n!> UPDATE name SET id = NULL\n!> UPDATE name SET non_null = NULL\n---\nError: invalid input: invalid primary key NULL\nError: invalid input: unknown column non_null\n"
  },
  {
    "path": "src/sql/testscripts/writes/update_primary_key",
    "content": "# Tests UPDATE primary key handling.\n\n# Boolean.\n> CREATE TABLE \"bool\" (id BOOLEAN PRIMARY KEY)\n> INSERT INTO \"bool\" VALUES (TRUE)\n> UPDATE \"bool\" SET id = FALSE\n> SELECT * FROM \"bool\"\n---\nFALSE\n\n> INSERT INTO \"bool\" VALUES (TRUE)\n!> UPDATE \"bool\" SET id = FALSE\n!> UPDATE \"bool\" SET id = FALSE WHERE id = TRUE\n---\nError: invalid input: primary key FALSE already exists\nError: invalid input: primary key FALSE already exists\n\n# Integer.\n> CREATE TABLE \"int\" (id INT PRIMARY KEY)\n> INSERT INTO \"int\" VALUES (0)\n> UPDATE \"int\" SET id = 1\n> SELECT * FROM \"int\"\n> UPDATE \"int\" SET id = -1\n> SELECT * FROM \"int\"\n> UPDATE \"int\" SET id = 9223372036854775807\n> SELECT * FROM \"int\"\n> UPDATE \"int\" SET id = -9223372036854775807\n> SELECT * FROM \"int\"\n---\n1\n-1\n9223372036854775807\n-9223372036854775807\n\n> INSERT INTO \"int\" VALUES (0)\n> UPDATE \"int\" SET id = 1 WHERE id = -9223372036854775807\n> SELECT * FROM \"int\"\n---\n0\n1\n\n!> UPDATE \"int\" SET id = 1\n!> UPDATE \"int\" SET id = 2\n---\nError: invalid input: primary key 1 already exists\nError: invalid input: primary key 2 already exists\n\n# Float.\n> CREATE TABLE \"float\" (id FLOAT PRIMARY KEY)\n> INSERT INTO \"float\" VALUES (0.0)\n> UPDATE \"float\" SET id = 3.14\n> SELECT * FROM \"float\"\n> UPDATE \"float\" SET id = -3.14\n> SELECT * FROM \"float\"\n> UPDATE \"float\" SET id = 0.0\n> SELECT * FROM \"float\"\n> UPDATE \"float\" SET id = -0.0\n> SELECT * FROM \"float\"\n> UPDATE \"float\" SET id = 1.23456789012345e308\n> SELECT * FROM \"float\"\n> UPDATE \"float\" SET id = -1.23456789012345e308\n> SELECT * FROM \"float\"\n> UPDATE \"float\" SET id = INFINITY\n> SELECT * FROM \"float\"\n> UPDATE \"float\" SET id = -INFINITY\n> SELECT * FROM \"float\"\n---\n3.14\n-3.14\n0.0\n0.0\n1.23456789012345e308\n-1.23456789012345e308\ninf\n-inf\n\n> INSERT INTO \"float\" VALUES (1.0)\n> UPDATE \"float\" SET id = 0.0 WHERE id = 1.0\n> SELECT * FROM \"float\"\n---\n-inf\n0.0\n\n!> UPDATE \"float\" SET id = 3.14\n!> UPDATE \"float\" SET id = -3.14\n!> UPDATE \"float\" SET id = 0.0\n!> UPDATE \"float\" SET id = -0.0\n!> UPDATE \"float\" SET id = 1.23456789012345e308\n!> UPDATE \"float\" SET id = -1.23456789012345e308\n!> UPDATE \"float\" SET id = INFINITY\n!> UPDATE \"float\" SET id = -INFINITY\n!> UPDATE \"float\" SET id = NAN\n!> UPDATE \"float\" SET id = NULL\n---\nError: invalid input: primary key 3.14 already exists\nError: invalid input: primary key -3.14 already exists\nError: invalid input: primary key 0.0 already exists\nError: invalid input: primary key -0.0 already exists\nError: invalid input: primary key 1.23456789012345e308 already exists\nError: invalid input: primary key -1.23456789012345e308 already exists\nError: invalid input: primary key inf already exists\nError: invalid input: primary key -inf already exists\nError: invalid input: invalid primary key NaN\nError: invalid input: invalid primary key NULL\n\n# String.\n> CREATE TABLE \"string\" (id STRING PRIMARY KEY)\n> INSERT INTO \"string\" VALUES ('')\n> UPDATE \"string\" SET id = ''\n> UPDATE \"string\" SET id = '  '\n> UPDATE \"string\" SET id = 'abc'\n> UPDATE \"string\" SET id = 'ABC'\n> UPDATE \"string\" SET id = 'Hi! 👋'\n> SELECT * FROM \"string\"\n---\n'Hi! 👋'\n\n> INSERT INTO \"string\" VALUES ('')\n> UPDATE \"string\" SET id = 'foo' WHERE id = ''\n> SELECT * FROM \"string\"\n---\n'Hi! 👋'\n'foo'\n\n!> UPDATE \"string\" SET id = ''\n!> UPDATE \"string\" SET id = '  '\n!> UPDATE \"string\" SET id = 'abc'\n!> UPDATE \"string\" SET id = 'ABC'\n!> UPDATE \"string\" SET id = 'Hi! 👋'\n!> UPDATE \"string\" SET id = NULL\n---\nError: invalid input: primary key '' already exists\nError: invalid input: primary key '  ' already exists\nError: invalid input: primary key 'abc' already exists\nError: invalid input: primary key 'ABC' already exists\nError: invalid input: primary key 'Hi! 👋' already exists\nError: invalid input: invalid primary key NULL\n\n# Primary key updates error if intermediate row updates violate primary key\n# uniqueness, even if the final state wouldn't violate the constraints. This is\n# also true with Postgres.\n> SELECT * FROM \"int\"\n---\n0\n1\n\n!> UPDATE \"int\" SET id = id + 1\n---\nError: invalid input: primary key 1 already exists\n\n# The updates happen in primary key order, so the reverse update does work.\n> UPDATE \"int\" SET id = id - 1\n> SELECT * FROM \"int\"\n---\n-1\n0\n"
  },
  {
    "path": "src/sql/testscripts/writes/update_reference",
    "content": "# Tests UPDATE foreign key references.\n\n# Create reference tables for all datatypes.\n> CREATE TABLE \"bool\" (id BOOL PRIMARY KEY)\n> INSERT INTO \"bool\" VALUES (true)\n\n> CREATE TABLE \"int\" (id INT PRIMARY KEY)\n> INSERT INTO \"int\" VALUES (-1), (0), (1)\n\n> CREATE TABLE \"float\" (id FLOAT PRIMARY KEY)\n> INSERT INTO \"float\" VALUES (3.14), (0.0), (INFINITY)\n\n> CREATE TABLE \"string\" (id STRING PRIMARY KEY)\n> INSERT INTO \"string\" VALUES (''), ('foo')\n\n> CREATE TABLE name ( \\\n    id INT PRIMARY KEY, \\\n    \"bool\" BOOL REFERENCES \"bool\", \\\n    \"int\" INT REFERENCES \"int\", \\\n    \"float\" FLOAT REFERENCES \"float\", \\\n    \"string\" STRING REFERENCES \"string\" \\\n)\n> INSERT INTO name VALUES (1, NULL, NULL, NULL, NULL)\n---\nok\n\n# UPDATEs with existing references work, and update the index entries.\n[ops]> UPDATE name SET \"bool\" = TRUE, \"int\" = 1, \"float\" = 3.14, \"string\" = 'foo'\n---\nset mvcc:NextVersion → 12 [\"\\x00\" → \"\\x0c\"]\nset mvcc:TxnActive(11) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\"]\nset mvcc:TxnWrite(11, sql:Index(name.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01name\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.bool, NULL), 11) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\\x00\"]\nset mvcc:TxnWrite(11, sql:Index(name.bool, TRUE)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01name\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.bool, TRUE), 11) → 1 [\"\\x04\\x01name\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(11, sql:Index(name.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01name\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.int, NULL), 11) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\\x00\"]\nset mvcc:TxnWrite(11, sql:Index(name.int, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01name\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.int, 1), 11) → 1 [\"\\x04\\x01name\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(11, sql:Index(name.float, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01name\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.float, NULL), 11) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\\x00\"]\nset mvcc:TxnWrite(11, sql:Index(name.float, 3.14)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01name\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.float, 3.14), 11) → 1 [\"\\x04\\x01name\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(11, sql:Index(name.string, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01name\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.string, NULL), 11) → None [\"\\x04\\x01name\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\\x00\"]\nset mvcc:TxnWrite(11, sql:Index(name.string, 'foo')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01name\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(name.string, 'foo'), 11) → 1 [\"\\x04\\x01name\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(11, sql:Row(name, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(name, 1), 11) → 1,TRUE,1,3.14,'foo' [\"\\x04\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\" → \"\\x01\\x15\\x05\\x02\\x02\\x01\\x01\\x02\\x02\\x03\\x1f\\x85\\xebQ\\xb8\\x1e\\t@\\x04\\x03foo\"]\ndelete mvcc:TxnWrite(11, sql:Index(name.bool, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01name\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(11, sql:Index(name.bool, TRUE)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01name\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(11, sql:Index(name.float, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01name\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(11, sql:Index(name.float, 3.14)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01name\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\"]\ndelete mvcc:TxnWrite(11, sql:Index(name.int, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01name\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(11, sql:Index(name.int, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01name\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(11, sql:Index(name.string, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01name\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(11, sql:Index(name.string, 'foo')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x01name\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04foo\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(11, sql:Row(name, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\\x02name\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnActive(11) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0b\"]\n\n# UPDATEs error on missing references.\n!> UPDATE name SET \"bool\" = FALSE\n!> UPDATE name SET \"int\" = 7\n!> UPDATE name SET \"float\" = 2.718\n!> UPDATE name SET \"string\" = 'bar'\n---\nError: invalid input: reference FALSE not in table bool\nError: invalid input: reference 7 not in table int\nError: invalid input: reference 2.718 not in table float\nError: invalid input: reference 'bar' not in table string\n\n# -0.0 equals 0.0.\n> UPDATE name SET \"float\" = -0.0\n---\nok\n\n# NaN is not valid as a missing reference marker.\n!> UPDATE name SET \"float\" = NAN\n---\nError: invalid input: reference NaN not in table float\n\n# INFINITY is also valid.\n> UPDATE name SET \"float\" = INFINITY\n---\nok\n\n# References are case sensitive.\n!> UPDATE name SET \"string\" = 'FOO'\n---\nError: invalid input: reference 'FOO' not in table string\n\n# Empty strings are valid references.\n> UPDATE name SET \"string\" = ''\n---\nok\n\n# NULLs are valid.\n> UPDATE name SET \"bool\" = NULL, \"int\" = NULL, \"float\" = NULL, \"string\" = NULL\n---\nok\n\n> SELECT * FROM name\n---\n1, NULL, NULL, NULL, NULL\n\n# Self references are fine.\n> CREATE TABLE self (id INT PRIMARY KEY, self_id INT REFERENCES self)\n> INSERT INTO self VALUES (1, NULL), (2, NULL), (3, NULL)\n---\nok\n\n[ops]> UPDATE self SET self_id = 1 WHERE id = 1\n---\nset mvcc:NextVersion → 25 [\"\\x00\" → \"\\x19\"]\nset mvcc:TxnActive(24) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x18\" → \"\"]\nset mvcc:TxnWrite(24, sql:Index(self.self_id, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x18\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(self.self_id, NULL), 24) → 2,3 [\"\\x04\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x18\" → \"\\x01\\x05\\x02\\x02\\x04\\x02\\x06\"]\nset mvcc:TxnWrite(24, sql:Index(self.self_id, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x18\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(self.self_id, 1), 24) → 1 [\"\\x04\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x18\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(24, sql:Row(self, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x18\\x02self\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(self, 1), 24) → 1,1 [\"\\x04\\x02self\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x18\" → \"\\x01\\x05\\x02\\x02\\x02\\x02\\x02\"]\ndelete mvcc:TxnWrite(24, sql:Index(self.self_id, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x18\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(24, sql:Index(self.self_id, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x18\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(24, sql:Row(self, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x18\\x02self\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnActive(24) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x18\"]\n\n[ops]> UPDATE self SET self_id = 1 WHERE id = 2\n---\nset mvcc:NextVersion → 26 [\"\\x00\" → \"\\x1a\"]\nset mvcc:TxnActive(25) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x19\" → \"\"]\nset mvcc:TxnWrite(25, sql:Index(self.self_id, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x19\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(self.self_id, NULL), 25) → 3 [\"\\x04\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x19\" → \"\\x01\\x03\\x01\\x02\\x06\"]\nset mvcc:TxnWrite(25, sql:Index(self.self_id, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x19\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(self.self_id, 1), 25) → 1,2 [\"\\x04\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x19\" → \"\\x01\\x05\\x02\\x02\\x02\\x02\\x04\"]\nset mvcc:TxnWrite(25, sql:Row(self, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x19\\x02self\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(self, 2), 25) → 2,1 [\"\\x04\\x02self\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x19\" → \"\\x01\\x05\\x02\\x02\\x04\\x02\\x02\"]\ndelete mvcc:TxnWrite(25, sql:Index(self.self_id, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x19\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(25, sql:Index(self.self_id, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x19\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(25, sql:Row(self, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x19\\x02self\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnActive(25) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x19\"]\n\n[ops]> UPDATE self SET self_id = 2 WHERE id = 3\n---\nset mvcc:NextVersion → 27 [\"\\x00\" → \"\\x1b\"]\nset mvcc:TxnActive(26) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1a\" → \"\"]\nset mvcc:TxnWrite(26, sql:Index(self.self_id, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1a\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(self.self_id, NULL), 26) → None [\"\\x04\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1a\" → \"\\x00\"]\nset mvcc:TxnWrite(26, sql:Index(self.self_id, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1a\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(self.self_id, 2), 26) → 3 [\"\\x04\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1a\" → \"\\x01\\x03\\x01\\x02\\x06\"]\nset mvcc:TxnWrite(26, sql:Row(self, 3)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1a\\x02self\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(self, 3), 26) → 3,2 [\"\\x04\\x02self\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1a\" → \"\\x01\\x05\\x02\\x02\\x06\\x02\\x04\"]\ndelete mvcc:TxnWrite(26, sql:Index(self.self_id, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1a\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(26, sql:Index(self.self_id, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1a\\x01self\\x00\\xff\\x00\\xffself_id\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnWrite(26, sql:Row(self, 3)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1a\\x02self\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x03\\x00\\x00\"]\ndelete mvcc:TxnActive(26) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1a\"]\n\n# Breaking the reference isn't.\n!> UPDATE self SET id = 4 WHERE id = 1\n!> UPDATE self SET id = 4 WHERE id = 2\n---\nError: invalid input: row referenced by self.id=2\nError: invalid input: row referenced by self.id=3\n\n# Not even when only this row points to itself.\n> UPDATE self SET self_id = NULL WHERE id > 1\n!> UPDATE self SET id = 4 WHERE id = 1\n---\nError: invalid input: reference 1 not in table self\n\n# Updates can't violate foreign key references in intermediate states even if\n# the final state retains foreign key integrity. Postgres can't either.\n> SELECT * FROM \"int\"\n---\n-1\n0\n1\n\n> INSERT INTO name (id, \"int\") VALUES (2, -1), (3, 0), (4, 1)\n> SELECT * FROM name\n---\n1, NULL, NULL, NULL, NULL\n2, NULL, -1, NULL, NULL\n3, NULL, 0, NULL, NULL\n4, NULL, 1, NULL, NULL\n\n!> UPDATE \"int\" SET id = -id\n---\nError: invalid input: row referenced by name.id=2\n"
  },
  {
    "path": "src/sql/testscripts/writes/update_unique",
    "content": "# Tests UPDATE unique index writes.\n\n> CREATE TABLE \"unique\" ( \\\n    id INT PRIMARY KEY, \\\n    \"bool\" BOOL UNIQUE, \\\n    \"int\" INT UNIQUE, \\\n    \"float\" FLOAT UNIQUE, \\\n    \"string\" STRING UNIQUE \\\n)\n> INSERT INTO \"unique\" VALUES (1, false, 1, 3.14, 'a')\n> INSERT INTO \"unique\" VALUES (2, NULL, NULL, NULL, NULL)\n---\nok\n\n# An UPDATE updates all indexes.\n[ops]> UPDATE \"unique\" \\\n    SET \"bool\" = true, \"int\" = 2, \"float\" = 2.718, \"string\" = 'b' \\\n    WHERE id = 2\n---\nset mvcc:NextVersion → 5 [\"\\x00\" → \"\\x05\"]\nset mvcc:TxnActive(4) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\"]\nset mvcc:TxnWrite(4, sql:Index(unique.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.bool, NULL), 4) → None [\"\\x04\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x00\"]\nset mvcc:TxnWrite(4, sql:Index(unique.bool, TRUE)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.bool, TRUE), 4) → 2 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nset mvcc:TxnWrite(4, sql:Index(unique.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.int, NULL), 4) → None [\"\\x04\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x00\"]\nset mvcc:TxnWrite(4, sql:Index(unique.int, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.int, 2), 4) → 2 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nset mvcc:TxnWrite(4, sql:Index(unique.float, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, NULL), 4) → None [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x00\"]\nset mvcc:TxnWrite(4, sql:Index(unique.float, 2.718)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\x05\\xbev\\xc8\\xb49X\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, 2.718), 4) → 2 [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\x05\\xbev\\xc8\\xb49X\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nset mvcc:TxnWrite(4, sql:Index(unique.string, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.string, NULL), 4) → None [\"\\x04\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x00\"]\nset mvcc:TxnWrite(4, sql:Index(unique.string, 'b')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04b\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.string, 'b'), 4) → 2 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04b\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nset mvcc:TxnWrite(4, sql:Row(unique, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(unique, 2), 4) → 2,TRUE,2,2.718,'b' [\"\\x04\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x13\\x05\\x02\\x04\\x01\\x01\\x02\\x04\\x03X9\\xb4\\xc8v\\xbe\\x05@\\x04\\x01b\"]\ndelete mvcc:TxnWrite(4, sql:Index(unique.bool, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(4, sql:Index(unique.bool, TRUE)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(4, sql:Index(unique.float, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(4, sql:Index(unique.float, 2.718)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\x05\\xbev\\xc8\\xb49X\\x00\\x00\"]\ndelete mvcc:TxnWrite(4, sql:Index(unique.int, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(4, sql:Index(unique.int, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnWrite(4, sql:Index(unique.string, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(4, sql:Index(unique.string, 'b')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04b\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(4, sql:Row(unique, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnActive(4) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\"]\n\n# An update that violates the unique constraint fails.\n!> UPDATE \"unique\" SET \"bool\" = FALSE WHERE id = 2\n!> UPDATE \"unique\" SET \"int\" = 1 WHERE id = 2\n!> UPDATE \"unique\" SET \"float\" = 3.14 WHERE id = 2\n!> UPDATE \"unique\" SET \"string\" = 'a' WHERE id = 2\n---\nError: invalid input: value FALSE already in unique column bool\nError: invalid input: value 1 already in unique column int\nError: invalid input: value 3.14 already in unique column float\nError: invalid input: value 'a' already in unique column string\n\n# It also fails when updating all rows.\n!> UPDATE \"unique\" SET \"bool\" = FALSE\n!> UPDATE \"unique\" SET \"int\" = 7\n!> UPDATE \"unique\" SET \"float\" = 0.0\n!> UPDATE \"unique\" SET \"string\" = 'abc'\n---\nError: invalid input: value FALSE already in unique column bool\nError: invalid input: value 7 already in unique column int\nError: invalid input: value 0.0 already in unique column float\nError: invalid input: value 'abc' already in unique column string\n\n# Updates with NULLS sets NULL entries. Duplicates are allowed.\n[ops]> UPDATE \"unique\" SET \"bool\" = NULL, \"int\" = NULL, \"float\" = NULL, \"string\" = NULL\n---\nset mvcc:NextVersion → 14 [\"\\x00\" → \"\\x0e\"]\nset mvcc:TxnActive(13) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\" → \"\"]\nset mvcc:TxnWrite(13, sql:Index(unique.bool, FALSE)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.bool, FALSE), 13) → None [\"\\x04\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\" → \"\\x00\"]\nset mvcc:TxnWrite(13, sql:Index(unique.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.bool, NULL), 13) → 1 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(13, sql:Index(unique.int, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.int, 1), 13) → None [\"\\x04\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\" → \"\\x00\"]\nset mvcc:TxnWrite(13, sql:Index(unique.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.int, NULL), 13) → 1 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(13, sql:Index(unique.float, 3.14)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, 3.14), 13) → None [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\" → \"\\x00\"]\nset mvcc:TxnWrite(13, sql:Index(unique.float, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, NULL), 13) → 1 [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(13, sql:Index(unique.string, 'a')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04a\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.string, 'a'), 13) → None [\"\\x04\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04a\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\" → \"\\x00\"]\nset mvcc:TxnWrite(13, sql:Index(unique.string, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.string, NULL), 13) → 1 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(13, sql:Row(unique, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(unique, 1), 13) → 1,NULL,NULL,NULL,NULL [\"\\x04\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\" → \"\\x01\\x07\\x05\\x02\\x02\\x00\\x00\\x00\\x00\"]\nset mvcc:TxnWrite(13, sql:Index(unique.bool, TRUE)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.bool, TRUE), 13) → None [\"\\x04\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\" → \"\\x00\"]\nset mvcc:TxnWrite(13, sql:Index(unique.bool, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.bool, NULL), 13) → 1,2 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\" → \"\\x01\\x05\\x02\\x02\\x02\\x02\\x04\"]\nset mvcc:TxnWrite(13, sql:Index(unique.int, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.int, 2), 13) → None [\"\\x04\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\" → \"\\x00\"]\nset mvcc:TxnWrite(13, sql:Index(unique.int, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.int, NULL), 13) → 1,2 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\" → \"\\x01\\x05\\x02\\x02\\x02\\x02\\x04\"]\nset mvcc:TxnWrite(13, sql:Index(unique.float, 2.718)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\x05\\xbev\\xc8\\xb49X\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, 2.718), 13) → None [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\x05\\xbev\\xc8\\xb49X\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\" → \"\\x00\"]\nset mvcc:TxnWrite(13, sql:Index(unique.float, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, NULL), 13) → 1,2 [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\" → \"\\x01\\x05\\x02\\x02\\x02\\x02\\x04\"]\nset mvcc:TxnWrite(13, sql:Index(unique.string, 'b')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04b\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.string, 'b'), 13) → None [\"\\x04\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04b\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\" → \"\\x00\"]\nset mvcc:TxnWrite(13, sql:Index(unique.string, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.string, NULL), 13) → 1,2 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\" → \"\\x01\\x05\\x02\\x02\\x02\\x02\\x04\"]\nset mvcc:TxnWrite(13, sql:Row(unique, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(unique, 2), 13) → 2,NULL,NULL,NULL,NULL [\"\\x04\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\" → \"\\x01\\x07\\x05\\x02\\x04\\x00\\x00\\x00\\x00\"]\ndelete mvcc:TxnWrite(13, sql:Index(unique.bool, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(13, sql:Index(unique.bool, FALSE)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(13, sql:Index(unique.bool, TRUE)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xffbool\\x00\\xff\\x00\\xff\\x01\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(13, sql:Index(unique.float, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(13, sql:Index(unique.float, 2.718)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\x05\\xbev\\xc8\\xb49X\\x00\\x00\"]\ndelete mvcc:TxnWrite(13, sql:Index(unique.float, 3.14)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xc0\\t\\x1e\\xb8Q\\xeb\\x85\\x1f\\x00\\x00\"]\ndelete mvcc:TxnWrite(13, sql:Index(unique.int, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(13, sql:Index(unique.int, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(13, sql:Index(unique.int, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xffint\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnWrite(13, sql:Index(unique.string, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(13, sql:Index(unique.string, 'a')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04a\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(13, sql:Index(unique.string, 'b')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04b\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(13, sql:Row(unique, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(13, sql:Row(unique, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnActive(13) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\r\"]\n\n> SELECT * FROM \"unique\"\n---\n1, NULL, NULL, NULL, NULL\n2, NULL, NULL, NULL, NULL\n\n# Float NaNs are considered different and allowed.\n[ops]> UPDATE \"unique\" SET \"float\" = NAN\n---\nset mvcc:NextVersion → 15 [\"\\x00\" → \"\\x0f\"]\nset mvcc:TxnActive(14) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\" → \"\"]\nset mvcc:TxnWrite(14, sql:Index(unique.float, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, NULL), 14) → 2 [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nset mvcc:TxnWrite(14, sql:Index(unique.float, NaN)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf8\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, NaN), 14) → 1 [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf8\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(14, sql:Row(unique, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(unique, 1), 14) → 1,NULL,NULL,NaN,NULL [\"\\x04\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\" → \"\\x01\\x0f\\x05\\x02\\x02\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\xf8\\x7f\\x00\"]\nset mvcc:TxnWrite(14, sql:Index(unique.float, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, NULL), 14) → None [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\" → \"\\x00\"]\nset mvcc:TxnWrite(14, sql:Index(unique.float, NaN)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf8\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, NaN), 14) → 1,2 [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf8\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\" → \"\\x01\\x05\\x02\\x02\\x02\\x02\\x04\"]\nset mvcc:TxnWrite(14, sql:Row(unique, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(unique, 2), 14) → 2,NULL,NULL,NaN,NULL [\"\\x04\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\" → \"\\x01\\x0f\\x05\\x02\\x04\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\xf8\\x7f\\x00\"]\ndelete mvcc:TxnWrite(14, sql:Index(unique.float, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(14, sql:Index(unique.float, NaN)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf8\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(14, sql:Row(unique, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnWrite(14, sql:Row(unique, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnActive(14) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0e\"]\n\n> SELECT * FROM \"unique\"\n---\n1, NULL, NULL, NaN, NULL\n2, NULL, NULL, NaN, NULL\n\n# Float 0.0 and -0.0 are considered equal.\n[ops]> UPDATE \"unique\" SET \"float\" = -0.0 WHERE id = 1\n!> UPDATE \"unique\" SET \"float\" = 0.0 WHERE id = 2\n---\nset mvcc:NextVersion → 16 [\"\\x00\" → \"\\x10\"]\nset mvcc:TxnActive(15) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\" → \"\"]\nset mvcc:TxnWrite(15, sql:Index(unique.float, NaN)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf8\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, NaN), 15) → 2 [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf8\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nset mvcc:TxnWrite(15, sql:Index(unique.float, 0.0)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, 0.0), 15) → 1 [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(15, sql:Row(unique, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(unique, 1), 15) → 1,NULL,NULL,0.0,NULL [\"\\x04\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\" → \"\\x01\\x0f\\x05\\x02\\x02\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\"]\ndelete mvcc:TxnWrite(15, sql:Index(unique.float, 0.0)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(15, sql:Index(unique.float, NaN)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf8\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(15, sql:Row(unique, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnActive(15) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0f\"]\nError: invalid input: value 0.0 already in unique column float\n\n> SELECT * FROM \"unique\"\n---\n1, NULL, NULL, 0.0, NULL\n2, NULL, NULL, NaN, NULL\n\n# Float INFINITY is considered equal.\n[ops]> UPDATE \"unique\" SET \"float\" = INFINITY WHERE id = 1\n[ops]> UPDATE \"unique\" SET \"float\" = -INFINITY WHERE id = 2\n---\nset mvcc:NextVersion → 18 [\"\\x00\" → \"\\x12\"]\nset mvcc:TxnActive(17) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x11\" → \"\"]\nset mvcc:TxnWrite(17, sql:Index(unique.float, 0.0)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x11\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, 0.0), 17) → None [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x11\" → \"\\x00\"]\nset mvcc:TxnWrite(17, sql:Index(unique.float, inf)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x11\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf0\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, inf), 17) → 1 [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf0\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x11\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(17, sql:Row(unique, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x11\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(unique, 1), 17) → 1,NULL,NULL,inf,NULL [\"\\x04\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x11\" → \"\\x01\\x0f\\x05\\x02\\x02\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\xf0\\x7f\\x00\"]\ndelete mvcc:TxnWrite(17, sql:Index(unique.float, 0.0)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x11\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(17, sql:Index(unique.float, inf)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x11\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf0\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(17, sql:Row(unique, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x11\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnActive(17) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x11\"]\nset mvcc:NextVersion → 19 [\"\\x00\" → \"\\x13\"]\nset mvcc:TxnActive(18) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\" → \"\"]\nset mvcc:TxnWrite(18, sql:Index(unique.float, NaN)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf8\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, NaN), 18) → None [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf8\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\" → \"\\x00\"]\nset mvcc:TxnWrite(18, sql:Index(unique.float, -inf)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x00\\xff\\x0f\\xff\\xff\\xff\\xff\\xff\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.float, -inf), 18) → 2 [\"\\x04\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x00\\xff\\x0f\\xff\\xff\\xff\\xff\\xff\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nset mvcc:TxnWrite(18, sql:Row(unique, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(unique, 2), 18) → 2,NULL,NULL,-inf,NULL [\"\\x04\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\" → \"\\x01\\x0f\\x05\\x02\\x04\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\xf0\\xff\\x00\"]\ndelete mvcc:TxnWrite(18, sql:Index(unique.float, -inf)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\x00\\xff\\x0f\\xff\\xff\\xff\\xff\\xff\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(18, sql:Index(unique.float, NaN)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\\x01unique\\x00\\xff\\x00\\xfffloat\\x00\\xff\\x00\\xff\\x03\\xff\\xf8\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(18, sql:Row(unique, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnActive(18) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\"]\n\n> SELECT * FROM \"unique\"\n---\n1, NULL, NULL, inf, NULL\n2, NULL, NULL, -inf, NULL\n\n!> UPDATE \"unique\" SET \"float\" = INFINITY WHERE id = 2\n---\nError: invalid input: value inf already in unique column float\n\n# Empty strings are considered equal.\n> UPDATE \"unique\" SET \"string\" = '' WHERE id = 1\n!> UPDATE \"unique\" SET \"string\" = '' WHERE id = 2\n---\nError: invalid input: value '' already in unique column string\n\n# Case differences are not considered equal.\n[ops]> UPDATE \"unique\" SET \"string\" = 'case' WHERE id = 1\n[ops]> UPDATE \"unique\" SET \"string\" = 'CaSe' WHERE id = 2\n---\nset mvcc:NextVersion → 23 [\"\\x00\" → \"\\x17\"]\nset mvcc:TxnActive(22) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x16\" → \"\"]\nset mvcc:TxnWrite(22, sql:Index(unique.string, '')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x16\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.string, ''), 22) → None [\"\\x04\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x16\" → \"\\x00\"]\nset mvcc:TxnWrite(22, sql:Index(unique.string, 'case')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x16\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04case\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.string, 'case'), 22) → 1 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04case\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x16\" → \"\\x01\\x03\\x01\\x02\\x02\"]\nset mvcc:TxnWrite(22, sql:Row(unique, 1)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x16\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(unique, 1), 22) → 1,NULL,NULL,inf,'case' [\"\\x04\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x16\" → \"\\x01\\x14\\x05\\x02\\x02\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\xf0\\x7f\\x04\\x04case\"]\ndelete mvcc:TxnWrite(22, sql:Index(unique.string, '')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x16\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(22, sql:Index(unique.string, 'case')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x16\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04case\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(22, sql:Row(unique, 1)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x16\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x01\\x00\\x00\"]\ndelete mvcc:TxnActive(22) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x16\"]\nset mvcc:NextVersion → 24 [\"\\x00\" → \"\\x18\"]\nset mvcc:TxnActive(23) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x17\" → \"\"]\nset mvcc:TxnWrite(23, sql:Index(unique.string, NULL)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x17\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.string, NULL), 23) → None [\"\\x04\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x17\" → \"\\x00\"]\nset mvcc:TxnWrite(23, sql:Index(unique.string, 'CaSe')) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x17\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04CaSe\\x00\\xff\\x00\\xff\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Index(unique.string, 'CaSe'), 23) → 2 [\"\\x04\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04CaSe\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x17\" → \"\\x01\\x03\\x01\\x02\\x04\"]\nset mvcc:TxnWrite(23, sql:Row(unique, 2)) → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x17\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nset mvcc:Version(sql:Row(unique, 2), 23) → 2,NULL,NULL,-inf,'CaSe' [\"\\x04\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x17\" → \"\\x01\\x14\\x05\\x02\\x04\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\xf0\\xff\\x04\\x04CaSe\"]\ndelete mvcc:TxnWrite(23, sql:Index(unique.string, NULL)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x17\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(23, sql:Index(unique.string, 'CaSe')) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x17\\x01unique\\x00\\xff\\x00\\xffstring\\x00\\xff\\x00\\xff\\x04CaSe\\x00\\xff\\x00\\xff\\x00\\x00\"]\ndelete mvcc:TxnWrite(23, sql:Row(unique, 2)) [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x17\\x02unique\\x00\\xff\\x00\\xff\\x02\\x80\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\"]\ndelete mvcc:TxnActive(23) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x17\"]\n\n> SELECT * FROM \"unique\"\n---\n1, NULL, NULL, inf, 'case'\n2, NULL, NULL, -inf, 'CaSe'\n\n# An UPDATE errors if intermediate states violate the uniqueness constraints,\n# even if the final state wouldn't. This is the same in Postgres.\n> UPDATE \"unique\" SET \"bool\" = id = 2\n> SELECT * FROM \"unique\"\n---\n1, FALSE, NULL, inf, 'case'\n2, TRUE, NULL, -inf, 'CaSe'\n\n!> UPDATE \"unique\" SET \"bool\" = NOT \"bool\"\n---\nError: invalid input: value TRUE already in unique column bool\n"
  },
  {
    "path": "src/sql/testscripts/writes/update_where",
    "content": "# Tests UPDATE with WHERE predicates.\n\n# Create a table with some data.\n> CREATE TABLE name (id INT PRIMARY KEY, value STRING, \"index\" INT INDEX)\n> INSERT INTO name VALUES (1, 'a', 1), (2, 'b', 2), (3, 'c', NULL);\n---\nok\n\n# Boolean filters work, and are trivial.\n> BEGIN\n[plan]> UPDATE name SET value = 'foo' WHERE TRUE\n> SELECT * FROM name\n> ROLLBACK\n---\nUpdate: name (value='foo')\n└─ Scan: name\n1, 'foo', 1\n2, 'foo', 2\n3, 'foo', NULL\n\n[plan]> UPDATE name SET value = 'foo' WHERE FALSE\n> SELECT * FROM name\n---\nUpdate: name (value='foo')\n└─ Nothing\n1, 'a', 1\n2, 'b', 2\n3, 'c', NULL\n\n# Updating by primary key lookup.\n> BEGIN\n[plan]> UPDATE name SET value = 'foo' WHERE id = 1 OR id = 3\n> SELECT * FROM name\n> ROLLBACK\n---\nUpdate: name (value='foo')\n└─ KeyLookup: name (1, 3)\n1, 'foo', 1\n2, 'b', 2\n3, 'foo', NULL\n\n# Updating by index lookup.\n> BEGIN\n[plan]> UPDATE name SET value = 'foo' WHERE \"index\" = 2\n> SELECT * FROM name\n> ROLLBACK\n---\nUpdate: name (value='foo')\n└─ IndexLookup: name.index (2)\n1, 'a', 1\n2, 'foo', 2\n3, 'c', NULL\n\n# Including IS NULL.\n> BEGIN\n[plan]> UPDATE name SET value = 'foo' WHERE \"index\" IS NULL\n> SELECT * FROM name\n> ROLLBACK\n---\nUpdate: name (value='foo')\n└─ IndexLookup: name.index (NULL)\n1, 'a', 1\n2, 'b', 2\n3, 'foo', NULL\n\n# Updating by arbitrary predicate over full scan.\n> BEGIN\n[plan]> UPDATE name SET value = 'foo' WHERE id >= 5 - 2 OR (value LIKE 'a') IS NULL\n> SELECT * FROM name\n> ROLLBACK\n---\nUpdate: name (value='foo')\n└─ Scan: name (name.id > 3 OR name.id = 3 OR name.value LIKE 'a' IS NULL)\n1, 'a', 1\n2, 'b', 2\n3, 'foo', NULL\n\n# Non-boolean predicates error, except NULL which is equivalent to FALSE.\n!> UPDATE name SET value = 'foo' WHERE 0\n!> UPDATE name SET value = 'foo' WHERE 1\n!> UPDATE name SET value = 'foo' WHERE 3.14\n!> UPDATE name SET value = 'foo' WHERE NaN\n!> UPDATE name SET value = 'foo' WHERE ''\n!> UPDATE name SET value = 'foo' WHERE 'true\n---\nError: invalid input: filter returned 0, expected boolean\nError: invalid input: filter returned 1, expected boolean\nError: invalid input: filter returned 3.14, expected boolean\nError: invalid input: filter returned NaN, expected boolean\nError: invalid input: filter returned '', expected boolean\nError: invalid input: unexpected end of string literal\n\n> UPDATE name SET value = 'foo' WHERE NULL\n> SELECT * FROM name\n---\n1, 'a', 1\n2, 'b', 2\n3, 'c', NULL\n\n# Bare WHERE errors.\n!> UPDATE name SET value = 'foo' WHERE\n---\nError: invalid input: unexpected end of input\n\n# Missing column errors.\n!> UPDATE name SET value = 'foo' WHERE missing = 'foo'\n---\nError: invalid input: unknown column missing\n"
  },
  {
    "path": "src/sql/types/expression.rs",
    "content": "use std::fmt::Display;\n\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\n\nuse super::{Label, Row, Value};\nuse crate::errinput;\nuse crate::error::Result;\nuse crate::sql::planner::Node;\n\n/// An expression, made up of nested operations and values. Values are either\n/// constants, or numeric column references which are looked up in rows.\n/// Evaluated to a final value during query execution.\n///\n/// Since this is a recursive data structure, we have to box each child\n/// expression, which incurs a heap allocation per expression node. There are\n/// clever ways to avoid this, but we keep it simple.\n#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]\npub enum Expression {\n    /// A constant value.\n    Constant(Value),\n    /// A column reference. Looks up the value in a row during evaluation.\n    Column(usize),\n\n    /// a AND b: logical AND of two booleans.\n    And(Box<Expression>, Box<Expression>),\n    /// a OR b: logical OR of two booleans.\n    Or(Box<Expression>, Box<Expression>),\n    /// NOT a: logical NOT of a boolean.\n    Not(Box<Expression>),\n\n    /// a = b: equality comparison of two values.\n    Equal(Box<Expression>, Box<Expression>),\n    /// Greater than comparison of two values: a > b.\n    GreaterThan(Box<Expression>, Box<Expression>),\n    /// a < b: less than comparison of two values.\n    LessThan(Box<Expression>, Box<Expression>),\n    /// a IS NULL or a IS NAN: checks for the given value.\n    Is(Box<Expression>, Value),\n\n    /// a + b: adds two numbers.\n    Add(Box<Expression>, Box<Expression>),\n    /// a / b: divides two numbers.\n    Divide(Box<Expression>, Box<Expression>),\n    /// a ^b: exponentiates two numbers.\n    Exponentiate(Box<Expression>, Box<Expression>),\n    /// a!: takes the factorial of a number (4! = 4*3*2*1).\n    Factorial(Box<Expression>),\n    /// +a: the identify function, which simply returns the same number.\n    Identity(Box<Expression>),\n    /// a * b: multiplies two numbers.\n    Multiply(Box<Expression>, Box<Expression>),\n    /// -a: negates the given number.\n    Negate(Box<Expression>),\n    /// a % b: the remainder after dividing two numbers.\n    Remainder(Box<Expression>, Box<Expression>),\n    /// √a: takes the square root of a number.\n    SquareRoot(Box<Expression>),\n    /// a - b: subtracts two numbers.\n    Subtract(Box<Expression>, Box<Expression>),\n\n    // a LIKE b: checks if a string matches a pattern.\n    Like(Box<Expression>, Box<Expression>),\n}\n\nimpl Expression {\n    /// Displays the expression, using the given plan node to look up labels for\n    /// column references.\n    pub fn display<'a>(&'a self, node: &'a Node) -> ExpressionDisplay<'a> {\n        ExpressionDisplay::new(self, node, 0)\n    }\n\n    /// Evaluates an expression, returning a constant value. Column references\n    /// are looked up in the given row (or panic if the row is None).\n    pub fn evaluate(&self, row: Option<&Row>) -> Result<Value> {\n        use Value::*;\n\n        Ok(match self {\n            // Constant values return themselves.\n            Self::Constant(value) => value.clone(),\n\n            // Column references look up a row value. The planner ensures that\n            // only constant expressions are evaluated without a row.\n            Self::Column(index) => row.and_then(|r| r.get(*index)).cloned().expect(\"invalid index\"),\n\n            // Logical AND. Inputs must be boolean or NULL. NULLs generally\n            // yield NULL, except the special case NULL AND false == false.\n            Self::And(lhs, rhs) => match (lhs.evaluate(row)?, rhs.evaluate(row)?) {\n                (Boolean(lhs), Boolean(rhs)) => Boolean(lhs && rhs),\n                (Boolean(b), Null) | (Null, Boolean(b)) if !b => Boolean(false),\n                (Boolean(_), Null) | (Null, Boolean(_)) | (Null, Null) => Null,\n                (lhs, rhs) => return errinput!(\"can't AND {lhs} and {rhs}\"),\n            },\n\n            // Logical OR. Inputs must be boolean or NULL. NULLs generally\n            // yield NULL, except the special case NULL OR true == true.\n            Self::Or(lhs, rhs) => match (lhs.evaluate(row)?, rhs.evaluate(row)?) {\n                (Boolean(lhs), Boolean(rhs)) => Boolean(lhs || rhs),\n                (Boolean(b), Null) | (Null, Boolean(b)) if b => Boolean(true),\n                (Boolean(_), Null) | (Null, Boolean(_)) | (Null, Null) => Null,\n                (lhs, rhs) => return errinput!(\"can't OR {lhs} and {rhs}\"),\n            },\n\n            // Logical NOT. Input must be boolean or NULL.\n            Self::Not(expr) => match expr.evaluate(row)? {\n                Boolean(b) => Boolean(!b),\n                Null => Null,\n                value => return errinput!(\"can't NOT {value}\"),\n            },\n\n            // Comparisons. Must be of same type, except floats and integers\n            // which are interchangeable. NULLs yield NULL, NaNs yield NaN.\n            //\n            // Does not dispatch to Value.cmp() because comparison and sorting\n            // is different for Nulls and NaNs in SQL and code.\n            #[allow(clippy::float_cmp)]\n            Self::Equal(lhs, rhs) => match (lhs.evaluate(row)?, rhs.evaluate(row)?) {\n                (Boolean(lhs), Boolean(rhs)) => Boolean(lhs == rhs),\n                (Integer(lhs), Integer(rhs)) => Boolean(lhs == rhs),\n                (Integer(lhs), Float(rhs)) => Boolean(lhs as f64 == rhs),\n                (Float(lhs), Integer(rhs)) => Boolean(lhs == rhs as f64),\n                (Float(lhs), Float(rhs)) => Boolean(lhs == rhs),\n                (String(lhs), String(rhs)) => Boolean(lhs == rhs),\n                (Null, _) | (_, Null) => Null,\n                (lhs, rhs) => return errinput!(\"can't compare {lhs} and {rhs}\"),\n            },\n\n            Self::GreaterThan(lhs, rhs) => match (lhs.evaluate(row)?, rhs.evaluate(row)?) {\n                #[allow(clippy::bool_comparison)]\n                (Boolean(lhs), Boolean(rhs)) => Boolean(lhs > rhs),\n                (Integer(lhs), Integer(rhs)) => Boolean(lhs > rhs),\n                (Integer(lhs), Float(rhs)) => Boolean(lhs as f64 > rhs),\n                (Float(lhs), Integer(rhs)) => Boolean(lhs > rhs as f64),\n                (Float(lhs), Float(rhs)) => Boolean(lhs > rhs),\n                (String(lhs), String(rhs)) => Boolean(lhs > rhs),\n                (Null, _) | (_, Null) => Null,\n                (lhs, rhs) => return errinput!(\"can't compare {lhs} and {rhs}\"),\n            },\n\n            Self::LessThan(lhs, rhs) => match (lhs.evaluate(row)?, rhs.evaluate(row)?) {\n                #[allow(clippy::bool_comparison)]\n                (Boolean(lhs), Boolean(rhs)) => Boolean(lhs < rhs),\n                (Integer(lhs), Integer(rhs)) => Boolean(lhs < rhs),\n                (Integer(lhs), Float(rhs)) => Boolean((lhs as f64) < rhs),\n                (Float(lhs), Integer(rhs)) => Boolean(lhs < rhs as f64),\n                (Float(lhs), Float(rhs)) => Boolean(lhs < rhs),\n                (String(lhs), String(rhs)) => Boolean(lhs < rhs),\n                (Null, _) | (_, Null) => Null,\n                (lhs, rhs) => return errinput!(\"can't compare {lhs} and {rhs}\"),\n            },\n\n            Self::Is(expr, Null) => Boolean(expr.evaluate(row)? == Null),\n            Self::Is(expr, Float(f)) if f.is_nan() => match expr.evaluate(row)? {\n                Float(f) => Boolean(f.is_nan()),\n                Null => Null,\n                v => return errinput!(\"IS NAN can't be used with {}\", v.datatype().unwrap()),\n            },\n            Self::Is(_, v) => panic!(\"invalid IS value {v}\"), // enforced by parser\n\n            // Mathematical operations. Inputs must be numbers, but integers and\n            // floats are interchangeable (float when mixed). NULLs yield NULL.\n            // Errors on integer overflow, but floats yield infinity or NaN.\n            Self::Add(lhs, rhs) => lhs.evaluate(row)?.checked_add(&rhs.evaluate(row)?)?,\n            Self::Divide(lhs, rhs) => lhs.evaluate(row)?.checked_div(&rhs.evaluate(row)?)?,\n            Self::Exponentiate(lhs, rhs) => lhs.evaluate(row)?.checked_pow(&rhs.evaluate(row)?)?,\n            Self::Factorial(expr) => match expr.evaluate(row)? {\n                Integer(i @ 0..) => {\n                    (1..=i).try_fold(Integer(1), |p, i| p.checked_mul(&Integer(i)))?\n                }\n                Null => Null,\n                value => return errinput!(\"can't take factorial of {value}\"),\n            },\n            Self::Identity(expr) => match expr.evaluate(row)? {\n                value @ (Integer(_) | Float(_) | Null) => value,\n                expr => return errinput!(\"can't take the identity of {expr}\"),\n            },\n            Self::Multiply(lhs, rhs) => lhs.evaluate(row)?.checked_mul(&rhs.evaluate(row)?)?,\n            Self::Negate(expr) => match expr.evaluate(row)? {\n                Integer(i) => Integer(-i),\n                Float(f) => Float(-f),\n                Null => Null,\n                value => return errinput!(\"can't negate {value}\"),\n            },\n            Self::Remainder(lhs, rhs) => lhs.evaluate(row)?.checked_rem(&rhs.evaluate(row)?)?,\n            Self::SquareRoot(expr) => match expr.evaluate(row)? {\n                Integer(i @ 0..) => Float((i as f64).sqrt()),\n                Float(f) => Float(f.sqrt()),\n                Null => Null,\n                value => return errinput!(\"can't take square root of {value}\"),\n            },\n            Self::Subtract(lhs, rhs) => lhs.evaluate(row)?.checked_sub(&rhs.evaluate(row)?)?,\n\n            // LIKE pattern matching, using _ and % as single- and\n            // multi-character wildcards. Inputs must be strings. NULLs yield\n            // NULL. There's no support for escaping an _ and %.\n            Self::Like(lhs, rhs) => match (lhs.evaluate(row)?, rhs.evaluate(row)?) {\n                (String(lhs), String(rhs)) => {\n                    // We could precompile the pattern if it's constant, instead\n                    // of recompiling it for every row, but we keep it simple.\n                    let pattern =\n                        format!(\"^{}$\", regex::escape(&rhs).replace('%', \".*\").replace('_', \".\"));\n                    Boolean(Regex::new(&pattern)?.is_match(&lhs))\n                }\n                (String(_), Null) | (Null, String(_)) | (Null, Null) => Null,\n                (lhs, rhs) => return errinput!(\"can't LIKE {lhs} and {rhs}\"),\n            },\n        })\n    }\n\n    /// Recursively walks the expression tree depth-first, calling the given\n    /// closure until it returns false. Returns true otherwise.\n    pub fn walk(&self, visitor: &mut impl FnMut(&Expression) -> bool) -> bool {\n        if !visitor(self) {\n            return false;\n        }\n        match self {\n            Self::Add(lhs, rhs)\n            | Self::And(lhs, rhs)\n            | Self::Divide(lhs, rhs)\n            | Self::Equal(lhs, rhs)\n            | Self::Exponentiate(lhs, rhs)\n            | Self::GreaterThan(lhs, rhs)\n            | Self::LessThan(lhs, rhs)\n            | Self::Like(lhs, rhs)\n            | Self::Multiply(lhs, rhs)\n            | Self::Or(lhs, rhs)\n            | Self::Remainder(lhs, rhs)\n            | Self::Subtract(lhs, rhs) => lhs.walk(visitor) && rhs.walk(visitor),\n\n            Self::Factorial(expr)\n            | Self::Identity(expr)\n            | Self::Is(expr, _)\n            | Self::Negate(expr)\n            | Self::Not(expr)\n            | Self::SquareRoot(expr) => expr.walk(visitor),\n\n            Self::Constant(_) | Self::Column(_) => true,\n        }\n    }\n\n    /// Recursively walks the expression tree depth-first, calling the given\n    /// closure until it returns true. Returns false otherwise. This is the\n    /// inverse of walk().\n    pub fn contains(&self, visitor: &impl Fn(&Expression) -> bool) -> bool {\n        !self.walk(&mut |e| !visitor(e))\n    }\n\n    /// Transforms the expression by recursively applying the given closures\n    /// depth-first to each node before/after descending.\n    pub fn transform(\n        mut self,\n        before: &impl Fn(Self) -> Result<Self>,\n        after: &impl Fn(Self) -> Result<Self>,\n    ) -> Result<Self> {\n        // Helper for transforming boxed expressions.\n        let xform = |mut expr: Box<Expression>| -> Result<Box<Expression>> {\n            *expr = expr.transform(before, after)?;\n            Ok(expr)\n        };\n\n        self = before(self)?;\n        self = match self {\n            Self::Add(lhs, rhs) => Self::Add(xform(lhs)?, xform(rhs)?),\n            Self::And(lhs, rhs) => Self::And(xform(lhs)?, xform(rhs)?),\n            Self::Divide(lhs, rhs) => Self::Divide(xform(lhs)?, xform(rhs)?),\n            Self::Equal(lhs, rhs) => Self::Equal(xform(lhs)?, xform(rhs)?),\n            Self::Exponentiate(lhs, rhs) => Self::Exponentiate(xform(lhs)?, xform(rhs)?),\n            Self::GreaterThan(lhs, rhs) => Self::GreaterThan(xform(lhs)?, xform(rhs)?),\n            Self::LessThan(lhs, rhs) => Self::LessThan(xform(lhs)?, xform(rhs)?),\n            Self::Like(lhs, rhs) => Self::Like(xform(lhs)?, xform(rhs)?),\n            Self::Multiply(lhs, rhs) => Self::Multiply(xform(lhs)?, xform(rhs)?),\n            Self::Or(lhs, rhs) => Self::Or(xform(lhs)?, xform(rhs)?),\n            Self::Remainder(lhs, rhs) => Self::Remainder(xform(lhs)?, xform(rhs)?),\n            Self::SquareRoot(expr) => Self::SquareRoot(xform(expr)?),\n            Self::Subtract(lhs, rhs) => Self::Subtract(xform(lhs)?, xform(rhs)?),\n\n            Self::Factorial(expr) => Self::Factorial(xform(expr)?),\n            Self::Identity(expr) => Self::Identity(xform(expr)?),\n            Self::Is(expr, value) => Self::Is(xform(expr)?, value),\n            Self::Negate(expr) => Self::Negate(xform(expr)?),\n            Self::Not(expr) => Self::Not(xform(expr)?),\n\n            expr @ (Self::Constant(_) | Self::Column(_)) => expr,\n        };\n        self = after(self)?;\n        Ok(self)\n    }\n\n    /// Converts the expression into conjunctive normal form, i.e. an AND of\n    /// ORs, useful during plan optimization. This is done by converting to\n    /// negation normal form and then applying De Morgan's distributive law.\n    pub fn into_cnf(self) -> Self {\n        use Expression::{And, Or};\n\n        let xform = |expr| {\n            // Can't use a single match; needs deref patterns.\n            let Or(lhs, rhs) = expr else {\n                return expr;\n            };\n            match (*lhs, *rhs) {\n                // (x AND y) OR z → (x OR z) AND (y OR z)\n                (And(l, r), rhs) => And(Or(l, rhs.clone().into()).into(), Or(r, rhs.into()).into()),\n                // x OR (y AND z) → (x OR y) AND (x OR z)\n                (lhs, And(l, r)) => And(Or(lhs.clone().into(), l).into(), Or(lhs.into(), r).into()),\n                // Otherwise, do nothing.\n                (lhs, rhs) => Or(lhs.into(), rhs.into()),\n            }\n        };\n        self.into_nnf().transform(&|e| Ok(xform(e)), &Ok).unwrap() // infallible\n    }\n\n    /// Converts the expression into conjunctive normal form as a vector of\n    /// ANDed expressions (instead of nested ANDs).\n    pub fn into_cnf_vec(self) -> Vec<Self> {\n        let mut cnf = Vec::new();\n        let mut stack = vec![self.into_cnf()];\n        while let Some(expr) = stack.pop() {\n            if let Self::And(lhs, rhs) = expr {\n                stack.extend([*rhs, *lhs]); // push lhs last to pop it first\n            } else {\n                cnf.push(expr);\n            }\n        }\n        cnf\n    }\n\n    /// Converts the expression into negation normal form. This pushes NOT\n    /// operators into the tree using De Morgan's laws, such that they're always\n    /// below other logical operators. It is a useful intermediate form for\n    /// applying other logical normalizations.\n    pub fn into_nnf(self) -> Self {\n        use Expression::{And, Not, Or};\n\n        let xform = |expr| {\n            // Can't use a single match; needs deref patterns.\n            let Not(inner) = expr else {\n                return expr;\n            };\n            match *inner {\n                // NOT (x AND y) → (NOT x) OR (NOT y)\n                And(lhs, rhs) => Or(Not(lhs).into(), Not(rhs).into()),\n                // NOT (x OR y) → (NOT x) AND (NOT y)\n                Or(lhs, rhs) => And(Not(lhs).into(), Not(rhs).into()),\n                // NOT NOT x → x\n                Not(inner) => *inner,\n                // Otherwise, do nothing.\n                expr => Not(expr.into()),\n            }\n        };\n        self.transform(&|e| Ok(xform(e)), &Ok).unwrap() // infallible\n    }\n\n    /// Creates an expression by ANDing together a vector, or None if empty.\n    pub fn and_vec(exprs: Vec<Expression>) -> Option<Self> {\n        let mut iter = exprs.into_iter();\n        let mut expr = iter.next()?;\n        for rhs in iter {\n            expr = Expression::And(expr.into(), rhs.into());\n        }\n        Some(expr)\n    }\n\n    /// Checks if an expression is a single column lookup (i.e. a disjunction of\n    /// = or IS NULL/NAN for a single column), returning the column index.\n    pub fn is_column_lookup(&self) -> Option<usize> {\n        use Expression::*;\n\n        match &self {\n            // Column/constant equality can use index lookups. NULL and NaN are\n            // handled in into_column_values().\n            Equal(lhs, rhs) => match (lhs.as_ref(), rhs.as_ref()) {\n                (Column(c), Constant(_)) | (Constant(_), Column(c)) => Some(*c),\n                _ => None,\n            },\n            // IS NULL and IS NAN can use index lookups.\n            Is(expr, _) => match expr.as_ref() {\n                Column(c) => Some(*c),\n                _ => None,\n            },\n            // All OR branches must be lookups on the same column:\n            // id = 1 OR id = 2 OR id = 3.\n            Or(lhs, rhs) => match (lhs.is_column_lookup(), rhs.is_column_lookup()) {\n                (Some(l), Some(r)) if l == r => Some(l),\n                _ => None,\n            },\n            _ => None,\n        }\n    }\n\n    /// Extracts column lookup values for the given column. Panics if the\n    /// expression isn't a lookup of the given column, i.e. is_column_lookup()\n    /// must return true for the expression.\n    pub fn into_column_values(self, index: usize) -> Vec<Value> {\n        use Expression::*;\n\n        match self {\n            Equal(lhs, rhs) => match (*lhs, *rhs) {\n                (Column(column), Constant(value)) | (Constant(value), Column(column)) => {\n                    assert_eq!(column, index, \"unexpected column\");\n                    // NULL and NAN index lookups are for IS NULL and IS NAN.\n                    // Equality shouldn't match anything, return empty vec.\n                    if value.is_undefined() { Vec::new() } else { vec![value] }\n                }\n                (lhs, rhs) => panic!(\"unexpected expression {:?}\", Equal(lhs.into(), rhs.into())),\n            },\n            // IS NULL and IS NAN can use index lookups.\n            Is(expr, value) => match *expr {\n                Column(column) => {\n                    assert_eq!(column, index, \"unexpected column\");\n                    vec![value]\n                }\n                expr => panic!(\"unexpected expression {expr:?}\"),\n            },\n            Or(lhs, rhs) => {\n                let mut values = lhs.into_column_values(index);\n                values.extend(rhs.into_column_values(index));\n                values\n            }\n            expr => panic!(\"unexpected expression {expr:?}\"),\n        }\n    }\n\n    /// Replaces column references from → to.\n    pub fn replace_column(self, from: usize, to: usize) -> Self {\n        let xform = |expr| match expr {\n            Expression::Column(i) if i == from => Expression::Column(to),\n            expr => expr,\n        };\n        self.transform(&|e| Ok(xform(e)), &Ok).unwrap() // infallible\n    }\n\n    /// Shifts column references by the given amount (can be negative).\n    pub fn shift_column(self, diff: isize) -> Self {\n        let xform = |expr| match expr {\n            Expression::Column(i) => Expression::Column((i as isize + diff) as usize),\n            expr => expr,\n        };\n        self.transform(&|e| Ok(xform(e)), &Ok).unwrap() // infallible\n    }\n}\n\n// NB: Display can't look up column labels, and will print numeric column\n// indexes instead. Use Expression::display() instead to print with labels\n// resolved from a given plan node.\nimpl Display for Expression {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        self.display(&Node::Nothing { columns: Vec::new() }).fmt(f)\n    }\n}\n\n// Helper to display expressions. Groups with () as needed by precedence rules,\n// and looks up column labels in the given plan node.\npub struct ExpressionDisplay<'a> {\n    expr: &'a Expression,\n    node: &'a Node,\n    parent_precedence: u8,\n}\n\nimpl<'a> Display for ExpressionDisplay<'a> {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        use Expression::*;\n\n        // Group the expression if its precedence is lower than the parent.\n        let precedence = Self::precedence(self.expr);\n        if precedence < self.parent_precedence {\n            write!(f, \"(\")?;\n        }\n\n        // Helper to display a boxed, grouped expression.\n        let group = |expr: &'a Expression| ExpressionDisplay::new(expr, self.node, precedence);\n\n        match self.expr {\n            Constant(value) => write!(f, \"{value}\")?,\n            Column(index) => match self.node.column_label(*index) {\n                Label::None => write!(f, \"#{index}\")?,\n                label => write!(f, \"{label}\")?,\n            },\n\n            And(lhs, rhs) => write!(f, \"{} AND {}\", group(lhs), group(rhs))?,\n            Or(lhs, rhs) => write!(f, \"{} OR {}\", group(lhs), group(rhs))?,\n            Not(expr) => write!(f, \"NOT {}\", group(expr))?,\n\n            Equal(lhs, rhs) => write!(f, \"{} = {}\", group(lhs), group(rhs))?,\n            GreaterThan(lhs, rhs) => write!(f, \"{} > {}\", group(lhs), group(rhs))?,\n            LessThan(lhs, rhs) => write!(f, \"{} < {}\", group(lhs), group(rhs))?,\n            Is(expr, Value::Null) => write!(f, \"{} IS NULL\", group(expr))?,\n            Is(expr, Value::Float(n)) if n.is_nan() => write!(f, \"{} IS NAN\", group(expr))?,\n            Is(_, v) => panic!(\"unexpected IS value {v}\"),\n\n            Add(lhs, rhs) => write!(f, \"{} + {}\", group(lhs), group(rhs))?,\n            Divide(lhs, rhs) => write!(f, \"{} / {}\", group(lhs), group(rhs))?,\n            Exponentiate(lhs, rhs) => write!(f, \"{} ^ {}\", group(lhs), group(rhs))?,\n            Factorial(expr) => write!(f, \"{}!\", group(expr))?,\n            Identity(expr) => write!(f, \"{}\", group(expr))?,\n            Multiply(lhs, rhs) => write!(f, \"{} * {}\", group(lhs), group(rhs))?,\n            Negate(expr) => write!(f, \"-{}\", group(expr))?,\n            Remainder(lhs, rhs) => write!(f, \"{} % {}\", group(lhs), group(rhs))?,\n            SquareRoot(expr) => write!(f, \"sqrt({})\", group(expr))?,\n            Subtract(lhs, rhs) => write!(f, \"{} - {}\", group(lhs), group(rhs))?,\n\n            Like(lhs, rhs) => write!(f, \"{} LIKE {}\", group(lhs), group(rhs))?,\n        }\n\n        if precedence < self.parent_precedence {\n            write!(f, \")\")?;\n        }\n\n        Ok(())\n    }\n}\n\nimpl<'a> ExpressionDisplay<'a> {\n    // Creates a new expression display.\n    pub fn new(expr: &'a Expression, node: &'a Node, parent_precedence: u8) -> Self {\n        Self { expr, node, parent_precedence }\n    }\n\n    // Precedence levels for () grouping. Matches the parser.\n    fn precedence(expr: &Expression) -> u8 {\n        use Expression::*;\n        match expr {\n            Column(_) | Constant(_) | SquareRoot(_) => 11,\n            Identity(_) | Negate(_) => 10,\n            Factorial(_) => 9,\n            Exponentiate(_, _) => 8,\n            Multiply(_, _) | Divide(_, _) | Remainder(_, _) => 7,\n            Add(_, _) | Subtract(_, _) => 6,\n            GreaterThan(_, _) | LessThan(_, _) => 5,\n            Equal(_, _) | Like(_, _) | Is(_, _) => 4,\n            Not(_) => 3,\n            And(_, _) => 2,\n            Or(_, _) => 1,\n        }\n    }\n}\n\nimpl From<Value> for Expression {\n    fn from(value: Value) -> Self {\n        Expression::Constant(value)\n    }\n}\n\nimpl From<Value> for Box<Expression> {\n    fn from(value: Value) -> Self {\n        Box::new(value.into())\n    }\n}\n"
  },
  {
    "path": "src/sql/types/mod.rs",
    "content": "//! The SQL data model, including data types, expressions, and schema objects.\n\nmod expression;\nmod schema;\nmod value;\n\npub use expression::Expression;\npub use schema::{Column, Table};\npub use value::{DataType, Label, Row, Rows, Value};\n"
  },
  {
    "path": "src/sql/types/schema.rs",
    "content": "use std::fmt::Display;\n\nuse serde::{Deserialize, Serialize};\n\nuse super::{DataType, Row, Value};\nuse crate::encoding;\nuse crate::errinput;\nuse crate::error::Result;\nuse crate::sql::engine::{Catalog, Transaction};\nuse crate::sql::parser::is_ident;\n\n/// A table schema, which specifies its data structure and constraints.\n///\n/// Tables can't change after they are created. There is no ALTER TABLE nor\n/// CREATE/DROP INDEX, only CREATE TABLE and DROP TABLE.\n#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]\npub struct Table {\n    /// The table name. Unique identifier for the table. Can't be empty.\n    pub name: String,\n    /// The primary key column index. A table must have a primary key, and it\n    /// can only be a single column.\n    pub primary_key: usize,\n    /// The table's columns. Must have at least one.\n    pub columns: Vec<Column>,\n}\n\nimpl encoding::Value for Table {}\n\n/// A table column.\n#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]\npub struct Column {\n    /// Column name. Can't be empty.\n    pub name: String,\n    /// Column datatype.\n    pub datatype: DataType,\n    /// Whether the column allows null values. Not legal for primary keys.\n    pub nullable: bool,\n    /// The column's default value. If None, the user must specify an explicit\n    /// value. Must match the column datatype. Nullable columns require a\n    /// default (often Null). Null is only a valid default when nullable.\n    pub default: Option<Value>,\n    /// Whether the column should only allow unique values (ignoring NULLs).\n    /// Must be true for a primary key column. Requires index.\n    pub unique: bool,\n    /// Whether the column should have a secondary index. Must be false for\n    /// primary keys, which are the primary index. Must be true for unique or\n    /// reference columns.\n    pub index: bool,\n    /// If set, this column is a foreign key reference to the given table's\n    /// primary key. Must be of the same type as the target primary key.\n    /// Requires index.\n    pub references: Option<String>,\n}\n\n// Formats the table as a SQL CREATE TABLE statement.\nimpl Display for Table {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        /// Formats an identifier as valid SQL, quoting it if necessary.\n        fn format_ident(ident: &str) -> String {\n            match is_ident(ident) {\n                true => ident.to_string(),\n                false => format!(\"\\\"{}\\\"\", ident.replace('\\\"', \"\\\"\\\"\")),\n            }\n        }\n\n        writeln!(f, \"CREATE TABLE {} (\", format_ident(&self.name))?;\n        for (i, column) in self.columns.iter().enumerate() {\n            write!(f, \"  {} {}\", format_ident(&column.name), column.datatype)?;\n            if i == self.primary_key {\n                write!(f, \" PRIMARY KEY\")?;\n            } else if !column.nullable {\n                write!(f, \" NOT NULL\")?;\n            }\n            if let Some(default) = &column.default {\n                write!(f, \" DEFAULT {default}\")?;\n            }\n            if i != self.primary_key {\n                if column.unique {\n                    write!(f, \" UNIQUE\")?;\n                }\n                if column.index {\n                    write!(f, \" INDEX\")?;\n                }\n            }\n            if let Some(reference) = &column.references {\n                write!(f, \" REFERENCES {reference}\")?;\n            }\n            if i < self.columns.len() - 1 {\n                write!(f, \",\")?;\n            }\n            writeln!(f)?;\n        }\n        write!(f, \")\")\n    }\n}\n\nimpl Table {\n    /// Validates the table schema, using the catalog to validate foreign key\n    /// references.\n    pub fn validate(&self, catalog: &impl Catalog) -> Result<()> {\n        if self.name.is_empty() {\n            return errinput!(\"table name can't be empty\");\n        }\n        if self.columns.is_empty() {\n            return errinput!(\"table has no columns\");\n        }\n        if self.columns.get(self.primary_key).is_none() {\n            return errinput!(\"invalid primary key index\");\n        }\n\n        for (i, column) in self.columns.iter().enumerate() {\n            if column.name.is_empty() {\n                return errinput!(\"column name can't be empty\");\n            }\n            let (cname, ctype) = (&column.name, &column.datatype); // for formatting convenience\n\n            // Validate primary key.\n            let is_primary_key = i == self.primary_key;\n            if is_primary_key {\n                if column.nullable {\n                    return errinput!(\"primary key {cname} cannot be nullable\");\n                }\n                if !column.unique {\n                    return errinput!(\"primary key {cname} must be unique\");\n                }\n                if column.index {\n                    return errinput!(\"primary key {cname} can't have an index\");\n                }\n            }\n\n            // Validate default value.\n            match column.default.as_ref().map(|v| v.datatype()) {\n                None if column.nullable => {\n                    return errinput!(\"nullable column {cname} must have a default value\");\n                }\n                Some(None) if !column.nullable => {\n                    return errinput!(\"invalid NULL default for non-nullable column {cname}\");\n                }\n                Some(Some(vtype)) if vtype != column.datatype => {\n                    return errinput!(\"invalid default type {vtype} for {ctype} column {cname}\");\n                }\n                Some(_) | None => {}\n            }\n\n            // Validate unique index.\n            if column.unique && !column.index && !is_primary_key {\n                return errinput!(\"unique column {cname} must have a secondary index\");\n            }\n\n            // Validate references.\n            if let Some(reference) = &column.references {\n                if !column.index && !is_primary_key {\n                    return errinput!(\"reference column {cname} must have a secondary index\");\n                }\n                let reftype = if reference == &self.name {\n                    self.columns[self.primary_key].datatype\n                } else if let Some(target) = catalog.get_table(reference)? {\n                    target.columns[target.primary_key].datatype\n                } else {\n                    return errinput!(\"unknown table {reference} referenced by column {cname}\");\n                };\n                if column.datatype != reftype {\n                    return errinput!(\n                        \"can't reference {reftype} primary key of {reference} from {ctype} column {cname}\"\n                    );\n                }\n            }\n        }\n        Ok(())\n    }\n\n    /// Validates a row, including uniqueness and reference checks using the\n    /// given transaction.\n    ///\n    /// If update is true, the row replaces an existing entry with the same\n    /// primary key. Otherwise, it is an insert. Primary key changes are\n    /// implemented as a delete+insert.\n    ///\n    /// Validating uniqueness and references individually for each row is not\n    /// performant, but it's fine for our purposes.\n    pub fn validate_row(&self, row: &Row, update: bool, txn: &impl Transaction) -> Result<()> {\n        if row.len() != self.columns.len() {\n            return errinput!(\"invalid row size for table {}\", self.name);\n        }\n\n        // Validate primary key.\n        let id = &row[self.primary_key];\n        let idslice = &row[self.primary_key..=self.primary_key];\n        if id.is_undefined() {\n            return errinput!(\"invalid primary key {id}\");\n        }\n        if !update && !txn.get(&self.name, idslice)?.is_empty() {\n            return errinput!(\"primary key {id} already exists\");\n        }\n\n        for (i, (column, value)) in self.columns.iter().zip(row).enumerate() {\n            let (cname, ctype) = (&column.name, &column.datatype);\n            let valueslice = &row[i..=i];\n\n            // Validate datatype.\n            if let Some(ref vtype) = value.datatype()\n                && vtype != ctype\n            {\n                return errinput!(\"invalid datatype {vtype} for {ctype} column {cname}\");\n            }\n            if value == &Value::Null && !column.nullable {\n                return errinput!(\"NULL value not allowed for column {cname}\");\n            }\n\n            // Validate outgoing references.\n            if let Some(target) = &column.references {\n                match value {\n                    // NB: NaN is not a valid primary key, and not valid as a\n                    // missing foreign key marker.\n                    Value::Null => {}\n                    v if target == &self.name && v == id => {}\n                    v if txn.get(target, valueslice)?.is_empty() => {\n                        return errinput!(\"reference {v} not in table {target}\");\n                    }\n                    _ => {}\n                }\n            }\n\n            // Validate uniqueness constraints. Unique columns are indexed.\n            if column.unique && i != self.primary_key && !value.is_undefined() {\n                let mut index = txn.lookup_index(&self.name, &column.name, valueslice)?;\n                if update {\n                    index.remove(id); // ignore existing version of this row\n                }\n                if !index.is_empty() {\n                    return errinput!(\"value {value} already in unique column {cname}\");\n                }\n            }\n        }\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "src/sql/types/value.rs",
    "content": "use std::borrow::Cow;\nuse std::cmp::{Eq, Ordering, PartialEq};\nuse std::fmt::Display;\nuse std::hash::{Hash, Hasher};\nuse std::result::Result as StdResult;\n\nuse dyn_clone::DynClone;\nuse serde::{Deserialize, Serialize, Serializer};\n\nuse crate::encoding;\nuse crate::error::{Error, Result};\nuse crate::sql::parser::ast;\nuse crate::{errdata, errinput};\n\n/// A primitive SQL data type. For simplicity, only a handful of scalar types\n/// are supported (no compound types).\n#[derive(Clone, Copy, Debug, Hash, PartialEq, Serialize, Deserialize)]\npub enum DataType {\n    /// A boolean: true or false.\n    Boolean,\n    /// A 64-bit signed integer.\n    Integer,\n    /// A 64-bit floating point number.\n    Float,\n    /// A UTF-8 encoded string.\n    String,\n}\n\nimpl Display for DataType {\n    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {\n        match self {\n            Self::Boolean => write!(f, \"BOOLEAN\"),\n            Self::Integer => write!(f, \"INTEGER\"),\n            Self::Float => write!(f, \"FLOAT\"),\n            Self::String => write!(f, \"STRING\"),\n        }\n    }\n}\n\n/// A primitive SQL value, represented as a native Rust type.\n#[derive(Clone, Debug, Serialize, Deserialize)]\npub enum Value {\n    /// An unknown value of unknown type (i.e. SQL NULL).\n    ///\n    /// In code, Null is considered equal to Null, so that we can detect, index,\n    /// and order these values. The SQL NULL semantics are implemented during\n    /// Expression evaluation.\n    Null,\n    /// A boolean.\n    Boolean(bool),\n    /// A 64-bit signed integer.\n    Integer(i64),\n    /// A 64-bit floating point number.\n    ///\n    /// In code, NaN is considered equal to NaN, so that we can detect, index,\n    /// and order these values. The SQL NAN semantics are implemented during\n    /// Expression evaluation.\n    ///\n    /// -0.0 and -NaN are considered equal to their positive counterpart, and\n    /// normalized as positive when serialized (for key lookups).\n    Float(#[serde(serialize_with = \"serialize_f64\")] f64),\n    /// A UTF-8 encoded string.\n    String(String),\n}\n\nimpl encoding::Value for Value {}\n\nimpl Display for Value {\n    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {\n        match self {\n            Self::Null => f.write_str(\"NULL\"),\n            Self::Boolean(true) => f.write_str(\"TRUE\"),\n            Self::Boolean(false) => f.write_str(\"FALSE\"),\n            Self::Integer(integer) => integer.fmt(f),\n            Self::Float(float) => write!(f, \"{float:?}\"),\n            Self::String(string) => write!(f, \"'{}'\", string.escape_debug()),\n        }\n    }\n}\n\n/// Serialize f64 -0.0 and -NaN as positive, such that they're considered equal\n/// in the key/value store (e.g. for index lookups).\nfn serialize_f64<S: Serializer>(value: &f64, serializer: S) -> StdResult<S::Ok, S::Error> {\n    let mut value = *value;\n    if (value.is_nan() || value == 0.0) && value.is_sign_negative() {\n        value = -value;\n    }\n    serializer.serialize_f64(value)\n}\n\n// Consider Nulls and ±NaNs equal. Rust already considers -0.0 == 0.0.\nimpl PartialEq for Value {\n    fn eq(&self, other: &Self) -> bool {\n        match (self, other) {\n            (Self::Boolean(a), Self::Boolean(b)) => a == b,\n            (Self::Integer(a), Self::Integer(b)) => a == b,\n            (Self::Integer(a), Self::Float(b)) => *a as f64 == *b,\n            (Self::Float(a), Self::Integer(b)) => *a == *b as f64,\n            (Self::Float(a), Self::Float(b)) => a == b || a.is_nan() && b.is_nan(),\n            (Self::String(a), Self::String(b)) => a == b,\n            (Self::Null, Self::Null) => true,\n            (_, _) => false,\n        }\n    }\n}\n\nimpl Eq for Value {}\n\n// Allow hashing Nulls and floats, and hash -0.0 and -NaN as positive.\nimpl Hash for Value {\n    fn hash<H: Hasher>(&self, hasher: &mut H) {\n        core::mem::discriminant(self).hash(hasher); // hash the type\n        match self {\n            Self::Null => {}\n            Self::Boolean(v) => v.hash(hasher),\n            Self::Integer(v) => v.hash(hasher),\n            Self::Float(v) => {\n                // Hash -NaN and -0.0 as positive.\n                let mut v = *v;\n                if (v.is_nan() || v == 0.0) && v.is_sign_negative() {\n                    v = -v;\n                }\n                v.to_bits().hash(hasher)\n            }\n            Self::String(v) => v.hash(hasher),\n        }\n    }\n}\n\n// Consider Nulls and NaNs equal when ordering.\n//\n// We establish a total order across all types, even though mixed types will\n// rarely/never come up: String > Integer/Float > Boolean > Null.\nimpl Ord for Value {\n    fn cmp(&self, other: &Self) -> Ordering {\n        match (self, other) {\n            (Self::Null, Self::Null) => Ordering::Equal,\n            (Self::Boolean(a), Self::Boolean(b)) => a.cmp(b),\n            (Self::Integer(a), Self::Integer(b)) => a.cmp(b),\n            (Self::Integer(a), Self::Float(b)) => (*a as f64).total_cmp(b),\n            (Self::Float(a), Self::Integer(b)) => a.total_cmp(&(*b as f64)),\n            (Self::Float(a), Self::Float(b)) => a.total_cmp(b),\n            (Self::String(a), Self::String(b)) => a.cmp(b),\n\n            (Self::Null, _) => Ordering::Less,\n            (_, Self::Null) => Ordering::Greater,\n            (Self::Boolean(_), _) => Ordering::Less,\n            (_, Self::Boolean(_)) => Ordering::Greater,\n            (Self::Float(_), _) => Ordering::Less,\n            (_, Self::Float(_)) => Ordering::Greater,\n            (Self::Integer(_), _) => Ordering::Less,\n            (_, Self::Integer(_)) => Ordering::Greater,\n            // String is ordered last.\n        }\n    }\n}\n\nimpl PartialOrd for Value {\n    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {\n        Some(self.cmp(other))\n    }\n}\n\nimpl Value {\n    /// Returns the value's datatype, or None for null values.\n    pub fn datatype(&self) -> Option<DataType> {\n        match self {\n            Self::Null => None,\n            Self::Boolean(_) => Some(DataType::Boolean),\n            Self::Integer(_) => Some(DataType::Integer),\n            Self::Float(_) => Some(DataType::Float),\n            Self::String(_) => Some(DataType::String),\n        }\n    }\n\n    /// Returns true if the value is undefined (NULL or NaN).\n    pub fn is_undefined(&self) -> bool {\n        match self {\n            Self::Null => true,\n            Self::Float(f) if f.is_nan() => true,\n            _ => false,\n        }\n    }\n\n    /// Adds two values. Errors if invalid.\n    pub fn checked_add(&self, other: &Self) -> Result<Self> {\n        use Value::*;\n\n        Ok(match (self, other) {\n            (Integer(lhs), Integer(rhs)) => match lhs.checked_add(*rhs) {\n                Some(i) => Integer(i),\n                None => return errinput!(\"integer overflow\"),\n            },\n            (Integer(lhs), Float(rhs)) => Float(*lhs as f64 + rhs),\n            (Float(lhs), Integer(rhs)) => Float(lhs + *rhs as f64),\n            (Float(lhs), Float(rhs)) => Float(lhs + rhs),\n            (Null, Integer(_) | Float(_) | Null) => Null,\n            (Integer(_) | Float(_), Null) => Null,\n            (lhs, rhs) => return errinput!(\"can't add {lhs} and {rhs}\"),\n        })\n    }\n\n    /// Divides two values. Errors if invalid.\n    pub fn checked_div(&self, other: &Self) -> Result<Self> {\n        use Value::*;\n\n        Ok(match (self, other) {\n            (Integer(_), Integer(0)) => return errinput!(\"can't divide by zero\"),\n            (Integer(lhs), Integer(rhs)) => Integer(lhs / rhs),\n            (Integer(lhs), Float(rhs)) => Float(*lhs as f64 / rhs),\n            (Float(lhs), Integer(rhs)) => Float(lhs / *rhs as f64),\n            (Float(lhs), Float(rhs)) => Float(lhs / rhs),\n            (Null, Integer(_) | Float(_) | Null) => Null,\n            (Integer(_) | Float(_), Null) => Null,\n            (lhs, rhs) => return errinput!(\"can't divide {lhs} and {rhs}\"),\n        })\n    }\n\n    /// Multiplies two values. Errors if invalid.\n    pub fn checked_mul(&self, other: &Self) -> Result<Self> {\n        use Value::*;\n\n        Ok(match (self, other) {\n            (Integer(lhs), Integer(rhs)) => match lhs.checked_mul(*rhs) {\n                Some(i) => Integer(i),\n                None => return errinput!(\"integer overflow\"),\n            },\n            (Integer(lhs), Float(rhs)) => Float(*lhs as f64 * rhs),\n            (Float(lhs), Integer(rhs)) => Float(lhs * *rhs as f64),\n            (Float(lhs), Float(rhs)) => Float(lhs * rhs),\n            (Null, Integer(_) | Float(_) | Null) => Null,\n            (Integer(_) | Float(_), Null) => Null,\n            (lhs, rhs) => return errinput!(\"can't multiply {lhs} and {rhs}\"),\n        })\n    }\n\n    /// Exponentiates two values. Errors if invalid.\n    pub fn checked_pow(&self, other: &Self) -> Result<Self> {\n        use Value::*;\n\n        Ok(match (self, other) {\n            (Integer(lhs), Integer(rhs)) if *rhs >= 0 => {\n                let rhs = (*rhs).try_into().or_else(|_| errinput!(\"integer overflow\"))?;\n                match lhs.checked_pow(rhs) {\n                    Some(i) => Integer(i),\n                    None => return errinput!(\"integer overflow\"),\n                }\n            }\n            (Integer(lhs), Integer(rhs)) => Float((*lhs as f64).powf(*rhs as f64)),\n            (Integer(lhs), Float(rhs)) => Float((*lhs as f64).powf(*rhs)),\n            (Float(lhs), Integer(rhs)) => Float((lhs).powi(*rhs as i32)),\n            (Float(lhs), Float(rhs)) => Float((lhs).powf(*rhs)),\n            (Integer(_) | Float(_), Null) => Null,\n            (Null, Integer(_) | Float(_) | Null) => Null,\n            (lhs, rhs) => return errinput!(\"can't exponentiate {lhs} and {rhs}\"),\n        })\n    }\n\n    /// Finds the remainder of two values. Errors if invalid.\n    ///\n    /// NB: uses the remainder, not modulo, like Postgres. This means that for\n    /// negative values, the result has the sign of the dividend, rather than\n    /// always returning a positive value.\n    pub fn checked_rem(&self, other: &Self) -> Result<Self> {\n        use Value::*;\n\n        Ok(match (self, other) {\n            (Integer(_), Integer(0)) => return errinput!(\"can't divide by zero\"),\n            (Integer(lhs), Integer(rhs)) => Integer(lhs % rhs),\n            (Integer(lhs), Float(rhs)) => Float(*lhs as f64 % rhs),\n            (Float(lhs), Integer(rhs)) => Float(lhs % *rhs as f64),\n            (Float(lhs), Float(rhs)) => Float(lhs % rhs),\n            (Integer(_) | Float(_) | Null, Null) => Null,\n            (Null, Integer(_) | Float(_)) => Null,\n            (lhs, rhs) => return errinput!(\"can't take remainder of {lhs} and {rhs}\"),\n        })\n    }\n\n    /// Subtracts two values. Errors if invalid.\n    pub fn checked_sub(&self, other: &Self) -> Result<Self> {\n        use Value::*;\n\n        Ok(match (self, other) {\n            (Integer(lhs), Integer(rhs)) => match lhs.checked_sub(*rhs) {\n                Some(i) => Integer(i),\n                None => return errinput!(\"integer overflow\"),\n            },\n            (Integer(lhs), Float(rhs)) => Float(*lhs as f64 - rhs),\n            (Float(lhs), Integer(rhs)) => Float(lhs - *rhs as f64),\n            (Float(lhs), Float(rhs)) => Float(lhs - rhs),\n            (Null, Integer(_) | Float(_) | Null) => Null,\n            (Integer(_) | Float(_), Null) => Null,\n            (lhs, rhs) => return errinput!(\"can't subtract {lhs} and {rhs}\"),\n        })\n    }\n}\n\nimpl From<bool> for Value {\n    fn from(v: bool) -> Self {\n        Value::Boolean(v)\n    }\n}\n\nimpl From<f64> for Value {\n    fn from(v: f64) -> Self {\n        Value::Float(v)\n    }\n}\n\nimpl From<i64> for Value {\n    fn from(v: i64) -> Self {\n        Value::Integer(v)\n    }\n}\n\nimpl From<String> for Value {\n    fn from(v: String) -> Self {\n        Value::String(v)\n    }\n}\n\nimpl From<&str> for Value {\n    fn from(v: &str) -> Self {\n        Value::String(v.to_owned())\n    }\n}\n\nimpl TryFrom<Value> for bool {\n    type Error = Error;\n\n    fn try_from(value: Value) -> Result<Self> {\n        let Value::Boolean(b) = value else {\n            return errdata!(\"not a boolean: {value}\");\n        };\n        Ok(b)\n    }\n}\n\nimpl TryFrom<Value> for f64 {\n    type Error = Error;\n\n    fn try_from(value: Value) -> Result<Self> {\n        let Value::Float(f) = value else {\n            return errdata!(\"not a float: {value}\");\n        };\n        Ok(f)\n    }\n}\n\nimpl TryFrom<Value> for i64 {\n    type Error = Error;\n\n    fn try_from(value: Value) -> Result<Self> {\n        let Value::Integer(i) = value else {\n            return errdata!(\"not an integer: {value}\");\n        };\n        Ok(i)\n    }\n}\n\nimpl TryFrom<Value> for String {\n    type Error = Error;\n\n    fn try_from(value: Value) -> Result<Self> {\n        let Value::String(s) = value else {\n            return errdata!(\"not a string: {value}\");\n        };\n        Ok(s)\n    }\n}\n\nimpl<'a> From<&'a Value> for Cow<'a, Value> {\n    fn from(v: &'a Value) -> Self {\n        Cow::Borrowed(v)\n    }\n}\n\n/// A row of values.\npub type Row = Vec<Value>;\n\n/// A row iterator.\npub type Rows = Box<dyn RowIterator>;\n\n/// A row iterator trait, which requires the iterator to be both clonable and\n/// object-safe. Cloning allows resetting an iterator back to an initial state,\n/// e.g. for nested loop joins. It uses a blanket implementation, and relies on\n/// dyn_clone to allow cloning trait objects.\npub trait RowIterator: Iterator<Item = Result<Row>> + DynClone {}\n\ndyn_clone::clone_trait_object!(RowIterator);\n\nimpl<I: Iterator<Item = Result<Row>> + DynClone> RowIterator for I {}\n\n/// A column label, used in query results and plans.\n#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]\npub enum Label {\n    /// No label.\n    None,\n    /// An unqualified column name.\n    Unqualified(String),\n    /// A fully qualified table/column name.\n    Qualified(String, String),\n}\n\nimpl Display for Label {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::None => write!(f, \"\"),\n            Self::Unqualified(name) => write!(f, \"{name}\"),\n            Self::Qualified(table, column) => write!(f, \"{table}.{column}\"),\n        }\n    }\n}\n\nimpl Label {\n    /// Formats the label as a short column header.\n    pub fn as_header(&self) -> &str {\n        match self {\n            Self::Qualified(_, column) | Self::Unqualified(column) => column.as_str(),\n            Self::None => \"?\",\n        }\n    }\n}\n\nimpl From<Label> for ast::Expression {\n    /// Builds an ast::Expression::Column for a label. Can't be None.\n    fn from(label: Label) -> Self {\n        match label {\n            Label::Qualified(table, column) => ast::Expression::Column(Some(table), column),\n            Label::Unqualified(column) => ast::Expression::Column(None, column),\n            Label::None => panic!(\"can't convert None label to AST expression\"),\n        }\n    }\n}\n\nimpl From<Option<String>> for Label {\n    fn from(name: Option<String>) -> Self {\n        name.map(Label::Unqualified).unwrap_or(Label::None)\n    }\n}\n"
  },
  {
    "path": "src/storage/bitcask.rs",
    "content": "use std::collections::BTreeMap;\nuse std::collections::btree_map::Range;\nuse std::fs::File;\nuse std::io::{BufReader, BufWriter, Read as _, Seek as _, SeekFrom, Write as _};\nuse std::ops::{Bound, RangeBounds};\nuse std::path::PathBuf;\nuse std::result::Result as StdResult;\n\nuse fs4::fs_std::FileExt;\nuse log::{error, info};\n\nuse super::{Engine, Status};\nuse crate::error::{Error, Result};\n\n/// A very simple variant of BitCask, itself a simple log-structured key-value\n/// engine used e.g. by the Riak database. This is not compatible with BitCask\n/// databases generated by other implementations. See:\n/// <https://riak.com/assets/bitcask-intro.pdf>\n///\n/// BitCask writes key-value pairs to an append-only log file, and keeps a\n/// mapping of keys to file offsets in memory. All live keys must fit in memory.\n/// Deletes write a tombstone value to the log file. To remove old garbage\n/// (deleted or replaced keys), logs can be compacted by writing new logs\n/// containing only live data, dropping replaced values and tombstones.\n///\n/// This implementation is significantly simpler than standard BitCask:\n///\n/// * Instead of writing multiple fixed-size log files, it uses a single\n///   append-only log file of arbitrary size. This increases the compaction\n///   volume, since the entire log file must be rewritten on every compaction.\n///   It can also exceed the filesystem's file size limit. However, toyDB\n///   databases are expected to be small.\n///\n/// * Compactions lock the database for reads and writes. This is ok since toyDB\n///   only compacts during node startup and files are expected to be small.\n///\n/// * Hint files are not used, the log itself is scanned when opened to\n///   build the keydir. Hint files only omit values, and toyDB values are\n///   expected to be small, so the hint files would be nearly as large as\n///   the compacted log files themselves.\n///\n/// * Log entries don't contain timestamps or checksums.\n///\n/// The structure of an encoded log entry is:\n///\n/// 1. Key length as big-endian u32 [4 bytes].\n/// 2. Value length as big-endian i32, or -1 for tombstones [4 bytes].\n/// 3. Key as raw bytes [<= 2 GB].\n/// 4. Value as raw bytes [<= 2 GB].\npub struct BitCask {\n    /// The current append-only log file.\n    log: Log,\n    /// Maps keys to a value's offset and length in [`BitCask::log`].\n    keydir: KeyDir,\n}\n\n/// Maps keys to a value's location in the log file.\ntype KeyDir = BTreeMap<Vec<u8>, ValueLocation>;\n\n/// The location of a value in the log file.\n#[derive(Clone, Copy)]\nstruct ValueLocation {\n    offset: u64,\n    length: usize,\n}\n\nimpl ValueLocation {\n    fn end(&self) -> u64 {\n        self.offset + self.length as u64\n    }\n}\n\nimpl BitCask {\n    /// Opens or creates a BitCask database in the given file.\n    pub fn new(path: PathBuf) -> Result<Self> {\n        let mut log = Log::new(path.clone())?;\n        let keydir = log.build_keydir()?;\n        info!(\"Opened {} with {} live keys\", path.display(), keydir.len());\n        Ok(Self { log, keydir })\n    }\n\n    /// Opens a BitCask database, and automatically compacts it if the amount\n    /// of garbage exceeds the given ratio and byte size when opened.\n    pub fn new_maybe_compact(\n        path: PathBuf,\n        garbage_min_fraction: f64,\n        garbage_min_bytes: u64,\n    ) -> Result<Self> {\n        let mut engine = Self::new(path)?;\n\n        let status = engine.status()?;\n        let total_size = status.disk_size;\n        let garbage_size = status.garbage_disk_size();\n        let garbage_fraction = garbage_size as f64 / total_size as f64;\n        if garbage_size > 0\n            && garbage_size >= garbage_min_bytes\n            && garbage_fraction >= garbage_min_fraction\n        {\n            info!(\n                \"Compacting {} to remove {:.0}% garbage ({:.1} MB out of {:.1} MB)\",\n                engine.log.path.display(),\n                garbage_fraction * 100.0,\n                garbage_size as f64 / 1024.0 / 1024.0,\n                total_size as f64 / 1024.0 / 1024.0\n            );\n            engine.compact()?;\n            info!(\n                \"Compacted {} to size {:.1} MB\",\n                engine.log.path.display(),\n                (total_size - garbage_size) as f64 / 1024.0 / 1024.0\n            );\n        }\n\n        Ok(engine)\n    }\n}\n\nimpl Engine for BitCask {\n    type ScanIterator<'a> = ScanIterator<'a>;\n\n    fn delete(&mut self, key: &[u8]) -> Result<()> {\n        self.log.write_entry(key, None)?;\n        self.keydir.remove(key);\n        Ok(())\n    }\n\n    fn flush(&mut self) -> Result<()> {\n        // Don't fsync in tests, to speed them up. We disable this here, instead\n        // of setting `raft::Log::fsync = false` in tests, because we want to\n        // assert that the Raft log flushes to disk even if the flush is a noop.\n        #[cfg(not(test))]\n        self.log.file.sync_all()?;\n        Ok(())\n    }\n\n    fn get(&mut self, key: &[u8]) -> Result<Option<Vec<u8>>> {\n        let Some(location) = self.keydir.get(key) else {\n            return Ok(None);\n        };\n        self.log.read_value(*location).map(Some)\n    }\n\n    fn scan(&mut self, range: impl RangeBounds<Vec<u8>>) -> Self::ScanIterator<'_> {\n        ScanIterator { inner: self.keydir.range(range), log: &mut self.log }\n    }\n\n    fn scan_dyn(\n        &mut self,\n        range: (Bound<Vec<u8>>, Bound<Vec<u8>>),\n    ) -> Box<dyn super::ScanIterator + '_> {\n        Box::new(self.scan(range))\n    }\n\n    fn set(&mut self, key: &[u8], value: Vec<u8>) -> Result<()> {\n        let value_location = self.log.write_entry(key, Some(&*value))?;\n        self.keydir.insert(key.to_vec(), value_location);\n        Ok(())\n    }\n\n    fn status(&mut self) -> Result<Status> {\n        let keys = self.keydir.len() as u64;\n        let size =\n            self.keydir.iter().map(|(key, value_loc)| (key.len() + value_loc.length) as u64).sum();\n        let disk_size = self.log.file.metadata()?.len();\n        let live_disk_size = size + 8 * keys; // account for length prefixes\n        Ok(Status { name: \"bitcask\".to_string(), keys, size, disk_size, live_disk_size })\n    }\n}\n\nimpl BitCask {\n    /// Compacts the current log file by writing out a new log file containing\n    /// only live keys and replacing the current file with it.\n    pub fn compact(&mut self) -> Result<()> {\n        // Create a new temporary log file, or truncate it if it already exists.\n        let new_path = self.log.path.with_extension(\"new\");\n        let mut new_log = Log::new(new_path)?;\n        new_log.file.set_len(0)?;\n\n        // Write all live entries into the new log, and generate a new KeyDir.\n        let mut new_keydir = KeyDir::new();\n        for (key, value_loc) in &self.keydir {\n            let value = self.log.read_value(*value_loc)?;\n            let value_loc = new_log.write_entry(key, Some(&value))?;\n            new_keydir.insert(key.clone(), value_loc);\n        }\n\n        // Replace the current log with the new one.\n        std::fs::rename(&new_log.path, &self.log.path)?;\n        new_log.path = self.log.path.clone();\n\n        self.log = new_log;\n        self.keydir = new_keydir;\n        Ok(())\n    }\n}\n\n/// Attempt to flush the file when the database is closed.\nimpl Drop for BitCask {\n    fn drop(&mut self) {\n        if let Err(error) = self.flush() {\n            error!(\"failed to flush file: {}\", error)\n        }\n    }\n}\n\npub struct ScanIterator<'a> {\n    inner: Range<'a, Vec<u8>, ValueLocation>,\n    log: &'a mut Log,\n}\n\nimpl ScanIterator<'_> {\n    fn map(&mut self, item: (&Vec<u8>, &ValueLocation)) -> <Self as Iterator>::Item {\n        let (key, value_loc) = item;\n        Ok((key.clone(), self.log.read_value(*value_loc)?))\n    }\n}\n\nimpl Iterator for ScanIterator<'_> {\n    type Item = Result<(Vec<u8>, Vec<u8>)>;\n\n    fn next(&mut self) -> Option<Self::Item> {\n        self.inner.next().map(|item| self.map(item))\n    }\n}\n\nimpl DoubleEndedIterator for ScanIterator<'_> {\n    fn next_back(&mut self) -> Option<Self::Item> {\n        self.inner.next_back().map(|item| self.map(item))\n    }\n}\n\n/// A BitCask append-only log file, containing a sequence of key/value\n/// entries encoded as follows;\n///\n/// 1. Key length as big-endian u32 [4 bytes].\n/// 2. Value length as big-endian i32, or -1 for tombstones [4 bytes].\n/// 3. Key as raw bytes [<= 2 GB].\n/// 4. Value as raw bytes [<= 2 GB].\nstruct Log {\n    /// The open log file.\n    file: File,\n    /// Path to the log file.\n    path: PathBuf,\n}\n\nimpl Log {\n    /// Opens a log file, or creates one if it does not exist. Takes out an\n    /// exclusive lock on the file until it is closed, or errors if the lock is\n    /// already held.\n    fn new(path: PathBuf) -> Result<Self> {\n        if let Some(dir) = path.parent() {\n            std::fs::create_dir_all(dir)?\n        }\n        let file = std::fs::OpenOptions::new()\n            .read(true)\n            .write(true)\n            .create(true)\n            .truncate(false)\n            .open(&path)?;\n        if !file.try_lock_exclusive()? {\n            return Err(Error::IO(format!(\"file {path:?} is already is use\")));\n        }\n        Ok(Self { file, path })\n    }\n\n    /// Builds a keydir by scanning the log file. If an incomplete entry is\n    /// encountered, it is assumed to be caused by an incomplete write operation\n    /// and the remainder of the file is truncated.\n    fn build_keydir(&mut self) -> Result<KeyDir> {\n        let mut len_buf = [0u8; 4];\n        let mut keydir = KeyDir::new();\n        let file_len = self.file.metadata()?.len();\n        let mut r = BufReader::new(&mut self.file);\n        let mut offset = r.seek(SeekFrom::Start(0))?;\n\n        while offset < file_len {\n            // Read the next entry from the file, returning the key and value\n            // location, or None for tombstones.\n            let result = || -> StdResult<(Vec<u8>, Option<ValueLocation>), std::io::Error> {\n                // Read the key length: 4-byte u32.\n                r.read_exact(&mut len_buf)?;\n                let key_len = u32::from_be_bytes(len_buf);\n\n                // Read the value length: 4-byte i32, -1 for tombstones.\n                r.read_exact(&mut len_buf)?;\n                let value_loc = match i32::from_be_bytes(len_buf) {\n                    ..0 => None, // tombstone\n                    len => Some(ValueLocation {\n                        offset: offset + 8 + key_len as u64,\n                        length: len as usize,\n                    }),\n                };\n\n                // Read the key.\n                let mut key = vec![0; key_len as usize];\n                r.read_exact(&mut key)?;\n\n                // Skip past the value.\n                if let Some(value_loc) = value_loc {\n                    if value_loc.end() > file_len {\n                        return Err(std::io::Error::new(\n                            std::io::ErrorKind::UnexpectedEof,\n                            \"value extends beyond end of file\",\n                        ));\n                    }\n                    r.seek_relative(value_loc.length as i64)?;\n                }\n\n                // Update the file offset.\n                offset += 8 + key_len as u64 + value_loc.map_or(0, |v| v.length) as u64;\n\n                Ok((key, value_loc))\n            }();\n\n            // Update the keydir with the entry.\n            match result {\n                Ok((key, Some(value_loc))) => keydir.insert(key, value_loc),\n                Ok((key, None)) => keydir.remove(&key),\n                // If an incomplete entry was found at the end of the file, assume an\n                // incomplete write and truncate the file.\n                Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => {\n                    error!(\"Found incomplete entry at offset {offset}, truncating file\");\n                    self.file.set_len(offset)?;\n                    break;\n                }\n                Err(err) => return Err(err.into()),\n            };\n        }\n\n        Ok(keydir)\n    }\n\n    /// Reads a value from the log file at the given location.\n    fn read_value(&mut self, location: ValueLocation) -> Result<Vec<u8>> {\n        let mut value = vec![0; location.length];\n        self.file.seek(SeekFrom::Start(location.offset))?;\n        self.file.read_exact(&mut value)?;\n        Ok(value)\n    }\n\n    /// Appends a key/value entry to the log file, using a None value for\n    /// tombstones. It returns the location of the entry's value in the log, for\n    /// use with the [`KeyDir`].\n    fn write_entry(&mut self, key: &[u8], value: Option<&[u8]>) -> Result<ValueLocation> {\n        let length = 8 + key.len() + value.map_or(0, |v| v.len());\n        let offset = self.file.seek(SeekFrom::End(0))?;\n        let mut w = BufWriter::with_capacity(length, &mut self.file);\n\n        // Key length: 4-byte u32.\n        w.write_all(&(key.len() as u32).to_be_bytes())?;\n\n        // Value length: 4-byte i32, -1 for tombstones.\n        w.write_all(&value.map_or(-1, |v| v.len() as i32).to_be_bytes())?;\n\n        // The actual key and value.\n        w.write_all(key)?;\n        w.write_all(value.unwrap_or_default())?;\n        w.flush()?;\n\n        // Translate the entry location into a value location.\n        Ok(ValueLocation {\n            offset: offset + 8 + key.len() as u64,\n            length: value.map_or(0, |v| v.len()),\n        })\n    }\n}\n\n/// Most storage tests are Goldenscripts under src/storage/testscripts.\n#[cfg(test)]\nmod tests {\n    use std::error::Error as StdError;\n    use std::fmt::Write as _;\n\n    use tempfile::TempDir;\n    use test_each_file::test_each_path;\n\n    use super::super::engine::test::Runner;\n    use super::*;\n    use crate::encoding::format::{self, Formatter as _};\n\n    // Run common goldenscript tests in src/storage/testscripts/engine.\n    test_each_path! { in \"src/storage/testscripts/engine\" as engine => test_goldenscript }\n\n    // Also run BitCask-specific tests in src/storage/testscripts/bitcask.\n    test_each_path! { in \"src/storage/testscripts/bitcask\" as scripts => test_goldenscript }\n\n    fn test_goldenscript(path: &std::path::Path) {\n        goldenscript::run(&mut BitCaskRunner::new(), path).expect(\"goldenscript failed\")\n    }\n\n    /// Tests that exclusive locks are taken out on log files, erroring if held,\n    /// and released when the database is closed.\n    #[test]\n    fn lock() -> Result<()> {\n        let path = TempDir::with_prefix(\"toydb\")?.path().join(\"bitcask\");\n        let engine = BitCask::new(path.clone()).expect(\"bitcask failed\");\n\n        // Opening another database with the same file should error.\n        assert!(BitCask::new(path.clone()).is_err());\n\n        // Opening another database after the current is closed works.\n        drop(engine);\n        assert!(BitCask::new(path).is_ok());\n        Ok(())\n    }\n\n    /// Tests that a log with an incomplete write at the end can be recovered by\n    /// discarding the last entry.\n    #[test]\n    fn recovery() -> Result<()> {\n        // Create an initial log file with a few entries. Keep track of where\n        // each entry ends.\n        let dir = TempDir::with_prefix(\"toydb\")?;\n        let path = dir.path().join(\"complete\");\n        let mut log = Log::new(path.clone())?;\n\n        let mut ends = vec![];\n        let value_loc = log.write_entry(\"deleted\".as_bytes(), Some(&[1, 2, 3]))?;\n        ends.push(value_loc.end());\n        let value_loc = log.write_entry(\"deleted\".as_bytes(), None)?;\n        ends.push(value_loc.end());\n        let value_loc = log.write_entry(&[], Some(&[]))?;\n        ends.push(value_loc.end());\n        let value_loc = log.write_entry(\"key\".as_bytes(), Some(&[1, 2, 3, 4, 5]))?;\n        ends.push(value_loc.end());\n        drop(log);\n\n        // Copy the file, and truncate it at each byte, then try to open it\n        // and assert that we always retain a prefix of entries.\n        let truncpath = dir.path().join(\"truncated\");\n        let size = std::fs::metadata(&path)?.len();\n        for pos in 0..=size {\n            std::fs::copy(&path, &truncpath)?;\n            let f = std::fs::OpenOptions::new().write(true).open(&truncpath)?;\n            f.set_len(pos)?;\n            drop(f);\n\n            let mut expect = vec![];\n            if pos >= ends[0] {\n                expect.push((b\"deleted\".to_vec(), vec![1, 2, 3]))\n            }\n            if pos >= ends[1] {\n                expect.pop(); // \"deleted\" key removed\n            }\n            if pos >= ends[2] {\n                expect.push((b\"\".to_vec(), vec![]))\n            }\n            if pos >= ends[3] {\n                expect.push((b\"key\".to_vec(), vec![1, 2, 3, 4, 5]))\n            }\n\n            let mut engine = BitCask::new(truncpath.clone())?;\n            assert_eq!(expect, engine.scan(..).collect::<Result<Vec<_>>>()?);\n        }\n        Ok(())\n    }\n\n    /// Tests key/value sizes up to 64 MB.\n    #[test]\n    fn point_ops_sizes() -> Result<()> {\n        let path = TempDir::with_prefix(\"toydb\")?.path().join(\"bitcask\");\n        let mut engine = BitCask::new(path.clone()).expect(\"bitcask failed\");\n\n        // Generate keys/values for increasing powers of two.\n        for size in (1..=26).map(|i| 1 << i) {\n            let value = vec![b'x'; size];\n            let key = value.as_slice();\n\n            assert_eq!(engine.get(key)?, None);\n            engine.set(key, value.clone())?;\n            assert_eq!(engine.get(key)?.as_ref(), Some(&value));\n            engine.delete(key)?;\n            assert_eq!(engine.get(key)?, None);\n        }\n        Ok(())\n    }\n\n    /// A BitCask-specific goldenscript runner, which dispatches through to the\n    /// standard Engine runner.\n    struct BitCaskRunner {\n        inner: Runner<BitCask>,\n        tempdir: TempDir,\n    }\n\n    impl goldenscript::Runner for BitCaskRunner {\n        fn run(&mut self, command: &goldenscript::Command) -> StdResult<String, Box<dyn StdError>> {\n            let mut output = String::new();\n            match command.name.as_str() {\n                // compact\n                // Compacts the BitCask entry log.\n                \"compact\" => {\n                    command.consume_args().reject_rest()?;\n                    self.inner.engine.compact()?;\n                }\n\n                // dump\n                // Dumps the full BitCask entry log.\n                \"dump\" => {\n                    command.consume_args().reject_rest()?;\n                    self.dump(&mut output)?;\n                }\n\n                // reopen [compact_fraction=FLOAT]\n                // Closes and reopens the BitCask database. If compact_ratio is\n                // given, it specifies a garbage ratio beyond which the log\n                // should be auto-compacted on open.\n                \"reopen\" => {\n                    let mut args = command.consume_args();\n                    let compact_fraction = args.lookup_parse(\"compact_fraction\")?;\n                    args.reject_rest()?;\n                    // We need to close the file before we can reopen it, which\n                    // happens when the database is dropped. Replace the engine\n                    // with a temporary empty engine then reopen the file.\n                    let path = self.inner.engine.log.path.clone();\n                    self.inner.engine = BitCask::new(self.tempdir.path().join(\"empty\"))?;\n                    if let Some(garbage_fraction) = compact_fraction {\n                        self.inner.engine = BitCask::new_maybe_compact(path, garbage_fraction, 0)?;\n                    } else {\n                        self.inner.engine = BitCask::new(path)?;\n                    }\n                }\n\n                // Pass other commands to the standard engine runner.\n                _ => return self.inner.run(command),\n            }\n            Ok(output)\n        }\n    }\n\n    impl BitCaskRunner {\n        fn new() -> Self {\n            let tempdir = TempDir::with_prefix(\"toydb\").expect(\"tempdir failed\");\n            let engine = BitCask::new(tempdir.path().join(\"bitcask\")).expect(\"bitcask failed\");\n            let inner = Runner::new(engine);\n            Self { inner, tempdir }\n        }\n\n        /// Dumps the full BitCask entry log.\n        fn dump(&mut self, output: &mut String) -> StdResult<(), Box<dyn StdError>> {\n            let file = &mut self.inner.engine.log.file;\n            let file_len = file.metadata()?.len();\n            let mut r = BufReader::new(file);\n            let mut pos = r.seek(SeekFrom::Start(0))?;\n            let mut len_buf = [0; 4];\n            let mut idx = 0;\n\n            while pos < file_len {\n                if idx > 0 {\n                    writeln!(output, \"--------\")?;\n                }\n                write!(output, \"{:<7}\", format!(\"{idx}@{pos}\"))?;\n\n                r.read_exact(&mut len_buf)?;\n                let key_len = u32::from_be_bytes(len_buf);\n                write!(output, \" keylen={key_len} [{}]\", hex::encode(len_buf))?;\n\n                r.read_exact(&mut len_buf)?;\n                let value_len_or_tombstone = i32::from_be_bytes(len_buf); // NB: -1 for tombstones\n                let value_len = value_len_or_tombstone.max(0) as u32;\n                writeln!(output, \" valuelen={value_len_or_tombstone} [{}]\", hex::encode(len_buf))?;\n\n                let mut key = vec![0; key_len as usize];\n                r.read_exact(&mut key)?;\n                let mut value = vec![0; value_len as usize];\n                r.read_exact(&mut value)?;\n                let size = 4 + 4 + key_len as u64 + value_len as u64;\n                writeln!(\n                    output,\n                    \"{:<7} key={} [{}] {}\",\n                    format!(\"{size}b\"),\n                    format::Raw::key(&key),\n                    hex::encode(key),\n                    match value_len_or_tombstone {\n                        -1 => \"tombstone\".to_string(),\n                        _ => format!(\n                            \"value={} [{}]\",\n                            format::Raw::bytes(&value),\n                            hex::encode(&value)\n                        ),\n                    },\n                )?;\n\n                pos += size;\n                idx += 1;\n            }\n            Ok(())\n        }\n    }\n}\n"
  },
  {
    "path": "src/storage/engine.rs",
    "content": "use std::ops::{Bound, RangeBounds};\n\nuse serde::{Deserialize, Serialize};\n\nuse crate::encoding::keycode;\nuse crate::error::Result;\n\n/// A key/value storage engine, which stores arbitrary byte strings. Keys are\n/// maintained in lexicographical order, which allows for range scans. This is\n/// needed e.g. to scan all rows in a specific SQL table (where all table rows\n/// have a common key prefix), or to scan the tail of the Raft log (after a\n/// given log entry index).\n///\n/// Keys should use the Keycode order-preserving encoding, see\n/// [`crate::encoding::keycode`].\n///\n/// Writes are only guaranteed durable after calling [`Engine::flush()`].\n///\n/// For simplicity, this only supports a single user at a time, so all methods\n/// (including reads) take a mutable reference. This isn't that big of a deal\n/// since Raft execution is serial anyway.\npub trait Engine: Send {\n    /// The iterator returned by [`Engine::scan`].\n    type ScanIterator<'a>: ScanIterator + 'a\n    where\n        Self: Sized + 'a; // omit in trait objects, for dyn compatibility\n\n    /// Deletes a key, or does nothing if it does not exist.\n    fn delete(&mut self, key: &[u8]) -> Result<()>;\n\n    /// Flushes any buffered data to disk.\n    fn flush(&mut self) -> Result<()>;\n\n    /// Gets a value for a key, if it exists.\n    fn get(&mut self, key: &[u8]) -> Result<Option<Vec<u8>>>;\n\n    /// Iterates over an ordered range of key/value pairs.\n    fn scan(&mut self, range: impl RangeBounds<Vec<u8>>) -> Self::ScanIterator<'_>\n    where\n        Self: Sized; // omit in trait objects, for dyn compatibility\n\n    /// Like scan, but can be used from trait objects (with dynamic dispatch).\n    fn scan_dyn(&mut self, range: (Bound<Vec<u8>>, Bound<Vec<u8>>)) -> Box<dyn ScanIterator + '_>;\n\n    /// Iterates over all key/value pairs starting with the given prefix.\n    fn scan_prefix(&mut self, prefix: &[u8]) -> Self::ScanIterator<'_>\n    where\n        Self: Sized, // omit in trait objects, for dyn compatibility\n    {\n        self.scan(keycode::prefix_range(prefix))\n    }\n\n    /// Sets a value for a key, replacing the existing value if any.\n    fn set(&mut self, key: &[u8], value: Vec<u8>) -> Result<()>;\n\n    /// Returns the engine status.\n    fn status(&mut self) -> Result<Status>;\n}\n\n/// A scan iterator over key/value pairs, returned by [`Engine::scan()`].\npub trait ScanIterator: DoubleEndedIterator<Item = Result<(Vec<u8>, Vec<u8>)>> {}\n\n/// Blanket implementation for all iterators that can act as a scan iterator.\nimpl<I: DoubleEndedIterator<Item = Result<(Vec<u8>, Vec<u8>)>>> ScanIterator for I {}\n\n/// Engine status.\n#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]\npub struct Status {\n    /// The name of the storage engine.\n    pub name: String,\n    /// The number of live keys in the engine.\n    pub keys: u64,\n    /// The logical size of live key/value pairs.\n    pub size: u64,\n    /// The on-disk size of all data, live and garbage.\n    pub disk_size: u64,\n    /// The on-disk size of live data, excluding garbage.\n    pub live_disk_size: u64,\n}\n\nimpl Status {\n    /// The on-disk size of garbage data.\n    pub fn garbage_disk_size(&self) -> u64 {\n        self.disk_size - self.live_disk_size\n    }\n\n    /// The ratio of on-disk garbage to total size.\n    pub fn garbage_disk_percent(&self) -> f64 {\n        if self.disk_size == 0 {\n            return 0.0;\n        }\n        self.garbage_disk_size() as f64 / self.disk_size as f64 * 100.0\n    }\n}\n\n/// Test helpers for engines.\n#[cfg(test)]\npub mod test {\n    use std::error::Error as StdError;\n    use std::fmt::Write as _;\n    use std::ops::{Bound, RangeBounds};\n    use std::result::Result as StdResult;\n\n    use crossbeam::channel::Sender;\n    use itertools::Itertools as _;\n    use regex::Regex;\n\n    use super::*;\n    use crate::encoding::format::{self, Formatter as _};\n\n    /// Goldenscript runner for engines. All engines use a common set of\n    /// goldenscripts in src/storage/testscripts/engine, as well as their own\n    /// engine-specific tests.\n    pub struct Runner<E: Engine> {\n        pub engine: E,\n    }\n\n    impl<E: Engine> Runner<E> {\n        pub fn new(engine: E) -> Self {\n            Self { engine }\n        }\n    }\n\n    impl<E: Engine> goldenscript::Runner for Runner<E> {\n        fn run(&mut self, command: &goldenscript::Command) -> StdResult<String, Box<dyn StdError>> {\n            let mut output = String::new();\n            match command.name.as_str() {\n                // delete KEY\n                \"delete\" => {\n                    let mut args = command.consume_args();\n                    let key = decode_binary(&args.next_pos().ok_or(\"key not given\")?.value);\n                    args.reject_rest()?;\n                    self.engine.delete(&key)?;\n                }\n\n                // get KEY\n                \"get\" => {\n                    let mut args = command.consume_args();\n                    let key = decode_binary(&args.next_pos().ok_or(\"key not given\")?.value);\n                    args.reject_rest()?;\n                    let value = self.engine.get(&key)?;\n                    writeln!(output, \"{}\", format::Raw::key_maybe_value(&key, value.as_deref()))?;\n                }\n\n                // scan [reverse=BOOL] RANGE\n                \"scan\" => {\n                    let mut args = command.consume_args();\n                    let reverse = args.lookup_parse(\"reverse\")?.unwrap_or(false);\n                    let range =\n                        parse_key_range(args.next_pos().map(|a| a.value.as_str()).unwrap_or(\"..\"))?;\n                    args.reject_rest()?;\n                    let items: Vec<_> = if reverse {\n                        self.engine.scan(range).rev().try_collect()?\n                    } else {\n                        self.engine.scan(range).try_collect()?\n                    };\n                    for (key, value) in items {\n                        let fmtkv = format::Raw::key_value(&key, &value);\n                        writeln!(output, \"{fmtkv}\")?;\n                    }\n                }\n\n                // scan_prefix PREFIX\n                \"scan_prefix\" => {\n                    let mut args = command.consume_args();\n                    let prefix = decode_binary(&args.next_pos().ok_or(\"prefix not given\")?.value);\n                    args.reject_rest()?;\n                    let mut scan = self.engine.scan_prefix(&prefix);\n                    while let Some((key, value)) = scan.next().transpose()? {\n                        let fmtkv = format::Raw::key_value(&key, &value);\n                        writeln!(output, \"{fmtkv}\")?;\n                    }\n                }\n\n                // set KEY=VALUE\n                \"set\" => {\n                    let mut args = command.consume_args();\n                    let kv = args.next_key().ok_or(\"key=value not given\")?.clone();\n                    let key = decode_binary(&kv.key.unwrap());\n                    let value = decode_binary(&kv.value);\n                    args.reject_rest()?;\n                    self.engine.set(&key, value)?;\n                }\n\n                // status\n                \"status\" => {\n                    command.consume_args().reject_rest()?;\n                    writeln!(output, \"{:#?}\", self.engine.status()?)?;\n                }\n\n                name => return Err(format!(\"invalid command {name}\").into()),\n            }\n            Ok(output)\n        }\n    }\n\n    /// Decodes a raw byte vector from a Unicode string. Code points in the\n    /// range U+0080 to U+00FF are converted back to bytes 0x80 to 0xff.\n    /// This allows using e.g. \\xff in the input string literal, and getting\n    /// back a 0xff byte in the byte vector. Otherwise, char(0xff) yields\n    /// the UTF-8 bytes 0xc3bf, which is the U+00FF code point as UTF-8.\n    /// These characters are effectively represented as ISO-8859-1 rather\n    /// than UTF-8, but it allows precise use of the entire u8 value range.\n    pub fn decode_binary(s: &str) -> Vec<u8> {\n        let mut buf = [0; 4];\n        let mut bytes = Vec::new();\n        for c in s.chars() {\n            // u32 is the Unicode code point, not the UTF-8 encoding.\n            match c as u32 {\n                b @ 0x80..=0xff => bytes.push(b as u8),\n                _ => bytes.extend(c.encode_utf8(&mut buf).as_bytes()),\n            }\n        }\n        bytes\n    }\n\n    /// Parses an binary key range, using Rust range syntax.\n    pub fn parse_key_range(s: &str) -> StdResult<impl RangeBounds<Vec<u8>>, Box<dyn StdError>> {\n        let mut bound = (Bound::<Vec<u8>>::Unbounded, Bound::<Vec<u8>>::Unbounded);\n        let re = Regex::new(r\"^(\\S+)?\\.\\.(=)?(\\S+)?\").expect(\"invalid regex\");\n        let groups = re.captures(s).ok_or_else(|| format!(\"invalid range {s}\"))?;\n        if let Some(start) = groups.get(1) {\n            bound.0 = Bound::Included(decode_binary(start.as_str()));\n        }\n        if let Some(end) = groups.get(3) {\n            let end = decode_binary(end.as_str());\n            if groups.get(2).is_some() {\n                bound.1 = Bound::Included(end)\n            } else {\n                bound.1 = Bound::Excluded(end)\n            }\n        }\n        Ok(bound)\n    }\n\n    /// Wraps another engine and emits write events to the given channel.\n    pub struct Emit<E: Engine> {\n        /// The wrapped engine.\n        inner: E,\n        /// Sends operation events.\n        tx: Sender<Operation>,\n    }\n\n    /// An engine operation emitted by the Emit engine.\n    pub enum Operation {\n        Delete { key: Vec<u8> },\n        Flush,\n        Set { key: Vec<u8>, value: Vec<u8> },\n    }\n\n    impl<E: Engine> Emit<E> {\n        pub fn new(inner: E, tx: Sender<Operation>) -> Self {\n            Self { inner, tx }\n        }\n    }\n\n    impl<E: Engine> Engine for Emit<E> {\n        type ScanIterator<'a>\n            = E::ScanIterator<'a>\n        where\n            E: 'a;\n\n        fn flush(&mut self) -> Result<()> {\n            self.inner.flush()?;\n            self.tx.send(Operation::Flush)?;\n            Ok(())\n        }\n\n        fn delete(&mut self, key: &[u8]) -> Result<()> {\n            self.inner.delete(key)?;\n            self.tx.send(Operation::Delete { key: key.to_vec() })?;\n            Ok(())\n        }\n\n        fn get(&mut self, key: &[u8]) -> Result<Option<Vec<u8>>> {\n            self.inner.get(key)\n        }\n\n        fn scan(&mut self, range: impl RangeBounds<Vec<u8>>) -> Self::ScanIterator<'_> {\n            self.inner.scan(range)\n        }\n\n        fn scan_dyn(\n            &mut self,\n            range: (Bound<Vec<u8>>, Bound<Vec<u8>>),\n        ) -> Box<dyn ScanIterator + '_> {\n            Box::new(self.scan(range))\n        }\n\n        fn set(&mut self, key: &[u8], value: Vec<u8>) -> Result<()> {\n            self.inner.set(key, value.clone())?;\n            self.tx.send(Operation::Set { key: key.to_vec(), value })?;\n            Ok(())\n        }\n\n        fn status(&mut self) -> Result<Status> {\n            self.inner.status()\n        }\n    }\n\n    /// An engine that wraps two others and mirrors operations across them,\n    /// panicking if they produce different results. Engine implementations\n    /// should not have any observable differences in behavior.\n    pub struct Mirror<A: Engine, B: Engine> {\n        pub a: A,\n        pub b: B,\n    }\n\n    impl<A: Engine, B: Engine> Mirror<A, B> {\n        pub fn new(a: A, b: B) -> Self {\n            Self { a, b }\n        }\n    }\n\n    impl<A: Engine, B: Engine> Engine for Mirror<A, B> {\n        type ScanIterator<'a>\n            = MirrorIterator<'a, A, B>\n        where\n            Self: Sized,\n            A: 'a,\n            B: 'a;\n\n        fn delete(&mut self, key: &[u8]) -> Result<()> {\n            self.a.delete(key)?;\n            self.b.delete(key)\n        }\n\n        fn flush(&mut self) -> Result<()> {\n            self.a.flush()?;\n            self.b.flush()\n        }\n\n        fn get(&mut self, key: &[u8]) -> Result<Option<Vec<u8>>> {\n            let a = self.a.get(key)?;\n            let b = self.b.get(key)?;\n            assert_eq!(a, b);\n            Ok(a)\n        }\n\n        fn scan(&mut self, range: impl RangeBounds<Vec<u8>>) -> Self::ScanIterator<'_>\n        where\n            Self: Sized,\n        {\n            let a = self.a.scan((range.start_bound().cloned(), range.end_bound().cloned()));\n            let b = self.b.scan(range);\n            MirrorIterator { a, b }\n        }\n\n        fn scan_dyn(\n            &mut self,\n            range: (Bound<Vec<u8>>, Bound<Vec<u8>>),\n        ) -> Box<dyn ScanIterator + '_> {\n            let a = self.a.scan(range.clone());\n            let b = self.b.scan(range);\n            Box::new(MirrorIterator::<A, B> { a, b })\n        }\n\n        fn set(&mut self, key: &[u8], value: Vec<u8>) -> Result<()> {\n            self.a.set(key, value.clone())?;\n            self.b.set(key, value)\n        }\n\n        fn status(&mut self) -> Result<Status> {\n            let a = self.a.status()?;\n            let b = self.b.status()?;\n            // Only some items are comparable.\n            assert_eq!(a.keys, b.keys);\n            assert_eq!(a.size, b.size);\n            Ok(a)\n        }\n    }\n\n    pub struct MirrorIterator<'a, A: Engine + 'a, B: Engine + 'a> {\n        a: A::ScanIterator<'a>,\n        b: B::ScanIterator<'a>,\n    }\n\n    impl<A: Engine, B: Engine> Iterator for MirrorIterator<'_, A, B> {\n        type Item = Result<(Vec<u8>, Vec<u8>)>;\n\n        fn next(&mut self) -> Option<Self::Item> {\n            let a = self.a.next();\n            let b = self.b.next();\n            assert_eq!(a, b);\n            a\n        }\n    }\n\n    impl<A: Engine, B: Engine> DoubleEndedIterator for MirrorIterator<'_, A, B> {\n        fn next_back(&mut self) -> Option<Self::Item> {\n            let a = self.a.next_back();\n            let b = self.b.next_back();\n            assert_eq!(a, b);\n            a\n        }\n    }\n}\n"
  },
  {
    "path": "src/storage/memory.rs",
    "content": "use std::collections::BTreeMap;\nuse std::collections::btree_map::Range;\nuse std::ops::{Bound, RangeBounds};\n\nuse super::{Engine, Status};\nuse crate::error::Result;\n\n/// An in-memory key-value storage engine using the Rust standard library's\n/// B-tree implementation. Data is not persisted. Primarily for testing.\n#[derive(Default)]\npub struct Memory(BTreeMap<Vec<u8>, Vec<u8>>);\n\nimpl Memory {\n    /// Creates a new Memory key-value storage engine.\n    pub fn new() -> Self {\n        Self::default()\n    }\n}\n\nimpl Engine for Memory {\n    type ScanIterator<'a> = ScanIterator<'a>;\n\n    fn delete(&mut self, key: &[u8]) -> Result<()> {\n        self.0.remove(key);\n        Ok(())\n    }\n\n    fn flush(&mut self) -> Result<()> {\n        Ok(())\n    }\n\n    fn get(&mut self, key: &[u8]) -> Result<Option<Vec<u8>>> {\n        Ok(self.0.get(key).cloned())\n    }\n\n    fn scan(&mut self, range: impl RangeBounds<Vec<u8>>) -> Self::ScanIterator<'_> {\n        ScanIterator(self.0.range(range))\n    }\n\n    fn scan_dyn(\n        &mut self,\n        range: (Bound<Vec<u8>>, Bound<Vec<u8>>),\n    ) -> Box<dyn super::ScanIterator + '_> {\n        Box::new(self.scan(range))\n    }\n\n    fn set(&mut self, key: &[u8], value: Vec<u8>) -> Result<()> {\n        self.0.insert(key.to_vec(), value);\n        Ok(())\n    }\n\n    fn status(&mut self) -> Result<Status> {\n        Ok(Status {\n            name: \"memory\".to_string(),\n            keys: self.0.len() as u64,\n            size: self.0.iter().map(|(k, v)| (k.len() + v.len()) as u64).sum(),\n            disk_size: 0,\n            live_disk_size: 0,\n        })\n    }\n}\n\npub struct ScanIterator<'a>(Range<'a, Vec<u8>, Vec<u8>>);\n\nimpl Iterator for ScanIterator<'_> {\n    type Item = Result<(Vec<u8>, Vec<u8>)>;\n\n    fn next(&mut self) -> Option<Self::Item> {\n        self.0.next().map(|(k, v)| Ok((k.clone(), v.clone())))\n    }\n}\n\nimpl DoubleEndedIterator for ScanIterator<'_> {\n    fn next_back(&mut self) -> Option<Self::Item> {\n        self.0.next_back().map(|(k, v)| Ok((k.clone(), v.clone())))\n    }\n}\n\n/// Most storage tests are Goldenscripts under src/storage/testscripts.\n#[cfg(test)]\nmod tests {\n    use std::path::Path;\n\n    use test_each_file::test_each_path;\n\n    use super::super::engine::test::Runner;\n    use super::*;\n\n    // Run common goldenscript tests in src/storage/testscripts/engine.\n    test_each_path! { in \"src/storage/testscripts/engine\" as engine => test_goldenscript }\n\n    // Also run Memory-specific tests in src/storage/testscripts/memory.\n    test_each_path! { in \"src/storage/testscripts/memory\" as scripts => test_goldenscript }\n\n    fn test_goldenscript(path: &Path) {\n        goldenscript::run(&mut Runner::new(Memory::new()), path).expect(\"goldenscript failed\")\n    }\n}\n"
  },
  {
    "path": "src/storage/mod.rs",
    "content": "//! Key/value storage engines, including an MVCC transaction layer. For details,\n//! see the [`engine`], [`bitcask`], and [`mvcc`] module documentation.\n\npub mod bitcask;\npub mod engine;\npub mod memory;\npub mod mvcc;\n\npub use bitcask::BitCask;\npub use engine::{Engine, ScanIterator, Status};\npub use memory::Memory;\n"
  },
  {
    "path": "src/storage/mvcc.rs",
    "content": "//! This module implements MVCC (Multi-Version Concurrency Control), a widely\n//! used method for ACID transactions and concurrency control. It allows\n//! multiple concurrent transactions to access and modify the same dataset,\n//! isolates them from each other, detects and handles conflicts, and commits\n//! their writes atomically as a single unit. It uses an underlying storage\n//! engine to store raw keys and values.\n//!\n//! VERSIONS\n//! ========\n//!\n//! MVCC handles concurrency control by managing multiple historical versions of\n//! keys, identified by a timestamp. Every write adds a new version at a higher\n//! timestamp, with deletes having a special tombstone value. For example, the\n//! keys a,b,c,d may have the following values at various logical timestamps (x\n//! is tombstone):\n//!\n//! Time\n//! 5\n//! 4  a4          \n//! 3      b3      x\n//! 2            \n//! 1  a1      c1  d1\n//!    a   b   c   d   Keys\n//!\n//! A transaction t2 that started at T=2 will see the values a=a1, c=c1, d=d1. A\n//! different transaction t5 running at T=5 will see a=a4, b=b3, c=c1.\n//!\n//! toyDB uses logical timestamps with a sequence number stored in\n//! Key::NextVersion. Each new read-write transaction takes its timestamp from\n//! the current value of Key::NextVersion and then increments the value for the\n//! next transaction.\n//!\n//! ISOLATION\n//! =========\n//!\n//! MVCC provides an isolation level called snapshot isolation. Briefly,\n//! transactions see a consistent snapshot of the database state as of their\n//! start time. Writes made by concurrent or subsequent transactions are never\n//! visible to it. If two concurrent transactions write to the same key they\n//! will conflict and one of them must retry. A transaction's writes become\n//! atomically visible to subsequent transactions only when they commit, and are\n//! rolled back on failure. Read-only transactions never conflict with other\n//! transactions.\n//!\n//! Transactions write new versions at their timestamp, storing them as\n//! Key::Version(key, version) => value. If a transaction writes to a key and\n//! finds a newer version, it returns an error and the client must retry.\n//!\n//! Active (uncommitted) read-write transactions record their version in the\n//! active set, stored as Key::Active(version). When new transactions begin, they\n//! take a snapshot of this active set, and any key versions that belong to a\n//! transaction in the active set are considered invisible (to anyone except that\n//! transaction itself). Writes to keys that already have a past version in the\n//! active set will also return an error.\n//!\n//! To commit, a transaction simply deletes its record in the active set. This\n//! will immediately (and, crucially, atomically) make all of its writes visible\n//! to subsequent transactions, but not ongoing ones. If the transaction is\n//! cancelled and rolled back, it maintains a record of all keys it wrote as\n//! Key::TxnWrite(version, key), so that it can find the corresponding versions\n//! and delete them before removing itself from the active set.\n//!\n//! Consider the following example, where we have two ongoing transactions at\n//! time T=2 and T=5, with some writes that are not yet committed marked in\n//! parentheses.\n//!\n//! Active set: [2, 5]\n//!\n//! Time\n//! 5 (a5)\n//! 4  a4          \n//! 3      b3      x\n//! 2         (x)     (e2)\n//! 1  a1      c1  d1\n//!    a   b   c   d   e   Keys\n//!\n//! Here, t2 will see a=a1, d=d1, e=e2 (it sees its own writes). t5 will see\n//! a=a5, b=b3, c=c1. t2 does not see any newer versions, and t5 does not see\n//! the tombstone at c@2 nor the value e=e2, because version=2 is in its active\n//! set.\n//!\n//! If t2 tries to write b=b2, it receives an error and must retry, because a\n//! newer version exists. Similarly, if t5 tries to write e=e5, it receives an\n//! error and must retry, because the version e=e2 is in its active set.\n//!\n//! To commit, t2 can remove itself from the active set. A new transaction t6\n//! starting after the commit will then see c as deleted and e=e2. t5 will still\n//! not see any of t2's writes, because it's still in its local snapshot of the\n//! active set at the time it began.\n//!\n//! READ-ONLY AND TIME TRAVEL QUERIES\n//! =================================\n//!\n//! Since MVCC stores historical versions, it can trivially support time travel\n//! queries where a transaction reads at a past timestamp and has a consistent\n//! view of the database at that time.\n//!\n//! This is done by a transaction simply using a past version, as if it had\n//! started far in the past, ignoring newer versions like any other transaction.\n//! This transaction cannot write, as it does not have a unique timestamp (the\n//! original read-write transaction originally owned this timestamp).\n//!\n//! The only wrinkle is that the time-travel query must also know what the active\n//! set was at that version. Otherwise, it may see past transactions that committed\n//! after that time, which were not visible to the original transaction that wrote\n//! at that version. Similarly, if a time-travel query reads at a version that is\n//! still active, it should not see its in-progress writes, and after it commits\n//! a different time-travel query should not see those writes either, to maintain\n//! version consistency.\n//!\n//! To achieve this, every read-write transaction stores its active set snapshot\n//! in the storage engine as well, as Key::TxnActiveSnapshot, such that later\n//! time-travel queries can restore its original snapshot. Furthermore, a\n//! time-travel query can only see versions below the snapshot version, otherwise\n//! it could see spurious in-progress or since-committed versions.\n//!\n//! In the following example, a time-travel query at version=3 would see a=a1,\n//! c=c1, d=d1.\n//!\n//! Time\n//! 5\n//! 4  a4          \n//! 3      b3      x\n//! 2            \n//! 1  a1      c1  d1\n//!    a   b   c   d   Keys\n//!\n//! Read-only queries work similarly to time-travel queries, with one exception:\n//! they read at the next (current) version, i.e. Key::NextVersion, and use the\n//! current active set, storing the snapshot in memory only. Read-only queries\n//! do not increment the version sequence number in Key::NextVersion.\n//!\n//! GARBAGE COLLECTION\n//! ==================\n//!\n//! Normally, old versions would be garbage collected regularly, when they are\n//! no longer needed by active transactions or time-travel queries. However,\n//! toyDB does not implement garbage collection, instead keeping all history\n//! forever, both out of laziness and also because it allows unlimited time\n//! travel queries (it's a feature, not a bug!).\n\nuse std::borrow::Cow;\nuse std::collections::{BTreeSet, VecDeque};\nuse std::ops::{Bound, RangeBounds};\nuse std::sync::{Arc, Mutex, MutexGuard};\n\nuse itertools::Itertools as _;\nuse serde::{Deserialize, Serialize};\n\nuse super::engine::{self, Engine};\nuse crate::encoding::{self, Key as _, Value as _, bincode, keycode};\nuse crate::error::{Error, Result};\nuse crate::{errdata, errinput};\n\n/// An MVCC version represents a logical timestamp. Each version belongs to a\n/// separate read/write transaction. The latest version is incremented when a\n/// new read-write transaction begins.\npub type Version = u64;\n\nimpl encoding::Value for Version {}\n\n/// MVCC keys, using the Keycode encoding which preserves the ordering and\n/// grouping of keys.\n///\n/// Cow byte slices allow encoding borrowed values and decoding owned values.\n#[derive(Debug, Deserialize, Serialize)]\npub enum Key<'a> {\n    /// The next available version.\n    NextVersion,\n    /// Active (uncommitted) transactions by version.\n    TxnActive(Version),\n    /// A snapshot of the active set at each version. Only written for\n    /// versions where the active set is non-empty (excluding itself).\n    TxnActiveSnapshot(Version),\n    /// Keeps track of all keys written to by an active transaction (identified\n    /// by its version), in case it needs to roll back.\n    TxnWrite(\n        Version,\n        #[serde(with = \"serde_bytes\")]\n        #[serde(borrow)]\n        Cow<'a, [u8]>,\n    ),\n    /// A versioned key/value pair.\n    Version(\n        #[serde(with = \"serde_bytes\")]\n        #[serde(borrow)]\n        Cow<'a, [u8]>,\n        Version,\n    ),\n    /// Unversioned non-transactional key/value pairs, mostly used for metadata.\n    /// These exist separately from versioned keys, i.e. the unversioned key\n    /// \"foo\" is entirely independent of the versioned key \"foo@7\".\n    Unversioned(\n        #[serde(with = \"serde_bytes\")]\n        #[serde(borrow)]\n        Cow<'a, [u8]>,\n    ),\n}\n\nimpl<'a> encoding::Key<'a> for Key<'a> {}\n\n/// MVCC key prefixes, for prefix scans. These must match the keys above,\n/// including the enum variant index.\n#[derive(Debug, Deserialize, Serialize)]\nenum KeyPrefix<'a> {\n    NextVersion,\n    TxnActive,\n    TxnActiveSnapshot,\n    TxnWrite(Version),\n    Version(\n        #[serde(with = \"serde_bytes\")]\n        #[serde(borrow)]\n        Cow<'a, [u8]>,\n    ),\n    Unversioned,\n}\n\nimpl<'a> encoding::Key<'a> for KeyPrefix<'a> {}\n\n/// An MVCC-based transactional key-value engine. It wraps an underlying storage\n/// engine that's used for raw key/value storage.\n///\n/// While it supports any number of concurrent transactions, individual read or\n/// write operations are executed sequentially, serialized via a mutex. There\n/// are two reasons for this: the storage engine itself is not thread-safe,\n/// requiring serialized access, and the Raft state machine that manages the\n/// MVCC engine applies commands one at a time from the Raft log, which will\n/// serialize them anyway.\npub struct MVCC<E: Engine> {\n    pub engine: Arc<Mutex<E>>,\n}\n\nimpl<E: Engine> MVCC<E> {\n    /// Creates a new MVCC engine with the given storage engine.\n    pub fn new(engine: E) -> Self {\n        Self { engine: Arc::new(Mutex::new(engine)) }\n    }\n\n    /// Begins a new read-write transaction.\n    pub fn begin(&self) -> Result<Transaction<E>> {\n        Transaction::begin(self.engine.clone())\n    }\n\n    /// Begins a new read-only transaction at the latest version.\n    pub fn begin_read_only(&self) -> Result<Transaction<E>> {\n        Transaction::begin_read_only(self.engine.clone(), None)\n    }\n\n    /// Begins a new read-only transaction as of the given version.\n    pub fn begin_as_of(&self, version: Version) -> Result<Transaction<E>> {\n        Transaction::begin_read_only(self.engine.clone(), Some(version))\n    }\n\n    /// Resumes a transaction from the given transaction state.\n    pub fn resume(&self, state: TransactionState) -> Result<Transaction<E>> {\n        Transaction::resume(self.engine.clone(), state)\n    }\n\n    /// Fetches the value of an unversioned key.\n    pub fn get_unversioned(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {\n        self.engine.lock()?.get(&Key::Unversioned(key.into()).encode())\n    }\n\n    /// Sets the value of an unversioned key.\n    pub fn set_unversioned(&self, key: &[u8], value: Vec<u8>) -> Result<()> {\n        self.engine.lock()?.set(&Key::Unversioned(key.into()).encode(), value)\n    }\n\n    /// Returns the status of the MVCC and storage engines.\n    pub fn status(&self) -> Result<Status> {\n        let mut engine = self.engine.lock()?;\n        let versions = match engine.get(&Key::NextVersion.encode())? {\n            Some(ref v) => Version::decode(v)? - 1,\n            None => 0,\n        };\n        let active_txns = engine.scan_prefix(&KeyPrefix::TxnActive.encode()).count() as u64;\n        Ok(Status { versions, active_txns, storage: engine.status()? })\n    }\n}\n\n/// MVCC engine status.\n#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]\npub struct Status {\n    /// The total number of MVCC versions (i.e. read-write transactions).\n    pub versions: u64,\n    /// Number of currently active transactions.\n    pub active_txns: u64,\n    /// The storage engine.\n    pub storage: super::engine::Status,\n}\n\nimpl encoding::Value for Status {}\n\n/// An MVCC transaction.\npub struct Transaction<E: Engine> {\n    /// The underlying engine, shared by all transactions.\n    engine: Arc<Mutex<E>>,\n    /// The transaction state.\n    state: TransactionState,\n}\n\n/// A Transaction's state, which determines its write version and isolation. It\n/// is separate from Transaction to allow it to be passed around independently\n/// of the engine. There are two main motivations for this:\n///\n/// * It can be exported via Transaction.state(), (de)serialized, and later used\n///   to instantiate a new functionally equivalent Transaction via\n///   Transaction::resume(). This allows passing the transaction between the\n///   storage engine and SQL engine (potentially running on a different node)\n///   across the Raft state machine boundary.\n///\n/// * It can be borrowed independently of Engine, allowing references to it\n///   in VisibleIterator, which would otherwise result in self-references.\n#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]\npub struct TransactionState {\n    /// The version this transaction is running at. Only one read-write\n    /// transaction can run at a given version, since this identifies its\n    /// writes.\n    pub version: Version,\n    /// If true, the transaction is read only.\n    pub read_only: bool,\n    /// The set of concurrent active (uncommitted) transactions, as of the start\n    /// of this transaction. Their writes should be invisible to this\n    /// transaction even if they're writing at a lower version, since they're\n    /// not committed yet. Uses a BTreeSet for test determinism.\n    pub active: BTreeSet<Version>,\n}\n\nimpl encoding::Value for TransactionState {}\n\nimpl TransactionState {\n    /// Checks whether the given version is visible to this transaction.\n    ///\n    /// Future versions, and versions belonging to active transactions as of\n    /// the start of this transaction, are never visible.\n    ///\n    /// Read-write transactions see their own writes at their version.\n    ///\n    /// Read-only queries only see versions below the transaction's version,\n    /// excluding the version itself. This is to ensure time-travel queries see\n    /// a consistent version both before and after any active transaction at\n    /// that version commits its writes. See the module documentation for\n    /// details.\n    fn is_visible(&self, version: Version) -> bool {\n        if self.active.contains(&version) {\n            false\n        } else if self.read_only {\n            version < self.version\n        } else {\n            version <= self.version\n        }\n    }\n}\n\nimpl From<TransactionState> for Cow<'_, TransactionState> {\n    fn from(txn: TransactionState) -> Self {\n        Cow::Owned(txn)\n    }\n}\n\nimpl<'a> From<&'a TransactionState> for Cow<'a, TransactionState> {\n    fn from(txn: &'a TransactionState) -> Self {\n        Cow::Borrowed(txn)\n    }\n}\n\nimpl<E: Engine> Transaction<E> {\n    /// Begins a new transaction in read-write mode. This will allocate a new\n    /// version that the transaction can write at, add it to the active set, and\n    /// record its active snapshot for time-travel queries.\n    fn begin(engine: Arc<Mutex<E>>) -> Result<Self> {\n        let mut session = engine.lock()?;\n\n        // Allocate a new version to write at.\n        let version = match session.get(&Key::NextVersion.encode())? {\n            Some(ref v) => Version::decode(v)?,\n            None => 1,\n        };\n        session.set(&Key::NextVersion.encode(), (version + 1).encode())?;\n\n        // Fetch the current set of active transactions, persist it for\n        // time-travel queries if non-empty, then add this txn to it.\n        let active = Self::scan_active(&mut session)?;\n        if !active.is_empty() {\n            session.set(&Key::TxnActiveSnapshot(version).encode(), active.encode())?\n        }\n        session.set(&Key::TxnActive(version).encode(), vec![])?;\n        drop(session);\n\n        Ok(Self { engine, state: TransactionState { version, read_only: false, active } })\n    }\n\n    /// Begins a new read-only transaction. If version is given it will see the\n    /// state as of the beginning of that version (ignoring writes at that\n    /// version). In other words, it sees the same state as the read-write\n    /// transaction at that version saw when it began.\n    fn begin_read_only(engine: Arc<Mutex<E>>, as_of: Option<Version>) -> Result<Self> {\n        let mut session = engine.lock()?;\n\n        // Fetch the latest version.\n        let mut version = match session.get(&Key::NextVersion.encode())? {\n            Some(ref v) => Version::decode(v)?,\n            None => 1,\n        };\n\n        // If requested, create the transaction as of a past version, restoring\n        // the active snapshot as of the beginning of that version. Otherwise,\n        // use the latest version and get the current, real-time snapshot.\n        let mut active = BTreeSet::new();\n        if let Some(as_of) = as_of {\n            if as_of >= version {\n                return errinput!(\"version {as_of} does not exist\");\n            }\n            version = as_of;\n            if let Some(value) = session.get(&Key::TxnActiveSnapshot(version).encode())? {\n                active = BTreeSet::<Version>::decode(&value)?;\n            }\n        } else {\n            active = Self::scan_active(&mut session)?;\n        }\n\n        drop(session);\n\n        Ok(Self { engine, state: TransactionState { version, read_only: true, active } })\n    }\n\n    /// Resumes a transaction from the given state.\n    fn resume(engine: Arc<Mutex<E>>, s: TransactionState) -> Result<Self> {\n        // For read-write transactions, verify that the transaction is still\n        // active before making further writes.\n        if !s.read_only && engine.lock()?.get(&Key::TxnActive(s.version).encode())?.is_none() {\n            return errinput!(\"no active transaction at version {}\", s.version);\n        }\n        Ok(Self { engine, state: s })\n    }\n\n    /// Fetches the set of currently active transactions.\n    fn scan_active(session: &mut MutexGuard<E>) -> Result<BTreeSet<Version>> {\n        let mut active = BTreeSet::new();\n        let mut scan = session.scan_prefix(&KeyPrefix::TxnActive.encode());\n        while let Some((key, _)) = scan.next().transpose()? {\n            match Key::decode(&key)? {\n                Key::TxnActive(version) => active.insert(version),\n                key => return errdata!(\"expected TxnActive key, got {key:?}\"),\n            };\n        }\n        Ok(active)\n    }\n\n    /// Returns the version the transaction is running at.\n    pub fn version(&self) -> Version {\n        self.state.version\n    }\n\n    /// Returns whether the transaction is read-only.\n    pub fn read_only(&self) -> bool {\n        self.state.read_only\n    }\n\n    /// Returns the transaction's state. This can be used to instantiate a\n    /// functionally equivalent transaction via resume().\n    pub fn state(&self) -> &TransactionState {\n        &self.state\n    }\n\n    /// Commits the transaction, by removing it from the active set. This will\n    /// immediately make its writes visible to subsequent transactions. Also\n    /// removes its TxnWrite records, which are no longer needed.\n    ///\n    /// NB: commit does not flush writes to durable storage, since we rely on\n    /// the Raft log for persistence.\n    pub fn commit(self) -> Result<()> {\n        if self.state.read_only {\n            return Ok(());\n        }\n        let mut engine = self.engine.lock()?;\n        let remove: Vec<_> = engine\n            .scan_prefix(&KeyPrefix::TxnWrite(self.state.version).encode())\n            .map_ok(|(k, _)| k)\n            .try_collect()?;\n        for key in remove {\n            engine.delete(&key)?\n        }\n        engine.delete(&Key::TxnActive(self.state.version).encode())\n    }\n\n    /// Rolls back the transaction, by undoing all written versions and removing\n    /// it from the active set. The active set snapshot is left behind, since\n    /// this is needed for time travel queries at this version.\n    pub fn rollback(self) -> Result<()> {\n        if self.state.read_only {\n            return Ok(());\n        }\n        let mut engine = self.engine.lock()?;\n        let mut rollback = Vec::new();\n        let mut scan = engine.scan_prefix(&KeyPrefix::TxnWrite(self.state.version).encode());\n        while let Some((key, _)) = scan.next().transpose()? {\n            match Key::decode(&key)? {\n                Key::TxnWrite(_, key) => {\n                    rollback.push(Key::Version(key, self.state.version).encode())\n                    // the version\n                }\n                key => return errdata!(\"expected TxnWrite, got {key:?}\"),\n            };\n            rollback.push(key); // the TxnWrite record\n        }\n        drop(scan);\n        for key in rollback.into_iter() {\n            engine.delete(&key)?;\n        }\n        engine.delete(&Key::TxnActive(self.state.version).encode()) // remove from active set\n    }\n\n    /// Deletes a key.\n    pub fn delete(&self, key: &[u8]) -> Result<()> {\n        self.write_version(key, None)\n    }\n\n    /// Sets a value for a key.\n    pub fn set(&self, key: &[u8], value: Vec<u8>) -> Result<()> {\n        self.write_version(key, Some(value))\n    }\n\n    /// Writes a new version for a key at the transaction's version. None writes\n    /// a deletion tombstone. If a write conflict is found (either a newer or\n    /// uncommitted version), a serialization error is returned.  Replacing our\n    /// own uncommitted write is fine.\n    fn write_version(&self, key: &[u8], value: Option<Vec<u8>>) -> Result<()> {\n        if self.state.read_only {\n            return Err(Error::ReadOnly);\n        }\n        let mut engine = self.engine.lock()?;\n\n        // Check for write conflicts, i.e. if the latest key is invisible to us\n        // (either a newer version, or an uncommitted version in our past). We\n        // can only conflict with the latest key, since all transactions enforce\n        // the same invariant.\n        let from = Key::Version(\n            key.into(),\n            self.state.active.first().copied().unwrap_or(self.state.version + 1),\n        )\n        .encode();\n        let to = Key::Version(key.into(), u64::MAX).encode();\n        if let Some((key, _)) = engine.scan(from..=to).last().transpose()? {\n            match Key::decode(&key)? {\n                Key::Version(_, version) => {\n                    if !self.state.is_visible(version) {\n                        return Err(Error::Serialization);\n                    }\n                }\n                key => return errdata!(\"expected Key::Version got {key:?}\"),\n            }\n        }\n\n        // Write the new version and its write record.\n        //\n        // NB: TxnWrite contains the provided user key, not the encoded engine\n        // key, since we can construct the engine key using the version.\n        engine.set(&Key::TxnWrite(self.state.version, key.into()).encode(), vec![])?;\n        engine\n            .set(&Key::Version(key.into(), self.state.version).encode(), bincode::serialize(&value))\n    }\n\n    /// Fetches a key's value, or None if it does not exist.\n    pub fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {\n        let mut engine = self.engine.lock()?;\n        let from = Key::Version(key.into(), 0).encode();\n        let to = Key::Version(key.into(), self.state.version).encode();\n        let mut scan = engine.scan(from..=to).rev();\n        while let Some((key, value)) = scan.next().transpose()? {\n            match Key::decode(&key)? {\n                Key::Version(_, version) => {\n                    if self.state.is_visible(version) {\n                        return bincode::deserialize(&value);\n                    }\n                }\n                key => return errdata!(\"expected Key::Version got {key:?}\"),\n            };\n        }\n        Ok(None)\n    }\n\n    /// Returns an iterator over the latest visible key/value pairs at the\n    /// transaction's version.\n    pub fn scan(&self, range: impl RangeBounds<Vec<u8>>) -> ScanIterator<E> {\n        let start = match range.start_bound() {\n            Bound::Excluded(k) => Bound::Excluded(Key::Version(k.into(), u64::MAX).encode()),\n            Bound::Included(k) => Bound::Included(Key::Version(k.into(), 0).encode()),\n            Bound::Unbounded => Bound::Included(Key::Version(vec![].into(), 0).encode()),\n        };\n        let end = match range.end_bound() {\n            Bound::Excluded(k) => Bound::Excluded(Key::Version(k.into(), 0).encode()),\n            Bound::Included(k) => Bound::Included(Key::Version(k.into(), u64::MAX).encode()),\n            Bound::Unbounded => Bound::Excluded(KeyPrefix::Unversioned.encode()),\n        };\n        ScanIterator::new(self.engine.clone(), self.state().clone(), (start, end))\n    }\n\n    /// Scans keys under a given prefix.\n    pub fn scan_prefix(&self, prefix: &[u8]) -> ScanIterator<E> {\n        // Normally, KeyPrefix::Version will only match all versions of the\n        // exact given key. We want all keys maching the prefix, so we chop off\n        // the Keycode byte slice terminator 0x0000 at the end.\n        let mut prefix = KeyPrefix::Version(prefix.into()).encode();\n        prefix.truncate(prefix.len() - 2);\n        let range = keycode::prefix_range(&prefix);\n        ScanIterator::new(self.engine.clone(), self.state().clone(), range)\n    }\n}\n\n/// An iterator over the latest live and visible key/value pairs for the txn.\n///\n/// The (single-threaded) engine is shared via mutex, and holding the mutex for\n/// the lifetime of the iterator can cause deadlocks (e.g. when the local SQL\n/// engine pulls from two tables concurrently during a join). Instead, we pull\n/// and buffer a batch of rows at a time, and release the mutex in between.\n///\n/// This does not implement DoubleEndedIterator (reverse scans), since the SQL\n/// layer doesn't currently need it.\npub struct ScanIterator<E: Engine> {\n    /// The engine.\n    engine: Arc<Mutex<E>>,\n    /// The transaction state.\n    txn: TransactionState,\n    /// A buffer of live and visible key/value pairs to emit.\n    buffer: VecDeque<(Vec<u8>, Vec<u8>)>,\n    /// The remaining range after the buffer.\n    remainder: Option<(Bound<Vec<u8>>, Bound<Vec<u8>>)>,\n}\n\n/// Implement [`Clone`] manually. `derive(Clone)` isn't smart enough to figure\n/// out that we don't need `Engine: Clone` when it's in an [`Arc`]. See:\n/// <https://github.com/rust-lang/rust/issues/26925>.\nimpl<E: Engine> Clone for ScanIterator<E> {\n    fn clone(&self) -> Self {\n        Self {\n            engine: self.engine.clone(),\n            txn: self.txn.clone(),\n            buffer: self.buffer.clone(),\n            remainder: self.remainder.clone(),\n        }\n    }\n}\n\nimpl<E: Engine> ScanIterator<E> {\n    /// The number of live key/value pairs to pull from the engine each time we\n    /// lock it. Uses 2 in tests to exercise the buffering code.\n    const BUFFER_SIZE: usize = if cfg!(test) { 2 } else { 32 };\n\n    /// Creates a new scan iterator.\n    fn new(\n        engine: Arc<Mutex<E>>,\n        txn: TransactionState,\n        range: (Bound<Vec<u8>>, Bound<Vec<u8>>),\n    ) -> Self {\n        let buffer = VecDeque::with_capacity(Self::BUFFER_SIZE);\n        Self { engine, txn, buffer, remainder: Some(range) }\n    }\n\n    /// Fills the buffer, if there's any pending items.\n    fn fill_buffer(&mut self) -> Result<()> {\n        // Check if there's anything to buffer.\n        if self.buffer.len() >= Self::BUFFER_SIZE {\n            return Ok(());\n        }\n        let Some(range) = self.remainder.take() else {\n            return Ok(());\n        };\n        let range_end = range.1.clone();\n\n        let mut engine = self.engine.lock()?;\n        let mut iter = VersionIterator::new(&self.txn, engine.scan(range)).peekable();\n        while let Some((key, _, value)) = iter.next().transpose()? {\n            // If the next key equals this one, we're not at the latest version.\n            match iter.peek() {\n                Some(Ok((next, _, _))) if next == &key => continue,\n                Some(Err(err)) => return Err(err.clone()),\n                Some(Ok(_)) | None => {}\n            }\n\n            // Decode the value, and skip deleted keys (tombstones).\n            let Some(value) = bincode::deserialize(&value)? else { continue };\n            self.buffer.push_back((key, value));\n\n            // If we filled the buffer, save the remaining range (if any) and\n            // return. peek() has already buffered next(), so pull it.\n            if self.buffer.len() == Self::BUFFER_SIZE {\n                if let Some((next, version, _)) = iter.next().transpose()? {\n                    // We have to re-encode it as a raw engine key, since we\n                    // only have access to the decoded MVCC user key.\n                    let range_start = Bound::Included(Key::Version(next.into(), version).encode());\n                    self.remainder = Some((range_start, range_end));\n                }\n                return Ok(());\n            }\n        }\n        Ok(())\n    }\n}\n\nimpl<E: Engine> Iterator for ScanIterator<E> {\n    type Item = Result<(Vec<u8>, Vec<u8>)>;\n\n    fn next(&mut self) -> Option<Self::Item> {\n        if self.buffer.is_empty()\n            && let Err(error) = self.fill_buffer()\n        {\n            return Some(Err(error));\n        }\n        self.buffer.pop_front().map(Ok)\n    }\n}\n\n/// An iterator that decodes raw engine key/value pairs into MVCC key/value\n/// versions, and skips invisible versions. Helper for ScanIterator.\nstruct VersionIterator<'a, I: engine::ScanIterator> {\n    /// The transaction the scan is running in.\n    txn: &'a TransactionState,\n    /// The inner engine scan iterator.\n    inner: I,\n}\n\nimpl<'a, I: engine::ScanIterator> VersionIterator<'a, I> {\n    /// Creates a new MVCC version iterator for the given engine iterator.\n    fn new(txn: &'a TransactionState, inner: I) -> Self {\n        Self { txn, inner }\n    }\n\n    // Fallible next(). Returns the next visible key/version/value tuple.\n    fn try_next(&mut self) -> Result<Option<(Vec<u8>, Version, Vec<u8>)>> {\n        while let Some((key, value)) = self.inner.next().transpose()? {\n            let Key::Version(key, version) = Key::decode(&key)? else {\n                return errdata!(\"expected Key::Version got {key:?}\");\n            };\n            if !self.txn.is_visible(version) {\n                continue;\n            }\n            return Ok(Some((key.into_owned(), version, value)));\n        }\n        Ok(None)\n    }\n}\n\nimpl<I: engine::ScanIterator> Iterator for VersionIterator<'_, I> {\n    type Item = Result<(Vec<u8>, Version, Vec<u8>)>;\n\n    fn next(&mut self) -> Option<Self::Item> {\n        self.try_next().transpose()\n    }\n}\n\n/// Most storage tests are Goldenscripts under src/storage/testscripts.\n#[cfg(test)]\npub mod tests {\n    use std::collections::HashMap;\n    use std::error::Error;\n    use std::fmt::Write as _;\n    use std::path::Path;\n    use std::result::Result;\n\n    use crossbeam::channel::Receiver;\n    use tempfile::TempDir;\n    use test_case::test_case;\n    use test_each_file::test_each_path;\n\n    use super::*;\n    use crate::encoding::format::{self, Formatter as _};\n    use crate::storage::engine::test::{Emit, Mirror, Operation, decode_binary, parse_key_range};\n    use crate::storage::{BitCask, Memory};\n\n    // Run goldenscript tests in src/storage/testscripts/mvcc.\n    test_each_path! { in \"src/storage/testscripts/mvcc\" as scripts => test_goldenscript }\n\n    fn test_goldenscript(path: &Path) {\n        goldenscript::run(&mut MVCCRunner::new(), path).expect(\"goldenscript failed\")\n    }\n\n    /// Tests that key prefixes are actually prefixes of keys.\n    #[test_case(KeyPrefix::NextVersion, Key::NextVersion; \"NextVersion\")]\n    #[test_case(KeyPrefix::TxnActive, Key::TxnActive(1); \"TxnActive\")]\n    #[test_case(KeyPrefix::TxnActiveSnapshot, Key::TxnActiveSnapshot(1); \"TxnActiveSnapshot\")]\n    #[test_case(KeyPrefix::TxnWrite(1), Key::TxnWrite(1, b\"foo\".as_slice().into()); \"TxnWrite\")]\n    #[test_case(KeyPrefix::Version(b\"foo\".as_slice().into()), Key::Version(b\"foo\".as_slice().into(), 1); \"Version\")]\n    #[test_case(KeyPrefix::Unversioned, Key::Unversioned(b\"foo\".as_slice().into()); \"Unversioned\")]\n    fn key_prefix(prefix: KeyPrefix, key: Key) {\n        let prefix = prefix.encode();\n        let key = key.encode();\n        assert_eq!(prefix, key[..prefix.len()])\n    }\n\n    /// Runs MVCC goldenscript tests.\n    pub struct MVCCRunner {\n        mvcc: MVCC<TestEngine>,\n        txns: HashMap<String, Transaction<TestEngine>>,\n        op_rx: Receiver<Operation>,\n        _tempdir: TempDir,\n    }\n\n    type TestEngine = Emit<Mirror<BitCask, Memory>>;\n\n    impl MVCCRunner {\n        fn new() -> Self {\n            // Use both a BitCask and a Memory engine, and mirror operations\n            // across them. Emit engine operations to op_rx.\n            let (op_tx, op_rx) = crossbeam::channel::unbounded();\n            let tempdir = TempDir::with_prefix(\"toydb\").expect(\"tempdir failed\");\n            let bitcask = BitCask::new(tempdir.path().join(\"bitcask\")).expect(\"bitcask failed\");\n            let memory = Memory::new();\n            let engine = Emit::new(Mirror::new(bitcask, memory), op_tx);\n            let mvcc = MVCC::new(engine);\n            Self { mvcc, op_rx, txns: HashMap::new(), _tempdir: tempdir }\n        }\n\n        /// Fetches the named transaction from a command prefix.\n        fn get_txn(\n            &mut self,\n            prefix: &Option<String>,\n        ) -> Result<&'_ mut Transaction<TestEngine>, Box<dyn Error>> {\n            let name = Self::txn_name(prefix)?;\n            self.txns.get_mut(name).ok_or(format!(\"unknown txn {name}\").into())\n        }\n\n        /// Fetches the txn name from a command prefix, or errors.\n        fn txn_name(prefix: &Option<String>) -> Result<&str, Box<dyn Error>> {\n            prefix.as_deref().ok_or(\"no txn name\".into())\n        }\n\n        /// Errors if a txn prefix is given.\n        fn no_txn(command: &goldenscript::Command) -> Result<(), Box<dyn Error>> {\n            if let Some(name) = &command.prefix {\n                return Err(format!(\"can't run {} with txn {name}\", command.name).into());\n            }\n            Ok(())\n        }\n    }\n\n    impl goldenscript::Runner for MVCCRunner {\n        fn run(&mut self, command: &goldenscript::Command) -> Result<String, Box<dyn Error>> {\n            let mut output = String::new();\n            let mut tags = command.tags.clone();\n\n            match command.name.as_str() {\n                // txn: begin [readonly] [as_of=VERSION]\n                \"begin\" => {\n                    let name = Self::txn_name(&command.prefix)?;\n                    if self.txns.contains_key(name) {\n                        return Err(format!(\"txn {name} already exists\").into());\n                    }\n                    let mut args = command.consume_args();\n                    let readonly = match args.next_pos().map(|a| a.value.as_str()) {\n                        Some(\"readonly\") => true,\n                        None => false,\n                        Some(v) => return Err(format!(\"invalid argument {v}\").into()),\n                    };\n                    let as_of = args.lookup_parse(\"as_of\")?;\n                    args.reject_rest()?;\n                    let txn = match (readonly, as_of) {\n                        (false, None) => self.mvcc.begin()?,\n                        (true, None) => self.mvcc.begin_read_only()?,\n                        (true, Some(v)) => self.mvcc.begin_as_of(v)?,\n                        (false, Some(_)) => return Err(\"as_of only valid for read-only txn\".into()),\n                    };\n                    self.txns.insert(name.to_string(), txn);\n                }\n\n                // txn: commit\n                \"commit\" => {\n                    let name = Self::txn_name(&command.prefix)?;\n                    let txn = self.txns.remove(name).ok_or(format!(\"unknown txn {name}\"))?;\n                    command.consume_args().reject_rest()?;\n                    txn.commit()?;\n                }\n\n                // txn: delete KEY...\n                \"delete\" => {\n                    let txn = self.get_txn(&command.prefix)?;\n                    let mut args = command.consume_args();\n                    for arg in args.rest_pos() {\n                        let key = decode_binary(&arg.value);\n                        txn.delete(&key)?;\n                    }\n                    args.reject_rest()?;\n                }\n\n                // dump\n                \"dump\" => {\n                    command.consume_args().reject_rest()?;\n                    let mut engine = self.mvcc.engine.lock().unwrap();\n                    let mut scan = engine.scan(..);\n                    while let Some((key, value)) = scan.next().transpose()? {\n                        let fmtkv = format::MVCC::<format::Raw>::key_value(&key, &value);\n                        let rawkv = format::Raw::key_value(&key, &value);\n                        writeln!(output, \"{fmtkv} [{rawkv}]\")?;\n                    }\n                }\n\n                // txn: get KEY...\n                \"get\" => {\n                    let txn = self.get_txn(&command.prefix)?;\n                    let mut args = command.consume_args();\n                    for arg in args.rest_pos() {\n                        let key = decode_binary(&arg.value);\n                        let value = txn.get(&key)?;\n                        let fmtkv = format::Raw::key_maybe_value(&key, value.as_deref());\n                        writeln!(output, \"{fmtkv}\")?;\n                    }\n                    args.reject_rest()?;\n                }\n\n                // get_unversioned KEY...\n                \"get_unversioned\" => {\n                    Self::no_txn(command)?;\n                    let mut args = command.consume_args();\n                    for arg in args.rest_pos() {\n                        let key = decode_binary(&arg.value);\n                        let value = self.mvcc.get_unversioned(&key)?;\n                        let fmtkv = format::Raw::key_maybe_value(&key, value.as_deref());\n                        writeln!(output, \"{fmtkv}\")?;\n                    }\n                    args.reject_rest()?;\n                }\n\n                // import [VERSION] KEY=VALUE...\n                \"import\" => {\n                    Self::no_txn(command)?;\n                    let mut args = command.consume_args();\n                    let version = args.next_pos().map(|a| a.parse()).transpose()?;\n                    let mut txn = self.mvcc.begin()?;\n                    if let Some(version) = version {\n                        if txn.version() > version {\n                            return Err(format!(\"version {version} already used\").into());\n                        }\n                        while txn.version() < version {\n                            txn = self.mvcc.begin()?;\n                        }\n                    }\n                    for kv in args.rest_key() {\n                        let key = decode_binary(kv.key.as_ref().unwrap());\n                        let value = decode_binary(&kv.value);\n                        if value.is_empty() {\n                            txn.delete(&key)?;\n                        } else {\n                            txn.set(&key, value)?;\n                        }\n                    }\n                    args.reject_rest()?;\n                    txn.commit()?;\n                }\n\n                // txn: resume JSON\n                \"resume\" => {\n                    let name = Self::txn_name(&command.prefix)?;\n                    let mut args = command.consume_args();\n                    let raw = &args.next_pos().ok_or(\"state not given\")?.value;\n                    args.reject_rest()?;\n                    let state: TransactionState = serde_json::from_str(raw)?;\n                    let txn = self.mvcc.resume(state)?;\n                    self.txns.insert(name.to_string(), txn);\n                }\n\n                // txn: rollback\n                \"rollback\" => {\n                    let name = Self::txn_name(&command.prefix)?;\n                    let txn = self.txns.remove(name).ok_or(format!(\"unknown txn {name}\"))?;\n                    command.consume_args().reject_rest()?;\n                    txn.rollback()?;\n                }\n\n                // txn: scan [RANGE]\n                \"scan\" => {\n                    let txn = self.get_txn(&command.prefix)?;\n                    let mut args = command.consume_args();\n                    let range =\n                        parse_key_range(args.next_pos().map(|a| a.value.as_str()).unwrap_or(\"..\"))?;\n                    args.reject_rest()?;\n\n                    let kvs: Vec<_> = txn.scan(range).try_collect()?;\n                    for (key, value) in kvs {\n                        writeln!(output, \"{}\", format::Raw::key_value(&key, &value))?;\n                    }\n                }\n\n                // txn: scan_prefix PREFIX\n                \"scan_prefix\" => {\n                    let txn = self.get_txn(&command.prefix)?;\n                    let mut args = command.consume_args();\n                    let prefix = decode_binary(&args.next_pos().ok_or(\"prefix not given\")?.value);\n                    args.reject_rest()?;\n\n                    let kvs: Vec<_> = txn.scan_prefix(&prefix).try_collect()?;\n                    for (key, value) in kvs {\n                        writeln!(output, \"{}\", format::Raw::key_value(&key, &value))?;\n                    }\n                }\n\n                // txn: set KEY=VALUE...\n                \"set\" => {\n                    let txn = self.get_txn(&command.prefix)?;\n                    let mut args = command.consume_args();\n                    for kv in args.rest_key() {\n                        let key = decode_binary(kv.key.as_ref().unwrap());\n                        let value = decode_binary(&kv.value);\n                        txn.set(&key, value)?;\n                    }\n                    args.reject_rest()?;\n                }\n\n                // set_unversioned KEY=VALUE...\n                \"set_unversioned\" => {\n                    Self::no_txn(command)?;\n                    let mut args = command.consume_args();\n                    for kv in args.rest_key() {\n                        let key = decode_binary(kv.key.as_ref().unwrap());\n                        let value = decode_binary(&kv.value);\n                        self.mvcc.set_unversioned(&key, value)?;\n                    }\n                    args.reject_rest()?;\n                }\n\n                // txn: state\n                \"state\" => {\n                    command.consume_args().reject_rest()?;\n                    let txn = self.get_txn(&command.prefix)?;\n                    let state = txn.state();\n                    write!(\n                        output,\n                        \"v{} {} active={{{}}}\",\n                        state.version,\n                        if state.read_only { \"ro\" } else { \"rw\" },\n                        state.active.iter().sorted().join(\",\")\n                    )?;\n                }\n\n                // status\n                \"status\" => writeln!(output, \"{:#?}\", self.mvcc.status()?)?,\n\n                name => return Err(format!(\"invalid command {name}\").into()),\n            }\n\n            // If requested, output engine operations.\n            if tags.remove(\"ops\") {\n                while let Ok(op) = self.op_rx.try_recv() {\n                    match op {\n                        Operation::Delete { key } => {\n                            let fmtkey = format::MVCC::<format::Raw>::key(&key);\n                            let rawkey = format::Raw::key(&key);\n                            writeln!(output, \"engine delete {fmtkey} [{rawkey}]\")?\n                        }\n                        Operation::Flush => writeln!(output, \"engine flush\")?,\n                        Operation::Set { key, value } => {\n                            let fmtkv = format::MVCC::<format::Raw>::key_value(&key, &value);\n                            let rawkv = format::Raw::key_value(&key, &value);\n                            writeln!(output, \"engine set {fmtkv} [{rawkv}]\")?\n                        }\n                    }\n                }\n            }\n\n            if let Some(tag) = tags.iter().next() {\n                return Err(format!(\"unknown tag {tag}\").into());\n            }\n\n            Ok(output)\n        }\n\n        // Drain unhandled engine operations.\n        fn end_command(&mut self, _: &goldenscript::Command) -> Result<String, Box<dyn Error>> {\n            while self.op_rx.try_recv().is_ok() {}\n            Ok(String::new())\n        }\n    }\n}\n"
  },
  {
    "path": "src/storage/testscripts/bitcask/compact",
    "content": "# Tests compaction.\n\n# Write some initial data out of order, with some overwrites and deletes.\nset foo=bar\nset b=1\nset b=2\nset e=5\ndelete e\nset c=0\ndelete c\nset c=3\nset \"\"=\"\"\nset a=1\ndelete f\ndelete d\nset d=4\nscan\n---\n\"\" → \"\"\n\"a\" → \"1\"\n\"b\" → \"2\"\n\"c\" → \"3\"\n\"d\" → \"4\"\n\"foo\" → \"bar\"\n\n# Show status.\nstatus\n---\nStatus {\n    name: \"bitcask\",\n    keys: 6,\n    size: 14,\n    disk_size: 128,\n    live_disk_size: 62,\n}\n\n# Dump the log.\ndump\n---\n0@0     keylen=3 [00000003] valuelen=3 [00000003]\n14b     key=\"foo\" [666f6f] value=\"bar\" [626172]\n--------\n1@14    keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"b\" [62] value=\"1\" [31]\n--------\n2@24    keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"b\" [62] value=\"2\" [32]\n--------\n3@34    keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"e\" [65] value=\"5\" [35]\n--------\n4@44    keylen=1 [00000001] valuelen=-1 [ffffffff]\n9b      key=\"e\" [65] tombstone\n--------\n5@53    keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"c\" [63] value=\"0\" [30]\n--------\n6@63    keylen=1 [00000001] valuelen=-1 [ffffffff]\n9b      key=\"c\" [63] tombstone\n--------\n7@72    keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"c\" [63] value=\"3\" [33]\n--------\n8@82    keylen=0 [00000000] valuelen=0 [00000000]\n8b      key=\"\" [] value=\"\" []\n--------\n9@90    keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"a\" [61] value=\"1\" [31]\n--------\n10@100  keylen=1 [00000001] valuelen=-1 [ffffffff]\n9b      key=\"f\" [66] tombstone\n--------\n11@109  keylen=1 [00000001] valuelen=-1 [ffffffff]\n9b      key=\"d\" [64] tombstone\n--------\n12@118  keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"d\" [64] value=\"4\" [34]\n\n# Compact it.\ncompact\n---\nok\n\n# Scan should still give same results.\nscan\n---\n\"\" → \"\"\n\"a\" → \"1\"\n\"b\" → \"2\"\n\"c\" → \"3\"\n\"d\" → \"4\"\n\"foo\" → \"bar\"\n\n# Status should show no garbage.\nstatus\n---\nStatus {\n    name: \"bitcask\",\n    keys: 6,\n    size: 14,\n    disk_size: 62,\n    live_disk_size: 62,\n}\n\n# Dump the compacted log.\ndump\n---\n0@0     keylen=0 [00000000] valuelen=0 [00000000]\n8b      key=\"\" [] value=\"\" []\n--------\n1@8     keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"a\" [61] value=\"1\" [31]\n--------\n2@18    keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"b\" [62] value=\"2\" [32]\n--------\n3@28    keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"c\" [63] value=\"3\" [33]\n--------\n4@38    keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"d\" [64] value=\"4\" [34]\n--------\n5@48    keylen=3 [00000003] valuelen=3 [00000003]\n14b     key=\"foo\" [666f6f] value=\"bar\" [626172]\n\n# Reopening the file works and shows the same data.\nreopen\nscan\n---\n\"\" → \"\"\n\"a\" → \"1\"\n\"b\" → \"2\"\n\"c\" → \"3\"\n\"d\" → \"4\"\n\"foo\" → \"bar\"\n"
  },
  {
    "path": "src/storage/testscripts/bitcask/compact_open",
    "content": "# Tests that the log is auto-compacted on startup if the fraction of garbage\n# exceeds the given threshold.\n\n# Write some initial data out of order, with some overwrites and deletes.\nset foo=bar\nset b=1\nset b=2\nset e=5\ndelete e\nset c=0\ndelete c\nset c=3\nset \"\"=\"\"\nset a=1\ndelete f\ndelete d\nset d=4\nscan\n---\n\"\" → \"\"\n\"a\" → \"1\"\n\"b\" → \"2\"\n\"c\" → \"3\"\n\"d\" → \"4\"\n\"foo\" → \"bar\"\n\n# Status shows the garbage fraction is 0.51.\nstatus\n---\nStatus {\n    name: \"bitcask\",\n    keys: 6,\n    size: 14,\n    disk_size: 128,\n    live_disk_size: 62,\n}\n\n# Reopening with a garbage fraction of 0.6 does not compact.\nreopen compact_fraction=0.6\nstatus\n---\nStatus {\n    name: \"bitcask\",\n    keys: 6,\n    size: 14,\n    disk_size: 128,\n    live_disk_size: 62,\n}\n\n# Reopening with a fraction of 0.5 does compact.\nreopen compact_fraction=0.5\nstatus\n---\nStatus {\n    name: \"bitcask\",\n    keys: 6,\n    size: 14,\n    disk_size: 62,\n    live_disk_size: 62,\n}\n\ndump\n---\n0@0     keylen=0 [00000000] valuelen=0 [00000000]\n8b      key=\"\" [] value=\"\" []\n--------\n1@8     keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"a\" [61] value=\"1\" [31]\n--------\n2@18    keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"b\" [62] value=\"2\" [32]\n--------\n3@28    keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"c\" [63] value=\"3\" [33]\n--------\n4@38    keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"d\" [64] value=\"4\" [34]\n--------\n5@48    keylen=3 [00000003] valuelen=3 [00000003]\n14b     key=\"foo\" [666f6f] value=\"bar\" [626172]\n"
  },
  {
    "path": "src/storage/testscripts/bitcask/log",
    "content": "# Assert the raw structure of the BitCask log.\n\n# Write some initial data out of order, with some overwrites and deletes.\nset foo=bar\nset b=1\nset b=2\nset e=5\ndelete e\nset c=0\ndelete c\nset c=3\nset \"\"=\"\"\nset a=1\ndelete f\ndelete d\nset d=4\nscan\n---\n\"\" → \"\"\n\"a\" → \"1\"\n\"b\" → \"2\"\n\"c\" → \"3\"\n\"d\" → \"4\"\n\"foo\" → \"bar\"\n\n# Dump the log.\ndump\n---\n0@0     keylen=3 [00000003] valuelen=3 [00000003]\n14b     key=\"foo\" [666f6f] value=\"bar\" [626172]\n--------\n1@14    keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"b\" [62] value=\"1\" [31]\n--------\n2@24    keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"b\" [62] value=\"2\" [32]\n--------\n3@34    keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"e\" [65] value=\"5\" [35]\n--------\n4@44    keylen=1 [00000001] valuelen=-1 [ffffffff]\n9b      key=\"e\" [65] tombstone\n--------\n5@53    keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"c\" [63] value=\"0\" [30]\n--------\n6@63    keylen=1 [00000001] valuelen=-1 [ffffffff]\n9b      key=\"c\" [63] tombstone\n--------\n7@72    keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"c\" [63] value=\"3\" [33]\n--------\n8@82    keylen=0 [00000000] valuelen=0 [00000000]\n8b      key=\"\" [] value=\"\" []\n--------\n9@90    keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"a\" [61] value=\"1\" [31]\n--------\n10@100  keylen=1 [00000001] valuelen=-1 [ffffffff]\n9b      key=\"f\" [66] tombstone\n--------\n11@109  keylen=1 [00000001] valuelen=-1 [ffffffff]\n9b      key=\"d\" [64] tombstone\n--------\n12@118  keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"d\" [64] value=\"4\" [34]\n\n# Reopen the log, which shows the same data.\nreopen\nscan\n---\n\"\" → \"\"\n\"a\" → \"1\"\n\"b\" → \"2\"\n\"c\" → \"3\"\n\"d\" → \"4\"\n\"foo\" → \"bar\"\n\ndump\n---\n0@0     keylen=3 [00000003] valuelen=3 [00000003]\n14b     key=\"foo\" [666f6f] value=\"bar\" [626172]\n--------\n1@14    keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"b\" [62] value=\"1\" [31]\n--------\n2@24    keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"b\" [62] value=\"2\" [32]\n--------\n3@34    keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"e\" [65] value=\"5\" [35]\n--------\n4@44    keylen=1 [00000001] valuelen=-1 [ffffffff]\n9b      key=\"e\" [65] tombstone\n--------\n5@53    keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"c\" [63] value=\"0\" [30]\n--------\n6@63    keylen=1 [00000001] valuelen=-1 [ffffffff]\n9b      key=\"c\" [63] tombstone\n--------\n7@72    keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"c\" [63] value=\"3\" [33]\n--------\n8@82    keylen=0 [00000000] valuelen=0 [00000000]\n8b      key=\"\" [] value=\"\" []\n--------\n9@90    keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"a\" [61] value=\"1\" [31]\n--------\n10@100  keylen=1 [00000001] valuelen=-1 [ffffffff]\n9b      key=\"f\" [66] tombstone\n--------\n11@109  keylen=1 [00000001] valuelen=-1 [ffffffff]\n9b      key=\"d\" [64] tombstone\n--------\n12@118  keylen=1 [00000001] valuelen=1 [00000001]\n10b     key=\"d\" [64] value=\"4\" [34]\n"
  },
  {
    "path": "src/storage/testscripts/bitcask/status",
    "content": "# Tests status for BitCask engine.\n\nset foo=123\nset bar=1\ndelete bar\nset baz=1\nset baz=2\nset baz=3\ndelete qux\n---\nok\n\nscan\n---\n\"baz\" → \"3\"\n\"foo\" → \"123\"\n\nstatus\n---\nStatus {\n    name: \"bitcask\",\n    keys: 2,\n    size: 10,\n    disk_size: 84,\n    live_disk_size: 26,\n}\n\n# Compact the log and show status again.\ncompact\nstatus\n---\nStatus {\n    name: \"bitcask\",\n    keys: 2,\n    size: 10,\n    disk_size: 26,\n    live_disk_size: 26,\n}\n"
  },
  {
    "path": "src/storage/testscripts/engine/keys",
    "content": "# Tests various keys.\n\n# Keys are case-sensitive.\nset a=1\nget a\nget A\n---\n\"a\" → \"1\"\n\"A\" → None\n\nset A=2\nget a\nget A\n---\n\"a\" → \"1\"\n\"A\" → \"2\"\n\ndelete a\ndelete A\nscan\n---\nok\n\n# Empty keys and values are valid.\nset \"\"=\"\"\nget \"\"\nscan\ndelete \"\"\n---\n\"\" → \"\"\n\"\" → \"\"\n\nscan\n---\nok\n\n# NUL keys and values are valid.\nset \"\\0\"=\"\\0\"\nget \"\\0\"\nscan\ndelete \"\\0\"\n---\n\"\\x00\" → \"\\x00\"\n\"\\x00\" → \"\\x00\"\n\nscan\n---\nok\n\n# Unicode keys and values work, but are shown as raw UTF-8 bytes.\nset \"👋\"=\"👋\"\nget \"👋\"\nscan\ndelete \"👋\"\n---\n\"\\xf0\\x9f\\x91\\x8b\" → \"\\xf0\\x9f\\x91\\x8b\"\n\"\\xf0\\x9f\\x91\\x8b\" → \"\\xf0\\x9f\\x91\\x8b\"\n\nscan\n---\nok\n"
  },
  {
    "path": "src/storage/testscripts/engine/point",
    "content": "# Tests basic point operations.\n\n# Getting a missing key in an empty store should return None.\nget a\n---\n\"a\" → None\n\n# Write a couple of keys.\nset a=1\nset b=2\n---\nok\n\n# Reading the value back should return it. An unknown key should return None.\nget a\nget b\nget c\n---\n\"a\" → \"1\"\n\"b\" → \"2\"\n\"c\" → None\n\n# Replacing a key should return the new value.\nset a=foo\nget a\n---\n\"a\" → \"foo\"\n\n# Deleting a key should remove it, but not affect other keys.\ndelete a\nget a\nget b\n---\n\"a\" → None\n\"b\" → \"2\"\n\n# Deletes are idempotent.\ndelete a\nget a\n---\n\"a\" → None\n\n# Writing a deleted key works fine.\nset a=1\nget a\n---\n\"a\" → \"1\"\n\n# Scan the final state.\nscan\n---\n\"a\" → \"1\"\n\"b\" → \"2\"\n"
  },
  {
    "path": "src/storage/testscripts/engine/scan",
    "content": "# Tests range scans.\n\n# Write some initial data.\nset a=1\nset b=2\nset ba=21\nset bb=22\nset c=3\nset C=3\n---\nok\n\n# Forward and reverse scans.\nscan\n---\n\"C\" → \"3\"\n\"a\" → \"1\"\n\"b\" → \"2\"\n\"ba\" → \"21\"\n\"bb\" → \"22\"\n\"c\" → \"3\"\n\nscan reverse=true\n---\n\"c\" → \"3\"\n\"bb\" → \"22\"\n\"ba\" → \"21\"\n\"b\" → \"2\"\n\"a\" → \"1\"\n\"C\" → \"3\"\n\n# Inclusive and exclusive ranges.\nscan b..bb\n---\n\"b\" → \"2\"\n\"ba\" → \"21\"\n\nscan \"b..=bb\"\n---\n\"b\" → \"2\"\n\"ba\" → \"21\"\n\"bb\" → \"22\"\n\nscan \"b..=bb\" reverse=true\n---\n\"bb\" → \"22\"\n\"ba\" → \"21\"\n\"b\" → \"2\"\n\n# Open ranges.\nscan bb..\n---\n\"bb\" → \"22\"\n\"c\" → \"3\"\n\nscan \"..=b\"\n---\n\"C\" → \"3\"\n\"a\" → \"1\"\n\"b\" → \"2\"\n"
  },
  {
    "path": "src/storage/testscripts/engine/scan_prefix",
    "content": "# Tests prefix scans.\n\n# Set up an initial dataset of keys with overlapping or adjacent prefixes.\nset a=1\nset b=2\nset ba=21\nset bb=22\nset \"b\\xff\"=2f\nset \"b\\xff\\x00\"=2f0\nset \"b\\xffb\"=2fb\nset \"b\\xff\\xff\"=2ff\nset c=3\nset \"\\xff\"=f\nset \"\\xff\\xff\"=ff\nset \"\\xff\\xff\\xff\"=fff\nset \"\\xff\\xff\\xff\\xff\"=ffff\nscan\n---\n\"a\" → \"1\"\n\"b\" → \"2\"\n\"ba\" → \"21\"\n\"bb\" → \"22\"\n\"b\\xff\" → \"2f\"\n\"b\\xff\\x00\" → \"2f0\"\n\"b\\xffb\" → \"2fb\"\n\"b\\xff\\xff\" → \"2ff\"\n\"c\" → \"3\"\n\"\\xff\" → \"f\"\n\"\\xff\\xff\" → \"ff\"\n\"\\xff\\xff\\xff\" → \"fff\"\n\"\\xff\\xff\\xff\\xff\" → \"ffff\"\n\n# An empty prefix returns everything.\nscan_prefix \"\"\n---\n\"a\" → \"1\"\n\"b\" → \"2\"\n\"ba\" → \"21\"\n\"bb\" → \"22\"\n\"b\\xff\" → \"2f\"\n\"b\\xff\\x00\" → \"2f0\"\n\"b\\xffb\" → \"2fb\"\n\"b\\xff\\xff\" → \"2ff\"\n\"c\" → \"3\"\n\"\\xff\" → \"f\"\n\"\\xff\\xff\" → \"ff\"\n\"\\xff\\xff\\xff\" → \"fff\"\n\"\\xff\\xff\\xff\\xff\" → \"ffff\"\n\n# A missing prefix returns nothing.\nscan_prefix bx\n---\nok\n\n# Various prefixes under b. In particular, this tests that the bounds generation\n# handles 0xff bytes properly.\nscan_prefix b\n---\n\"b\" → \"2\"\n\"ba\" → \"21\"\n\"bb\" → \"22\"\n\"b\\xff\" → \"2f\"\n\"b\\xff\\x00\" → \"2f0\"\n\"b\\xffb\" → \"2fb\"\n\"b\\xff\\xff\" → \"2ff\"\n\nscan_prefix bb\n---\n\"bb\" → \"22\"\n\nscan_prefix \"b\\xff\"\n---\n\"b\\xff\" → \"2f\"\n\"b\\xff\\x00\" → \"2f0\"\n\"b\\xffb\" → \"2fb\"\n\"b\\xff\\xff\" → \"2ff\"\n\nscan_prefix \"b\\xff\\x00\"\n---\n\"b\\xff\\x00\" → \"2f0\"\n\nscan_prefix \"b\\xff\\xff\"\n---\n\"b\\xff\\xff\" → \"2ff\"\n\n# Chains of \\xff prefixes.\nscan_prefix \"\\xff\"\n---\n\"\\xff\" → \"f\"\n\"\\xff\\xff\" → \"ff\"\n\"\\xff\\xff\\xff\" → \"fff\"\n\"\\xff\\xff\\xff\\xff\" → \"ffff\"\n\nscan_prefix \"\\xff\\xff\"\n---\n\"\\xff\\xff\" → \"ff\"\n\"\\xff\\xff\\xff\" → \"fff\"\n\"\\xff\\xff\\xff\\xff\" → \"ffff\"\n\nscan_prefix \"\\xff\\xff\\xff\"\n---\n\"\\xff\\xff\\xff\" → \"fff\"\n\"\\xff\\xff\\xff\\xff\" → \"ffff\"\n\nscan_prefix \"\\xff\\xff\\xff\\xff\"\n---\n\"\\xff\\xff\\xff\\xff\" → \"ffff\"\n\nscan_prefix \"\\xff\\xff\\xff\\xff\\xff\"\n---\nok\n"
  },
  {
    "path": "src/storage/testscripts/memory/status",
    "content": "# Tests status for Memory engine.\n\nset foo=123\nset bar=1\ndelete bar\nset baz=1\nset baz=2\nset baz=3\ndelete qux\n---\nok\n\nstatus\n---\nStatus {\n    name: \"memory\",\n    keys: 2,\n    size: 10,\n    disk_size: 0,\n    live_disk_size: 0,\n}\n"
  },
  {
    "path": "src/storage/testscripts/mvcc/anomaly_dirty_read",
    "content": "# A dirty read is when t2 can read an uncommitted value set by t1. Snapshot\n# isolation prevents this.\n\nt1: begin\nt1: set key=1\n---\nok\n\nt2: begin\nt2: get key\n---\nt2: \"key\" → None\n"
  },
  {
    "path": "src/storage/testscripts/mvcc/anomaly_dirty_write",
    "content": "# A dirty write is when t2 overwrites an uncommitted value written by t1.\n# Snapshot isolation prevents this.\n\nt1: begin\nt1: set key=1\n---\nok\n\nt2: begin\nt2: !set key=2\n---\nt2: Error: serialization failure, retry transaction\n"
  },
  {
    "path": "src/storage/testscripts/mvcc/anomaly_fuzzy_read",
    "content": "# A fuzzy (or unrepeatable) read is when t2 sees a value change after t1\n# updates it. Snapshot isolation prevents this.\n\n# Set up some initial data.\nimport key=0\n---\nok\n\nt1: begin\nt2: begin\n---\nok\n\nt2: get key\n---\nt2: \"key\" → \"0\"\n\nt1: set key=1\nt1: commit\n---\nok\n\nt2: get key\n---\nt2: \"key\" → \"0\"\n"
  },
  {
    "path": "src/storage/testscripts/mvcc/anomaly_lost_update",
    "content": "# A lost update is when t1 and t2 both read a value and update it, where\n# t2's update replaces t1. Snapshot isolation prevents this.\n\nt1: begin\nt1: get key\n---\nt1: \"key\" → None\n\nt2: begin\nt2: get key\n---\nt2: \"key\" → None\n\nt1: set key=1\nt2: !set key=2\n---\nt2: Error: serialization failure, retry transaction\n"
  },
  {
    "path": "src/storage/testscripts/mvcc/anomaly_phantom_read",
    "content": "# A phantom read is when t1 reads entries matching some predicate, but a\n# modification by t2 changes which entries match the predicate such that a later\n# read by t1 returns them. Snapshot isolation prevents this.\n#\n# We use a prefix scan as our predicate.\n\n# Write some initial data.\nimport a=0 ba=0 bb=0\n---\nok\n\nt1: begin\nt2: begin\n---\nok\n\nt1: scan_prefix b\n---\nt1: \"ba\" → \"0\"\nt1: \"bb\" → \"0\"\n\nt2: delete ba\nt2: set bc=2\nt2: commit\n---\nok\n\nt1: scan_prefix b\n---\nt1: \"ba\" → \"0\"\nt1: \"bb\" → \"0\"\n"
  },
  {
    "path": "src/storage/testscripts/mvcc/anomaly_read_skew",
    "content": "# Read skew is when t1 reads a and b, but t2 modifies b in between the\n# reads. Snapshot isolation prevents this.\n\n# Set up some initial data.\nimport a=0 b=0\n---\nok\n\nt1: begin\nt2: begin\n---\nok\n\nt1: get a\n---\nt1: \"a\" → \"0\"\n\nt2: set a=2\nt2: set b=2\nt2: commit\n---\nok\n\nt1: get b\n---\nt1: \"b\" → \"0\"\n"
  },
  {
    "path": "src/storage/testscripts/mvcc/anomaly_write_skew",
    "content": "# Write skew is when t1 reads a and writes it to b while t2 reads b and writes\n# it to a. Snapshot isolation does not prevent this, which is expected, so we\n# assert the anomalous behavior. Fixing this would require implementing\n# serializable snapshot isolation.\n\n# Write some initial data.\nimport a=1 b=2\n---\nok\n\nt1: begin\nt2: begin\n---\nok\n\nt1: get a\nt2: get b\n---\nt1: \"a\" → \"1\"\nt2: \"b\" → \"2\"\n\nt1: set b=1\nt2: set a=2\n---\nok\n\nt1: commit\nt2: commit\n---\nok\n\nt3: begin readonly\nt3: scan\n---\nt3: \"a\" → \"2\"\nt3: \"b\" → \"1\"\n"
  },
  {
    "path": "src/storage/testscripts/mvcc/bank",
    "content": "# A simple illustration of MVCC transactions with bank transfers.\n#\n# We start with three bank accounts A, B, and C, each with a balance of 100.\nimport A=100 B=100 C=100\n---\nok\n\n# Alice wants to transfer 100 from B to A. She begins a transaction and\n# checks the balance of all accounts.\nalice: begin\nalice: scan\n---\nalice: \"A\" → \"100\"\nalice: \"B\" → \"100\"\nalice: \"C\" → \"100\"\n\n# She then subtracts 100 from B, and is about to add 100 to A.\nalice: set B=0\n---\nok\n\n# Bob comes along and wants to transfer 100 from B to C. He begins a transaction\n# and checks the balances.\n#\n# Bob might freak out if there was no money in B and only 200 total in all\n# accounts, but Alice hasn't yet committed her change to B so it's not visible.\n# If the system were to crash or Alice disconnects, B would still have 100.\nbob: begin\nbob: scan\n---\nbob: \"A\" → \"100\"\nbob: \"B\" → \"100\"\nbob: \"C\" → \"100\"\n\n# Alice now completes the transfer by adding 100 to A and committing to finalize\n# the transaction.\nalice: set A=200\nalice: scan\n---\nalice: \"A\" → \"200\"\nalice: \"B\" → \"0\"\nalice: \"C\" → \"100\"\n\nalice: commit\n---\nok\n\n# But what about Bob? If he now sets C=200 and B=0, we'll have A=200 B=0 C=200,\n# and 100 would have appeared out of thin air! Thankfully, MVCC saves us:\nbob: set C=200\n---\nok\n\nbob: !set B=0\n---\nbob: Error: serialization failure, retry transaction\n\n# MVCC caught the conflict, and Bob has to roll back and retry.\nbob: rollback\n---\nok\n\n# He then finds there's no money left in B anymore, and can't make the transfer.\nbob: begin\nbob: scan\n---\nbob: \"A\" → \"200\"\nbob: \"B\" → \"0\"\nbob: \"C\" → \"100\"\n"
  },
  {
    "path": "src/storage/testscripts/mvcc/begin",
    "content": "# Begin creates new transactions at increasing versions, with concurrent\n# transactions in their active sets.\n\n# Start t1 at v1, with an empty active set. Dump raw engine operations to ensure\n# it bumps the next version and registers itself as active.\nt1: begin [ops]\nt1: state\n---\nt1: engine set mvcc:NextVersion → 2 [\"\\x00\" → \"\\x02\"]\nt1: engine set mvcc:TxnActive(1) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\"]\nt1: v1 rw active={}\n\n# t2 should have v2, and t1 in its active set. It should persist a snapshot of\n# its active set.\nt2: begin [ops]\nt2: state\n---\nt2: engine set mvcc:NextVersion → 3 [\"\\x00\" → \"\\x03\"]\nt2: engine set mvcc:TxnActiveSnapshot(2) → {1} [\"\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x01\"]\nt2: engine set mvcc:TxnActive(2) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\"]\nt2: v2 rw active={1}\n\n# Similarly for t3.\nt3: begin [ops]\nt3: state\n---\nt3: engine set mvcc:NextVersion → 4 [\"\\x00\" → \"\\x04\"]\nt3: engine set mvcc:TxnActiveSnapshot(3) → {1,2} [\"\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x02\\x01\\x02\"]\nt3: engine set mvcc:TxnActive(3) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\"]\nt3: v3 rw active={1,2}\n\n# Now, commit t2, which unregisters it.\nt2: commit [ops]\n---\nt2: engine delete mvcc:TxnActive(2) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\"]\n\n# It should still be in t3's active set.\nt3: state\n---\nt3: v3 rw active={1,2}\n\n# But not in a new t4.\nt4: begin [ops]\nt4: state\n---\nt4: engine set mvcc:NextVersion → 5 [\"\\x00\" → \"\\x05\"]\nt4: engine set mvcc:TxnActiveSnapshot(4) → {1,3} [\"\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x02\\x01\\x03\"]\nt4: engine set mvcc:TxnActive(4) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\"]\nt4: v4 rw active={1,3}\n"
  },
  {
    "path": "src/storage/testscripts/mvcc/begin_as_of",
    "content": "# Begin read-only as-of should provide a view of a historical version.\n\n# Start a concurrent transaction at v1 that should be invisible.\nt1: begin\nt1: set other=1\n---\nok\n\n# Write and commit a key at v2.\nt2: begin\nt2: set key=2\nt2: commit\n---\nok\n\n# Write another version at v3, but don't commit it yet.\nt3: begin\nt3: set key=3\n---\nok\n\ndump\n---\nmvcc:NextVersion → 4 [\"\\x00\" → \"\\x04\"]\nmvcc:TxnActive(1) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\"]\nmvcc:TxnActive(3) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\"]\nmvcc:TxnActiveSnapshot(2) → {1} [\"\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x01\"]\nmvcc:TxnActiveSnapshot(3) → {1} [\"\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x01\"]\nmvcc:TxnWrite(1, \"other\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01other\\x00\\x00\" → \"\"]\nmvcc:TxnWrite(3, \"key\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03key\\x00\\x00\" → \"\"]\nmvcc:Version(\"key\", 2) → \"2\" [\"\\x04key\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x012\"]\nmvcc:Version(\"key\", 3) → \"3\" [\"\\x04key\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x013\"]\nmvcc:Version(\"other\", 1) → \"1\" [\"\\x04other\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x011\"]\n\n# Start a read-only transaction as-of version 3. It should only see key=2\n# because t1 and t3 haven't committed yet. It shouldn't write any state.\nt4: begin readonly as_of=3 [ops]\nt4: state\n---\nt4: v3 ro active={1}\n\nt4: scan\n---\nt4: \"key\" → \"2\"\n\n# Writes should error.\nt4: !set foo=bar\nt4: !delete foo\n---\nt4: Error: read-only transaction\nt4: Error: read-only transaction\n\n# t1 and t3 commit. Their writes still shouldn't be visible to t4, since\n# versions must be stable.\nt1: commit\nt3: commit\n---\nok\n\nt4: scan\n---\nt4: \"key\" → \"2\"\n\n# A new transaction t5 running as-of v3 shouldn't see them either.\nt5: begin readonly as_of=3\nt5: state\n---\nt5: v3 ro active={1}\n\nt5: scan\n---\nt5: \"key\" → \"2\"\n\n# Committing and rolling back readonly txns is a noop.\nt4: commit [ops]\nt5: rollback [ops]\n---\nok\n\n# Commit a new value at version 4.\nt6: begin\nt6: state\nt6: set key=4\nt6: commit\n---\nt6: v4 rw active={}\n\n# A snapshot at version 4 should see the old writes, but not those of t6 at v4\n# because as_of is at the start of the version.\nt7: begin readonly as_of=4\nt7: scan\n---\nt7: \"key\" → \"3\"\nt7: \"other\" → \"1\"\n\n# Running as_of future versions should error, including the next version.\nt8: !begin readonly as_of=5\nt8: !begin readonly as_of=9\n---\nt8: Error: invalid input: version 5 does not exist\nt8: Error: invalid input: version 9 does not exist\n\n# Version 0 works though, but doesn't show anything.\nt8: begin readonly as_of=0\nt8: state\nt8: scan\n---\nt8: v0 ro active={}\n"
  },
  {
    "path": "src/storage/testscripts/mvcc/begin_readonly",
    "content": "# Begin read-only should not create a new version, it should run in the next\n# version but using the current active set.\n\n# Start t1 read-only at v1. It shouldn't bump the version nor write any state.\nt1: begin readonly [ops]\nt1: state\n---\nt1: v1 ro active={}\n\n# Writes should error.\nt1: !set foo=bar\nt1: !delete foo\n---\nt1: Error: read-only transaction\nt1: Error: read-only transaction\n\n# Start a new read-write transaction, then another read-only transaction which\n# should have it in its active set. t1 should not be in the active set, because\n# it's read-only.\nt2: begin\nt2: state\n---\nt2: v1 rw active={}\n\nt3: begin readonly [ops]\nt3: state\n---\nt3: v2 ro active={1}\n\n# t2 also shouldn't be in t1's active set. Visibility for t2's writes are\n# handled explicitly for t1.\nt2: state\n---\nt2: v1 rw active={}\n\n# Both committing and rolling back read-only transactions are noops.\nt1: commit [ops]\nt3: commit [ops]\n---\nok\n"
  },
  {
    "path": "src/storage/testscripts/mvcc/delete",
    "content": "# Deletes should work on both existing, missing, and deleted keys.\n\nimport 1 a=1 b=1 x=\n---\nok\n\n# Delete an existing, missing, and deleted key. Show engine operations.\nt1: begin\nt1: delete a m x [ops]\n---\nt1: engine set mvcc:TxnWrite(2, \"a\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02a\\x00\\x00\" → \"\"]\nt1: engine set mvcc:Version(\"a\", 2) → None [\"\\x04a\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x00\"]\nt1: engine set mvcc:TxnWrite(2, \"m\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02m\\x00\\x00\" → \"\"]\nt1: engine set mvcc:Version(\"m\", 2) → None [\"\\x04m\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x00\"]\nt1: engine set mvcc:TxnWrite(2, \"x\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02x\\x00\\x00\" → \"\"]\nt1: engine set mvcc:Version(\"x\", 2) → None [\"\\x04x\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x00\"]\n\nt1: scan\n---\nt1: \"b\" → \"1\"\n\n# Set and then delete a key, both an existing an missing one.\nt1: set b=2 c=2 [ops]\n---\nt1: engine set mvcc:TxnWrite(2, \"b\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02b\\x00\\x00\" → \"\"]\nt1: engine set mvcc:Version(\"b\", 2) → \"2\" [\"\\x04b\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x012\"]\nt1: engine set mvcc:TxnWrite(2, \"c\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02c\\x00\\x00\" → \"\"]\nt1: engine set mvcc:Version(\"c\", 2) → \"2\" [\"\\x04c\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x012\"]\n\nt1: scan\n---\nt1: \"b\" → \"2\"\nt1: \"c\" → \"2\"\n\nt1: delete b c [ops]\n---\nt1: engine set mvcc:TxnWrite(2, \"b\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02b\\x00\\x00\" → \"\"]\nt1: engine set mvcc:Version(\"b\", 2) → None [\"\\x04b\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x00\"]\nt1: engine set mvcc:TxnWrite(2, \"c\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02c\\x00\\x00\" → \"\"]\nt1: engine set mvcc:Version(\"c\", 2) → None [\"\\x04c\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x00\"]\n\nt1: scan\n---\nok\n\ndump\n---\nmvcc:NextVersion → 3 [\"\\x00\" → \"\\x03\"]\nmvcc:TxnActive(2) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\"]\nmvcc:TxnWrite(2, \"a\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02a\\x00\\x00\" → \"\"]\nmvcc:TxnWrite(2, \"b\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02b\\x00\\x00\" → \"\"]\nmvcc:TxnWrite(2, \"c\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02c\\x00\\x00\" → \"\"]\nmvcc:TxnWrite(2, \"m\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02m\\x00\\x00\" → \"\"]\nmvcc:TxnWrite(2, \"x\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02x\\x00\\x00\" → \"\"]\nmvcc:Version(\"a\", 1) → \"1\" [\"\\x04a\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x011\"]\nmvcc:Version(\"a\", 2) → None [\"\\x04a\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x00\"]\nmvcc:Version(\"b\", 1) → \"1\" [\"\\x04b\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x011\"]\nmvcc:Version(\"b\", 2) → None [\"\\x04b\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x00\"]\nmvcc:Version(\"c\", 2) → None [\"\\x04c\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x00\"]\nmvcc:Version(\"m\", 2) → None [\"\\x04m\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x00\"]\nmvcc:Version(\"x\", 1) → None [\"\\x04x\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x00\"]\nmvcc:Version(\"x\", 2) → None [\"\\x04x\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x00\"]\n"
  },
  {
    "path": "src/storage/testscripts/mvcc/delete_conflict",
    "content": "# Delete should return serialization errors both for uncommitted versions\n# (past and future), and future committed versions.\n\nt1: begin\nt2: begin\nt3: begin\nt4: begin\n---\nok\n\nt1: set a=1\nt3: set c=3\nt4: set d=4\nt4: commit\n---\nok\n\nt2: !delete a # past uncommitted\nt2: !delete c # future uncommitted\nt2: !delete d # future committed\n---\nt2: Error: serialization failure, retry transaction\nt2: Error: serialization failure, retry transaction\nt2: Error: serialization failure, retry transaction\n\ndump\n---\nmvcc:NextVersion → 5 [\"\\x00\" → \"\\x05\"]\nmvcc:TxnActive(1) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\"]\nmvcc:TxnActive(2) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\"]\nmvcc:TxnActive(3) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\"]\nmvcc:TxnActiveSnapshot(2) → {1} [\"\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x01\"]\nmvcc:TxnActiveSnapshot(3) → {1,2} [\"\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x02\\x01\\x02\"]\nmvcc:TxnActiveSnapshot(4) → {1,2,3} [\"\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x03\\x01\\x02\\x03\"]\nmvcc:TxnWrite(1, \"a\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01a\\x00\\x00\" → \"\"]\nmvcc:TxnWrite(3, \"c\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03c\\x00\\x00\" → \"\"]\nmvcc:Version(\"a\", 1) → \"1\" [\"\\x04a\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x011\"]\nmvcc:Version(\"c\", 3) → \"3\" [\"\\x04c\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x013\"]\nmvcc:Version(\"d\", 4) → \"4\" [\"\\x04d\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x014\"]\n"
  },
  {
    "path": "src/storage/testscripts/mvcc/get",
    "content": "# Get should return the correct latest value.\n\nimport 1 key=1 updated=1 deleted=1 tombstone=\nimport 2 updated=2 deleted=\n---\nok\n\nt1: begin readonly\nt1: scan\n---\nt1: \"key\" → \"1\"\nt1: \"updated\" → \"2\"\n\n# Get results should mirror scan.\nt1: get key updated deleted tombstone missing\n---\nt1: \"key\" → \"1\"\nt1: \"updated\" → \"2\"\nt1: \"deleted\" → None\nt1: \"tombstone\" → None\nt1: \"missing\" → None\n"
  },
  {
    "path": "src/storage/testscripts/mvcc/get_isolation",
    "content": "# Get should be isolated from concurrent transactions.\n\n# Past committed.\nt1: begin\nt1: set a=1 b=1 d=1 e=1\nt1: commit\n---\nok\n\n# Past uncommitted.\nt2: begin\nt2: set a=2 c=2\nt2: delete b\n---\nok\n\n# Begin the read transaction.\nt3: begin readonly\n---\nok\n\n# Future committed.\nt4: begin\nt4: set d=3 f=3\nt4: delete e\nt4: commit\n---\nok\n\n# Future uncommitted.\nt5: begin\nt5: set d=4 g=4\nt5: delete f\n---\nok\n\n# Get each key.\nt3: get a b c d e f g\n---\nt3: \"a\" → \"1\"\nt3: \"b\" → \"1\"\nt3: \"c\" → None\nt3: \"d\" → \"1\"\nt3: \"e\" → \"1\"\nt3: \"f\" → None\nt3: \"g\" → None\n"
  },
  {
    "path": "src/storage/testscripts/mvcc/resume",
    "content": "# Resume should resume a transaction with the same state.\n\n# Commit some visible values.\nt1: begin\nt1: set a=1 b=1\nt1: commit\n---\nok\n\n# We then start three transactions, of which we will resume t3.  We commit t2\n# and t4's changes, which should not be visible, and write a change for t3 which\n# should be visible.\nt2: begin\nt3: begin\nt4: begin\n---\nok\n\nt2: set a=2\nt3: set b=3\nt4: set c=4\nt2: commit\nt4: commit\n---\nok\n\n# We now resume t3 as t5.\nt3: state\n---\nt3: v3 rw active={2}\n\nt5: resume '{\"version\":3, \"read_only\":false, \"active\":[2]}'\nt5: state\n---\nt5: v3 rw active={2}\n\n# t5 can see its own changes, but not the others.\nt5: scan\n---\nt5: \"a\" → \"1\"\nt5: \"b\" → \"3\"\n\n# A new transaction should not see t3/5's uncommitted changes.\nt6: begin\nt6: scan\n---\nt6: \"a\" → \"2\"\nt6: \"b\" → \"1\"\nt6: \"c\" → \"4\"\n\n# Once t5 commits, a separate transaction should see its changes.\nt5: commit [ops]\n---\nt5: engine delete mvcc:TxnWrite(3, \"b\") [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03b\\x00\\x00\"]\nt5: engine delete mvcc:TxnActive(3) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\"]\n\nt7: begin\nt7: scan\n---\nt7: \"a\" → \"2\"\nt7: \"b\" → \"3\"\nt7: \"c\" → \"4\"\n\n# Resuming a committed transaction should error.\nt8: !resume '{\"version\":3, \"read_only\":false, \"active\":[2]}'\n---\nt8: Error: invalid input: no active transaction at version 3\n\n# It should also be possible to start a snapshot transaction in t3 and resume\n# it. It should not see t3's writes, nor t2's.\nt8: begin readonly as_of=3\nt8: state\n---\nt8: v3 ro active={2}\n\nt8: scan\n---\nt8: \"a\" → \"1\"\nt8: \"b\" → \"1\"\n\nt9: resume '{\"version\":3, \"read_only\":true, \"active\":[2]}'\nt9: state\n---\nt9: v3 ro active={2}\n\nt9: scan\n---\nt9: \"a\" → \"1\"\nt9: \"b\" → \"1\"\n"
  },
  {
    "path": "src/storage/testscripts/mvcc/rollback",
    "content": "# Tests that transaction rollback properly rolls back uncommitted writes\n# allowing other concurrent transactions to write the keys.\n\nimport 1 a=0 b=0 c=0 d=0\n---\nok\n\n# t2 will be rolled back. t1 and t3 are concurrent transactions.\nt1: begin\nt2: begin\nt3: begin\n---\nok\n\nt1: set a=1\nt2: set b=2\nt2: delete c\nt3: set d=3\n---\nok\n\ndump\n---\nmvcc:NextVersion → 5 [\"\\x00\" → \"\\x05\"]\nmvcc:TxnActive(2) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\"]\nmvcc:TxnActive(3) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\"]\nmvcc:TxnActive(4) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\"]\nmvcc:TxnActiveSnapshot(3) → {2} [\"\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x02\"]\nmvcc:TxnActiveSnapshot(4) → {2,3} [\"\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x02\\x02\\x03\"]\nmvcc:TxnWrite(2, \"a\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02a\\x00\\x00\" → \"\"]\nmvcc:TxnWrite(3, \"b\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03b\\x00\\x00\" → \"\"]\nmvcc:TxnWrite(3, \"c\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03c\\x00\\x00\" → \"\"]\nmvcc:TxnWrite(4, \"d\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04d\\x00\\x00\" → \"\"]\nmvcc:Version(\"a\", 1) → \"0\" [\"\\x04a\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x010\"]\nmvcc:Version(\"a\", 2) → \"1\" [\"\\x04a\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x011\"]\nmvcc:Version(\"b\", 1) → \"0\" [\"\\x04b\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x010\"]\nmvcc:Version(\"b\", 3) → \"2\" [\"\\x04b\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x012\"]\nmvcc:Version(\"c\", 1) → \"0\" [\"\\x04c\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x010\"]\nmvcc:Version(\"c\", 3) → None [\"\\x04c\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x00\"]\nmvcc:Version(\"d\", 1) → \"0\" [\"\\x04d\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x010\"]\nmvcc:Version(\"d\", 4) → \"3\" [\"\\x04d\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x013\"]\n\n# Both t1 and t3 will conflict with t2.\nt1: !set b=1\nt3: !set c=3\n---\nt1: Error: serialization failure, retry transaction\nt3: Error: serialization failure, retry transaction\n\n# When t2 is rolled back, none of its writes will be visible, and t1 and t3 can\n# perform their writes and successfully commit.\nt2: rollback [ops]\n---\nt2: engine delete mvcc:Version(\"b\", 3) [\"\\x04b\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\"]\nt2: engine delete mvcc:TxnWrite(3, \"b\") [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03b\\x00\\x00\"]\nt2: engine delete mvcc:Version(\"c\", 3) [\"\\x04c\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\"]\nt2: engine delete mvcc:TxnWrite(3, \"c\") [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03c\\x00\\x00\"]\nt2: engine delete mvcc:TxnActive(3) [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\"]\n\nt4: begin readonly\nt4: scan\n---\nt4: \"a\" → \"0\"\nt4: \"b\" → \"0\"\nt4: \"c\" → \"0\"\nt4: \"d\" → \"0\"\n\nt1: set b=1\nt1: commit\nt3: set c=3\nt3: commit\n---\nok\n\nt5: begin readonly\nt5: scan\n---\nt5: \"a\" → \"1\"\nt5: \"b\" → \"1\"\nt5: \"c\" → \"3\"\nt5: \"d\" → \"3\"\n\ndump\n---\nmvcc:NextVersion → 5 [\"\\x00\" → \"\\x05\"]\nmvcc:TxnActiveSnapshot(3) → {2} [\"\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x02\"]\nmvcc:TxnActiveSnapshot(4) → {2,3} [\"\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x02\\x02\\x03\"]\nmvcc:Version(\"a\", 1) → \"0\" [\"\\x04a\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x010\"]\nmvcc:Version(\"a\", 2) → \"1\" [\"\\x04a\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x011\"]\nmvcc:Version(\"b\", 1) → \"0\" [\"\\x04b\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x010\"]\nmvcc:Version(\"b\", 2) → \"1\" [\"\\x04b\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x011\"]\nmvcc:Version(\"c\", 1) → \"0\" [\"\\x04c\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x010\"]\nmvcc:Version(\"c\", 4) → \"3\" [\"\\x04c\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x013\"]\nmvcc:Version(\"d\", 1) → \"0\" [\"\\x04d\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x010\"]\nmvcc:Version(\"d\", 4) → \"3\" [\"\\x04d\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x013\"]\n"
  },
  {
    "path": "src/storage/testscripts/mvcc/scan",
    "content": "# Scans should use correct key and time bounds. Sets up this dataset:\n# \n# T\n# 4             x    ba4\n# 3   x    a3   b3        x\n# 2        x         ba2  bb2  bc2\n# 1   B1   a1   x                   c1\n#     B    a    b    ba   bb   bc   c\n\nimport 1 B=B1 a=a1 b= c=c1\nimport 2 a= ba=ba2 bb=bb2 bc=bc2\nimport 3 B= a=a3 b=b3 bb=\nimport 4 b= ba=ba4\n---\nok\n\n# Full scans at all timestamps.\nt1: begin readonly as_of=1\nt1: scan\n---\nok\n\nt2: begin readonly as_of=2\nt2: scan\n---\nt2: \"B\" → \"B1\"\nt2: \"a\" → \"a1\"\nt2: \"c\" → \"c1\"\n\nt3: begin readonly as_of=3\nt3: scan\n---\nt3: \"B\" → \"B1\"\nt3: \"ba\" → \"ba2\"\nt3: \"bb\" → \"bb2\"\nt3: \"bc\" → \"bc2\"\nt3: \"c\" → \"c1\"\n\nt4: begin readonly as_of=4\nt4: scan\n---\nt4: \"a\" → \"a3\"\nt4: \"b\" → \"b3\"\nt4: \"ba\" → \"ba2\"\nt4: \"bc\" → \"bc2\"\nt4: \"c\" → \"c1\"\n\nt5: begin readonly\nt5: scan\n---\nt5: \"a\" → \"a3\"\nt5: \"ba\" → \"ba4\"\nt5: \"bc\" → \"bc2\"\nt5: \"c\" → \"c1\"\n\n# Various bounded scans around ba-bc at version 3.\nt3: scan ba..bc\n---\nt3: \"ba\" → \"ba2\"\nt3: \"bb\" → \"bb2\"\n\nt3: scan \"ba..=bc\"\n---\nt3: \"ba\" → \"ba2\"\nt3: \"bb\" → \"bb2\"\nt3: \"bc\" → \"bc2\"\n\nt3: scan ba..\n---\nt3: \"ba\" → \"ba2\"\nt3: \"bb\" → \"bb2\"\nt3: \"bc\" → \"bc2\"\nt3: \"c\" → \"c1\"\n\nt3: scan \"..bc\"\n---\nt3: \"B\" → \"B1\"\nt3: \"ba\" → \"ba2\"\nt3: \"bb\" → \"bb2\"\n\nt3: scan \"..=bc\"\n---\nt3: \"B\" → \"B1\"\nt3: \"ba\" → \"ba2\"\nt3: \"bb\" → \"bb2\"\nt3: \"bc\" → \"bc2\"\n"
  },
  {
    "path": "src/storage/testscripts/mvcc/scan_isolation",
    "content": "# Scan should be isolated from concurrent transactions.\n\n# Past committed.\nt1: begin\nt1: set a=1 b=1 d=1 e=1\nt1: commit\n---\nok\n\n# Past uncommitted.\nt2: begin\nt2: set a=2 c=2\nt2: delete b\n---\nok\n\n# Begin the read transaction.\nt3: begin readonly\n---\nok\n\n# Future committed.\nt4: begin\nt4: set d=3 f=3\nt4: delete e\nt4: commit\n---\nok\n\n# Future uncommitted.\nt5: begin\nt5: set d=4 g=4\nt5: delete f\n---\nok\n\n# Scan keys.\nt3: scan\n---\nt3: \"a\" → \"1\"\nt3: \"b\" → \"1\"\nt3: \"d\" → \"1\"\nt3: \"e\" → \"1\"\n"
  },
  {
    "path": "src/storage/testscripts/mvcc/scan_key_version_encoding",
    "content": "# Tests that the key encoding is resistant to key/version overlap.\n# For example, a naïve concatenation of keys and versions would\n# produce incorrect ordering in this case:\n#\n# 00|00 00 00 00 00 00 00 01\n# 00 00 00 00 00 00 00 00 02|00 00 00 00 00 00 00 02\n# 00|00 00 00 00 00 00 00 03\n\nt1: begin\nt1: set \"\\x00\"=\"\\x01\" [ops]\nt1: commit\n---\nt1: engine set mvcc:TxnWrite(1, \"\\x00\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\xff\\x00\\x00\" → \"\"]\nt1: engine set mvcc:Version(\"\\x00\", 1) → \"\\x01\" [\"\\x04\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x01\\x01\"]\n\nt2: begin\nt2: set \"\\x00\"=\"\\x02\" [ops]\nt2: set \"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\"=\"\\x02\" [ops]\nt2: commit\n---\nt2: engine set mvcc:TxnWrite(2, \"\\x00\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\xff\\x00\\x00\" → \"\"]\nt2: engine set mvcc:Version(\"\\x00\", 2) → \"\\x02\" [\"\\x04\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x01\\x02\"]\nt2: engine set mvcc:TxnWrite(2, \"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\" → \"\"]\nt2: engine set mvcc:Version(\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\", 2) → \"\\x02\" [\"\\x04\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x01\\x02\"]\n\nt3: begin\nt3: set \"\\x00\"=\"\\x03\" [ops]\nt3: commit\n---\nt3: engine set mvcc:TxnWrite(3, \"\\x00\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x00\\xff\\x00\\x00\" → \"\"]\nt3: engine set mvcc:Version(\"\\x00\", 3) → \"\\x03\" [\"\\x04\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x01\\x03\"]\n\nt4: begin readonly\nt4: scan\n---\nt4: \"\\x00\" → \"\\x03\"\nt4: \"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x02\"\n"
  },
  {
    "path": "src/storage/testscripts/mvcc/scan_prefix",
    "content": "# Prefix scans should use correct key and time bounds. Sets up this dataset:\n# \n# T\n# 4             x    ba4\n# 3   x    a3   b3        x\n# 2        x         ba2  bb2  bc2\n# 1   B1   a1   x                   c1\n#     B    a    b    ba   bb   bc   c\n\nimport 1 B=B1 a=a1 b= c=c1\nimport 2 a= ba=ba2 bb=bb2 bc=bc2\nimport 3 B= a=a3 b=b3 bb=\nimport 4 b= ba=ba4\n---\nok\n\n# Full scans at all timestamps.\nt1: begin readonly as_of=1\nt1: scan_prefix \"\"\n---\nok\n\nt2: begin readonly as_of=2\nt2: scan_prefix \"\"\n---\nt2: \"B\" → \"B1\"\nt2: \"a\" → \"a1\"\nt2: \"c\" → \"c1\"\n\nt3: begin readonly as_of=3\nt3: scan_prefix \"\"\n---\nt3: \"B\" → \"B1\"\nt3: \"ba\" → \"ba2\"\nt3: \"bb\" → \"bb2\"\nt3: \"bc\" → \"bc2\"\nt3: \"c\" → \"c1\"\n\nt4: begin readonly as_of=4\nt4: scan_prefix \"\"\n---\nt4: \"a\" → \"a3\"\nt4: \"b\" → \"b3\"\nt4: \"ba\" → \"ba2\"\nt4: \"bc\" → \"bc2\"\nt4: \"c\" → \"c1\"\n\nt5: begin readonly\nt5: scan_prefix \"\"\n---\nt5: \"a\" → \"a3\"\nt5: \"ba\" → \"ba4\"\nt5: \"bc\" → \"bc2\"\nt5: \"c\" → \"c1\"\n\n# Various prefixes at version 3.\nt3: scan_prefix B\n---\nt3: \"B\" → \"B1\"\n\nt3: scan_prefix b\n---\nt3: \"ba\" → \"ba2\"\nt3: \"bb\" → \"bb2\"\nt3: \"bc\" → \"bc2\"\n\nt3: scan_prefix bb\n---\nt3: \"bb\" → \"bb2\"\n\nt3: scan_prefix bbb\n---\nok\n\n# Various prefixes at version 4.\nt4: scan_prefix B\n---\nok\n\nt4: scan_prefix b\n---\nt4: \"b\" → \"b3\"\nt4: \"ba\" → \"ba2\"\nt4: \"bc\" → \"bc2\"\n\nt4: scan_prefix bb\n---\nok\n"
  },
  {
    "path": "src/storage/testscripts/mvcc/set",
    "content": "# Sets should work on both existing, missing, and deleted keys.\n\nimport a=1 b=1 x=\n---\nok\n\n# Can replace an existing key and tombstone.\nt1: begin\nt1: set a=2 x=2 [ops]\n---\nt1: engine set mvcc:TxnWrite(2, \"a\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02a\\x00\\x00\" → \"\"]\nt1: engine set mvcc:Version(\"a\", 2) → \"2\" [\"\\x04a\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x012\"]\nt1: engine set mvcc:TxnWrite(2, \"x\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02x\\x00\\x00\" → \"\"]\nt1: engine set mvcc:Version(\"x\", 2) → \"2\" [\"\\x04x\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x012\"]\n\nt1: scan\n---\nt1: \"a\" → \"2\"\nt1: \"b\" → \"1\"\nt1: \"x\" → \"2\"\n\n# Can write a new key, replace it, and be idempotent.\nt1: set c=1 c=2 c=2 [ops]\n---\nt1: engine set mvcc:TxnWrite(2, \"c\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02c\\x00\\x00\" → \"\"]\nt1: engine set mvcc:Version(\"c\", 2) → \"1\" [\"\\x04c\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x011\"]\nt1: engine set mvcc:TxnWrite(2, \"c\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02c\\x00\\x00\" → \"\"]\nt1: engine set mvcc:Version(\"c\", 2) → \"2\" [\"\\x04c\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x012\"]\nt1: engine set mvcc:TxnWrite(2, \"c\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02c\\x00\\x00\" → \"\"]\nt1: engine set mvcc:Version(\"c\", 2) → \"2\" [\"\\x04c\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x012\"]\n\nt1: scan\n---\nt1: \"a\" → \"2\"\nt1: \"b\" → \"1\"\nt1: \"c\" → \"2\"\nt1: \"x\" → \"2\"\n\ndump\n---\nmvcc:NextVersion → 3 [\"\\x00\" → \"\\x03\"]\nmvcc:TxnActive(2) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\"]\nmvcc:TxnWrite(2, \"a\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02a\\x00\\x00\" → \"\"]\nmvcc:TxnWrite(2, \"c\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02c\\x00\\x00\" → \"\"]\nmvcc:TxnWrite(2, \"x\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02x\\x00\\x00\" → \"\"]\nmvcc:Version(\"a\", 1) → \"1\" [\"\\x04a\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x011\"]\nmvcc:Version(\"a\", 2) → \"2\" [\"\\x04a\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x012\"]\nmvcc:Version(\"b\", 1) → \"1\" [\"\\x04b\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x011\"]\nmvcc:Version(\"c\", 2) → \"2\" [\"\\x04c\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x012\"]\nmvcc:Version(\"x\", 1) → None [\"\\x04x\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x00\"]\nmvcc:Version(\"x\", 2) → \"2\" [\"\\x04x\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x012\"]\n"
  },
  {
    "path": "src/storage/testscripts/mvcc/set_conflict",
    "content": "# Set should return serialization errors both for uncommitted versions\n# (past and future), and future committed versions.\n\nt1: begin\nt2: begin\nt3: begin\nt4: begin\n---\nok\n\nt1: set a=1\nt3: set c=3\nt4: set d=4\nt4: commit\n---\nok\n\nt2: !set a=2 # past uncommitted\nt2: !set c=2 # future uncommitted\nt2: !set d=2 # future committed\n---\nt2: Error: serialization failure, retry transaction\nt2: Error: serialization failure, retry transaction\nt2: Error: serialization failure, retry transaction\n\ndump\n---\nmvcc:NextVersion → 5 [\"\\x00\" → \"\\x05\"]\nmvcc:TxnActive(1) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\"]\nmvcc:TxnActive(2) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\"]\nmvcc:TxnActive(3) → \"\" [\"\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\"]\nmvcc:TxnActiveSnapshot(2) → {1} [\"\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\" → \"\\x01\\x01\"]\nmvcc:TxnActiveSnapshot(3) → {1,2} [\"\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x02\\x01\\x02\"]\nmvcc:TxnActiveSnapshot(4) → {1,2,3} [\"\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x03\\x01\\x02\\x03\"]\nmvcc:TxnWrite(1, \"a\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01a\\x00\\x00\" → \"\"]\nmvcc:TxnWrite(3, \"c\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03c\\x00\\x00\" → \"\"]\nmvcc:Version(\"a\", 1) → \"1\" [\"\\x04a\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x011\"]\nmvcc:Version(\"c\", 3) → \"3\" [\"\\x04c\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\" → \"\\x01\\x013\"]\nmvcc:Version(\"d\", 4) → \"4\" [\"\\x04d\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\" → \"\\x01\\x014\"]\n"
  },
  {
    "path": "src/storage/testscripts/mvcc/unversioned",
    "content": "# Tests unversioned keys.\n\n# Getting a missing unversioned key returns None.\nget_unversioned a\n---\n\"a\" → None\n\n# Setting and getting an unversioned key should work. Dump engine operations.\nset_unversioned a=0 [ops]\nget_unversioned a\n---\nengine set mvcc:Unversioned(\"a\") → \"0\" [\"\\x05a\\x00\\x00\" → \"0\"]\n\"a\" → \"0\"\n\n# Write some versioned keys with the same keys, interleaved between unversioned.\n# The raw engine writes show that the internal keys are different.\nt1: begin\nt1: set a=1 b=1 c=1 [ops]\nt1: commit\n---\nt1: engine set mvcc:TxnWrite(1, \"a\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01a\\x00\\x00\" → \"\"]\nt1: engine set mvcc:Version(\"a\", 1) → \"1\" [\"\\x04a\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x011\"]\nt1: engine set mvcc:TxnWrite(1, \"b\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01b\\x00\\x00\" → \"\"]\nt1: engine set mvcc:Version(\"b\", 1) → \"1\" [\"\\x04b\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x011\"]\nt1: engine set mvcc:TxnWrite(1, \"c\") → \"\" [\"\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01c\\x00\\x00\" → \"\"]\nt1: engine set mvcc:Version(\"c\", 1) → \"1\" [\"\\x04c\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\" → \"\\x01\\x011\"]\n\n# Set another unversioned key overlapping a versioned key.\nset_unversioned b=0 d=0 [ops]\n---\nengine set mvcc:Unversioned(\"b\") → \"0\" [\"\\x05b\\x00\\x00\" → \"0\"]\nengine set mvcc:Unversioned(\"d\") → \"0\" [\"\\x05d\\x00\\x00\" → \"0\"]\n\n# An MVCC scan shouldn't see the unversioned keys.\nt2: begin readonly\nt2: scan\n---\nt2: \"a\" → \"1\"\nt2: \"b\" → \"1\"\nt2: \"c\" → \"1\"\n\n# Unversioned gets should not see versioned keys.\nget_unversioned a b c d\n---\n\"a\" → \"0\"\n\"b\" → \"0\"\n\"c\" → None\n\"d\" → \"0\"\n\n# Replacing an unversioned key should work too.\nset_unversioned a=2 [ops]\nget_unversioned a\n---\nengine set mvcc:Unversioned(\"a\") → \"2\" [\"\\x05a\\x00\\x00\" → \"2\"]\n\"a\" → \"2\"\n"
  },
  {
    "path": "tests/scripts/anomalies",
    "content": "# Tests transaction anomalies. This is also tested at the MVCC and SQL\n# levels, but we may as well have an end-to-end test for them.\n#\n# Uses a single script to avoid cluster startup times for each test.\n\ncluster nodes=5\n---\nok\n\n> CREATE TABLE test (id INT PRIMARY KEY, value STRING)\n---\nok\n\n# Dirty read: when c2 can read an uncommitted value written by c1. Snapshot\n# isolation prevents this.\nc1:> BEGIN\nc1:> INSERT INTO test VALUES (1, 'a')\n---\nok\n\nc2:> BEGIN\nc2:> SELECT * FROM test WHERE id = 1\n---\nok\n\nc1:> ROLLBACK\nc2:> ROLLBACK\n---\nok\n\n# Dirty write: when c2 overwrites an uncommitted value written by c1. Snapshot\n# isolation prevents this.\n\nc1:> BEGIN\nc1:> INSERT INTO test VALUES (1, 'a')\n---\nok\n\nc2:> BEGIN\nc2:!> INSERT INTO test VALUES (1, 'a')\n---\nc2: Error: serialization failure, retry transaction\n\nc1:> ROLLBACK\nc2:> ROLLBACK\n---\nok\n\n# Fuzzy (or unrepeatable) read: when c2 sees a value change after c1 updates it.\n# Snapshot isolation prevents this.\n\n> INSERT INTO test VALUES (1, 'a')\n---\nok\n\nc1:> BEGIN\nc2:> BEGIN\n---\nok\n\nc2:> SELECT * FROM test WHERE id = 1\n---\nc2: 1, 'a'\n\nc1:> UPDATE test SET value = 'b' WHERE id = 1\nc1:> COMMIT\nc1:> SELECT * FROM test\n---\nc1: 1, 'b'\n\nc2:> SELECT * FROM test WHERE id = 1\n---\nc2: 1, 'a'\n\nc2:> ROLLBACK\n> DELETE FROM test\n---\nok\n\n# Lost update: when c1 and c2 both read a value and update it, where c2's update\n# replaces c1. Snapshot isolation prevents this.\n\nc1:> BEGIN\nc1:> SELECT * FROM test WHERE id = 1\n---\nok\n\nc2:> BEGIN\nc2:> SELECT * FROM test WHERE id = 1\n---\nok\n\nc1:> INSERT INTO test VALUES (1, 'a')\nc1:> COMMIT\n---\nok\n\nc2:!> INSERT INTO test VALUES (1, 'a')\n---\nc2: Error: serialization failure, retry transaction\n\nc2:> ROLLBACK\n> DELETE FROM test\n---\nok\n\n# Phantom read: when c1 reads entries matching some predicate, but a\n# modification by c2 changes which entries match the predicate such that a later\n# read by c1 returns them. Snapshot isolation prevents this.\n\n> INSERT INTO test VALUES (1, 'a'), (2, 'b'), (3, 'c')\n---\nok\n\nc1:> BEGIN\nc2:> BEGIN\n---\nok\n\nc1:> SELECT * FROM test WHERE id > 1\n---\nc1: 2, 'b'\nc1: 3, 'c'\n\nc2:> DELETE FROM test WHERE id = 2\nc2:> INSERT INTO test VALUES (4, 'd')\nc2:> COMMIT\n---\nok\n\nc1:> SELECT * FROM test WHERE id > 1\n---\nc1: 2, 'b'\nc1: 3, 'c'\n\nc1:> ROLLBACK\n> DELETE FROM test\n---\nok\n\n# Read skew: when c1 reads a and b, but c2 modifies b in between the reads.\n# Snapshot isolation prevents this.\n\n> INSERT INTO test VALUES (1, 'a'), (2, 'b')\n---\nok\n\nc1:> BEGIN\nc2:> BEGIN\n---\nok\n\nc1:> SELECT * FROM test WHERE id = 1\n---\nc1: 1, 'a'\n\nc2:> UPDATE test SET value = 'b' WHERE id = 1\nc2:> UPDATE test SET value = 'a' WHERE id = 2\nc2:> COMMIT\n---\nok\n\nc1:> SELECT * FROM test WHERE id = 2\n---\nc1: 2, 'b'\n\nc1:> ROLLBACK\n> DELETE FROM test\n---\nok\n\n# Write skew: when c1 reads a and writes it to b while c2 reads b and writes it\n# to a. Snapshot isolation does not prevent this, which is expected, so we\n# assert the anomalous behavior. Fixing this would require implementing\n# serializable snapshot isolation.\n\n> INSERT INTO test VALUES (1, 'a'), (2, 'b')\n---\nok\n\nc1:> BEGIN\nc2:> BEGIN\n---\nok\n\nc1:> SELECT * FROM test WHERE id = 1\nc2:> SELECT * FROM test WHERE id = 2\n---\nc1: 1, 'a'\nc2: 2, 'b'\n\nc1:> UPDATE test SET value = 'a' WHERE id = 2\nc2:> UPDATE test SET value = 'b' WHERE id = 1\n---\nok\n\nc1:> COMMIT\nc2:> COMMIT\n---\nok\n\n> SELECT * FROM test\n---\n1, 'b'\n2, 'a'\n"
  },
  {
    "path": "tests/scripts/client",
    "content": "# Tests various client operations.\n#\n# Uses a single-node cluster for determinism.\n\ncluster nodes=1\n---\nok\n\n# Add some tables and data.\n> CREATE TABLE countries (id STRING PRIMARY KEY, name STRING NOT NULL)\n> INSERT INTO countries VALUES ('fr', 'France'), ('ru', 'Russia'), ('us', 'United States of America')\n> CREATE TABLE genres (id INTEGER PRIMARY KEY, name STRING NOT NULL)\n> INSERT INTO genres VALUES (1, 'Science Fiction'), (2, 'Action'), (3, 'Comedy')\n> CREATE TABLE studios (id INTEGER PRIMARY KEY, name STRING NOT NULL, country_id STRING REFERENCES countries)\n> INSERT INTO studios VALUES (1, 'Mosfilm', 'ru'), (2, 'Lionsgate', 'us'), (3, 'StudioCanal', 'fr'), (4, 'Warner Bros', 'us')\n> CREATE TABLE movies ( \\\n    id INTEGER PRIMARY KEY, \\\n    title STRING NOT NULL, \\\n    studio_id INTEGER NOT NULL REFERENCES studios, \\\n    genre_id INTEGER NOT NULL REFERENCES genres, \\\n    released INTEGER NOT NULL, \\\n    rating FLOAT, \\\n    ultrahd BOOLEAN \\\n)\n> INSERT INTO movies VALUES \\\n    (1, 'Stalker', 1, 1, 1979, 8.2, NULL), \\\n    (2, 'Sicario', 2, 2, 2015, 7.6, TRUE), \\\n    (3, 'Primer', 3, 1, 2004, 6.9, NULL), \\\n    (4, 'Heat', 4, 2, 1995, 8.2, TRUE), \\\n    (5, 'The Fountain', 4, 1, 2006, 7.2, FALSE), \\\n    (6, 'Solaris', 1, 1, 1972, 8.1, NULL), \\\n    (7, 'Gravity', 4, 1, 2013, 7.7, TRUE), \\\n    (8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE), \\\n    (9, 'Birdman', 4, 3, 2014, 7.7, TRUE), \\\n    (10, 'Inception', 4, 1, 2010, 8.8, TRUE)\n---\nok\n\n# List the tables and display table schemas. Error on missing table.\ntables\n---\ncountries\ngenres\nmovies\nstudios\n\ntable movies\n---\nCREATE TABLE movies (\n  id INTEGER PRIMARY KEY,\n  title STRING NOT NULL,\n  studio_id INTEGER NOT NULL INDEX REFERENCES studios,\n  genre_id INTEGER NOT NULL INDEX REFERENCES genres,\n  released INTEGER NOT NULL,\n  rating FLOAT DEFAULT NULL,\n  ultrahd BOOLEAN DEFAULT NULL\n)\n\ntable movies raw=true\n---\nTable {\n    name: \"movies\",\n    primary_key: 0,\n    columns: [\n        Column {\n            name: \"id\",\n            datatype: Integer,\n            nullable: false,\n            default: None,\n            unique: true,\n            index: false,\n            references: None,\n        },\n        Column {\n            name: \"title\",\n            datatype: String,\n            nullable: false,\n            default: None,\n            unique: false,\n            index: false,\n            references: None,\n        },\n        Column {\n            name: \"studio_id\",\n            datatype: Integer,\n            nullable: false,\n            default: None,\n            unique: false,\n            index: true,\n            references: Some(\n                \"studios\",\n            ),\n        },\n        Column {\n            name: \"genre_id\",\n            datatype: Integer,\n            nullable: false,\n            default: None,\n            unique: false,\n            index: true,\n            references: Some(\n                \"genres\",\n            ),\n        },\n        Column {\n            name: \"released\",\n            datatype: Integer,\n            nullable: false,\n            default: None,\n            unique: false,\n            index: false,\n            references: None,\n        },\n        Column {\n            name: \"rating\",\n            datatype: Float,\n            nullable: true,\n            default: Some(\n                Null,\n            ),\n            unique: false,\n            index: false,\n            references: None,\n        },\n        Column {\n            name: \"ultrahd\",\n            datatype: Boolean,\n            nullable: true,\n            default: Some(\n                Null,\n            ),\n            unique: false,\n            index: false,\n            references: None,\n        },\n    ],\n}\n\ntable countries\ntable genres\ntable studios\n---\nCREATE TABLE countries (\n  id STRING PRIMARY KEY,\n  name STRING NOT NULL\n)\nCREATE TABLE genres (\n  id INTEGER PRIMARY KEY,\n  name STRING NOT NULL\n)\nCREATE TABLE studios (\n  id INTEGER PRIMARY KEY,\n  name STRING NOT NULL,\n  country_id STRING DEFAULT NULL INDEX REFERENCES countries\n)\n\n!table missing\n---\nError: invalid input: table missing does not exist\n\n# Fetch server status.\nstatus\n---\nStatus {\n    server: 1,\n    raft: Status {\n        leader: 1,\n        term: 1,\n        match_index: {\n            1: 25,\n        },\n        commit_index: 25,\n        applied_index: 25,\n        storage: Status {\n            name: \"bitcask\",\n            keys: 27,\n            size: 1169,\n            disk_size: 1649,\n            live_disk_size: 1385,\n        },\n    },\n    mvcc: Status {\n        versions: 8,\n        active_txns: 0,\n        storage: Status {\n            name: \"bitcask\",\n            keys: 36,\n            size: 2177,\n            disk_size: 8259,\n            live_disk_size: 2465,\n        },\n    },\n}\n"
  },
  {
    "path": "tests/scripts/errors",
    "content": "# Tests various error handling.\n\ncluster nodes=5\n---\nok\n\n# A transaction can continue and commit after encountering an error.\n> BEGIN\n> CREATE TABLE test (id INT PRIMARY KEY, value STRING)\n> INSERT INTO test VALUES (1, 'a')\n!> INSERT INTO test VALUES (NULL, 'b')\n> INSERT INTO test VALUES (2, 'b')\n> COMMIT\n---\nError: invalid input: invalid primary key NULL\n\n> SELECT * FROM test\n---\n1, 'a'\n2, 'b'\n\n# Closing/disconnecting a client rolls back an open transaction.\nc1:> BEGIN\nc1:> INSERT INTO test VALUES (3, 'c')\n---\nok\n\nc2:!> INSERT INTO test VALUES (3, 'c')\n---\nc2: Error: serialization failure, retry transaction\n\nc1:> close\n---\nok\n\nc2:> INSERT INTO test VALUES (3, 'c')\nc2:> SELECT * FROM test\n---\nc2: 1, 'a'\nc2: 2, 'b'\nc2: 3, 'c'\n"
  },
  {
    "path": "tests/scripts/isolation",
    "content": "# Tests transaction isolation.\n#\n# Transactions are tested more thoroughly in the MVCC tests, this just does some\n# basic SQL-level testing.\n#\n# Sets up a sequence of transactions that each perform a write, and checks\n# what they can see.\n#\n# c1: past, committed before c4 began\n# c2: past, commits after c4 began\n# c3: past, uncommitted\n# c4: test transaction\n# c5: future, committed\n# c6: future, uncommitted\n# c7: future, AS OF version 4\n\ncluster nodes=5\n---\nok\n\n# c1: past, committed before c4 began\nc1:> BEGIN\nc1:> CREATE TABLE test (id INT PRIMARY KEY, value STRING)\nc1:> INSERT INTO test VALUES (1, 'a')\nc1:> COMMIT\n---\nok\n\n# c2: past, commits after c4 began\nc2:> BEGIN\nc2:> INSERT INTO test VALUES (2, 'b')\n---\nok\n\n# c3: past, uncommitted\nc3:> BEGIN\nc3:> INSERT INTO test VALUES (3, 'c')\n---\nok\n\n# c4: test transaction\nc4:[result]> BEGIN\nc4:> INSERT INTO test VALUES (4, 'd')\n---\nc4: Begin(TransactionState { version: 4, read_only: false, active: {2, 3} })\n\n# Commit c2.\nc2:> COMMIT\n---\nok\n\n# c5: future, committed\nc5:> BEGIN\nc5:> INSERT INTO test VALUES (5, 'e')\nc5:> COMMIT\n---\nok\n\n# c6: future, uncommitted\nc6:> BEGIN\nc6:> INSERT INTO test VALUES (6, 'f')\n---\nok\n\n# When c4 scans, it should only see the write of c1 and itself.\nc4:> SELECT * FROM test\n---\nc4: 1, 'a'\nc4: 4, 'd'\n\n# An AS OF transaction in version 4 should not see c4's uncomitted write.\nc7:> BEGIN READ ONLY AS OF SYSTEM TIME 4\nc7:> SELECT * FROM test\nc7:> ROLLBACK\n---\nc7: 1, 'a'\n\n# c4 can commit.\nc4:> COMMIT\n---\nok\n\n# An implicit transaction should see c1, c2, c4, c5:\n> SELECT * FROM test\n---\n1, 'a'\n2, 'b'\n4, 'd'\n5, 'e'\n\n# An AS OF transaction in version 4 should not see c4's write even after it\n# has committed, such that it's consistent with the previous AS OF 4. The\n# snapshot is taken out at the start of the version.\nc7:> BEGIN READ ONLY AS OF SYSTEM TIME 4\nc7:> SELECT * FROM test\nc7:> ROLLBACK\n---\nc7: 1, 'a'\n"
  },
  {
    "path": "tests/scripts/queries",
    "content": "# Tests some basic queries. This is more thorougly tested in the SQL tests, this\n# just tries a few basic things.\n\ncluster nodes=5\n---\nok\n\n# Add a movie dataset.\n> CREATE TABLE countries (id STRING PRIMARY KEY, name STRING NOT NULL)\n> INSERT INTO countries VALUES ('fr', 'France'), ('ru', 'Russia'), ('us', 'United States of America')\n> CREATE TABLE genres (id INTEGER PRIMARY KEY, name STRING NOT NULL)\n> INSERT INTO genres VALUES (1, 'Science Fiction'), (2, 'Action'), (3, 'Comedy')\n> CREATE TABLE studios (id INTEGER PRIMARY KEY, name STRING NOT NULL, country_id STRING REFERENCES countries)\n> INSERT INTO studios VALUES (1, 'Mosfilm', 'ru'), (2, 'Lionsgate', 'us'), (3, 'StudioCanal', 'fr'), (4, 'Warner Bros', 'us')\n> CREATE TABLE movies ( \\\n    id INTEGER PRIMARY KEY, \\\n    title STRING NOT NULL, \\\n    studio_id INTEGER NOT NULL REFERENCES studios, \\\n    genre_id INTEGER NOT NULL REFERENCES genres, \\\n    released INTEGER NOT NULL, \\\n    rating FLOAT, \\\n    ultrahd BOOLEAN \\\n)\n> INSERT INTO movies VALUES \\\n    (1, 'Stalker', 1, 1, 1979, 8.2, NULL), \\\n    (2, 'Sicario', 2, 2, 2015, 7.6, TRUE), \\\n    (3, 'Primer', 3, 1, 2004, 6.9, NULL), \\\n    (4, 'Heat', 4, 2, 1995, 8.2, TRUE), \\\n    (5, 'The Fountain', 4, 1, 2006, 7.2, FALSE), \\\n    (6, 'Solaris', 1, 1, 1972, 8.1, NULL), \\\n    (7, 'Gravity', 4, 1, 2013, 7.7, TRUE), \\\n    (8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE), \\\n    (9, 'Birdman', 4, 3, 2014, 7.7, TRUE), \\\n    (10, 'Inception', 4, 1, 2010, 8.8, TRUE)\n---\nok\n\n# Full table scan and point queries. With/without column headers.\n[header]> SELECT * FROM movies\n---\nmovies.id, movies.title, movies.studio_id, movies.genre_id, movies.released, movies.rating, movies.ultrahd\n1, 'Stalker', 1, 1, 1979, 8.2, NULL\n2, 'Sicario', 2, 2, 2015, 7.6, TRUE\n3, 'Primer', 3, 1, 2004, 6.9, NULL\n4, 'Heat', 4, 2, 1995, 8.2, TRUE\n5, 'The Fountain', 4, 1, 2006, 7.2, FALSE\n6, 'Solaris', 1, 1, 1972, 8.1, NULL\n7, 'Gravity', 4, 1, 2013, 7.7, TRUE\n8, 'Blindspotting', 2, 3, 2018, 7.4, TRUE\n9, 'Birdman', 4, 3, 2014, 7.7, TRUE\n10, 'Inception', 4, 1, 2010, 8.8, TRUE\n\n> SELECT * FROM genres WHERE id = 2\n---\n2, 'Action'\n\n# Aggregate query.\n[header]> SELECT s.name AS studio, COUNT(*) AS movies, AVG(m.rating) AS rating \\\n    FROM movies m JOIN studios s ON m.studio_id = s.id \\\n    GROUP BY s.name ORDER BY rating DESC\n---\nstudio, movies, rating\n'Mosfilm', 2, 8.149999999999999\n'Warner Bros', 5, 7.919999999999999\n'Lionsgate', 2, 7.5\n'StudioCanal', 1, 6.9\n\n# Try a complex multi-way join with multiple joins of the same table. Uses GROUP\n# BY to discard duplicates from the cross join. The query finds all movies\n# belonging to a studio that's released at least one movies rated 8 or higher.\n> SELECT m.id, m.title, g.name AS genre, s.name AS studio, m.rating \\\n  FROM movies m JOIN genres g ON m.genre_id = g.id, \\\n    studios s JOIN movies good ON good.studio_id = s.id AND good.rating >= 8 \\\n  WHERE m.studio_id = s.id \\\n  GROUP BY m.id, m.title, g.name, s.name, m.rating, m.released \\\n  ORDER BY m.rating DESC, m.released ASC, m.id ASC\n---\n10, 'Inception', 'Science Fiction', 'Warner Bros', 8.8\n1, 'Stalker', 'Science Fiction', 'Mosfilm', 8.2\n4, 'Heat', 'Action', 'Warner Bros', 8.2\n6, 'Solaris', 'Science Fiction', 'Mosfilm', 8.1\n7, 'Gravity', 'Science Fiction', 'Warner Bros', 7.7\n9, 'Birdman', 'Comedy', 'Warner Bros', 7.7\n5, 'The Fountain', 'Science Fiction', 'Warner Bros', 7.2\n\n# Explain that query.\n> EXPLAIN SELECT m.id, m.title, g.name AS genre, s.name AS studio, m.rating \\\n  FROM movies m JOIN genres g ON m.genre_id = g.id, \\\n    studios s JOIN movies good ON good.studio_id = s.id AND good.rating >= 8 \\\n  WHERE m.studio_id = s.id \\\n  GROUP BY m.id, m.title, g.name, s.name, m.rating, m.released \\\n  ORDER BY m.rating DESC, m.released ASC, m.id ASC\n---\nRemap: m.id, m.title, genre, studio, m.rating (dropped: m.released)\n└─ Order: m.rating desc, m.released asc, m.id asc\n   └─ Projection: m.id, m.title, g.name as genre, s.name as studio, m.rating, m.released\n      └─ Aggregate: m.id, m.title, g.name, s.name, m.rating, m.released\n         └─ HashJoin: inner on m.studio_id = s.id\n            ├─ HashJoin: inner on m.genre_id = g.id\n            │  ├─ Scan: movies as m\n            │  └─ Scan: genres as g\n            └─ HashJoin: inner on s.id = good.studio_id\n               ├─ Scan: studios as s\n               └─ Scan: movies as good (good.rating > 8 OR good.rating = 8)\n"
  },
  {
    "path": "tests/testcluster.rs",
    "content": "use std::collections::BTreeMap;\nuse std::error::Error;\nuse std::fmt::Write as _;\nuse std::path::Path;\nuse std::time::Duration;\n\nuse rand::RngExt as _;\n\nuse toydb::Client;\nuse toydb::raft::NodeID;\n\n/// Timeout for node readiness.\nconst TIMEOUT: Duration = Duration::from_secs(5);\n\n/// The base SQL port (+id).\nconst SQL_BASE_PORT: u16 = 19600;\n\n/// The base Raft port (+id).\nconst RAFT_BASE_PORT: u16 = 19700;\n\n/// Runs a toyDB cluster using the built binary in a temporary directory. The\n/// cluster will be killed and removed when dropped.\n///\n/// This runs the cluster as child processes using the built binary instead of\n/// spawning in-memory threads for a couple of reasons: it avoids having to\n/// gracefully shut down the server (which is complicated by e.g.\n/// TcpListener::accept() not being interruptable), and it tests the entire\n/// server (and eventually the toySQL client) end-to-end.\npub struct TestCluster {\n    servers: BTreeMap<NodeID, TestServer>,\n    #[allow(dead_code)]\n    dir: tempfile::TempDir, // deleted when dropped\n}\n\ntype NodePorts = BTreeMap<NodeID, (u16, u16)>; // raft,sql on localhost\n\nimpl TestCluster {\n    /// Runs and returns a test cluster. It keeps running until dropped.\n    pub fn run(nodes: u8) -> Result<Self, Box<dyn Error>> {\n        // Create temporary directory.\n        let dir = tempfile::TempDir::with_prefix(\"toydb\")?;\n\n        // Allocate port numbers for nodes.\n        let ports: NodePorts = (1..=nodes)\n            .map(|id| (id, (RAFT_BASE_PORT + id as u16, SQL_BASE_PORT + id as u16)))\n            .collect();\n\n        // Start nodes.\n        let mut servers = BTreeMap::new();\n        for id in 1..=nodes {\n            let dir = dir.path().join(format!(\"toydb{id}\"));\n            servers.insert(id, TestServer::run(id, &dir, &ports)?);\n        }\n\n        // Wait for the nodes to be ready, by fetching the server status.\n        let started = std::time::Instant::now();\n        for server in servers.values_mut() {\n            while let Err(error) = server.connect().and_then(|mut c| Ok(c.status()?)) {\n                server.assert_alive();\n                if started.elapsed() >= TIMEOUT {\n                    return Err(error);\n                }\n                std::thread::sleep(Duration::from_millis(200));\n            }\n        }\n\n        Ok(Self { servers, dir })\n    }\n\n    /// Connects to a random cluster node using a Rust client. Testing with\n    /// toysql is too annoying, since we have to deal with rustyline, PTYs,\n    /// echoing, multiline editing, etc.\n    pub fn connect(&self) -> Result<Client, Box<dyn Error>> {\n        let id = rand::rng().random_range(1..=self.servers.len()) as NodeID;\n        self.servers.get(&id).unwrap().connect()\n    }\n}\n\n/// A toyDB server.\npub struct TestServer {\n    id: NodeID,\n    child: std::process::Child,\n    sql_port: u16,\n}\n\nimpl TestServer {\n    /// Runs a toyDB server.\n    fn run(id: NodeID, dir: &Path, ports: &NodePorts) -> Result<Self, Box<dyn Error>> {\n        // Build and write the configuration file.\n        let configfile = dir.join(\"toydb.yaml\");\n        std::fs::create_dir_all(dir)?;\n        std::fs::write(&configfile, Self::build_config(id, dir, ports)?)?;\n\n        // Build the binary.\n        //\n        // TODO: this may contribute to slow tests, consider building once.\n        let build = escargot::CargoBuild::new().bin(\"toydb\").run()?;\n\n        // Spawn process. Discard output.\n        let child = build\n            .command()\n            .args([\"-c\", &configfile.to_string_lossy()])\n            .stdout(std::process::Stdio::null())\n            .stderr(std::process::Stdio::null())\n            .spawn()?;\n\n        let (_, sql_port) = ports.get(&id).copied().expect(\"node not in ports\");\n        Ok(Self { id, child, sql_port })\n    }\n\n    /// Generates a config file for the given node.\n    fn build_config(id: NodeID, dir: &Path, ports: &NodePorts) -> Result<String, Box<dyn Error>> {\n        let (raft_port, sql_port) = ports.get(&id).expect(\"node not in ports\");\n        let mut cfg = String::new();\n        writeln!(cfg, \"id: {id}\")?;\n        writeln!(cfg, \"data_dir: {}\", dir.to_string_lossy())?;\n        writeln!(cfg, \"listen_raft: localhost:{raft_port}\")?;\n        writeln!(cfg, \"listen_sql: localhost:{sql_port}\")?;\n        write!(cfg, \"peers: {{\")?;\n        if ports.len() > 1 {\n            writeln!(cfg)?;\n        }\n        for (peer_id, (peer_raft_port, _)) in ports.iter().filter(|(peer, _)| **peer != id) {\n            write!(cfg, \"  '{peer_id}': localhost:{peer_raft_port},\")?;\n        }\n        writeln!(cfg, \"}}\")?;\n        Ok(cfg)\n    }\n\n    /// Asserts that the server is still running.\n    fn assert_alive(&mut self) {\n        if let Some(status) = self.child.try_wait().expect(\"failed to check exit status\") {\n            panic!(\"node {id} exited with {status}\", id = self.id)\n        }\n    }\n\n    /// Connects to the server using a regular client.\n    fn connect(&self) -> Result<Client, Box<dyn Error>> {\n        Ok(Client::connect((\"localhost\", self.sql_port))?)\n    }\n}\n\nimpl Drop for TestServer {\n    // Kills the child process when dropped.\n    fn drop(&mut self) {\n        self.child.kill().expect(\"failed to kill node\");\n        self.child.wait().expect(\"failed to wait for node to terminate\");\n    }\n}\n"
  },
  {
    "path": "tests/tests.rs",
    "content": "//! A basic set of end-to-end tests as Goldenscripts under tests/scripts/. These\n//! spin up actual clusters using the built binary and run operations against\n//! them from multiple clients.\n//!\n//! There are more comprehensive tests elsewhere in the codebase, see the various\n//! src/*/testscript scripts.\n\n#![warn(clippy::all)]\n\nmod testcluster;\n\nuse std::collections::HashMap;\nuse std::error::Error;\nuse std::fmt::Write as _;\nuse std::path::Path;\nuse std::sync::{LazyLock, Mutex};\n\nuse itertools::Itertools as _;\nuse test_each_file::test_each_path;\n\nuse testcluster::TestCluster;\nuse toydb::{Client, StatementResult};\n\n// Run goldenscript tests in tests/scripts.\ntest_each_path! { in \"tests/scripts\" => test_goldenscript }\n\nfn test_goldenscript(path: &Path) {\n    // We can't run tests concurrently, because the test clusters end up using\n    // the same ports. We also don't want to run a bunch of them concurrently.\n    // We can't use the #[serial_test] macro either, since it doesn't work with\n    // test_each_path. Just use a mutex to serialize them.\n    static MUTEX: LazyLock<Mutex<()>> = LazyLock::new(Mutex::default);\n    let _guard = MUTEX.lock().ok(); // ignore poisoning\n\n    goldenscript::run(&mut Runner::new(), path).expect(\"goldenscript failed\")\n}\n\n/// Runs Raft goldenscript tests. See run() for available commands.\n#[derive(Default)]\nstruct Runner {\n    cluster: Option<TestCluster>,\n    clients: HashMap<String, Client>,\n}\n\nimpl Runner {\n    fn new() -> Self {\n        Self::default()\n    }\n\n    /// Fetches a client for the given prefix, or creates a new one.\n    fn get_client(&mut self, prefix: &Option<String>) -> Result<&mut Client, Box<dyn Error>> {\n        let name = Self::client_name(prefix);\n        if !self.clients.contains_key(name) {\n            let Some(cluster) = self.cluster.as_mut() else {\n                return Err(\"no cluster\".into());\n            };\n            let client = cluster.connect()?;\n            self.clients.insert(name.to_string(), client);\n        }\n        Ok(self.clients.get_mut(name).expect(\"no client\"))\n    }\n\n    /// Returns a client name for a prefix.\n    fn client_name(prefix: &Option<String>) -> &str {\n        prefix.as_deref().unwrap_or_default()\n    }\n}\n\nimpl goldenscript::Runner for Runner {\n    /// Runs a goldenscript command.\n    fn run(&mut self, command: &goldenscript::Command) -> Result<String, Box<dyn Error>> {\n        let mut output = String::new();\n        let mut tags = command.tags.clone();\n\n        // Handle simple, non-SQL commands.\n        match command.name.as_str() {\n            // close\n            \"close\" => {\n                command.consume_args().reject_rest()?;\n                let name = Self::client_name(&command.prefix);\n                if self.clients.remove(name).is_none() {\n                    return Err(\"no client to close\".into());\n                }\n                return Ok(output);\n            }\n\n            // cluster nodes=N\n            \"cluster\" => {\n                let mut args = command.consume_args();\n                let nodes = args.lookup_parse(\"nodes\")?.unwrap_or(0);\n                args.reject_rest()?;\n                if self.cluster.is_some() {\n                    return Err(\"cluster already exists\".into());\n                }\n                self.cluster = Some(TestCluster::run(nodes)?);\n                return Ok(output);\n            }\n\n            // status\n            \"status\" => {\n                command.consume_args().reject_rest()?;\n                let status = self.get_client(&command.prefix)?.status()?;\n                write!(output, \"{status:#?}\")?;\n                return Ok(output);\n            }\n\n            // table [TABLE]\n            \"table\" => {\n                let mut args = command.consume_args();\n                let name = &args.next_pos().ok_or(\"table not given\")?.value;\n                let raw = args.lookup_parse(\"raw\")?.unwrap_or(false);\n                args.reject_rest()?;\n                let table = self.get_client(&command.prefix)?.get_table(name)?;\n                if raw {\n                    write!(output, \"{table:#?}\")?;\n                } else {\n                    write!(output, \"{table}\")?;\n                }\n                return Ok(output);\n            }\n\n            // tables\n            \"tables\" => {\n                command.consume_args().reject_rest()?;\n                let tables = self.get_client(&command.prefix)?.list_tables()?;\n                for table in tables {\n                    writeln!(output, \"{table}\")?;\n                }\n                return Ok(output);\n            }\n\n            _ => {}\n        }\n\n        // Otherwise, interpret the entire command as a SQL statement.\n        if !command.args.is_empty() {\n            return Err(\"statements should be given as a command with no args\".into());\n        }\n        let client = self.get_client(&command.prefix)?;\n        let input = &command.name;\n\n        // Execute the command and display the result if requested.\n        // SELECT and EXPLAIN results are always output.\n        let result = client.execute(input)?;\n\n        match result {\n            StatementResult::Select { columns, rows } => {\n                if tags.remove(\"header\") {\n                    writeln!(output, \"{}\", columns.into_iter().join(\", \"))?;\n                }\n                for row in rows {\n                    writeln!(output, \"{}\", row.into_iter().join(\", \"))?;\n                }\n            }\n            StatementResult::Explain(root) => writeln!(output, \"{root}\")?,\n            result if tags.remove(\"result\") => writeln!(output, \"{result:?}\")?,\n            _ => {}\n        }\n\n        if let Some(tag) = tags.iter().next() {\n            return Err(format!(\"invalid tag {tag}\").into());\n        }\n\n        Ok(output)\n    }\n}\n"
  }
]