Danilo Fragoso преди 4 месеца
родител
ревизия
d527250a60
променени са 11 файла, в които са добавени 884 реда и са изтрити 1060 реда
  1. 1 1
      .gitignore
  2. 14 1
      Makefile
  3. 227 620
      README.md
  4. BIN
      bin/pizzasql
  5. 114 185
      main.go
  6. 31 202
      pkg/kvmanager/kvmanager.go
  7. 33 39
      pkg/kvmanager/kvmanager_test.go
  8. 185 0
      pkg/runtime/runtime.go
  9. 12 1
      pkg/storage/kv.go
  10. 93 11
      pkg/storage/schema.go
  11. 174 0
      pkg/storage/schema_test.go

+ 1 - 1
.gitignore

@@ -3,4 +3,4 @@
 testdata
 .db
 *.log
-.pizzakv.json
+# runtime state lives in /tmp/pizzasql/runtime.json — no local files to ignore

+ 14 - 1
Makefile

@@ -1,9 +1,22 @@
-.PHONY: build test test-v test-cover bench clean fmt lint sqllogictest sqllogictest-basic sqllogictest-download build-sqllogictest
+.PHONY: build test test-v test-cover bench clean fmt lint sqllogictest sqllogictest-basic sqllogictest-download build-sqllogictest install uninstall
+
+PREFIX ?= /usr/local
 
 # Build the project
 build:
 	go build -o ./bin/pizzasql ./main.go
 
+# Install pizzasql to PREFIX/bin (default: /usr/local/bin)
+install: build
+	sudo install -d $(DESTDIR)$(PREFIX)/bin
+	sudo install -m 755 ./bin/pizzasql $(DESTDIR)$(PREFIX)/bin/pizzasql
+	@echo "Installed to $(DESTDIR)$(PREFIX)/bin/pizzasql"
+
+# Remove installed binary
+uninstall:
+	sudo rm -f $(DESTDIR)$(PREFIX)/bin/pizzasql
+	@echo "Removed $(DESTDIR)$(PREFIX)/bin/pizzasql"
+
 build-linux-amd64:
 	GOOS=linux GOARCH=amd64 go build -o ./bin/pizzasql-linux-amd64 ./main.go
 

+ 227 - 620
README.md

@@ -20,21 +20,20 @@ SQLite-compatible SQL · PostgreSQL wire protocol · HTTP/JSON API · Built-in s
 
 PizzaSQL is a SQL database engine built from the ground up in Go. It features a hand-written recursive descent parser, SQLite-compatible SQL syntax, and multiple access methods.
 
-PizzaSQL passes **100% of the SQLite SQLLogicTest suite** - over 5 million individual SQL tests covering edge cases, type coercion, complex queries, and SQLite compatibility.
+PizzaSQL passes **100% of the SQLite SQLLogicTest suite**  over 5 million individual SQL tests covering edge cases, type coercion, complex queries, and SQLite compatibility.
 
 ### Access Methods
 
-- **PostgreSQL Wire Protocol** - Connect with `psql`, any PostgreSQL client library (psycopg2, node-postgres, etc.)
-- **HTTP/JSON API** - Query via REST endpoints from any language and the web
-- **CLI & REPL** - Interactive shell and command-line execution
+- **PostgreSQL Wire Protocol**  Connect with `psql`, any PostgreSQL client library (psycopg2, node-postgres, etc.)
+- **HTTP/JSON API**  Query via REST endpoints from any language and the web
+- **CLI & REPL**  Interactive shell and command-line execution
 
 ### Architecture
 
-- **Hand-Written Lexer & Parser** - Pure Go implementation
-- **PizzaKV Storage** - Custom high-performance Zig backend with radix trie indexes
-- **Thread-Safe** - Concurrent query execution with mutex-based locking
-
-PizzaSQL provides SQLite SQL compatibility with PostgreSQL wire protocol support, making it easy to integrate with existing tools and libraries while maintaining full control over the SQL dialect.
+- **Hand-Written Lexer & Parser** — Pure Go implementation
+- **PizzaKV Storage** — Custom high-performance Zig backend with radix trie indexes
+- **Unix Socket Transport** — Low-latency communication between PizzaSQL and PizzaKV via Unix domain sockets
+- **Thread-Safe** — Concurrent query execution with mutex-based locking
 
 ---
 
@@ -55,7 +54,7 @@ PizzaSQL provides SQLite SQL compatibility with PostgreSQL wire protocol support
 **PostgreSQL Wire Protocol**
 ```bash
 # Start server
-./pizzasql -pg -pg-port 5432
+./pizzasql -kv -pg
 
 # Connect with psql
 psql -h localhost -p 5432 -d pizzasql
@@ -67,7 +66,7 @@ postgresql://localhost:5432/pizzasql
 **HTTP/JSON API**
 ```bash
 # Start HTTP server
-./pizzasql -http
+./pizzasql -kv -http
 
 # Query via REST
 curl -X POST http://localhost:8080/query \
@@ -77,29 +76,30 @@ curl -X POST http://localhost:8080/query \
 
 **CLI/REPL**
 ```bash
-# Interactive mode
-./pizzasql
+# Interactive mode with storage
+./pizzasql -kv
 
 # Single statement
-./pizzasql -e "SELECT * FROM users LIMIT 10"
+./pizzasql "SELECT * FROM users LIMIT 10"
 ```
 
 ### SQLite Compatibility
 
-- ROWID Support - Implicit rowid column for all tables
-- AUTOINCREMENT - Sequential ID generation
-- Type Affinity - SQLite-compatible type system
-- PRAGMA Statements - table_info, database_list, table_list, version
-- SQLite Functions - printf, hex, random, glob, instr, zeroblob
-- Conflict Resolution - INSERT OR REPLACE/IGNORE/FAIL/ABORT
+- ROWID Support  Implicit rowid column for all tables
+- AUTOINCREMENT  Sequential ID generation
+- Type Affinity  SQLite-compatible type system
+- PRAGMA Statements  table_info, database_list, table_list, version
+- SQLite Functions  printf, hex, random, glob, instr, zeroblob
+- Conflict Resolution  INSERT OR REPLACE/IGNORE/FAIL/ABORT
 
 ### Performance & Architecture
 
-- Hand-Written Parser - 176,000 statements/sec 
-- Fast Lexer - 227,000 ops/sec tokenization
-- Automatic Indexing - 10-100x faster than full table scans
-- Thread-Safe - Concurrent query execution with mutex-based locking
-- Connection Pooling - Efficient resource management
+- Hand-Written Parser — 176,000 statements/sec
+- Fast Lexer — 227,000 ops/sec tokenization
+- Automatic Indexing — 10-100x faster than full table scans
+- Unix Socket Transport — Sub-millisecond KV latency
+- Thread-Safe — Concurrent query execution with mutex-based locking
+- Connection Pooling — Efficient resource management
 
 ---
 
@@ -109,141 +109,157 @@ curl -X POST http://localhost:8080/query \
 
 **Prerequisites:**
 - Go 1.21+
-- PizzaKV - Storage backend (auto-launched with `-kv` flag)
+- PizzaKV  Storage backend (auto-launched with `-kv` flag)
 
-**Build from source:**
+**Build and install:**
 ```bash
 git clone https://github.com/danfragoso/pizzasql.git
 cd pizzasql
-make build
+make install        # installs to /usr/local/bin
+# or
+make install PREFIX=~/.local   # user install, no sudo
+```
+
+**Build only:**
+```bash
+make build          # output: ./bin/pizzasql
 ```
 
 ### Start the Server
 
-**Option 1: HTTP API (recommended for web apps)**
+**Option 1: HTTP API**
 ```bash
-# Auto-launch PizzaKV and start HTTP server
-./pizzasql -http -kv
-
-# Server available at http://localhost:8080
+pizzasql -kv -http
+# Listening on http://localhost:8080
 ```
 
-**Option 2: PostgreSQL Wire Protocol (recommended for existing PostgreSQL tools)**
+**Option 2: PostgreSQL wire protocol**
 ```bash
-# Auto-launch PizzaKV and start PostgreSQL-compatible server
-./pizzasql -pg -kv
-
-# Connect with psql
-psql -h localhost -p 5432 -d pizzasql
+pizzasql -kv -pg
+# psql -h localhost -p 5432 -d pizzasql
+```
 
-# Or use any PostgreSQL client library
+**Option 3: Both at once**
+```bash
+pizzasql -kv -http -pg
 ```
 
-**Option 3: Interactive CLI**
+**Option 4: Interactive REPL**
 ```bash
-# Auto-launch PizzaKV and start REPL
-./pizzasql -kv
+pizzasql -kv
 ```
 
+The `-kv` flag auto-launches a PizzaKV storage process connected via Unix socket (`.pizzakv.sock` in the working directory). PizzaSQL writes its runtime state to `/tmp/pizzasql/<pid>/runtime.json` and cleans up on exit.
+
 ### Your First Query
 
-**Using psql (PostgreSQL wire protocol):**
+**Using psql:**
 ```sql
--- Connect
 psql -h localhost -p 5432 -d pizzasql
 
--- Create table (SQLite syntax!)
 CREATE TABLE users (
-  id INTEGER PRIMARY KEY AUTOINCREMENT,
-  name TEXT NOT NULL,
+  id    INTEGER PRIMARY KEY AUTOINCREMENT,
+  name  TEXT NOT NULL,
   email TEXT UNIQUE
 );
 
--- Insert data
-INSERT INTO users (name, email) VALUES 
+INSERT INTO users (name, email) VALUES
   ('Alice', 'alice@example.com'),
-  ('Bob', 'bob@example.com');
+  ('Bob',   'bob@example.com');
 
--- Query
 SELECT * FROM users;
 ```
 
 **Using HTTP API:**
 ```bash
-# Create table
-curl -X POST http://localhost:8080/query \
-  -H "Content-Type: application/json" \
-  -d '{
-    "sql": "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)"
-  }'
-
-# Insert with parameters
-curl -X POST http://localhost:8080/query \
-  -H "Content-Type: application/json" \
-  -d '{
-    "sql": "INSERT INTO users (name, email) VALUES (?, ?)",
-    "params": ["Alice", "alice@example.com"]
-  }'
-
-# Query
 curl -X POST http://localhost:8080/query \
   -H "Content-Type: application/json" \
-  -d '{
-    "sql": "SELECT * FROM users WHERE name = ?",
-    "params": ["Alice"]
-  }'
+  -d '{"sql": "SELECT * FROM users"}'
 ```
 
 **Response:**
 ```json
 {
   "columns": [
-    {"name": "id", "type": "INTEGER"},
-    {"name": "name", "type": "TEXT"},
+    {"name": "id",    "type": "INTEGER"},
+    {"name": "name",  "type": "TEXT"},
     {"name": "email", "type": "TEXT"}
   ],
   "rows": [
-    [1, "Alice", "alice@example.com"]
+    [1, "Alice", "alice@example.com"],
+    [2, "Bob",   "bob@example.com"]
   ],
   "rowsAffected": 0,
-  "lastInsertId": 0,
-  "executionTime": "1.234ms"
+  "executionTimeMicro": 108
 }
 ```
 
 ---
 
-## Benchmarks
+## Process Management
 
-Performance comparison between PizzaSQL, SQLite, and PostgreSQL.
+PizzaSQL uses a per-instance runtime directory at `/tmp/pizzasql/<pid>/` to track process state. Each directory contains a `runtime.json` with the PizzaSQL and PizzaKV PIDs and connection info.
 
-| Operation | SQLite | PizzaSQL (HTTP) | PizzaSQL (PG Wire) | PostgreSQL |
-|-----------|--------|-----------------|--------------------|------------|
-| **INSERT (1000 rows)** | 308 ops/s | 138 ops/s | 5780 ops/s | 12820 ops/s |
-| **SELECT (no index)** | 288 q/s | 128 q/s | 165 q/s | 414 q/s |
-| **CREATE INDEX** | 6 ms | 69 ms | 63 ms | 31 ms |
-| **SELECT (indexed)** | 318 q/s | 136 q/s | 1562 q/s | 2222 q/s |
-| **AGGREGATE** | 313 q/s | 137 q/s | 1052 q/s | 980 q/s |
+```
+/tmp/pizzasql/
+  12345/
+    runtime.json    ← { "pizzasql": { "pid": 12345, ... }, "pizzakv": { "pid": 12346, ... } }
+  67890/
+    runtime.json
+```
 
-**Note:** Wire protocol benchmarks (PizzaSQL PG Wire and PostgreSQL) use connection reuse with transactions, which is what client libraries do automatically. PizzaSQL's aggregate queries outperform PostgreSQL in this benchmark.
+**Multiple instances** are supported as long as each runs from a different working directory (each needs its own `.db` and `.pizzakv.sock` file). If you try to launch `-kv` in a directory that already has a `.db` file and another instance is running, PizzaSQL will refuse and tell you the conflicting PID.
 
-### Run Your Own Benchmarks
+**Stale entries** (from crashed processes) are cleaned up automatically on the next startup.
+
+**Startup prompt** — if another live pizzasql instance is detected you'll see:
+```
+Warning: 1 pizzasql instance(s) already running:
+  PID 12345 http=:8080 kv=unix:.pizzakv.sock
+Continue anyway? [y/N]
+```
 
+**Connecting to an external PizzaKV** (without `-kv`):
 ```bash
-./benchmarks/quick_bench.sh
+pizzasql -kvaddr localhost:8085 -http
 ```
 
 ---
 
-## PostgreSQL Wire Protocol Support
+## Benchmarks
+
+Performance on an M2 MacBook Air, 10,000-row table, 200 repetitions.
+
+| Workload | SQLite | PostgreSQL | PizzaSQL |
+|---|---|---|---|
+| Point lookup by PK | 0.003 ms | 0.092 ms | 15.628 ms |
+| Category scan (no index) | 0.666 ms | 0.604 ms | 16.080 ms |
+| Value range (no index) | 1.531 ms | 1.116 ms | 17.909 ms |
+| COUNT(*) | 0.004 ms | 0.305 ms | 15.651 ms |
+| Aggregate by category | 2.419 ms | 1.070 ms | 16.667 ms |
+| Top-10 ORDER BY DESC | 0.861 ms | 1.020 ms | 21.735 ms |
+| **Category scan (indexed)** | 0.537 ms | 0.279 ms | **0.108 ms** |
+| Value range (indexed) | 2.456 ms | 0.828 ms | 17.854 ms |
 
-PizzaSQL implements the PostgreSQL wire protocol, allowing you to connect with any PostgreSQL client while using SQLite-compatible SQL syntax.
+Raw PizzaKV single-key read: **0.024 ms**. Full-table prefix scan (10k rows): **0.791 ms**. The dominant cost for full-scan queries is JSON deserialization (~15 ms for 10k rows).
 
-### Protocol Features
+Indexed equality lookups are faster than both SQLite and PostgreSQL because PizzaKV's radix trie resolves the index directly to rowids with no B-tree traversal overhead.
+
+### Run Benchmarks
+
+```bash
+# Requires PizzaSQL running at :8080 and PostgreSQL at :5432
+go run ./cmd/bench/
+
+# Include raw KV benchmark (find the KV port in /tmp/pizzasql/<pid>/runtime.json)
+go run ./cmd/bench/ -kvaddr localhost:<port>
+```
+
+---
+
+## PostgreSQL Wire Protocol Support
 
-- **Universal compatibility** - Works with thousands of PostgreSQL tools and libraries
-- **SQLite simplicity** - No complex types, permissions, or schemas to manage
-- **Binary protocol** - Efficient data transfer
+PizzaSQL implements the PostgreSQL wire protocol, allowing you to use any PostgreSQL client with SQLite-compatible SQL syntax.
 
 ### Connecting with Client Libraries
 
@@ -251,33 +267,12 @@ PizzaSQL implements the PostgreSQL wire protocol, allowing you to connect with a
 ```python
 import psycopg2
 
-# Connect to PizzaSQL
-conn = psycopg2.connect(
-    host="localhost",
-    port=5432,
-    database="pizzasql"
-)
-
-# Use SQLite-compatible SQL
+conn = psycopg2.connect(host="localhost", port=5432, database="pizzasql")
 cur = conn.cursor()
-cur.execute("""
-    CREATE TABLE users (
-        id INTEGER PRIMARY KEY AUTOINCREMENT,
-        name TEXT NOT NULL,
-        email TEXT
-    )
-""")
-
-# Parameterized queries work as expected
-cur.execute("INSERT INTO users (name, email) VALUES (%s, %s)", 
-            ("Alice", "alice@example.com"))
-
-# Query results
-cur.execute("SELECT * FROM users WHERE name = %s", ("Alice",))
-rows = cur.fetchall()
-for row in rows:
-    print(row)
-
+cur.execute("CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)")
+cur.execute("INSERT INTO users (name) VALUES (%s)", ("Alice",))
+cur.execute("SELECT * FROM users")
+print(cur.fetchall())
 conn.commit()
 conn.close()
 ```
@@ -285,600 +280,212 @@ conn.close()
 **Node.js (node-postgres):**
 ```javascript
 const { Client } = require('pg');
-
-// Connect to PizzaSQL
-const client = new Client({
-  host: 'localhost',
-  port: 5432,
-  database: 'pizzasql'
-});
-
+const client = new Client({ host: 'localhost', port: 5432, database: 'pizzasql' });
 await client.connect();
-
-// Use SQLite-compatible SQL
-await client.query(`
-  CREATE TABLE users (
-    id INTEGER PRIMARY KEY AUTOINCREMENT,
-    name TEXT NOT NULL,
-    email TEXT
-  )
-`);
-
-// Parameterized queries
-await client.query(
-  'INSERT INTO users (name, email) VALUES ($1, $2)',
-  ['Alice', 'alice@example.com']
-);
-
-// Query results
-const res = await client.query(
-  'SELECT * FROM users WHERE name = $1',
-  ['Alice']
-);
+await client.query("CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)");
+await client.query("INSERT INTO users (name) VALUES ($1)", ['Alice']);
+const res = await client.query("SELECT * FROM users");
 console.log(res.rows);
-
 await client.end();
 ```
 
 **Go (lib/pq):**
 ```go
-package main
-
-import (
-    "database/sql"
-    _ "github.com/lib/pq"
-)
-
-func main() {
-    // Connect to PizzaSQL
-    db, err := sql.Open("postgres", 
-        "host=localhost port=5432 dbname=pizzasql sslmode=disable")
-    if err != nil {
-        panic(err)
-    }
-    defer db.Close()
-
-    // Use SQLite-compatible SQL
-    _, err = db.Exec(`
-        CREATE TABLE users (
-            id INTEGER PRIMARY KEY AUTOINCREMENT,
-            name TEXT NOT NULL,
-            email TEXT
-        )
-    `)
-
-    // Parameterized queries
-    _, err = db.Exec(
-        "INSERT INTO users (name, email) VALUES ($1, $2)",
-        "Alice", "alice@example.com")
-
-    // Query results
-    rows, err := db.Query(
-        "SELECT * FROM users WHERE name = $1",
-        "Alice")
-    defer rows.Close()
-
-    for rows.Next() {
-        var id int
-        var name, email string
-        rows.Scan(&id, &name, &email)
-        fmt.Printf("%d: %s (%s)\n", id, name, email)
-    }
-}
-```
-
-**Ruby (pg gem):**
-```ruby
-require 'pg'
-
-# Connect to PizzaSQL
-conn = PG.connect(
-  host: 'localhost',
-  port: 5432,
-  dbname: 'pizzasql'
-)
-
-# Use SQLite-compatible SQL
-conn.exec(<<-SQL)
-  CREATE TABLE users (
-    id INTEGER PRIMARY KEY AUTOINCREMENT,
-    name TEXT NOT NULL,
-    email TEXT
-  )
-SQL
-
-# Parameterized queries
-conn.exec_params(
-  'INSERT INTO users (name, email) VALUES ($1, $2)',
-  ['Alice', 'alice@example.com']
-)
-
-# Query results
-result = conn.exec_params(
-  'SELECT * FROM users WHERE name = $1',
-  ['Alice']
-)
-
-result.each do |row|
-  puts "#{row['id']}: #{row['name']} (#{row['email']})"
-end
-
-conn.close
-```
-
-**PHP (PDO):**
-```php
-<?php
-// Connect to PizzaSQL
-$dsn = "pgsql:host=localhost;port=5432;dbname=pizzasql";
-$pdo = new PDO($dsn);
-
-// Use SQLite-compatible SQL
-$pdo->exec("
-    CREATE TABLE users (
-        id INTEGER PRIMARY KEY AUTOINCREMENT,
-        name TEXT NOT NULL,
-        email TEXT
-    )
-");
-
-// Parameterized queries
-$stmt = $pdo->prepare(
-    "INSERT INTO users (name, email) VALUES (?, ?)"
-);
-$stmt->execute(['Alice', 'alice@example.com']);
-
-// Query results
-$stmt = $pdo->prepare(
-    "SELECT * FROM users WHERE name = ?"
-);
-$stmt->execute(['Alice']);
-$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
-
-foreach ($rows as $row) {
-    echo "{$row['id']}: {$row['name']} ({$row['email']})\n";
-}
-?>
-```
-
-**Rust (tokio-postgres):**
-```rust
-use tokio_postgres::{NoTls, Error};
-
-#[tokio::main]
-async fn main() -> Result<(), Error> {
-    // Connect to PizzaSQL
-    let (client, connection) = tokio_postgres::connect(
-        "host=localhost port=5432 dbname=pizzasql", 
-        NoTls
-    ).await?;
-
-    tokio::spawn(async move {
-        if let Err(e) = connection.await {
-            eprintln!("connection error: {}", e);
-        }
-    });
-
-    // Use SQLite-compatible SQL
-    client.execute(
-        "CREATE TABLE users (
-            id INTEGER PRIMARY KEY AUTOINCREMENT,
-            name TEXT NOT NULL,
-            email TEXT
-        )", &[]
-    ).await?;
-
-    // Parameterized queries
-    client.execute(
-        "INSERT INTO users (name, email) VALUES ($1, $2)",
-        &[&"Alice", &"alice@example.com"]
-    ).await?;
-
-    // Query results
-    let rows = client.query(
-        "SELECT * FROM users WHERE name = $1",
-        &[&"Alice"]
-    ).await?;
-
-    for row in rows {
-        let id: i32 = row.get(0);
-        let name: &str = row.get(1);
-        let email: &str = row.get(2);
-        println!("{}: {} ({})", id, name, email);
-    }
-
-    Ok(())
-}
+db, _ := sql.Open("postgres", "host=localhost port=5432 dbname=pizzasql sslmode=disable")
+db.Exec("CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)")
+db.Exec("INSERT INTO users (name) VALUES ($1)", "Alice")
+rows, _ := db.Query("SELECT * FROM users")
 ```
 
 ### Command-Line Tools
 
-**psql (PostgreSQL CLI):**
 ```bash
-# Connect interactively
+# Interactive
 psql -h localhost -p 5432 -d pizzasql
 
-# Execute single command
-psql -h localhost -p 5432 -d pizzasql \
-  -c "SELECT * FROM users WHERE age > 25"
+# Single command
+psql -h localhost -p 5432 -d pizzasql -c "SELECT * FROM users"
 
-# Execute SQL file
+# SQL file
 psql -h localhost -p 5432 -d pizzasql -f schema.sql
 
-# CSV output
-psql -h localhost -p 5432 -d pizzasql \
-  -c "SELECT * FROM users" --csv > users.csv
-```
-
-**pgcli (Enhanced PostgreSQL CLI):**
-```bash
+# pgcli
 pgcli postgresql://localhost:5432/pizzasql
 ```
 
-**DBeaver, pgAdmin, DataGrip:**
-- Connection type: PostgreSQL
-- Host: localhost
-- Port: 5432
-- Database: pizzasql
-- No username/password required
+**DBeaver / DataGrip / pgAdmin:** connection type PostgreSQL, host `localhost`, port `5432`, database `pizzasql`, no credentials.
 
 ### Important Notes
 
-1. **SQL Dialect**: PizzaSQL uses **SQLite SQL syntax**, not PostgreSQL syntax
+1. **SQL Dialect**: PizzaSQL uses **SQLite syntax**, not PostgreSQL syntax
    - Use `INTEGER PRIMARY KEY AUTOINCREMENT`, not `SERIAL`
-   - Use `TEXT` type, not `VARCHAR` with enforced length
-   - SQLite type affinity rules apply
-
-2. **Parameter Placeholders**: Client libraries use their standard placeholders
-   - Python/Node.js: `$1, $2, $3...`
-   - Go: `$1, $2, $3...`
-   - PHP: `?` or named parameters
-   - These are automatically converted to PizzaSQL's `?` placeholder
-
-3. **Compatibility**: Works with PostgreSQL clients, not PostgreSQL-specific features
-   - No schemas, roles, or permissions
-   - No PostgreSQL-specific types (ARRAY, JSON, etc.)
-   - Use SQLite functions, not PostgreSQL functions
+   - Use `TEXT`, not `VARCHAR` with enforced length
+2. **Parameter placeholders**: use `$1, $2...` (PostgreSQL style) or `?` (SQLite style)
+3. **No PostgreSQL-specific features**: no schemas, roles, `ARRAY`, `JSONB`, etc.
 
 ---
 
 ## Architecture
 
-PizzaSQL is built with a clean, modular architecture that processes SQL queries through distinct stages:
-
 ```mermaid
 graph TD
-    Client[Client Applications<br/>CLI, HTTP API, PostgreSQL Protocol]
-
+    Client[Client Applications<br/>CLI · HTTP API · PostgreSQL Protocol]
     Client --> Lexer
 
     subgraph PizzaSQL Core
-        Lexer[Lexer - SQL Tokenizer<br/>• 100+ token types<br/>• 227,000 ops/sec]
-        Parser[Parser - AST Builder<br/>• Hand-written recursive descent<br/>• 176,000 statements/sec<br/>• Operator precedence]
-        Analyzer[Analyzer - Semantic Analysis<br/>• Type checking<br/>• Scope resolution<br/>• Function validation<br/>• Thread-safe sync.RWMutex]
-        Executor[Executor - Query Engine<br/>• Query execution<br/>• Index optimization<br/>• Transaction management<br/>• Expression evaluation]
-
-        Lexer --> Parser
-        Parser --> Analyzer
-        Analyzer --> Executor
+        Lexer[Lexer - SQL Tokenizer<br/>227,000 ops/sec]
+        Parser[Parser - AST Builder<br/>176,000 statements/sec]
+        Analyzer[Analyzer - Semantic Analysis<br/>Type checking · Scope resolution]
+        Executor[Executor - Query Engine<br/>Index optimization · Transactions]
+        Lexer --> Parser --> Analyzer --> Executor
     end
 
-    Executor --> Storage[Storage Layer - PizzaKV<br/>• Custom high-performance Zig backend<br/>• Radix trie indexes<br/>• Persistent storage<br/>• Connection pooling]
+    Executor --> Storage[PizzaKV Storage<br/>Unix socket · Radix trie · Persistent]
 
     style Client fill:#e1f5ff,stroke:#0288d1,stroke-width:2px
     style PizzaSQL Core fill:#fff3e0,stroke:#f57c00,stroke-width:2px
     style Storage fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
-    style Lexer fill:#fff9c4,stroke:#fbc02d
-    style Parser fill:#fff9c4,stroke:#fbc02d
-    style Analyzer fill:#fff9c4,stroke:#fbc02d
-    style Executor fill:#fff9c4,stroke:#fbc02d
 ```
 
 ---
 
 ## Documentation
 
-### CLI Usage
+### CLI Reference
 
-**Interactive REPL:**
 ```bash
-./pizzasql -kv
-```
-
-Built-in commands:
-- `help` - Show available commands
-- `quit` - Exit the REPL
-- `tables` - List all tables
-- `clear` - Clear screen
-
-**Single Statement:**
-```bash
-./pizzasql -e "SELECT * FROM users LIMIT 10"
-```
-
-**Piped Input:**
-```bash
-cat schema.sql | ./pizzasql
-```
-
-**Expression-Only Mode:**
-```bash
-./pizzasql -e "SELECT 2 + 2 * 10"
-# Result: 22
+# Storage
+-kv                  Launch PizzaKV automatically (Unix socket)
+-kvaddr string       Connect to existing PizzaKV (e.g. localhost:8085)
+-kvflags string      Extra flags forwarded to pizzakv (e.g. "-iwal")
+-db string           Database name (default "pizzasql")
+-pool int            KV connection pool size (default 5)
+
+# HTTP server
+-http                Enable HTTP server
+-http-host string    Host (default "localhost")
+-http-port int       Port (default 8080)
+-http-cors           Enable CORS headers (default true)
+-http-compression    Enable gzip compression (default true)
+-http-auth           Enable API key authentication
+-api-keys string     Comma-separated API keys
+
+# PostgreSQL wire protocol
+-pg                  Enable PostgreSQL server
+-pg-host string      Host (default "localhost")
+-pg-port int         Port (default 5432)
+
+# Export / Import
+-o string            Output file (export)
+-i string            Input file (import)
+-table string        Table name (required for CSV)
+-format string       Format: sql, csv (auto-detected from extension)
+-drop                Include DROP TABLE in SQL export
+-create-table        Create table from CSV schema on import
+-ignore-errors       Continue import on row errors
+
+# Misc
+-quiet               Suppress request/query logging
 ```
 
 ### HTTP API
 
-#### POST /query - Execute SQL Query
-
-```bash
-curl -X POST http://localhost:8080/query \
-  -H "Content-Type: application/json" \
-  -d '{
-    "sql": "SELECT * FROM users WHERE age > ?",
-    "params": [25]
-  }'
-```
+| Method | Path | Description |
+|--------|------|-------------|
+| POST | `/query` | Execute SQL, return rows |
+| POST | `/execute` | Batch statements |
+| GET | `/schema/tables` | List all tables |
+| GET | `/schema/tables/{name}` | Table schema |
+| GET | `/health` | Health check |
+| GET | `/stats` | Runtime statistics |
+| GET | `/metrics` | Prometheus metrics |
+| POST | `/transaction/begin` | Begin transaction |
+| POST | `/transaction/commit` | Commit |
+| POST | `/transaction/rollback` | Rollback |
 
-Response:
-```json
-{
-  "columns": [
-    {"name": "id", "type": "INTEGER"},
-    {"name": "name", "type": "TEXT"}
-  ],
-  "rows": [[1, "Alice"], [2, "Bob"]],
-  "rowsAffected": 0,
-  "lastInsertId": 0,
-  "executionTime": "1.2ms"
-}
-```
-
-#### POST /execute - Batch Execution
-
-```bash
-curl -X POST http://localhost:8080/execute \
-  -H "Content-Type: application/json" \
-  -d '{
-    "statements": [
-      {"sql": "INSERT INTO users (name) VALUES (?)", "params": ["Alice"]},
-      {"sql": "INSERT INTO users (name) VALUES (?)", "params": ["Bob"]}
-    ],
-    "transaction": true
-  }'
-```
+**Multi-database:** pass `X-Database: <name>` header to route queries to a specific database. Databases are created on first access.
 
-#### GET /schema/tables - List Tables
+### Database Export / Import
 
 ```bash
-curl http://localhost:8080/schema/tables
-```
+# Export full database
+pizzasql -db mydb -o backup.sql
 
-#### GET /schema/tables/{name} - Table Schema
+# Export with DROP TABLE
+pizzasql -db mydb -o backup.sql -drop
 
-```bash
-curl http://localhost:8080/schema/tables/users
-```
+# Export single table
+pizzasql -db mydb -table users -o users.sql
 
-#### GET /health - Health Check
-
-```bash
-curl http://localhost:8080/health
-```
+# Export to CSV
+pizzasql -db mydb -table users -o users.csv
 
-#### GET /metrics - Prometheus Metrics
+# Import SQL
+pizzasql -db mydb -i backup.sql
 
-```bash
-curl http://localhost:8080/metrics
+# Import CSV (create table from header)
+pizzasql -db mydb -table users -i users.csv -create-table
 ```
 
-### Database Export/Import
-
-**Export entire database:**
-```bash
-./pizzasql -db mydb -o backup.sql
-```
+### SQL Support
 
-**Export with DROP TABLE statements:**
-```bash
-./pizzasql -db mydb -o backup.sql -drop
-```
+**Data Types:** `INTEGER` (INT, BIGINT, BOOLEAN) · `REAL` (FLOAT, DOUBLE, DECIMAL) · `TEXT` (VARCHAR, CHAR) · `BLOB` · `NUMERIC`
 
-**Export specific table:**
-```bash
-./pizzasql -db mydb -table users -o users.sql
-```
+**Joins:** INNER · LEFT · RIGHT · FULL OUTER · CROSS
 
-**Export to CSV:**
-```bash
-./pizzasql -db mydb -table users -o users.csv
-```
+**Aggregates:** COUNT · COUNT(DISTINCT) · SUM · AVG · MIN · MAX
 
-**Import SQL file:**
-```bash
-./pizzasql -db mydb -i backup.sql
-```
+**Functions:** UPPER, LOWER, LENGTH, SUBSTR, TRIM, REPLACE, CONCAT, ABS, ROUND, CEIL, FLOOR, MOD, COALESCE, NULLIF, IFNULL, printf, hex, random, glob, instr, zeroblob
 
-**Import CSV:**
-```bash
-./pizzasql -db mydb -table users -i users.csv -create-table
-```
-
-### SQL Support
-
-**Data Types:**
-- `INTEGER` (INT, SMALLINT, BIGINT, BOOLEAN)
-- `REAL` (FLOAT, DOUBLE, DECIMAL)
-- `TEXT` (VARCHAR, CHAR, CHARACTER)
-- `BLOB` (binary data)
-- `NUMERIC` (flexible numeric)
-
-**Joins:**
-- INNER JOIN
-- LEFT JOIN / LEFT OUTER JOIN
-- RIGHT JOIN / RIGHT OUTER JOIN
-- FULL OUTER JOIN
-- CROSS JOIN
-
-**Aggregates:**
-- COUNT, COUNT(DISTINCT)
-- SUM, AVG, MIN, MAX
-
-**Functions:**
-- String: UPPER, LOWER, LENGTH, SUBSTR, TRIM, REPLACE, CONCAT
-- Numeric: ABS, ROUND, CEIL, FLOOR, MOD
-- Null handling: COALESCE, NULLIF, IFNULL
-- SQLite: printf, hex, random, glob, instr, zeroblob
-
-**Transaction Support:**
+**Transactions:**
 ```sql
 BEGIN;
-INSERT INTO accounts (name, balance) VALUES ('Alice', 1000);
 UPDATE accounts SET balance = balance - 100 WHERE name = 'Alice';
-COMMIT;
-
--- Rollback on error
-BEGIN;
-UPDATE accounts SET balance = balance - 100 WHERE name = 'Alice';
-ROLLBACK;
-
--- Savepoints
-BEGIN;
-INSERT INTO users (name) VALUES ('Alice');
-SAVEPOINT sp1;
-INSERT INTO users (name) VALUES ('Bob');
-ROLLBACK TO SAVEPOINT sp1;
+UPDATE accounts SET balance = balance + 100 WHERE name = 'Bob';
 COMMIT;
 ```
 
 **Indexes:**
 ```sql
--- Create index
 CREATE INDEX idx_users_email ON users(email);
-
--- Multi-column index
-CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);
-
--- Unique index
 CREATE UNIQUE INDEX idx_users_email ON users(email);
-
--- Drop index
 DROP INDEX idx_users_email;
 ```
 
-**PRAGMA Statements:**
+**PRAGMA:**
 ```sql
--- Table schema
 PRAGMA table_info(users);
-
--- List tables
 PRAGMA table_list;
-
--- Database list
 PRAGMA database_list;
-
--- Version
-PRAGMA version;
-```
-
-**Query Plans:**
-```sql
--- Show execution plan
-EXPLAIN SELECT * FROM users WHERE id = 1;
-
--- Detailed query plan
-EXPLAIN QUERY PLAN SELECT * FROM users WHERE id = 1;
-```
-
-### Configuration
-
-**Command-Line Options:**
-```bash
-# Database options
--kvaddr string    PizzaKV server address (default "localhost:8085")
--kv               Launch PizzaKV automatically
--kvflags string   Flags to pass to PizzaKV (e.g., "-iwal -port=9090")
--db string        Database name (default "pizzasql")
--e string         Execute single statement and exit
-
-# PostgreSQL server options
--pg               Enable PostgreSQL wire protocol server
--pg-host string   PostgreSQL server host (default "localhost")
--pg-port int      PostgreSQL server port (default 5432)
-
-# HTTP server options
--http             Start HTTP server
--http-host string HTTP server host (default "localhost")
--http-port int    HTTP server port (default 8080)
--http-cors        Enable CORS headers
--http-auth        Enable authentication
--api-keys string  Comma-separated API keys
-
-# Export/Import options
--o string         Output file for export
--i string         Input file for import
--table string     Specific table to export/import
--format string    Export/import format: sql, csv
--drop             Include DROP TABLE statements in export
--create-table     Create table if not exists (CSV import)
--ignore-errors    Continue import on errors
-
-# Other options
--version          Print version and exit
--help             Show help message
--quiet            Disable query logging
 ```
 
 ---
 
 ## Testing
 
-PizzaSQL has comprehensive test coverage across all components.
-
-### Running Tests
-
 ```bash
-# All tests
-make test
-
-# Specific component
-make test-lexer
-make test-parser
-go test ./pkg/analyzer/...
+make test           # all tests
+make test-v         # verbose
+make test-cover     # coverage report → coverage.html
+make test-race      # race detector
+make bench          # benchmarks
+
+# Component tests
+go test ./pkg/lexer/...
+go test ./pkg/parser/...
 go test ./pkg/executor/...
-
-# With verbose output
-make test-v
-
-# With coverage report
-make test-cover
-open coverage.html
-
-# With race detection
-make test-race
-
-# Benchmarks
-make bench
 ```
 
-### Test Coverage
-
-| Component | Coverage | Test Count |
-|-----------|----------|------------|
-| Lexer | ~95% | 15 test functions |
-| Parser | ~90% | 35+ test functions |
-| Analyzer | ~85% | 20+ test functions |
-| Executor | ~80% | 25+ test functions |
-| HTTP Server | ~75% | 15+ test functions |
-| PostgreSQL Server | ~70% | 10+ test functions |
-
 ### SQLLogicTest
 
-PizzaSQL passes **100% of the SQLite SQLLogicTest suite** - over 5 million individual SQL tests. This comprehensive test suite validates:
+```bash
+# Run against a live server
+make sqllogictest URL=http://localhost:8080
+
+# Quick smoke test
+make sqllogictest-basic
 
-- Complex SQL query correctness
-- SQLite compatibility and edge cases
-- Type coercion and affinity
-- Aggregates and joins
-- Transaction semantics
-- Index optimization
+# Download full SQLite corpus (~5M tests)
+make sqllogictest-download
+make sqllogictest
+```

BIN
bin/pizzasql


+ 114 - 185
main.go

@@ -20,19 +20,19 @@ import (
 	"github.com/danfragoso/pizzasql-next/pkg/lexer"
 	"github.com/danfragoso/pizzasql-next/pkg/parser"
 	"github.com/danfragoso/pizzasql-next/pkg/pgserver"
+	pizzaruntime "github.com/danfragoso/pizzasql-next/pkg/runtime"
 	"github.com/danfragoso/pizzasql-next/pkg/sqlexport"
 	"github.com/danfragoso/pizzasql-next/pkg/sqlimport"
 	"github.com/danfragoso/pizzasql-next/pkg/storage"
 )
 
 var (
-	kvAddr          = flag.String("kvaddr", "localhost:8085", "PizzaKV server address (ignored if -kv is set)")
+	kvAddr          = flag.String("kvaddr", "", "PizzaKV server address (default: auto-connect to managed instance)")
 	kvLaunch        = flag.Bool("kv", false, "Launch PizzaKV automatically")
-	kvFlags         = flag.String("kvflags", "", "Flags to pass to PizzaKV (e.g., \"-iwal -port=9090\")")
-	kvInfoFile      = flag.String("kvinfo", ".pizzakv.json", "Path to PizzaKV info file")
+	kvFlags         = flag.String("kvflags", "", "Flags to pass to PizzaKV (e.g., \"-iwal\")")
 	database        = flag.String("db", "pizzasql", "Database name")
-	poolSize        = flag.Int("pool", 5, "Connection pool size")
-	timeout         = flag.Duration("timeout", 30*time.Second, "Query timeout")
+	poolSize        = flag.Int("pool", 100, "Connection pool size")
+	timeout         = flag.Duration("timeout", 120*time.Second, "Query timeout")
 	httpEnable      = flag.Bool("http", false, "Enable HTTP server")
 	httpHost        = flag.String("http-host", "localhost", "HTTP server host")
 	httpPort        = flag.Int("http-port", 8080, "HTTP server port")
@@ -63,34 +63,52 @@ var startPprofServerHook func() *http.Server
 func main() {
 	flag.Parse()
 
-	// If -kv flag is set, launch PizzaKV
+	// Warn if other pizzasql instances are running; prompt to continue.
+	if err := pizzaruntime.CheckExistingInstances(); err != nil {
+		fmt.Fprintf(os.Stderr, "%v\n", err)
+		os.Exit(1)
+	}
+
+	// Register this process in its own runtime directory.
+	pizzaruntime.WritePizzaSQL(os.Getpid(), 0, 0)
+	defer pizzaruntime.Cleanup()
+
+	// Set up signal handling for graceful shutdown.
+	sigChan := make(chan os.Signal, 1)
+	signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
+	go func() {
+		<-sigChan
+		fmt.Println("\nShutting down...")
+		stopPizzaKV()
+		pizzaruntime.Cleanup()
+		os.Exit(0)
+	}()
+
+	// If -kv flag is set, always launch a dedicated PizzaKV for this instance.
 	if *kvLaunch {
 		if err := launchPizzaKV(); err != nil {
 			fmt.Fprintf(os.Stderr, "Failed to launch PizzaKV: %v\n", err)
 			os.Exit(1)
 		}
 		defer stopPizzaKV()
-
-		// Set up signal handling for graceful shutdown
-		sigChan := make(chan os.Signal, 1)
-		signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
-		go func() {
-			<-sigChan
-			fmt.Println("\nShutting down...")
-			stopPizzaKV()
-			os.Exit(0)
-		}()
+	} else if *kvAddr == "" {
+		*kvAddr = "localhost:8085"
 	}
 
-	// Check if HTTP server mode is enabled
-	if *httpEnable {
-		runHTTPServer()
-		return
-	}
-
-	// Check if PostgreSQL server mode is enabled
-	if *pgEnable {
-		runPGServer()
+	// Start whichever servers are enabled, then block until signal.
+	if *httpEnable || *pgEnable {
+		httpRuntimePort := 0
+		pgRuntimePort := 0
+		if *httpEnable {
+			httpRuntimePort = *httpPort
+		}
+		if *pgEnable {
+			pgRuntimePort = *pgPort
+		}
+		if err := pizzaruntime.WritePizzaSQL(os.Getpid(), httpRuntimePort, pgRuntimePort); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to write runtime info: %v\n", err)
+		}
+		runServers()
 		return
 	}
 
@@ -740,200 +758,122 @@ func detectFileFormat(filename string) string {
 	return "sql"
 }
 
-func runHTTPServer() {
-	// Connect to PizzaKV
+func runServers() {
 	pool, err := storage.NewKVPool(*kvAddr, *poolSize, *timeout)
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "Failed to connect to PizzaKV at %s: %v\n", *kvAddr, err)
-		fmt.Fprintf(os.Stderr, "Make sure PizzaKV is running: pizzakv\n")
 		os.Exit(1)
 	}
 	defer pool.Close()
 
-	// Create database manager for multi-database support
 	dbManagerConfig := &storage.DatabaseManagerConfig{
 		DefaultDatabase: *database,
-		AutoCreate:      true, // Auto-create databases on first access
+		AutoCreate:      true,
 	}
 	dbManager := storage.NewDatabaseManager(pool, dbManagerConfig)
 
-	// Configure HTTP server
-	config := httpserver.DefaultConfig()
-	config.Host = *httpHost
-	config.Port = *httpPort
-	config.EnableCORS = *httpCORS
-	config.EnableAuth = *httpAuth
-	config.EnableCompression = *httpCompression
-	config.EnableLogging = !*quiet
-
-	if *apiKeys != "" {
-		config.APIKeys = strings.Split(*apiKeys, ",")
-	}
-
-	// Create and start server with multi-database support
-	server := httpserver.NewWithDatabaseManager(config, dbManager)
-	var pprofServer *http.Server
-	if startPprofServerHook != nil {
-		pprofServer = startPprofServerHook()
-	}
-
-	// Handle graceful shutdown
 	stop := make(chan os.Signal, 1)
 	signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
 
-	// Start server in goroutine
-	go func() {
-		if err := server.Start(); err != nil && err != http.ErrServerClosed {
-			fmt.Fprintf(os.Stderr, "HTTP server error: %v\n", err)
-			os.Exit(1)
+	var httpSrv *httpserver.Server
+	var pprofSrv *http.Server
+	var pgSrv *pgserver.Server
+
+	if *httpEnable {
+		config := httpserver.DefaultConfig()
+		config.Host = *httpHost
+		config.Port = *httpPort
+		config.EnableCORS = *httpCORS
+		config.EnableAuth = *httpAuth
+		config.EnableCompression = *httpCompression
+		config.EnableLogging = !*quiet
+		if *apiKeys != "" {
+			config.APIKeys = strings.Split(*apiKeys, ",")
+		}
+		httpSrv = httpserver.NewWithDatabaseManager(config, dbManager)
+		if startPprofServerHook != nil {
+			pprofSrv = startPprofServerHook()
 		}
-	}()
+		go func() {
+			if err := httpSrv.Start(); err != nil && err != http.ErrServerClosed {
+				fmt.Fprintf(os.Stderr, "HTTP server error: %v\n", err)
+				os.Exit(1)
+			}
+		}()
+		fmt.Printf("HTTP  http://%s:%d\n", *httpHost, *httpPort)
+	}
 
-	fmt.Printf("PizzaSQL HTTP server started on http://%s:%d\n", *httpHost, *httpPort)
-	if pprofServer != nil {
-		fmt.Printf("pprof debug server started on http://%s/debug/pprof/\n", pprofServer.Addr)
+	if *pgEnable {
+		config := pgserver.DefaultConfig()
+		config.Host = *pgHost
+		config.Port = *pgPort
+		config.DefaultDatabase = *database
+		config.Quiet = *quiet
+		pgSrv = pgserver.New(config, dbManager)
+		go func() {
+			if err := pgSrv.Start(); err != nil {
+				fmt.Fprintf(os.Stderr, "PostgreSQL server error: %v\n", err)
+				os.Exit(1)
+			}
+		}()
+		fmt.Printf("PG    postgresql://%s:%d/%s\n", *pgHost, *pgPort, *database)
 	}
-	fmt.Printf("Default database: %s\n", *database)
-	fmt.Printf("PizzaKV: %s\n", *kvAddr)
-	fmt.Println()
-	fmt.Println("Multi-database support enabled!")
-	fmt.Println("Use the X-Database header to select a database per request.")
-	fmt.Println()
-	fmt.Println("Endpoints:")
-	fmt.Println("  POST   /query                - Execute SQL query")
-	fmt.Println("  POST   /execute              - Batch execution")
-	fmt.Println("  GET    /schema/tables        - List tables")
-	fmt.Println("  GET    /schema/tables/{name} - Table schema")
-	fmt.Println("  GET    /health               - Health check")
-	fmt.Println("  GET    /stats                - Statistics")
-	fmt.Println("  GET    /metrics              - Prometheus metrics")
-	fmt.Println("  POST   /transaction/begin    - Begin transaction")
-	fmt.Println("  POST   /transaction/commit   - Commit transaction")
-	fmt.Println("  POST   /transaction/rollback - Rollback transaction")
-	fmt.Println()
-	fmt.Println("Examples:")
-	fmt.Printf("  # Query default database\n")
-	fmt.Printf("  curl -X POST http://%s:%d/query -H 'Content-Type: application/json' -d '{\"sql\":\"SELECT 1+1\"}'\n", *httpHost, *httpPort)
-	fmt.Println()
-	fmt.Printf("  # Query specific database using X-Database header\n")
-	fmt.Printf("  curl -X POST http://%s:%d/query -H 'Content-Type: application/json' -H 'X-Database: tenant_db' -d '{\"sql\":\"SELECT * FROM users\"}'\n", *httpHost, *httpPort)
-	fmt.Println()
+
+	fmt.Printf("KV    %s\n", *kvAddr)
+	fmt.Printf("DB    %s\n", *database)
 	fmt.Println("Press Ctrl+C to stop")
 
 	<-stop
-	fmt.Println("\nShutting down server...")
+	fmt.Println("\nShutting down...")
 
 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 	defer cancel()
 
-	if err := server.Shutdown(ctx); err != nil {
-		fmt.Fprintf(os.Stderr, "Error during shutdown: %v\n", err)
-	}
-	if pprofServer != nil {
-		if err := pprofServer.Shutdown(ctx); err != nil {
-			fmt.Fprintf(os.Stderr, "Error during pprof shutdown: %v\n", err)
+	if httpSrv != nil {
+		if err := httpSrv.Shutdown(ctx); err != nil {
+			fmt.Fprintf(os.Stderr, "HTTP shutdown error: %v\n", err)
 		}
 	}
-
-	fmt.Println("Server stopped")
-}
-
-func runPGServer() {
-	// Connect to PizzaKV
-	pool, err := storage.NewKVPool(*kvAddr, *poolSize, *timeout)
-	if err != nil {
-		fmt.Fprintf(os.Stderr, "Failed to connect to PizzaKV at %s: %v\n", *kvAddr, err)
-		fmt.Fprintf(os.Stderr, "Make sure PizzaKV is running: pizzakv\n")
-		os.Exit(1)
-	}
-	defer pool.Close()
-
-	// Create database manager for multi-database support
-	dbManagerConfig := &storage.DatabaseManagerConfig{
-		DefaultDatabase: *database,
-		AutoCreate:      true,
+	if pprofSrv != nil {
+		pprofSrv.Shutdown(ctx)
 	}
-	dbManager := storage.NewDatabaseManager(pool, dbManagerConfig)
-
-	// Configure PostgreSQL server
-	config := pgserver.DefaultConfig()
-	config.Host = *pgHost
-	config.Port = *pgPort
-	config.DefaultDatabase = *database
-	config.Quiet = *quiet
-
-	// Create and start server
-	server := pgserver.New(config, dbManager)
-
-	// Handle graceful shutdown
-	stop := make(chan os.Signal, 1)
-	signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
-
-	// Start server in goroutine
-	go func() {
-		if err := server.Start(); err != nil {
-			fmt.Fprintf(os.Stderr, "PostgreSQL server error: %v\n", err)
-			os.Exit(1)
+	if pgSrv != nil {
+		if err := pgSrv.Shutdown(ctx); err != nil {
+			fmt.Fprintf(os.Stderr, "PG shutdown error: %v\n", err)
 		}
-	}()
-
-	fmt.Printf("PizzaSQL PostgreSQL Server started on %s:%d\n", *pgHost, *pgPort)
-	fmt.Printf("Default database: %s\n", *database)
-	fmt.Printf("PizzaKV: %s\n", *kvAddr)
-	fmt.Println()
-	fmt.Println("Connect using psql:")
-	fmt.Printf("  psql -h %s -p %d -d %s\n", *pgHost, *pgPort, *database)
-	fmt.Println()
-	fmt.Println("Or any PostgreSQL client library:")
-	fmt.Printf("  postgresql://%s:%d/%s\n", *pgHost, *pgPort, *database)
-	fmt.Println()
-	fmt.Println("Press Ctrl+C to stop")
-
-	<-stop
-	fmt.Println("\nShutting down server...")
-
-	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
-	defer cancel()
-
-	if err := server.Shutdown(ctx); err != nil {
-		fmt.Fprintf(os.Stderr, "Error during shutdown: %v\n", err)
 	}
-
-	fmt.Println("Server stopped")
 }
 
-// launchPizzaKV starts a PizzaKV instance and updates kvAddr
+// launchPizzaKV starts a dedicated PizzaKV instance for this pizzasql process.
 func launchPizzaKV() error {
-	kvManager = kvmanager.NewManager()
-	kvManager.SetInfoFile(*kvInfoFile)
-
-	// Clean up any stale process info
-	if err := kvmanager.CleanupStaleProcess(*kvInfoFile); err != nil {
-		// Check if it's an "already running" error
-		if strings.Contains(err.Error(), "already running") {
-			fmt.Fprintf(os.Stderr, "Warning: %v\n", err)
-			fmt.Fprintf(os.Stderr, "\nOptions:\n")
-			fmt.Fprintf(os.Stderr, "  1. Use the existing instance: remove -kv flag and use -kvaddr=localhost:<port>\n")
-			fmt.Fprintf(os.Stderr, "  2. Stop it: kill %d\n", getPIDFromFile(*kvInfoFile))
-			fmt.Fprintf(os.Stderr, "  3. Delete the info file: rm %s\n\n", *kvInfoFile)
-			return err
+	if _, err := os.Stat(".db"); err == nil {
+		if live := pizzaruntime.LiveInstances(); len(live) > 0 {
+			inst := live[0]
+			kvAddr := "<addr>"
+			if inst.PizzaKV != nil {
+				kvAddr = inst.PizzaKV.Addr
+			}
+			return fmt.Errorf(".db file already exists and another pizzasql instance is running (PID %d)\n"+
+				"  To connect to its pizzakv:      pizzasql -kvaddr=%s\n"+
+				"  To start fresh (removes data):  rm .db && pizzasql -kv\n"+
+				"  To run a separate instance:     cd /other/dir && pizzasql -kv",
+				inst.PizzaSQL.PID, kvAddr)
 		}
-		return fmt.Errorf("failed to cleanup stale process: %w", err)
 	}
 
+	kvManager = kvmanager.NewManager()
+
 	fmt.Println("Starting PizzaKV...")
 	info, err := kvManager.Start(*kvFlags)
 	if err != nil {
 		return err
 	}
 
-	fmt.Printf("\nPizzaKV started on %s (PID: %d)\n", info.Addr, info.PID)
-	fmt.Printf("Info written to: %s\n", *kvInfoFile)
+	fmt.Printf("PizzaKV started on %s (PID: %d)\n", info.Addr, info.PID)
+	fmt.Printf("Runtime: %s\n", pizzaruntime.File)
 	fmt.Println("PizzaKV is ready!")
 
-	// Update kvAddr to use the launched instance
 	*kvAddr = info.Addr
-
 	return nil
 }
 
@@ -948,14 +888,3 @@ func stopPizzaKV() {
 		}
 	}
 }
-
-// getPIDFromFile reads the PID from the info file
-func getPIDFromFile(path string) int {
-	mgr := kvmanager.NewManager()
-	mgr.SetInfoFile(path)
-	info, err := mgr.LoadInfo()
-	if err != nil {
-		return 0
-	}
-	return info.PID
-}

+ 31 - 202
pkg/kvmanager/kvmanager.go

@@ -5,52 +5,32 @@ import (
 	"net"
 	"os"
 	"os/exec"
-	"path/filepath"
 	"strconv"
 	"strings"
 	"syscall"
 	"time"
 
-	"github.com/goccy/go-json"
+	pizzaruntime "github.com/danfragoso/pizzasql-next/pkg/runtime"
 )
 
-// KVInfo contains information about the running PizzaKV instance
-type KVInfo struct {
-	PID  int    `json:"pid"`
-	Port int    `json:"port"`
-	Addr string `json:"addr"`
-}
+// KVInfo is an alias for the runtime package type.
+type KVInfo = pizzaruntime.KVInfo
 
 // Manager handles the lifecycle of a PizzaKV process
 type Manager struct {
-	cmd      *exec.Cmd
-	infoFile string
-	info     *KVInfo
+	cmd  *exec.Cmd
+	info *KVInfo
 }
 
 // NewManager creates a new KVManager
 func NewManager() *Manager {
-	return &Manager{
-		infoFile: ".pizzakv.json",
-	}
-}
-
-// SetInfoFile sets a custom path for the info file
-func (m *Manager) SetInfoFile(path string) {
-	m.infoFile = path
+	return &Manager{}
 }
 
-// Start launches pizzakv with the given flags on a random available port
+// Start launches pizzakv with the given flags using a Unix socket by default.
 func (m *Manager) Start(kvFlags string) (*KVInfo, error) {
-	// Find an available port between 1024-9999
-	port, err := findAvailablePortInRange(1024, 9999)
-	if err != nil {
-		return nil, fmt.Errorf("failed to find available port: %w", err)
-	}
-
-	// Build the command arguments
-	// PizzaKV uses -port=XXXX format (single dash)
-	args := []string{fmt.Sprintf("-port=%d", port)}
+	sockPath := ".pizzakv.sock"
+	args := []string{"-unix"}
 
 	// Parse and add custom flags if provided
 	if kvFlags != "" {
@@ -78,31 +58,25 @@ func (m *Manager) Start(kvFlags string) (*KVInfo, error) {
 	m.cmd = cmd
 	m.info = &KVInfo{
 		PID:  cmd.Process.Pid,
-		Port: port,
-		Addr: fmt.Sprintf("localhost:%d", port),
+		Addr: "unix:" + sockPath,
 	}
 
-	// Wait for the process to start and begin listening
-	// We need to wait longer to ensure PizzaKV is actually listening
 	time.Sleep(500 * time.Millisecond)
 
-	// Check if process is still running
 	if !m.IsRunning() {
 		return nil, fmt.Errorf("pizzakv process exited immediately after starting")
 	}
 
 	fmt.Println("Waiting for PizzaKV to be ready...")
 
-	// Wait for PizzaKV to be ready (finish restoring records, etc.)
-	if err := m.waitForReady(port, 30*time.Second); err != nil {
+	if err := m.waitForReady(m.info.Addr, 30*time.Second); err != nil {
 		m.Stop()
 		return nil, fmt.Errorf("pizzakv did not become ready: %w", err)
 	}
 
-	// Write info to file
-	if err := m.writeInfoFile(); err != nil {
+	if err := pizzaruntime.WriteKV(m.info); err != nil {
 		m.Stop()
-		return nil, fmt.Errorf("failed to write info file: %w", err)
+		return nil, fmt.Errorf("failed to write runtime file: %w", err)
 	}
 
 	return m.info, nil
@@ -114,15 +88,12 @@ func (m *Manager) Stop() error {
 		return nil
 	}
 
-	// Try graceful shutdown first
 	if err := m.cmd.Process.Signal(syscall.SIGTERM); err != nil {
-		// If SIGTERM fails, try SIGKILL
 		if err := m.cmd.Process.Kill(); err != nil {
 			return fmt.Errorf("failed to kill process: %w", err)
 		}
 	}
 
-	// Wait for process to exit with timeout
 	done := make(chan error, 1)
 	go func() {
 		_, err := m.cmd.Process.Wait()
@@ -131,15 +102,10 @@ func (m *Manager) Stop() error {
 
 	select {
 	case <-done:
-		// Process exited
 	case <-time.After(5 * time.Second):
-		// Timeout, force kill
 		m.cmd.Process.Kill()
 	}
 
-	// Clean up info file
-	os.Remove(m.infoFile)
-
 	return nil
 }
 
@@ -154,30 +120,33 @@ func (m *Manager) IsRunning() bool {
 	return err == nil
 }
 
-// waitForReady waits for PizzaKV to be ready to accept connections
-func (m *Manager) waitForReady(port int, timeout time.Duration) error {
-	addr := fmt.Sprintf("127.0.0.1:%d", port)
+// waitForReady waits for PizzaKV to be ready to accept connections.
+func (m *Manager) waitForReady(addr string, timeout time.Duration) error {
+	network, target := parseKVAddr(addr)
 	deadline := time.Now().Add(timeout)
 
 	for time.Now().Before(deadline) {
-		// Check if process is still running
 		if !m.IsRunning() {
 			return fmt.Errorf("process died while waiting for ready")
 		}
 
-		// Try to connect
-		conn, err := net.DialTimeout("tcp", addr, 500*time.Millisecond)
+		conn, err := net.DialTimeout(network, target, 500*time.Millisecond)
 		if err == nil {
 			conn.Close()
-			// Successfully connected, PizzaKV is ready
 			return nil
 		}
 
-		// Wait a bit before retrying
 		time.Sleep(100 * time.Millisecond)
 	}
 
-	return fmt.Errorf("timeout waiting for PizzaKV to become ready on port %d", port)
+	return fmt.Errorf("timeout waiting for PizzaKV to become ready at %s", addr)
+}
+
+func parseKVAddr(addr string) (network, target string) {
+	if strings.HasPrefix(addr, "unix:") {
+		return "unix", strings.TrimPrefix(addr, "unix:")
+	}
+	return "tcp", addr
 }
 
 // GetInfo returns the KVInfo for the running instance
@@ -185,69 +154,16 @@ func (m *Manager) GetInfo() *KVInfo {
 	return m.info
 }
 
-// LoadInfo loads KVInfo from the info file
+// LoadInfo loads KVInfo from the runtime file
 func (m *Manager) LoadInfo() (*KVInfo, error) {
-	data, err := os.ReadFile(m.infoFile)
-	if err != nil {
-		return nil, fmt.Errorf("failed to read info file: %w", err)
-	}
-
-	var info KVInfo
-	if err := json.Unmarshal(data, &info); err != nil {
-		return nil, fmt.Errorf("failed to parse info file: %w", err)
-	}
-
-	return &info, nil
-}
-
-// writeInfoFile writes the KVInfo to a file
-func (m *Manager) writeInfoFile() error {
-	data, err := json.MarshalIndent(m.info, "", "  ")
+	info, err := pizzaruntime.Load()
 	if err != nil {
-		return fmt.Errorf("failed to marshal info: %w", err)
-	}
-
-	// Create directory if it doesn't exist
-	dir := filepath.Dir(m.infoFile)
-	if dir != "." {
-		if err := os.MkdirAll(dir, 0755); err != nil {
-			return fmt.Errorf("failed to create directory: %w", err)
-		}
-	}
-
-	if err := os.WriteFile(m.infoFile, data, 0644); err != nil {
-		return fmt.Errorf("failed to write info file: %w", err)
+		return nil, err
 	}
-
-	return nil
-}
-
-// findAvailablePort finds a random available port (kept for compatibility)
-func findAvailablePort() (int, error) {
-	return findAvailablePortInRange(1024, 65535)
-}
-
-// findAvailablePortInRange finds a random available port within the specified range
-func findAvailablePortInRange(minPort, maxPort int) (int, error) {
-	// Try up to 100 times to find an available port
-	for i := 0; i < 100; i++ {
-		// Generate random port in range
-		port := minPort + (int(time.Now().UnixNano()) % (maxPort - minPort + 1))
-
-		// Try to listen on this port
-		addr := fmt.Sprintf("127.0.0.1:%d", port)
-		listener, err := net.Listen("tcp", addr)
-		if err != nil {
-			// Port is in use, try another
-			continue
-		}
-		defer listener.Close()
-
-		// Port is available
-		return port, nil
+	if info.PizzaKV == nil {
+		return nil, fmt.Errorf("no pizzakv info in runtime file")
 	}
-
-	return 0, fmt.Errorf("could not find available port in range %d-%d after 100 attempts", minPort, maxPort)
+	return info.PizzaKV, nil
 }
 
 // parseFlags parses a flag string like "-iwal -port=9090" into a slice of strings
@@ -288,93 +204,6 @@ func parseFlags(flags string) []string {
 	return result
 }
 
-// CleanupStaleProcess checks if there's a stale PID file and cleans it up
-func CleanupStaleProcess(infoFile string) error {
-	data, err := os.ReadFile(infoFile)
-	if err != nil {
-		if os.IsNotExist(err) {
-			return nil // No file, nothing to clean
-		}
-		return err
-	}
-
-	var info KVInfo
-	if err := json.Unmarshal(data, &info); err != nil {
-		// Invalid file, just remove it
-		return os.Remove(infoFile)
-	}
-
-	// Check if process is still running
-	process, err := os.FindProcess(info.PID)
-	if err != nil {
-		// Process doesn't exist, remove file
-		return os.Remove(infoFile)
-	}
-
-	// Try to signal the process
-	err = process.Signal(syscall.Signal(0))
-	if err != nil {
-		// Process is dead, remove file
-		return os.Remove(infoFile)
-	}
-
-	// Process exists, but is it actually PizzaKV responding on that port?
-	// Try to connect to the port
-	addr := fmt.Sprintf("127.0.0.1:%d", info.Port)
-	conn, err := net.DialTimeout("tcp", addr, 1*time.Second)
-	if err != nil {
-		// Port is not responding, process might be stale or not PizzaKV
-		// Remove the file and let user launch a new instance
-		return os.Remove(infoFile)
-	}
-	conn.Close()
-
-	// Process is still running and responding on the port
-	return fmt.Errorf("pizzakv process (PID %d) is already running on port %d", info.PID, info.Port)
-}
-
-// KillExisting kills an existing pizzakv process based on the info file
-func KillExisting(infoFile string) error {
-	data, err := os.ReadFile(infoFile)
-	if err != nil {
-		if os.IsNotExist(err) {
-			return nil // No file, nothing to kill
-		}
-		return err
-	}
-
-	var info KVInfo
-	if err := json.Unmarshal(data, &info); err != nil {
-		// Invalid file, just remove it
-		return os.Remove(infoFile)
-	}
-
-	// Try to kill the process
-	process, err := os.FindProcess(info.PID)
-	if err != nil {
-		// Process doesn't exist, remove file
-		return os.Remove(infoFile)
-	}
-
-	// Try SIGTERM first
-	if err := process.Signal(syscall.SIGTERM); err == nil {
-		// Wait a bit for graceful shutdown
-		time.Sleep(1 * time.Second)
-
-		// Check if still running
-		if err := process.Signal(syscall.Signal(0)); err == nil {
-			// Still running, force kill
-			process.Kill()
-		}
-	} else {
-		// SIGTERM failed, try SIGKILL
-		process.Kill()
-	}
-
-	// Remove the info file
-	return os.Remove(infoFile)
-}
-
 // ParsePort parses a port from a string (e.g., "localhost:8085" -> 8085)
 func ParsePort(addr string) (int, error) {
 	parts := strings.Split(addr, ":")

+ 33 - 39
pkg/kvmanager/kvmanager_test.go

@@ -1,39 +1,38 @@
 package kvmanager
 
-import (
-	"testing"
-)
+import "testing"
 
-func TestFindAvailablePort(t *testing.T) {
-	port, err := findAvailablePort()
-	if err != nil {
-		t.Fatalf("Failed to find available port: %v", err)
-	}
-
-	if port < 1024 || port > 65535 {
-		t.Errorf("Port %d is outside valid range 1024-65535", port)
-	}
-}
-
-func TestFindAvailablePortInRange(t *testing.T) {
-	// Test PizzaKV range (1024-9999)
-	port, err := findAvailablePortInRange(1024, 9999)
-	if err != nil {
-		t.Fatalf("Failed to find available port: %v", err)
-	}
-
-	if port < 1024 || port > 9999 {
-		t.Errorf("Port %d is outside requested range 1024-9999", port)
-	}
-
-	// Test custom range
-	port, err = findAvailablePortInRange(5000, 5100)
-	if err != nil {
-		t.Fatalf("Failed to find available port in custom range: %v", err)
+func TestParseKVAddr(t *testing.T) {
+	tests := []struct {
+		name        string
+		input       string
+		wantNetwork string
+		wantTarget  string
+	}{
+		{
+			name:        "tcp address",
+			input:       "localhost:8085",
+			wantNetwork: "tcp",
+			wantTarget:  "localhost:8085",
+		},
+		{
+			name:        "unix socket",
+			input:       "unix:.pizzakv.sock",
+			wantNetwork: "unix",
+			wantTarget:  ".pizzakv.sock",
+		},
 	}
 
-	if port < 5000 || port > 5100 {
-		t.Errorf("Port %d is outside requested range 5000-5100", port)
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			network, target := parseKVAddr(tt.input)
+			if network != tt.wantNetwork {
+				t.Errorf("Expected network %q, got %q", tt.wantNetwork, network)
+			}
+			if target != tt.wantTarget {
+				t.Errorf("Expected target %q, got %q", tt.wantTarget, target)
+			}
+		})
 	}
 }
 
@@ -147,19 +146,14 @@ func TestParsePort(t *testing.T) {
 func TestKVInfo(t *testing.T) {
 	info := &KVInfo{
 		PID:  12345,
-		Port: 8085,
-		Addr: "localhost:8085",
+		Addr: "unix:.pizzakv.sock",
 	}
 
 	if info.PID != 12345 {
 		t.Errorf("Expected PID 12345, got %d", info.PID)
 	}
 
-	if info.Port != 8085 {
-		t.Errorf("Expected Port 8085, got %d", info.Port)
-	}
-
-	if info.Addr != "localhost:8085" {
-		t.Errorf("Expected Addr 'localhost:8085', got %s", info.Addr)
+	if info.Addr != "unix:.pizzakv.sock" {
+		t.Errorf("Expected Addr 'unix:.pizzakv.sock', got %s", info.Addr)
 	}
 }

+ 185 - 0
pkg/runtime/runtime.go

@@ -0,0 +1,185 @@
+package runtime
+
+import (
+	"bufio"
+	"fmt"
+	"net"
+	"os"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+
+	"github.com/goccy/go-json"
+)
+
+var BaseDir = filepath.Join(os.TempDir(), "pizzasql")
+
+type KVInfo struct {
+	PID  int    `json:"pid"`
+	Port int    `json:"port"`
+	Addr string `json:"addr"`
+}
+
+type ProcessInfo struct {
+	PID      int `json:"pid"`
+	HTTPPort int `json:"http_port,omitempty"`
+	PGPort   int `json:"pg_port,omitempty"`
+}
+
+type Info struct {
+	PizzaSQL *ProcessInfo `json:"pizzasql,omitempty"`
+	PizzaKV  *KVInfo      `json:"pizzakv,omitempty"`
+}
+
+func instanceDir(pid int) string {
+	return filepath.Join(BaseDir, strconv.Itoa(pid))
+}
+
+func instanceFile(pid int) string {
+	return filepath.Join(instanceDir(pid), "runtime.json")
+}
+
+// File returns the runtime file path for the current process.
+var File = instanceFile(os.Getpid())
+
+func loadFile(path string) (*Info, error) {
+	data, err := os.ReadFile(path)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return &Info{}, nil
+		}
+		return nil, err
+	}
+	var info Info
+	if err := json.Unmarshal(data, &info); err != nil {
+		os.Remove(path)
+		return &Info{}, nil
+	}
+	return &info, nil
+}
+
+func Load() (*Info, error) {
+	return loadFile(instanceFile(os.Getpid()))
+}
+
+func write(info *Info) error {
+	dir := instanceDir(os.Getpid())
+	if err := os.MkdirAll(dir, 0755); err != nil {
+		return err
+	}
+	data, err := json.MarshalIndent(info, "", "  ")
+	if err != nil {
+		return err
+	}
+	return os.WriteFile(instanceFile(os.Getpid()), data, 0644)
+}
+
+func pidAlive(pid int) bool {
+	p, err := os.FindProcess(pid)
+	if err != nil {
+		return false
+	}
+	return p.Signal(syscall.Signal(0)) == nil
+}
+
+func addrResponds(addr string) bool {
+	network, target := "tcp", addr
+	if strings.HasPrefix(addr, "unix:") {
+		network, target = "unix", strings.TrimPrefix(addr, "unix:")
+	}
+	conn, err := net.DialTimeout(network, target, time.Second)
+	if err != nil {
+		return false
+	}
+	conn.Close()
+	return true
+}
+
+// LiveInstances returns all runtime files from other instances that have a live pizzasql PID.
+func LiveInstances() []*Info {
+	entries, err := os.ReadDir(BaseDir)
+	if err != nil {
+		return nil
+	}
+	selfPID := os.Getpid()
+	var live []*Info
+	for _, e := range entries {
+		if !e.IsDir() {
+			continue
+		}
+		pid, err := strconv.Atoi(e.Name())
+		if err != nil || pid == selfPID {
+			continue
+		}
+		path := filepath.Join(BaseDir, e.Name(), "runtime.json")
+		info, err := loadFile(path)
+		if err != nil || info.PizzaSQL == nil {
+			continue
+		}
+		if pidAlive(info.PizzaSQL.PID) {
+			live = append(live, info)
+		} else {
+			os.RemoveAll(filepath.Join(BaseDir, e.Name()))
+		}
+	}
+	return live
+}
+
+// CheckExistingInstances warns about live instances and prompts the user.
+// Returns an error only if the user declines to continue.
+func CheckExistingInstances() error {
+	live := LiveInstances()
+	if len(live) == 0 {
+		return nil
+	}
+
+	fmt.Fprintf(os.Stderr, "Warning: %d pizzasql instance(s) already running:\n", len(live))
+	for _, info := range live {
+		extra := ""
+		if info.PizzaSQL.HTTPPort != 0 {
+			extra += fmt.Sprintf(" http=:%d", info.PizzaSQL.HTTPPort)
+		}
+		if info.PizzaSQL.PGPort != 0 {
+			extra += fmt.Sprintf(" pg=:%d", info.PizzaSQL.PGPort)
+		}
+		if info.PizzaKV != nil {
+			extra += fmt.Sprintf(" kv=%s", info.PizzaKV.Addr)
+		}
+		fmt.Fprintf(os.Stderr, "  PID %d%s\n", info.PizzaSQL.PID, extra)
+	}
+	fmt.Fprintf(os.Stderr, "Continue anyway? [y/N] ")
+
+	reader := bufio.NewReader(os.Stdin)
+	line, _ := reader.ReadString('\n')
+	if line != "y\n" && line != "Y\n" {
+		return fmt.Errorf("aborted")
+	}
+	return nil
+}
+
+// WritePizzaSQL records the pizzasql process in this instance's runtime file.
+func WritePizzaSQL(pid, httpPort, pgPort int) error {
+	info, err := Load()
+	if err != nil {
+		info = &Info{}
+	}
+	info.PizzaSQL = &ProcessInfo{PID: pid, HTTPPort: httpPort, PGPort: pgPort}
+	return write(info)
+}
+
+// WriteKV records the pizzakv process in this instance's runtime file.
+func WriteKV(kv *KVInfo) error {
+	info, err := Load()
+	if err != nil {
+		info = &Info{}
+	}
+	info.PizzaKV = kv
+	return write(info)
+}
+
+// Cleanup removes this instance's runtime directory.
+func Cleanup() {
+	os.RemoveAll(instanceDir(os.Getpid()))
+}

+ 12 - 1
pkg/storage/kv.go

@@ -18,8 +18,10 @@ type KVClient struct {
 }
 
 // NewKVClient creates a new KV client connected to the given address.
+// addr may be "host:port" for TCP or "unix:<path>" for a Unix socket.
 func NewKVClient(addr string) (*KVClient, error) {
-	conn, err := net.Dial("tcp", addr)
+	network, target := parseAddr(addr)
+	conn, err := net.Dial(network, target)
 	if err != nil {
 		return nil, fmt.Errorf("failed to connect to PizzaKV: %w", err)
 	}
@@ -31,6 +33,15 @@ func NewKVClient(addr string) (*KVClient, error) {
 	}, nil
 }
 
+// parseAddr splits an addr string into (network, address).
+// "unix:<path>" → ("unix", "<path>"), anything else → ("tcp", addr).
+func parseAddr(addr string) (string, string) {
+	if strings.HasPrefix(addr, "unix:") {
+		return "unix", strings.TrimPrefix(addr, "unix:")
+	}
+	return "tcp", addr
+}
+
 // Close closes the connection.
 func (c *KVClient) Close() error {
 	c.mu.Lock()

+ 93 - 11
pkg/storage/schema.go

@@ -82,6 +82,11 @@ func (m *SchemaManager) catalogKey() string {
 	return fmt.Sprintf("%s:_sys:tables", m.database)
 }
 
+// rowIDKey returns the key for a table's next ROWID counter.
+func (m *SchemaManager) rowIDKey(table string) string {
+	return fmt.Sprintf("%s:_sys:rowid:%s", m.database, strings.ToLower(table))
+}
+
 // CreateTable creates a new table.
 func (m *SchemaManager) CreateTable(schema *Schema) error {
 	m.mu.Lock()
@@ -183,6 +188,11 @@ func (m *SchemaManager) DropTable(name string) error {
 		return fmt.Errorf("failed to delete schema: %w", err)
 	}
 
+	// Delete ROWID state.
+	m.pool.WithClient(func(c *KVClient) error {
+		return c.Delete(m.rowIDKey(name))
+	})
+
 	// Update catalog
 	if err := m.removeFromCatalog(name); err != nil {
 		return err
@@ -361,19 +371,16 @@ func (m *SchemaManager) GetNextRowID(table string) (int64, error) {
 		return 0, err
 	}
 
-	// Get current and increment
-	rowid := schema.NextRowID
-	if rowid == 0 {
-		rowid = 1
+	nextRowID, err := m.getNextRowIDLocked(schema)
+	if err != nil {
+		return 0, err
 	}
-	schema.NextRowID = rowid + 1
 
-	// Save updated schema
-	if err := m.saveSchemaLocked(schema); err != nil {
+	if err := m.saveNextRowIDLocked(schema.Name, nextRowID+1); err != nil {
 		return 0, err
 	}
 
-	return rowid, nil
+	return nextRowID, nil
 }
 
 // UpdateMaxRowID updates the next ROWID if the provided value is higher.
@@ -386,11 +393,63 @@ func (m *SchemaManager) UpdateMaxRowID(table string, rowid int64) error {
 		return err
 	}
 
-	if rowid >= schema.NextRowID {
-		schema.NextRowID = rowid + 1
-		return m.saveSchemaLocked(schema)
+	nextRowID, err := m.getNextRowIDLocked(schema)
+	if err != nil {
+		return err
+	}
+
+	if rowid >= nextRowID {
+		return m.saveNextRowIDLocked(schema.Name, rowid+1)
+	}
+
+	return nil
+}
+
+// getNextRowIDLocked reads a table's next ROWID counter (must hold lock).
+func (m *SchemaManager) getNextRowIDLocked(schema *Schema) (int64, error) {
+	key := m.rowIDKey(schema.Name)
+	var data string
+	err := m.pool.WithClient(func(c *KVClient) error {
+		var err error
+		data, err = c.Read(key)
+		return err
+	})
+	if err == nil {
+		var nextRowID int64
+		if _, scanErr := fmt.Sscanf(data, "%d", &nextRowID); scanErr != nil {
+			return 0, fmt.Errorf("failed to parse rowid counter: %w", scanErr)
+		}
+		if nextRowID < 1 {
+			nextRowID = 1
+		}
+		return nextRowID, nil
+	}
+	if err != ErrKeyNotFound {
+		return 0, err
+	}
+
+	if schema.NextRowID > 0 {
+		return schema.NextRowID, nil
+	}
+	return 1, nil
+}
+
+// saveNextRowIDLocked saves a table's next ROWID counter (must hold lock).
+func (m *SchemaManager) saveNextRowIDLocked(table string, nextRowID int64) error {
+	if nextRowID < 1 {
+		nextRowID = 1
+	}
+
+	err := m.pool.WithClient(func(c *KVClient) error {
+		return c.Write(m.rowIDKey(table), fmt.Sprintf("%d", nextRowID))
+	})
+	if err != nil {
+		return fmt.Errorf("failed to write rowid counter: %w", err)
 	}
 
+	if schema, ok := m.cache[strings.ToLower(table)]; ok {
+		schema.NextRowID = nextRowID
+	}
 	return nil
 }
 
@@ -722,6 +781,16 @@ func (m *SchemaManager) RenameTable(oldName, newName string) error {
 	// Update schema name
 	schema.Name = newName
 
+	var nextRowID string
+	rowIDKey := m.rowIDKey(oldName)
+	m.pool.WithClient(func(c *KVClient) error {
+		data, err := c.Read(rowIDKey)
+		if err == nil {
+			nextRowID = data
+		}
+		return nil
+	})
+
 	// Delete old schema
 	oldKey := m.schemaKey(oldName)
 	err = m.pool.WithClient(func(c *KVClient) error {
@@ -734,6 +803,19 @@ func (m *SchemaManager) RenameTable(oldName, newName string) error {
 	// Remove from catalog
 	m.removeFromCatalog(oldName)
 
+	// Move ROWID state.
+	m.pool.WithClient(func(c *KVClient) error {
+		return c.Delete(rowIDKey)
+	})
+	if nextRowID != "" {
+		err = m.pool.WithClient(func(c *KVClient) error {
+			return c.Write(m.rowIDKey(newName), nextRowID)
+		})
+		if err != nil {
+			return err
+		}
+	}
+
 	// Update cache
 	delete(m.cache, strings.ToLower(oldName))
 

+ 174 - 0
pkg/storage/schema_test.go

@@ -0,0 +1,174 @@
+package storage
+
+import (
+	"bufio"
+	"fmt"
+	"net"
+	"strings"
+	"sync"
+	"testing"
+	"time"
+)
+
+type testKVServer struct {
+	mu      sync.Mutex
+	data    map[string]string
+	writes  map[string]int
+	closers []net.Conn
+}
+
+func newTestKVServer(t *testing.T) *testKVServer {
+	t.Helper()
+
+	return &testKVServer{
+		data:   make(map[string]string),
+		writes: make(map[string]int),
+	}
+}
+
+func newTestKVPool(kv *testKVServer, size int, timeout time.Duration) *KVPool {
+	pool := &KVPool{
+		pool:    make(chan *KVClient, size),
+		size:    size,
+		timeout: timeout,
+	}
+	for i := 0; i < size; i++ {
+		pool.pool <- kv.client()
+	}
+	return pool
+}
+
+func (s *testKVServer) close() {
+	s.mu.Lock()
+	closers := append([]net.Conn(nil), s.closers...)
+	s.mu.Unlock()
+
+	for _, conn := range closers {
+		_ = conn.Close()
+	}
+}
+
+func (s *testKVServer) client() *KVClient {
+	clientConn, serverConn := net.Pipe()
+
+	s.mu.Lock()
+	s.closers = append(s.closers, clientConn, serverConn)
+	s.mu.Unlock()
+
+	go s.handle(serverConn)
+	return &KVClient{
+		conn:   clientConn,
+		reader: bufio.NewReader(clientConn),
+		writer: bufio.NewWriter(clientConn),
+	}
+}
+
+func (s *testKVServer) writeCount(prefix string) int {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	var count int
+	for key, writes := range s.writes {
+		if strings.Contains(key, prefix) {
+			count += writes
+		}
+	}
+	return count
+}
+
+func (s *testKVServer) handle(conn net.Conn) {
+	defer conn.Close()
+
+	r := bufio.NewReader(conn)
+	for {
+		cmd, err := r.ReadString('\r')
+		if err != nil {
+			return
+		}
+		cmd = strings.TrimSuffix(cmd, "\r")
+
+		resp := s.execute(cmd)
+		if _, err := fmt.Fprintf(conn, "%s\r", resp); err != nil {
+			return
+		}
+	}
+}
+
+func (s *testKVServer) execute(cmd string) string {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	switch {
+	case strings.HasPrefix(cmd, "write "):
+		parts := strings.SplitN(strings.TrimPrefix(cmd, "write "), "|", 2)
+		if len(parts) != 2 {
+			return "error"
+		}
+		s.data[parts[0]] = parts[1]
+		s.writes[parts[0]]++
+		return "success"
+	case strings.HasPrefix(cmd, "read "):
+		key := strings.TrimPrefix(cmd, "read ")
+		value, ok := s.data[key]
+		if !ok {
+			return "error"
+		}
+		return value
+	case strings.HasPrefix(cmd, "delete "):
+		key := strings.TrimPrefix(cmd, "delete ")
+		delete(s.data, key)
+		return "success"
+	case strings.HasPrefix(cmd, "reads "):
+		prefix := strings.TrimPrefix(cmd, "reads ")
+		values := make([]string, 0)
+		for key, value := range s.data {
+			if strings.HasPrefix(key, prefix) {
+				values = append(values, value)
+			}
+		}
+		return strings.Join(values, "\n")
+	default:
+		return "error"
+	}
+}
+
+func TestInsertDoesNotRewriteSchemaForRowIDUpdates(t *testing.T) {
+	kv := newTestKVServer(t)
+	defer kv.close()
+
+	pool := newTestKVPool(kv, 2, 5*time.Second)
+	defer pool.Close()
+
+	schemas := NewSchemaManager(pool, "testdb")
+	tables := NewTableManager(pool, schemas, "testdb")
+
+	err := schemas.CreateTable(&Schema{
+		Name: "users",
+		Columns: []Column{
+			{Name: "id", Type: "INTEGER", Nullable: false, PrimaryKey: true},
+			{Name: "name", Type: "TEXT", Nullable: true},
+		},
+	})
+	if err != nil {
+		t.Fatalf("create table: %v", err)
+	}
+
+	initialSchemaWrites := kv.writeCount(":_schema:")
+	if initialSchemaWrites != 1 {
+		t.Fatalf("expected create table to write schema once, got %d", initialSchemaWrites)
+	}
+
+	for i := int64(1); i <= 3; i++ {
+		err := tables.Insert("users", Row{"id": i, "name": fmt.Sprintf("user-%d", i)})
+		if err != nil {
+			t.Fatalf("insert %d: %v", i, err)
+		}
+	}
+
+	if got := kv.writeCount(":_schema:"); got != initialSchemaWrites {
+		t.Fatalf("expected inserts not to rewrite schema, got %d schema writes", got)
+	}
+	if got := kv.writeCount(":_sys:rowid:"); got != 3 {
+		t.Fatalf("expected rowid counter writes for inserts, got %d", got)
+	}
+}