sqlrite-engine 0.10.0

Light version of SQLite developed with Rust. Published as `sqlrite-engine` on crates.io; import as `use sqlrite::…`.
Documentation
"use client";

import { useEffect, useRef, useState } from "react";
import { SITE } from "@/lib/site";

type Line =
  | { type: "in"; text: string }
  | { type: "out"; text: string; cls?: "cmt" | "tbl" | "ok" | "dim" };

const SCRIPT: Line[] = [
  { type: "out", text: `SQLRite — ${SITE.version}` },
  {
    type: "out",
    text: "Connected to a transient in-memory database.",
    cls: "cmt",
  },
  {
    type: "in",
    text: "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER);",
  },
  { type: "out", text: "ok" },
  { type: "in", text: "INSERT INTO users (name, age) VALUES ('alice', 30);" },
  { type: "out", text: "1 row inserted." },
  { type: "in", text: "INSERT INTO users (name, age) VALUES ('bob', 25);" },
  { type: "out", text: "1 row inserted." },
  {
    type: "in",
    text: "SELECT name FROM users WHERE age > 25 ORDER BY age DESC;",
  },
  {
    type: "out",
    text: "+-------+\n| name  |\n+-------+\n| alice |\n+-------+",
    cls: "tbl",
  },
  { type: "out", text: "SELECT executed. 1 row returned.", cls: "ok" },
  { type: "in", text: "BEGIN;" },
  { type: "in", text: "UPDATE users SET age = age + 1 WHERE name = 'bob';" },
  { type: "in", text: "COMMIT;" },
  {
    type: "out",
    text: "transaction committed (WAL frame #4127)",
    cls: "cmt",
  },
];

const KEYWORDS = new Set([
  "CREATE",
  "TABLE",
  "INSERT",
  "INTO",
  "VALUES",
  "SELECT",
  "FROM",
  "WHERE",
  "ORDER",
  "BY",
  "DESC",
  "ASC",
  "LIMIT",
  "UPDATE",
  "SET",
  "DELETE",
  "BEGIN",
  "COMMIT",
  "ROLLBACK",
  "PRIMARY",
  "KEY",
  "UNIQUE",
  "NOT",
  "NULL",
  "TEXT",
  "INTEGER",
  "REAL",
  "AND",
  "OR",
  "IF",
  "EXISTS",
  "INDEX",
]);

type Token = { kind: "kw" | "str" | "num" | "raw"; value: string };

function tokenize(line: string): Token[] {
  const out: Token[] = [];
  // Match strings, numbers, words, or one char of "other"
  const re = /'[^']*'|\d+|[A-Za-z_][A-Za-z0-9_]*|[^A-Za-z0-9_']+/g;
  let m: RegExpExecArray | null;
  while ((m = re.exec(line)) !== null) {
    const piece = m[0];
    if (piece.startsWith("'")) {
      out.push({ kind: "str", value: piece });
    } else if (/^\d+$/.test(piece)) {
      out.push({ kind: "num", value: piece });
    } else if (/^[A-Za-z_]/.test(piece) && KEYWORDS.has(piece.toUpperCase())) {
      out.push({ kind: "kw", value: piece });
    } else {
      out.push({ kind: "raw", value: piece });
    }
  }
  return out;
}

function HighlightedSql({ text }: { text: string }) {
  const tokens = tokenize(text);
  return (
    <>
      {tokens.map((t, i) => {
        if (t.kind === "kw") return <span key={i} className="kw">{t.value}</span>;
        if (t.kind === "str") return <span key={i} className="str">{t.value}</span>;
        if (t.kind === "num") return <span key={i} className="num">{t.value}</span>;
        return <span key={i}>{t.value}</span>;
      })}
    </>
  );
}

export function Terminal() {
  const [lines, setLines] = useState<Line[]>([]);
  const [typing, setTyping] = useState("");
  const [step, setStep] = useState(0);
  const bodyRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (step >= SCRIPT.length) {
      const t = setTimeout(() => {
        setLines([]);
        setStep(0);
      }, 4500);
      return () => clearTimeout(t);
    }
    const item = SCRIPT[step];
    if (item.type === "out") {
      const t = setTimeout(() => {
        setLines((l) => [...l, item]);
        setStep((s) => s + 1);
      }, 240);
      return () => clearTimeout(t);
    }
    let i = 0;
    setTyping("");
    const id = setInterval(() => {
      i += 1;
      setTyping(item.text.slice(0, i));
      if (i >= item.text.length) {
        clearInterval(id);
        setTimeout(() => {
          setLines((l) => [...l, item]);
          setTyping("");
          setStep((s) => s + 1);
        }, 380);
      }
    }, 22);
    return () => clearInterval(id);
  }, [step]);

  useEffect(() => {
    if (bodyRef.current) bodyRef.current.scrollTop = bodyRef.current.scrollHeight;
  }, [lines, typing]);

  const currentItem = step < SCRIPT.length ? SCRIPT[step] : null;

  return (
    <div className="term" aria-label="SQLRite REPL demo">
      <div className="term-bar">
        <span className="term-dot" />
        <span className="term-dot" />
        <span className="term-dot" />
        <span className="term-title">sqlrite — repl — in-memory</span>
      </div>
      <div className="term-body" ref={bodyRef}>
        {lines.map((l, i) => (
          <span key={i} className="term-line">
            {l.type === "in" ? (
              <>
                <span className="prompt">sqlrite{">"} </span>
                <HighlightedSql text={l.text} />
              </>
            ) : (
              <span className={l.cls ?? "dim"}>{l.text}</span>
            )}
          </span>
        ))}
        {currentItem && currentItem.type === "in" && (
          <span className="term-line">
            <span className="prompt">sqlrite{">"} </span>
            <HighlightedSql text={typing} />
            <span className="cursor" />
          </span>
        )}
      </div>
    </div>
  );
}