diff --git a/.github/scripts/readme_bench.py b/.github/scripts/readme_bench.py index 678b99c..7077bd4 100644 --- a/.github/scripts/readme_bench.py +++ b/.github/scripts/readme_bench.py @@ -1,4 +1,8 @@ -"""Rewrite the README bench section from the newest main run. +"""Rewrite the published bench numbers from the newest main run. + +Rewrites the marked section of README.md and the marked blocks of +web/index.html from the same result artifacts, so the site and the readme +can never disagree. Usage: readme_bench.py """ @@ -90,6 +94,182 @@ def field_table(profiles, stamp): return lines +SITE = pathlib.Path("web/index.html") +FIELD_BEGIN = "" +FIELD_END = "" +MATRIX_BEGIN = "" +MATRIX_END = "" + +ENGINE_NAMES = { + "quantadb": "QuantaDB", + "postgres18": "PostgreSQL 18", + "mariadb114": "MariaDB 11.4", + "mysql84": "MySQL 8.4", +} +WORKLOAD_LABELS = {"reads": "Reads", "mixed": "Mixed", "writes": "Writes"} + +# A profile is called out when its writes fall under a quarter of the best +# profile's, or its write tail passes 20 ms. Both are visible on the page +# rather than dropped. +BAD_WRITE_SHARE = 0.25 +BAD_P99_MS = 20.0 + + +def value(records, workload, engine=None): + record = records.get((engine, workload)) + return None if record is None else record["ops_per_s"] + + +def field_html(profiles, stamp, sha): + records = profiles.get(FIELD_PROFILE) + if not records: + return None + + engines = [e for e in FIELD_ENGINES if any(key[0] == e for key in records)] + if not engines: + return None + + peak = {} + for workload in WORKLOADS: + found = [value(records, workload, e) for e in engines] + found = [v for v in found if v is not None] + peak[workload] = max(found) if found else None + + rows = [ + '
', + ' ", + ] + + for engine in engines: + self_row = engine == "quantadb" + rows.append("") + rows.append( + f'
' + ) + rows.append( + f' {ENGINE_NAMES.get(engine, engine)}' + ) + for workload in WORKLOADS: + found = value(records, workload, engine) + best = peak[workload] + label = WORKLOAD_LABELS[workload] + if found is None or not best: + rows.append( + f' ' + f'-' + ) + continue + width = f"{found / best * 100:.1f}".rstrip("0").rstrip(".") + leads = "" + if found == best: + leads = ' data-leads="yes"' if self_row else ' data-leads="rival"' + rows.append( + f' ' + ) + rows.append(f' ') + rows.append(f" {found:,}") + rows.append(" ") + rows.append("
") + + rows.append("
") + rows.extend( + [ + "", + '
', + f"
Measured
{stamp}
", + f"
Commit
{sha[:9]}
", + "
Limits
2 CPU, 2 GiB
", + "
Load
4 conn, 10 s
", + "
", + ] + ) + return "\n".join(rows) + + +def matrix_html(profiles, sha): + if not profiles: + return None + + ordered = sorted( + profiles.items(), + key=lambda item: value(item[1], "writes") or 0, + reverse=True, + ) + writes = [value(records, "writes") or 0 for _, records in ordered] + best_writes = max(writes) if writes else 0 + + rows = [ + '
', + ' ', + " ", + " ", + " ", + ' ', + ' ', + ' ', + ' ', + ' ', + " ", + " ", + " ", + ] + + for name, records in ordered: + write_ops = value(records, "writes") + tail = wp99(records, "writes") + bad = False + if write_ops is not None and best_writes: + bad = write_ops < best_writes * BAD_WRITE_SHARE + if tail != "-" and float(tail) > BAD_P99_MS: + bad = True + cells = [] + for workload in WORKLOADS: + found = value(records, workload) + cells.append("-" if found is None else f"{found:,}") + marker = ' class="is-bad"' if bad else "" + unit = "" if tail == "-" else " ms" + rows.append(f" ") + rows.append(f' ') + rows.append( + f" " + f"" + ) + rows.append(" ") + + rows.extend([" ", "
QuantaDB on GitHub runners, measured on main at " + f"{sha[:9]}
ProfileReadsMixedWritesWrite p99
{name}{cells[0]}{cells[1]}{cells[2]}{tail}{unit}
", "
"]) + return "\n".join(rows) + + +def splice(text, begin, end, body): + head, rest = text.split(begin, 1) + middle, tail = rest.split(end, 1) + trailing = middle.rsplit("\n", 1)[-1] + indent = trailing if trailing.strip() == "" else "" + return f"{head}{begin}\n{body}\n{indent}{end}{tail}" + + +def publish_site(results, reference, stamp, sha): + if not SITE.exists(): + return + text = SITE.read_text(encoding="utf-8") + + field = field_html(reference, stamp, sha) + if field is not None: + text = splice(text, FIELD_BEGIN, FIELD_END, field) + + matrix = matrix_html(results, sha) + if matrix is not None: + text = splice(text, MATRIX_BEGIN, MATRIX_END, matrix) + + SITE.write_text(text, encoding="utf-8") + + def main(): results_dir, reference_dir, stamp, sha = ( sys.argv[1], @@ -113,5 +293,7 @@ def main(): _, tail = rest.split(END, 1) readme.write_text(head + "\n".join(section) + tail, encoding="utf-8") + publish_site(results, reference, stamp, sha) + main() diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 4746e81..ade0583 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -327,12 +327,12 @@ jobs: python .github/scripts/readme_bench.py results reference "$stamp" "$GITHUB_SHA" - name: Commit if changed run: | - if git diff --quiet README.md; then - echo "README already current" + if git diff --quiet README.md web/index.html; then + echo "published numbers already current" exit 0 fi git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add README.md - git commit -m "Refresh README bench numbers [skip ci]" + git add README.md web/index.html + git commit -m "Refresh published bench numbers [skip ci]" git push diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..760f3fe --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,61 @@ +# QuantaDB design system + +Scope: the static marketing site in `web/`. Written 2026-07-25 with the +rebuild of `web/index.html`. + +## Direction contract + +**Thesis.** The comparison table is the argument. It sits in the first +viewport at full width, carries its own provenance, and shows the workloads +QuantaDB loses as plainly as the ones it wins. It refuses the arrangement +this category ships by default, where a claim headline sits above three +feature cards and the measurements are a link. + +**Own world.** Near-black ground, one violet accent, one neutral ramp, and +figures set in mono on a shared baseline. Data bars are structural, not +decorative: every bar is a real ratio against the leader of its own column. +No gradients, no glass, no rounded card stacks. + +**Story.** A skeptical engineer sees four engines measured under identical +limits, finds the commit and date attached, sees QuantaDB lose on writes, +and concludes the numbers were not selected for flattery. + +**First viewport.** Wordmark and nav, one headline line, one supporting +line, two actions, then the field table edge to edge with its provenance +strip. The primary action sits above the table on desktop and directly +under the headline on mobile. + +**Form.** Developer infrastructure convention, played straight, at the craft +level of Neon, Turso and ClickHouse. Chosen by the user as the standing exit +from the direction roll on 2026-07-25. + +## Color + +Ground `#0B0C0E`. Raised surface `#131519`. Rule `#23262C`. Body text +`#E6E8EC`. Secondary text `#9AA1AC`. Accent `#9B7BFF` for interactive and +data, brand `#7132F5` reserved for the mark. Amber `#E0A458` marks the +engine leading a column when it is not QuantaDB. + +Strategy is restrained: neutrals plus one accent, because the visitor came +to read numbers. Dark is chosen from the scene, not the category: this is +read on a laptop beside a terminal. + +## Type + +System grotesque for prose, system mono for every figure and identifier. +No webfonts, because the site loads no external resources. Hierarchy is +carried by scale and weight, not by family. Display caps at 4rem. Body +measure stays inside 68ch. + +## Rules + +- Figures always in mono, tabular numerals, right aligned on their column. +- A number never appears without its unit and its provenance reachable in + one glance or one link. +- Bars share one scale per column and are labelled; a bar is never a + decoration in a cell that has no ratio to express. +- Sections vary: table, matrix, prose pair, code, list. No repeating rig of + eyebrow over heading over paragraph. +- One authored motion moment, on the bars, from an already-visible default. +- The benchmark blocks between the `bench:` markers belong to CI. Do not + hand-edit them. diff --git a/PRODUCT.md b/PRODUCT.md new file mode 100644 index 0000000..abc54e5 --- /dev/null +++ b/PRODUCT.md @@ -0,0 +1,67 @@ +# QuantaDB product truth + +## What it is + +An experimental relational OLTP database written in Rust, rebuilt from a +retired v0.1 prototype. Version 0.2.0. Apache-2.0. Not a database anyone +should store real data in yet, and the site says so. + +## The mechanism nobody else claims + +Every performance number QuantaDB publishes carries the machine it ran on, +the commit that produced it, the container limits, and the runs it lost. +CI rewrites the published tables from the newest run on main, so the numbers +are current rather than curated. A result is discarded if a correctness test +fails. + +## Audience and scene + +Database and systems engineers evaluating an unfamiliar engine, usually +arriving from a repository link with a few minutes of skeptical attention. +They have read benchmark marketing before and discount it by habit. They +want the method before the number. + +## What the first surface must prove + +That the numbers are real, reproducible, and include the losses. + +## Shape of the system + +Six crates with an enforced acyclic graph: `syntax` (span-aware SQL parser), +`storage` (checksummed pages, two-segment WAL, group commit, recovery), +`index` (immutable B+ tree generations published online), `mvcc` (snapshot +isolation, first-committer-wins), `engine` (catalog, constraints, +transactional CRUD), `server` (bounded concurrent TCP server). + +## Protocol + +One native protocol. JSON frames, one per line, over TCP on port 6626. +Documented in `docs/protocol-v1.md`. The PostgreSQL wire listener was +removed; nothing in the product speaks Postgres any more. + +## Constraints that bind the site + +- No authentication, authorisation, or TLS on any listener. +- No replication. +- No published container image, so the quickstart builds from source. +- The site is static, deployed from `web/` to GitHub Pages, and loads no + external resources. + +## Brand commitments + +- Purple mark, `#7132F5`, already carried by `web/logo.svg` and the favicons. +- Benchmark tables on the site and in the README are written by CI from the + same run artifacts. They are never edited by hand. +- Standing preference recorded 2026-07-25: the site follows the developer + infrastructure convention, executed at full fidelity, with Neon, Turso and + ClickHouse as the craft bar. This was chosen deliberately over two more + expressive directions and holds until the user says otherwise. + +## Known gap the site must not paper over + +The published field comparison drove QuantaDB through its PostgreSQL port +with psycopg2, the same driver the real Postgres got. That port no longer +exists. The numbers remain real measurements of the engine, but the run is +not reproducible against the current commit until `.github/workflows/ +reference.yml` and `.github/scripts/field_bench.py` are re-pointed at the +native client. Until then the table states the commit it was measured at. diff --git a/web/index.html b/web/index.html index 17da9cc..1ed1b16 100644 --- a/web/index.html +++ b/web/index.html @@ -4,14 +4,14 @@ QuantaDB - + - + - + @@ -19,263 +19,362 @@ -
-
- QuantaDB - v0.2.0, experimental -
-
-
-

Protocol v1

-

One JSON frame per line. Deliberately boring.

-

- The current wire protocol is built for debugging a young database: you - can drive it with a terminal and read every byte. PostgreSQL wire - compatibility is on the roadmap; this is the scaffolding that gets us - there honestly. +

+
+

Get started

+

+ There is no published image yet, so the quickstart builds from source. + The server speaks one protocol: JSON frames, one per line, over TCP on + port 6626.

-
- + +
+
+
Build and run
+
# with docker
+docker build -t quantadb .
+docker run -p 6626:6626 quantadb
+
+# or straight from cargo
+cargo run --release -p quantadb-server
+
+ +
+
Speak to it
+
# every frame is one line of JSON
+printf '{"protocol_version":1,"request_id":1,
+"request":{"type":"execute",
+"sql":"CREATE TABLE notes (id BIGINT PRIMARY KEY,
+body TEXT NOT NULL)"}}\n' | nc 127.0.0.1 6626
+
+
+ +

+ The protocol is deliberately boring so a young database stays debuggable: + you can drive it from a terminal and read every byte that crosses the + socket. It is documented frame by frame in + protocol-v1. +

-
-
-

Status

-

Do not store data you love in this. Yet.

-

- QuantaDB is a working engine and an honest work in progress. The v0.1 - prototype was retired; the current codebase is a clean foundation with - real durability guarantees and a long road ahead. Milestones are - numbered because they are a sequence: each one is load-bearing for the - next. +

+
+

Where this actually is

+

+ QuantaDB is experimental. Do not put data you care about in it. The + milestones are numbered because each one is load bearing for the next.

-
-
    -
  1. M1

    Syntax and server foundation

    Span-aware parser, versioned protocol, bounded connections.

  2. -
  3. M2

    Durable storage kernel

    Pages, WAL, group commit, crash recovery, corruption detection.

  4. -
  5. M3

    Transactions and indexes

    MVCC, online index generations, reclamation, and a concurrent - history checker that has already caught two real bugs. Range conflict - tracking remains.

  6. -
  7. M4

    Execution engine

    Planner, joins, ordering, secondary indexes, cost-based decisions.

  8. -
  9. M5

    Production operations

    PostgreSQL wire protocol, auth, TLS, backup, format stability.

  10. -
  11. M6

    Replication

    Replicated log, failover, and adversarial failure testing before any - talk of distribution.

  12. + + +
      +
    1. + Done +
      +

      Syntax and server foundation

      +

      Span-aware parser, versioned protocol, bounded connections.

      +
      +
    2. +
    3. + Done +
      +

      Durable storage kernel

      +

      Checksummed pages, two-segment WAL, group commit, crash recovery.

      +
      +
    4. +
    5. + Now +
      +

      Transactions and indexes

      +

      + MVCC snapshots and online B+ tree generations are in. Range conflict + tracking and the remaining write throughput gap are open. +

      +
      +
    6. +
    7. + Next +
      +

      Execution engine

      +

      Planner, joins, ordering, secondary indexes, cost-based decisions.

      +
      +
    8. +
    9. + Next +
      +

      Production operations

      +

      Authentication, TLS, backup, and a stable on-disk format.

      +
      +
    10. +
    11. + Next +
      +

      Replication

      +

      Replicated log and failover, with adversarial failure testing.

      +
      +
    + +

    + There is no authentication, no authorisation, and no TLS on any listener. + Anything that can reach the port can read and write everything. This is + documented rather than fixed, for now. +