Kaynağa Gözat

clients, imports, fixes

Danilo Fragoso 7 ay önce
ebeveyn
işleme
fafdc1ccd9

+ 1 - 0
.gitignore

@@ -1 +1,2 @@
 .DS_Store
+.claude

+ 0 - 609
API.md

@@ -1,609 +0,0 @@
-# PizzaSQL HTTP API Documentation
-
-PizzaSQL provides a RESTful HTTP API for executing SQL queries and managing your database. This document describes all available endpoints and how to use them.
-
-## Starting the HTTP Server
-
-```bash
-# Start with default settings (localhost:8080)
-./pizzasql -http
-
-# Custom host and port
-./pizzasql -http -http-host 0.0.0.0 -http-port 3000
-
-# With authentication enabled
-./pizzasql -http -http-auth -api-keys "key1,key2,key3"
-
-# Full example with all options
-./pizzasql -http \
-  -http-host 0.0.0.0 \
-  -http-port 8080 \
-  -http-cors \
-  -http-auth \
-  -api-keys "your-secret-api-key" \
-  -kv localhost:8085 \
-  -db mydb
-```
-
-## Authentication
-
-When authentication is enabled (`-http-auth`), all requests must include an `Authorization` header with a valid API key:
-
-```bash
-curl -H "Authorization: Bearer your-secret-api-key" ...
-```
-
----
-
-## Endpoints
-
-### POST /query
-
-Execute a single SQL query.
-
-**Request:**
-```json
-{
-  "sql": "SELECT * FROM users WHERE id = ?",
-  "params": [1]
-}
-```
-
-**Response:**
-```json
-{
-  "columns": [
-    {"name": "id", "type": "INTEGER"},
-    {"name": "name", "type": "TEXT"},
-    {"name": "email", "type": "TEXT"}
-  ],
-  "rows": [
-    [1, "Alice", "alice@example.com"]
-  ],
-  "rowsAffected": 0,
-  "lastInsertId": 0,
-  "executionTime": "1.234ms"
-}
-```
-
-**Query Parameters:**
-- `?pretty=true` - Format JSON output with indentation
-- `?readonly=true` - Reject write operations (INSERT, UPDATE, DELETE)
-- `?timeout=5000` - Query timeout in milliseconds
-- `?explain=true` - Include query plan in response
-
-**Examples:**
-
-```bash
-# Simple SELECT
-curl -X POST http://localhost:8080/query \
-  -H "Content-Type: application/json" \
-  -d '{"sql": "SELECT 1 + 1 AS result"}'
-
-# SELECT with pretty output
-curl -X POST "http://localhost:8080/query?pretty=true" \
-  -H "Content-Type: application/json" \
-  -d '{"sql": "SELECT * FROM users"}'
-
-# SELECT DISTINCT to remove duplicates
-curl -X POST http://localhost:8080/query \
-  -H "Content-Type: application/json" \
-  -d '{"sql": "SELECT DISTINCT status FROM orders"}'
-
-# 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"]
-  }'
-
-# SELECT with DISTINCT
-curl -X POST http://localhost:8080/query \
-  -H "Content-Type: application/json" \
-  -d '{"sql": "SELECT DISTINCT status FROM orders"}'
-
-# SELECT with parameters
-curl -X POST http://localhost:8080/query \
-  -H "Content-Type: application/json" \
-  -d '{
-    "sql": "SELECT * FROM users WHERE name LIKE ?",
-    "params": ["%alice%"]
-  }'
-
-# CREATE TABLE
-curl -X POST http://localhost:8080/query \
-  -H "Content-Type: application/json" \
-  -d '{"sql": "CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price REAL)"}'
-```
-
----
-
-### POST /execute
-
-Execute multiple SQL statements, optionally within a transaction.
-
-**Request:**
-```json
-{
-  "statements": [
-    {"sql": "INSERT INTO users (name) VALUES (?)", "params": ["Alice"]},
-    {"sql": "INSERT INTO users (name) VALUES (?)", "params": ["Bob"]},
-    {"sql": "UPDATE users SET active = 1"}
-  ],
-  "transaction": true
-}
-```
-
-**Response:**
-```json
-{
-  "results": [
-    {"rowsAffected": 1, "lastInsertId": 1},
-    {"rowsAffected": 1, "lastInsertId": 2},
-    {"rowsAffected": 2, "lastInsertId": 0}
-  ],
-  "totalRowsAffected": 4,
-  "executionTime": "5.678ms"
-}
-```
-
-**Examples:**
-
-```bash
-# Batch insert with transaction
-curl -X POST http://localhost:8080/execute \
-  -H "Content-Type: application/json" \
-  -d '{
-    "statements": [
-      {"sql": "INSERT INTO products (name, price) VALUES (?, ?)", "params": ["Widget", 9.99]},
-      {"sql": "INSERT INTO products (name, price) VALUES (?, ?)", "params": ["Gadget", 19.99]},
-      {"sql": "INSERT INTO products (name, price) VALUES (?, ?)", "params": ["Gizmo", 29.99]}
-    ],
-    "transaction": true
-  }'
-
-# Multiple operations without transaction
-curl -X POST http://localhost:8080/execute \
-  -H "Content-Type: application/json" \
-  -d '{
-    "statements": [
-      {"sql": "DELETE FROM logs WHERE created_at < date(\"now\", \"-30 days\")"},
-      {"sql": "VACUUM"}
-    ],
-    "transaction": false
-  }'
-```
-
----
-
-### GET /schema/tables
-
-List all tables in the database.
-
-**Response:**
-```json
-{
-  "tables": ["users", "products", "orders"],
-  "count": 3
-}
-```
-
-**Example:**
-```bash
-curl http://localhost:8080/schema/tables
-```
-
----
-
-### GET /schema/tables/{name}
-
-Get detailed schema information for a specific table.
-
-**Response:**
-```json
-{
-  "name": "users",
-  "columns": [
-    {
-      "name": "id",
-      "type": "INTEGER",
-      "nullable": false,
-      "primaryKey": true,
-      "default": null
-    },
-    {
-      "name": "name",
-      "type": "TEXT",
-      "nullable": true,
-      "primaryKey": false,
-      "default": null
-    },
-    {
-      "name": "email",
-      "type": "TEXT",
-      "nullable": true,
-      "primaryKey": false,
-      "default": null
-    }
-  ],
-  "primaryKey": "id",
-  "autoIncrement": true
-}
-```
-
-**Example:**
-```bash
-curl http://localhost:8080/schema/tables/users
-```
-
----
-
-### GET /health
-
-Health check endpoint for monitoring and load balancers.
-
-**Response:**
-```json
-{
-  "status": "ok",
-  "database": "mydb",
-  "timestamp": "2024-01-15T10:30:00Z"
-}
-```
-
-**Example:**
-```bash
-curl http://localhost:8080/health
-```
-
----
-
-### GET /stats
-
-Server statistics and metrics.
-
-**Response:**
-```json
-{
-  "queriesExecuted": 1234,
-  "queriesSuccess": 1200,
-  "queriesError": 34,
-  "uptime": "2h30m15s",
-  "startTime": "2024-01-15T08:00:00Z",
-  "tables": 5
-}
-```
-
-**Example:**
-```bash
-curl http://localhost:8080/stats
-```
-
----
-
-### GET /metrics
-
-Prometheus-format metrics for monitoring systems.
-
-**Response:**
-```
-# HELP pizzasql_queries_total Total number of queries executed
-# TYPE pizzasql_queries_total counter
-pizzasql_queries_total{status="success"} 1200
-pizzasql_queries_total{status="error"} 34
-
-# HELP pizzasql_queries_executed_total Total queries executed (all statuses)
-# TYPE pizzasql_queries_executed_total counter
-pizzasql_queries_executed_total 1234
-
-# HELP pizzasql_tables_count Number of tables in the database
-# TYPE pizzasql_tables_count gauge
-pizzasql_tables_count 5
-
-# HELP pizzasql_uptime_seconds Server uptime in seconds
-# TYPE pizzasql_uptime_seconds gauge
-pizzasql_uptime_seconds 9015.00
-
-# HELP pizzasql_info PizzaSQL server information
-# TYPE pizzasql_info gauge
-pizzasql_info{version="0.1.0"} 1
-```
-
-**Example:**
-```bash
-curl http://localhost:8080/metrics
-```
-
----
-
-### POST /transaction/begin
-
-Start a new transaction.
-
-**Response:**
-```json
-{
-  "status": "started"
-}
-```
-
-**Example:**
-```bash
-curl -X POST http://localhost:8080/transaction/begin
-```
-
----
-
-### POST /transaction/commit
-
-Commit the current transaction.
-
-**Response:**
-```json
-{
-  "status": "committed"
-}
-```
-
-**Example:**
-```bash
-curl -X POST http://localhost:8080/transaction/commit
-```
-
----
-
-### POST /transaction/rollback
-
-Rollback the current transaction.
-
-**Response:**
-```json
-{
-  "status": "rolled back"
-}
-```
-
-**Example:**
-```bash
-curl -X POST http://localhost:8080/transaction/rollback
-```
-
----
-
-## Error Responses
-
-All error responses follow this format:
-
-```json
-{
-  "error": {
-    "code": "ERROR_CODE",
-    "message": "Human-readable error message",
-    "details": {}
-  }
-}
-```
-
-**Error Codes:**
-
-| Code | HTTP Status | Description |
-|------|-------------|-------------|
-| `MISSING_SQL` | 400 | No SQL statement provided |
-| `SYNTAX_ERROR` | 400 | SQL syntax error |
-| `EXECUTION_ERROR` | 500 | Error executing query |
-| `READ_ONLY_MODE` | 403 | Write operation in read-only mode |
-| `MISSING_AUTH` | 401 | Authorization header required |
-| `INVALID_API_KEY` | 403 | Invalid API key |
-| `TABLE_NOT_FOUND` | 404 | Table does not exist |
-| `METHOD_NOT_ALLOWED` | 405 | Invalid HTTP method |
-| `TIMEOUT` | 408 | Query timeout exceeded |
-
----
-
-## Parameterized Queries
-
-Use `?` placeholders in your SQL and provide values in the `params` array:
-
-```json
-{
-  "sql": "SELECT * FROM users WHERE name = ? AND age > ?",
-  "params": ["Alice", 25]
-}
-```
-
-**Supported Parameter Types:**
-- `null` → `NULL`
-- `string` → `'escaped''string'`
-- `integer` → `123`
-- `float` → `3.14`
-- `boolean` → `1` (true) or `0` (false)
-
-**SQL Injection Prevention:**
-Strings are automatically escaped (single quotes are doubled).
-
-```json
-{
-  "sql": "SELECT * FROM users WHERE name = ?",
-  "params": ["O'Brien"]
-}
-// Becomes: SELECT * FROM users WHERE name = 'O''Brien'
-```
-
----
-
-## Response Compression
-
-The server automatically compresses responses with gzip when the client sends:
-
-```
-Accept-Encoding: gzip
-```
-
-Example:
-```bash
-curl -H "Accept-Encoding: gzip" http://localhost:8080/query \
-  -H "Content-Type: application/json" \
-  -d '{"sql": "SELECT * FROM large_table"}' \
-  --compressed
-```
-
----
-
-## CORS Support
-
-CORS is enabled by default (`-http-cors`), allowing requests from any origin. Headers sent:
-
-```
-Access-Control-Allow-Origin: *
-Access-Control-Allow-Methods: GET, POST, OPTIONS
-Access-Control-Allow-Headers: Content-Type, Authorization
-```
-
----
-
-## Complete Usage Examples
-
-### Create a Database Schema
-
-```bash
-# Create users table
-curl -X POST http://localhost:8080/query \
-  -H "Content-Type: application/json" \
-  -d '{"sql": "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE, created_at TEXT DEFAULT CURRENT_TIMESTAMP)"}'
-
-# Create posts table with foreign key
-curl -X POST http://localhost:8080/query \
-  -H "Content-Type: application/json" \
-  -d '{"sql": "CREATE TABLE posts (id INTEGER PRIMARY KEY, user_id INTEGER, title TEXT, content TEXT, FOREIGN KEY (user_id) REFERENCES users(id))"}'
-
-# Create index
-curl -X POST http://localhost:8080/query \
-  -H "Content-Type: application/json" \
-  -d '{"sql": "CREATE INDEX idx_posts_user ON posts(user_id)"}'
-```
-
-### CRUD Operations
-
-```bash
-# Create (INSERT)
-curl -X POST http://localhost:8080/query \
-  -H "Content-Type: application/json" \
-  -d '{
-    "sql": "INSERT INTO users (name, email) VALUES (?, ?)",
-    "params": ["John Doe", "john@example.com"]
-  }'
-
-# Read (SELECT)
-curl -X POST http://localhost:8080/query \
-  -H "Content-Type: application/json" \
-  -d '{"sql": "SELECT * FROM users WHERE id = ?", "params": [1]}'
-
-# Update
-curl -X POST http://localhost:8080/query \
-  -H "Content-Type: application/json" \
-  -d '{
-    "sql": "UPDATE users SET name = ? WHERE id = ?",
-    "params": ["Jane Doe", 1]
-  }'
-
-# Delete
-curl -X POST http://localhost:8080/query \
-  -H "Content-Type: application/json" \
-  -d '{"sql": "DELETE FROM users WHERE id = ?", "params": [1]}'
-```
-
-### Transaction Example
-
-```bash
-# Start transaction
-curl -X POST http://localhost:8080/transaction/begin
-
-# Execute multiple queries
-curl -X POST http://localhost:8080/query \
-  -H "Content-Type: application/json" \
-  -d '{"sql": "INSERT INTO accounts (name, balance) VALUES (?, ?)", "params": ["Alice", 1000]}'
-
-curl -X POST http://localhost:8080/query \
-  -H "Content-Type: application/json" \
-  -d '{"sql": "INSERT INTO accounts (name, balance) VALUES (?, ?)", "params": ["Bob", 500]}'
-
-# Commit if successful
-curl -X POST http://localhost:8080/transaction/commit
-
-# Or rollback on error
-# curl -X POST http://localhost:8080/transaction/rollback
-```
-
-### Using with JavaScript/Node.js
-
-```javascript
-async function query(sql, params = []) {
-  const response = await fetch('http://localhost:8080/query', {
-    method: 'POST',
-    headers: {
-      'Content-Type': 'application/json',
-      'Authorization': 'Bearer your-api-key'  // if auth enabled
-    },
-    body: JSON.stringify({ sql, params })
-  });
-
-  if (!response.ok) {
-    const error = await response.json();
-    throw new Error(error.error.message);
-  }
-
-  return response.json();
-}
-
-// Usage
-const users = await query('SELECT * FROM users WHERE active = ?', [true]);
-console.log(users.rows);
-```
-
-### Using with Python
-
-```python
-import requests
-
-def query(sql, params=None):
-    response = requests.post(
-        'http://localhost:8080/query',
-        json={'sql': sql, 'params': params or []},
-        headers={
-            'Content-Type': 'application/json',
-            'Authorization': 'Bearer your-api-key'  # if auth enabled
-        }
-    )
-    response.raise_for_status()
-    return response.json()
-
-# Usage
-result = query('SELECT * FROM users WHERE name LIKE ?', ['%john%'])
-for row in result['rows']:
-    print(row)
-```
-
----
-
-## Test Coverage
-
-The HTTP API has comprehensive test coverage for all endpoints:
-
-| Test | Description |
-|------|-------------|
-| `TestQueryEndpoint` | Basic query execution (CREATE, INSERT, SELECT) |
-| `TestExecuteEndpoint` | Batch execution with transactions |
-| `TestSchemaEndpoints` | Table listing and schema introspection |
-| `TestHealthEndpoint` | Health check response |
-| `TestStatsEndpoint` | Statistics response |
-| `TestMetricsEndpoint` | Prometheus metrics format |
-| `TestReadOnlyMode` | Write rejection in readonly mode |
-| `TestCORSMiddleware` | CORS headers on OPTIONS request |
-| `TestAuthMiddleware` | API key authentication |
-| `TestCompressionMiddleware` | Gzip compression |
-| `TestParameterizedQuery` | Parameter substitution |
-| `TestTransactionEndpoints` | BEGIN/COMMIT/ROLLBACK |
-| `TestQueryEndpointErrors` | Error handling cases |
-| `TestPrettyPrintOption` | Pretty JSON formatting |
-| `TestSchemaTableNotFound` | 404 for missing tables |

+ 0 - 894
IMPLEMENTATION_PLAN.md

@@ -1,894 +0,0 @@
-# PizzaSQL-Next Implementation Plan
-
-## Overview
-
-Build a SQL-92 compliant database with SQLite compatibility, using PizzaKV as the storage backend. This is a fresh implementation with a hand-written recursive descent parser.
-
-## Project Structure
-
-```
-pizzasql-next/
-├── main.go                     # Entry point
-├── go.mod
-├── go.sum
-├── Makefile
-│
-├── pkg/
-│   ├── lexer/                  # SQL tokenizer
-│   │   ├── lexer.go            # Token scanner
-│   │   ├── token.go            # Token types and definitions
-│   │   └── lexer_test.go
-│   │
-│   ├── parser/                 # SQL-92 parser
-│   │   ├── parser.go           # Recursive descent parser
-│   │   ├── ast.go              # Abstract Syntax Tree definitions
-│   │   ├── errors.go           # Parser error types
-│   │   └── parser_test.go
-│   │
-│   ├── analyzer/               # Semantic analysis (Phase 2)
-│   │   ├── types.go            # Type system definitions
-│   │   ├── scope.go            # Symbol tables and scoping
-│   │   ├── analyzer.go         # Type checking, validation
-│   │   └── analyzer_test.go
-│   │
-│   ├── executor/               # Query execution (Phase 3)
-│   │   ├── executor.go
-│   │   └── executor_test.go
-│   │
-│   └── storage/                # PizzaKV integration (Phase 3)
-│       ├── kv.go               # KV client
-│       ├── schema.go           # Schema management
-│       ├── table.go            # Table operations
-│       └── storage_test.go
-│
-├── sql-92.bnf                  # BNF grammar reference
-└── testdata/                   # SQL test files
-    ├── valid/                  # Valid SQL statements
-    └── invalid/                # Invalid SQL for error testing
-```
-
----
-
-## Phase 1: Lexer & Parser Foundation ✅ COMPLETED
-
-### Status: Complete
-
-**Performance Achieved:**
-- Lexer: ~227,000 ops/sec (4.7µs per token stream)
-- Parser SELECT: ~176,000 ops/sec (6.9µs per statement)
-- Parser CREATE TABLE: ~265,000 ops/sec (4.5µs per statement)
-- **Exceeds target of 10,000 statements/second by 17x**
-
-### 1.1 Token Types ✅
-
-Implemented 100+ token types including:
-- Core tokens: EOF, Error, Comment, Ident, Number, String
-- Operators: +, -, *, /, %, ||, =, <>, <, <=, >, >=
-- Punctuation: (, ), ,, ;, .
-- SQL-92 Keywords: SELECT, FROM, WHERE, AND, OR, NOT, etc.
-- DDL Keywords: CREATE, DROP, ALTER, TABLE, INDEX, VIEW
-- Constraint Keywords: PRIMARY, KEY, FOREIGN, REFERENCES, UNIQUE, CHECK
-- Join Keywords: JOIN, INNER, LEFT, RIGHT, FULL, OUTER, CROSS, NATURAL
-- Data Types: INTEGER, REAL, TEXT, BLOB, VARCHAR, BOOLEAN, TIMESTAMP
-- SQLite Extensions: PRAGMA, EXPLAIN, VACUUM, ANALYZE, AUTOINCREMENT
-
-### 1.2 AST Node Types ✅
-
-Implemented all planned AST types:
-- Statements: SelectStmt, InsertStmt, UpdateStmt, DeleteStmt, CreateTableStmt, DropTableStmt
-- Expressions: BinaryExpr, UnaryExpr, LiteralExpr, ColumnRef, FunctionCall
-- Advanced: SubqueryExpr, CaseExpr, InExpr, BetweenExpr, LikeExpr, IsNullExpr, CastExpr, ExistsExpr
-
-### 1.3 Parser Implementation ✅
-
-Recursive descent parser with operator precedence climbing:
-- Full expression parsing with correct precedence
-- JOIN parsing (INNER, LEFT, RIGHT, FULL, CROSS)
-- Subquery support in expressions
-- CASE WHEN expressions
-- Function calls including keyword-functions (COALESCE, NULLIF)
-
-### 1.4 Test Coverage ✅
-
-- Lexer: 15 test functions covering all token types
-- Parser: 35+ test functions covering all statement types
-- Error cases: 6 specific error condition tests
-- Benchmarks: 3 performance benchmarks
-
----
-
-## Phase 2: Semantic Analysis ✅ COMPLETED
-
-### Status: Complete
-
-### 2.1 Type System
-
-SQLite-compatible type affinity system:
-- **INTEGER**: Whole numbers (INT, SMALLINT, BIGINT, BOOLEAN)
-- **REAL**: Floating point (FLOAT, DOUBLE, DECIMAL)
-- **TEXT**: Strings (VARCHAR, CHAR, CHARACTER)
-- **BLOB**: Binary data
-- **NUMERIC**: Flexible numeric (can store INTEGER or REAL)
-- **NULL**: Null value type
-- **ANY**: Unknown/unresolved type
-
-### 2.2 Scope & Symbol Tables
-
-Hierarchical scope management:
-- Global scope for tables and databases
-- Query scope for table aliases and CTEs
-- Column scope for resolving column references
-- Support for qualified names (table.column)
-
-### 2.3 Analyzer Features
-
-- **Column Resolution**: Resolve column references against schema
-- **Type Inference**: Infer types for expressions and operations
-- **Type Checking**: Validate type compatibility in operations
-- **Function Validation**: Check function signatures and argument counts
-- **Aggregate Detection**: Identify aggregate vs scalar expressions
-- **Schema Validation**: Validate table/column existence
-
-### 2.4 Built-in Functions
-
-Aggregate functions:
-- COUNT, SUM, AVG, MIN, MAX
-
-Scalar functions:
-- String: UPPER, LOWER, LENGTH, SUBSTR, TRIM, REPLACE, CONCAT
-- Numeric: ABS, ROUND, CEIL, FLOOR, MOD
-- Null handling: COALESCE, NULLIF, IFNULL
-- Type: TYPEOF, CAST
-- Date: DATE, TIME, DATETIME
-
-### 2.5 Analysis Errors
-
-Detailed error reporting with:
-- Error type classification
-- Line/column position
-- Context information
-- Helpful error messages
-
----
-
-## Phase 3: Execution Engine ✅ COMPLETED
-
-### Status: Complete
-
-### 3.1 Storage Layer (PizzaKV Integration)
-
-- **KVClient**: TCP connection to PizzaKV with read/write/delete/reads commands
-- **KVPool**: Connection pooling with configurable size and timeout
-- **SchemaManager**: Table schema storage and caching
-- **TableManager**: Row-level CRUD operations with JSON serialization
-
-### 3.2 Query Execution
-
-Full SQL execution support:
-- **SELECT**: FROM, WHERE, JOIN (INNER/LEFT/CROSS), GROUP BY, HAVING, ORDER BY, LIMIT/OFFSET, DISTINCT
-- **INSERT**: Single and multi-row inserts, named or positional columns
-- **UPDATE**: SET with expressions, WHERE filtering
-- **DELETE**: WHERE filtering
-- **CREATE TABLE**: Constraints (PRIMARY KEY, NOT NULL, DEFAULT)
-- **DROP TABLE**: IF EXISTS support
-
-### 3.3 Expression Evaluation
-
-- Arithmetic: +, -, *, /, %
-- Comparison: =, <>, <, <=, >, >=
-- Logical: AND, OR, NOT
-- String: || (concat), LIKE
-- Null handling: IS NULL, IS NOT NULL, COALESCE, NULLIF, IFNULL
-- CASE WHEN expressions
-- IN, BETWEEN operators
-- CAST type conversion
-
-### 3.4 Aggregate Functions
-
-- COUNT(*), COUNT(column), COUNT(DISTINCT column)
-- SUM, AVG, MIN, MAX
-
-### 3.7 DISTINCT Implementation ✅
-
-- **SELECT DISTINCT**: Remove duplicate rows from result set
-- **Hash-based deduplication**: Efficient row uniqueness checking
-- **Multi-column support**: DISTINCT across all selected columns
-- **Null byte separator**: Prevents hash collisions between values
-- **Post-processing**: Applied after filtering/joining/ordering
-- **Parser support**: stmt.Distinct boolean flag
-- **Executor**: applyDistinct() method with O(n) complexity
-- **Unit tests**: Direct function testing (TestDistinct)
-- **Integration tests**: End-to-end validation in stress test
-
-**Implementation Details:**
-```go
-// Hash-based deduplication in executor.go
-func (e *Executor) applyDistinct(rows [][]interface{}) [][]interface{} {
-    seen := make(map[string]bool)
-    uniqueRows := make([][]interface{}, 0)
-    
-    for _, row := range rows {
-        key := "" // Concatenate all column values
-        for i, val := range row {
-            if i > 0 {
-                key += "\x00" // Null byte separator
-            }
-            key += fmt.Sprintf("%v", val)
-        }
-        
-        if !seen[key] {
-            seen[key] = true
-            uniqueRows = append(uniqueRows, row)
-        }
-    }
-    
-    return uniqueRows
-}
-```
-
-**Performance:**
-- Time complexity: O(n) where n = number of rows
-- Space complexity: O(n) for hash map storage
-- Applied after ORDER BY/LIMIT for correct behavior
-- Tested with 10-element dataset: 10 rows → 3 unique values
-
-### 3.5 Built-in Functions
-
-- String: UPPER, LOWER, LENGTH, SUBSTR, TRIM, REPLACE
-- Numeric: ABS
-- Type: TYPEOF
-
-### 3.8 CLI Interface
-
-- Interactive REPL with multi-line input
-- Command-line single statement execution
-- Piped input support
-- Expression-only mode (no PizzaKV required)
-- Commands: help, quit, tables, clear
-
----
-
-## Phase 4: SQLite Compatibility ✅ COMPLETED
-
-### Status: Complete
-
-SQLite-specific features implemented:
-
-### 4.1 ROWID Support ✅
-- Implicit ROWID column for all tables
-- `SELECT rowid, * FROM table`
-- ROWID as default primary key when none specified
-- Support for `oid` and `_rowid_` aliases
-
-### 4.2 AUTOINCREMENT ✅
-- Parser support for AUTOINCREMENT keyword
-- Auto-generate sequential IDs on INSERT
-- Track max ROWID per table
-- Prevent ROWID reuse after deletion
-
-### 4.3 PRAGMA Statements ✅
-- `PRAGMA table_info(table_name)` - column metadata
-- `PRAGMA database_list` - list databases
-- `PRAGMA table_list` - list tables
-- `PRAGMA version` - database version
-
-### 4.4 EXPLAIN Support ✅
-- `EXPLAIN query` - show execution plan (opcodes)
-- `EXPLAIN QUERY PLAN query` - detailed query plan
-
-### 4.5 Additional SQLite Functions ✅
-- `printf()` - formatted output
-- `hex()`, `unhex()` - hex encoding
-- `random()`, `randomblob()` - random values
-- `zeroblob()` - zero-filled blob
-- `instr()` - find substring position
-- `glob()` - glob pattern matching
-- `round()` - number rounding
-- `concat()` - string concatenation
-
-### 4.6 SQLite SQL Dialect ✅
-- `INSERT OR REPLACE` / `INSERT OR IGNORE` / `INSERT OR FAIL` / `INSERT OR ABORT`
-- Conflict resolution on INSERT
-
----
-
-## Phase 5: Transactions & Indexes ✅ COMPLETED
-
-### Status: Complete
-
-Advanced features for transaction management, query optimization, and schema modification:
-
-### 5.1 Transaction Support ✅
-- ✅ `BEGIN [TRANSACTION]` - start transaction
-- ✅ `COMMIT` - commit changes
-- ✅ `ROLLBACK` - rollback changes  
-- ✅ `SAVEPOINT name` - create savepoint
-- ✅ `RELEASE SAVEPOINT name` - release savepoint
-- ✅ `ROLLBACK TO SAVEPOINT name` - partial rollback
-- ✅ Transaction log for rollback support
-- ✅ Lexer tokens (BEGIN, COMMIT, ROLLBACK, SAVEPOINT, RELEASE)
-- ✅ Parser AST nodes (BeginStmt, CommitStmt, RollbackStmt, SavepointStmt, ReleaseStmt)
-- ✅ Executor implementation with transaction state management
-- ✅ Parser tests for all transaction statements (8 tests)
-- ✅ Executor tests for all transaction statements (8 test cases)
-
-**Note**: Current implementation builds transaction log but rollback doesn't restore state due to PizzaKV limitations
-
-### 5.2 Index Support ✅
-- ✅ `CREATE INDEX name ON table (columns)` - create index
-- ✅ `CREATE UNIQUE INDEX` - unique constraint via index
-- ✅ `DROP INDEX [IF EXISTS]` - drop index
-- ✅ Index-based query optimization in SELECT (automatic)
-- ✅ Automatic index maintenance on INSERT/UPDATE/DELETE
-- ✅ Index storage using PizzaKV radix trie (prefix-based lookups)
-- ✅ Multi-column index support
-- ✅ Index lookup methods (SelectByIndex, LookupIndex)
-- ✅ Parser AST nodes (CreateIndexStmt, DropIndexStmt)
-- ✅ Schema manager index operations (Create, Drop, List)
-- ✅ Parser tests for CREATE/DROP INDEX (6 tests)
-- ✅ Index benchmarks (2 benchmark functions showing significant speedup)
-
-**Performance**: Index-based queries show dramatic speedup over full table scans
-
-### 5.3 Subquery Execution ✅
-- ✅ Scalar subqueries in SELECT and WHERE clauses
-- ✅ Subqueries in IN expressions (IN subquery)
-- ✅ Subqueries in FROM clause (derived tables)
-- ✅ EXISTS/NOT EXISTS subquery execution
-- ✅ Correlated subquery support
-- ✅ Nested subquery support
-- ✅ evalSubqueryExpr for scalar subquery evaluation
-- ✅ executeSelectFromSubquery for derived tables
-- ✅ Parser tests for subqueries in FROM (5 tests)
-- ✅ Executor tests (TestEvalSubqueryExpr with 5 test cases)
-- ✅ Executor tests for FROM clause subqueries (5 test cases)
-
-### 5.4 ALTER TABLE ✅
-- ✅ `ALTER TABLE ADD COLUMN` - add new column to table
-- ✅ `ALTER TABLE DROP COLUMN` - remove column from table
-- ✅ `ALTER TABLE RENAME TO` - rename table
-- ✅ `ALTER TABLE RENAME COLUMN` - rename column
-- ✅ Lexer tokens (ADD, COLUMN, RENAME, TO)
-- ✅ Parser AST nodes (AlterTableStmt with action types)
-- ✅ Schema manager methods (AddColumn, DropColumn, RenameTable, RenameColumn)
-- ✅ Executor implementation for all ALTER TABLE variants
-- ✅ Parser tests (5 tests covering all ALTER TABLE variants)
-- ✅ Executor tests (5 test cases covering all operations)
-- ✅ Automatic catalog synchronization after schema changes
-
-### 5.5 Multi-Database Support ✅
-- ✅ `ATTACH DATABASE 'path' AS alias` - attach additional database
-- ✅ `DETACH DATABASE alias` - detach previously attached database
-- ✅ Multi-database namespace support in PizzaKV
-- ✅ Database alias tracking and resolution
-- ✅ Reserved aliases (main, temp) protection
-- ✅ Lexer tokens (ATTACH, DETACH, DATABASE, AS)
-- ✅ Parser AST nodes (AttachStmt, DetachStmt)
-- ✅ Executor multi-database management (attachedDatabases map)
-- ✅ DatabaseConnection struct for tracking schema/table managers
-- ✅ GetDatabaseName and GetPool methods in SchemaManager
-- ✅ Parser tests (4 tests for ATTACH/DETACH syntax)
-- ✅ Executor tests (8 test cases covering all scenarios)
-
----
-
-## Phase 6: HTTP/JSON API Server ✅ COMPLETED
-
-### Status: Complete
-
-**Goal:** Provide a simple HTTP/REST API for SQL execution, making PizzaSQL-Next accessible from any programming language or tool that can make HTTP requests.
-
-### 6.1 HTTP API Endpoints
-
-#### Core Query Endpoint
-```
-POST /query
-Content-Type: application/json
-
-Request:
-{
-  "sql": "SELECT * FROM users WHERE id = ?",
-  "params": [42]
-}
-
-Response:
-{
-  "columns": [
-    {"name": "id", "type": "INTEGER"},
-    {"name": "name", "type": "TEXT"},
-    {"name": "email", "type": "TEXT"}
-  ],
-  "rows": [
-    [42, "John Doe", "john@example.com"]
-  ],
-  "rowsAffected": 1,
-  "lastInsertId": 0,
-  "executionTime": "2.3ms"
-}
-```
-
-#### Batch Execution Endpoint
-```
-POST /execute
-Content-Type: application/json
-
-Request:
-{
-  "statements": [
-    {
-      "sql": "INSERT INTO users (name, email) VALUES (?, ?)",
-      "params": ["Alice", "alice@example.com"]
-    },
-    {
-      "sql": "INSERT INTO users (name, email) VALUES (?, ?)",
-      "params": ["Bob", "bob@example.com"]
-    }
-  ],
-  "transaction": true  // Execute all in a transaction
-}
-
-Response:
-{
-  "results": [
-    {"rowsAffected": 1, "lastInsertId": 1},
-    {"rowsAffected": 1, "lastInsertId": 2}
-  ],
-  "executionTime": "5.1ms"
-}
-```
-
-#### Transaction Management
-```
-POST /transaction/begin
-Response: {"transactionId": "tx-12345"}
-
-POST /transaction/commit
-Body: {"transactionId": "tx-12345"}
-
-POST /transaction/rollback
-Body: {"transactionId": "tx-12345"}
-```
-
-#### Schema Introspection
-```
-GET /schema/tables
-Response:
-{
-  "tables": ["users", "orders", "products"]
-}
-
-GET /schema/tables/users
-Response:
-{
-  "name": "users",
-  "columns": [
-    {"name": "id", "type": "INTEGER", "nullable": false, "primaryKey": true},
-    {"name": "name", "type": "TEXT", "nullable": false},
-    {"name": "email", "type": "TEXT", "nullable": true}
-  ]
-}
-```
-
-#### Health & Status
-```
-GET /health
-Response:
-{
-  "status": "ok",
-  "version": "0.1.0",
-  "uptime": "2h15m30s",
-  "connections": 5
-}
-
-GET /stats
-Response:
-{
-  "queriesExecuted": 12453,
-  "tablesCount": 15,
-  "avgQueryTime": "1.2ms",
-  "cacheHitRate": 0.87
-}
-```
-
-### 6.2 Error Handling
-
-**Standard Error Response:**
-```json
-{
-  "error": {
-    "code": "SYNTAX_ERROR",
-    "message": "syntax error at position 15: unexpected token 'FORM'",
-    "details": {
-      "line": 1,
-      "column": 15,
-      "sql": "SELECT * FROM users"
-    }
-  }
-}
-```
-
-**HTTP Status Codes:**
-- `200 OK` - Successful query execution
-- `400 Bad Request` - Invalid SQL or parameters
-- `401 Unauthorized` - Authentication required
-- `403 Forbidden` - Insufficient permissions
-- `404 Not Found` - Table/resource not found
-- `409 Conflict` - Constraint violation (duplicate key, etc.)
-- `500 Internal Server Error` - Server/database error
-- `503 Service Unavailable` - Database unavailable
-
-### 6.3 Authentication & Security
-
-**API Key Authentication:**
-```
-POST /query
-Authorization: Bearer sk_live_abc123...
-```
-
-**Basic Authentication:**
-```
-POST /query
-Authorization: Basic dXNlcjpwYXNz
-```
-
-**Request Signing (Optional):**
-```
-POST /query
-X-API-Key: abc123
-X-Signature: sha256=...
-X-Timestamp: 1705334400
-```
-
-### 6.4 Query Parameters & Options
-
-**Pretty Printing:**
-```
-POST /query?pretty=true
-```
-
-**Timeout:**
-```
-POST /query?timeout=5s
-```
-
-**Read-Only Mode:**
-```
-POST /query?readonly=true
-// Returns 403 for INSERT/UPDATE/DELETE
-```
-
-**Explain Query Plan:**
-```
-POST /query?explain=true
-Response includes "queryPlan": [...]
-```
-
-### 6.5 Streaming Results (Optional)
-
-**For large result sets:**
-```
-POST /query/stream
-Content-Type: application/json
-Accept: application/x-ndjson
-
-Response (newline-delimited JSON):
-{"columns":[...]}
-{"row":[1,"Alice","alice@example.com"]}
-{"row":[2,"Bob","bob@example.com"]}
-...
-{"complete":true,"rowCount":1000}
-```
-
-### 6.6 WebSocket Support (Optional)
-
-**For real-time queries and subscriptions:**
-```javascript
-ws://localhost:8080/ws
-
-// Client sends:
-{
-  "type": "query",
-  "id": "q1",
-  "sql": "SELECT * FROM users"
-}
-
-// Server responds:
-{"type": "columns", "id": "q1", "data": [...]}
-{"type": "row", "id": "q1", "data": [...]}
-{"type": "complete", "id": "q1", "rowCount": 10}
-```
-
-### 6.7 CORS & Web Browser Support
-
-**Enable CORS for browser access:**
-```
-Access-Control-Allow-Origin: *
-Access-Control-Allow-Methods: GET, POST, OPTIONS
-Access-Control-Allow-Headers: Content-Type, Authorization
-```
-
-### 6.8 Implementation Structure
-
-```
-pkg/httpserver/
-├── server.go        // HTTP server setup
-├── handler.go       // Request handlers
-├── middleware.go    // Auth, CORS, logging, rate limiting
-├── response.go      // Response formatting
-├── error.go         // Error handling
-└── server_test.go   // HTTP API tests
-```
-
-### 6.9 Configuration
-
-**Server Configuration:**
-```go
-type ServerConfig struct {
-    Host            string        // "localhost"
-    Port            int           // 8080
-    ReadTimeout     time.Duration // 30s
-    WriteTimeout    time.Duration // 30s
-    MaxConnections  int           // 1000
-    EnableCORS      bool          // true
-    EnableAuth      bool          // false
-    APIKeys         []string      // ["key1", "key2"]
-    TLSCertFile     string        // "/path/to/cert.pem"
-    TLSKeyFile      string        // "/path/to/key.pem"
-}
-```
-
-### 6.10 Client Libraries (Future)
-
-**Official clients to build:**
-- **Go:** `pizzasql-go`
-- **Python:** `pizzasql-python`
-- **Node.js:** `pizzasql-js`
-- **Rust:** `pizzasql-rs`
-
-**Example Go Client:**
-```go
-client := pizzasql.New("http://localhost:8080", "api-key-123")
-result, err := client.Query("SELECT * FROM users WHERE id = ?", 42)
-for result.Next() {
-    var id int
-    var name string
-    result.Scan(&id, &name)
-}
-```
-
-### 6.11 Performance Considerations
-
-- **Connection pooling:** Reuse executor instances
-- **Query caching:** Cache parsed ASTs for prepared statements
-- **Response compression:** gzip/brotli for large responses
-- **Rate limiting:** Per-IP or per-API-key limits
-- **Request size limits:** Prevent abuse with large payloads
-
-### 6.12 Monitoring & Observability
-
-**Metrics endpoint:**
-```
-GET /metrics (Prometheus format)
-
-# HELP pizzasql_queries_total Total queries executed
-# TYPE pizzasql_queries_total counter
-pizzasql_queries_total{status="success"} 1234
-pizzasql_queries_total{status="error"} 56
-
-# HELP pizzasql_query_duration_seconds Query execution time
-# TYPE pizzasql_query_duration_seconds histogram
-pizzasql_query_duration_seconds_bucket{le="0.001"} 100
-...
-```
-
-### 6.13 Success Criteria
-
-All success criteria have been met:
-
-- ✅ Execute SQL queries via HTTP POST (`POST /query`)
-- ✅ Return results as JSON with columns, types, and rows
-- ✅ Support parameterized queries (`?` placeholders with params array)
-- ✅ Batch execution with optional transactions (`POST /execute`)
-- ✅ Schema introspection endpoints (`GET /schema/tables`, `GET /schema/tables/{name}`)
-- ✅ Proper error handling with HTTP status codes (400, 401, 403, 404, 500, etc.)
-- ✅ Authentication support (Bearer token API keys)
-- ✅ CORS support for browser access (middleware with preflight handling)
-- ✅ Response compression (gzip middleware with Accept-Encoding detection)
-- ✅ Prometheus metrics endpoint (`GET /metrics`)
-- ✅ Comprehensive tests for all endpoints (15+ test functions)
-
----
-
-## Beyond Phase 6: Full SQLite Parity
-
-Features NOT planned but needed for 100% SQLite compatibility:
-
-### Database Features
-- **Views**: CREATE VIEW, DROP VIEW, updatable views
-- **Triggers**: CREATE TRIGGER, BEFORE/AFTER/INSTEAD OF, row triggers
-- **Foreign Keys**: REFERENCES, ON DELETE/UPDATE CASCADE/SET NULL/RESTRICT
-- **CHECK Constraints**: Runtime constraint validation
-- **Collation**: COLLATE NOCASE, COLLATE BINARY, custom collations
-
-### Virtual Tables & Extensions
-- **FTS (Full-Text Search)**: FTS3, FTS4, FTS5 virtual tables
-- **R-Tree**: Spatial indexing
-- **JSON1**: json_extract, json_set, json_array, etc.
-- **CSV**: CSV virtual table
-- **Generate Series**: generate_series() table-valued function
-
-### Advanced SQL
-- **Window Functions**: ROW_NUMBER, RANK, LAG, LEAD, OVER clause
-- **Common Table Expressions**: WITH clause, recursive CTEs
-- **UNION/INTERSECT/EXCEPT**: Set operations
-- **NATURAL JOIN**: Implicit join on matching columns
-- **USING clause**: JOIN ... USING (column)
-
-### Administrative
-- **VACUUM**: Database compaction
-- **ANALYZE**: Statistics collection
-- **REINDEX**: Index rebuild
-- **.dump/.import**: SQLite CLI commands
-
-### Compatibility
-- **SQLite file format**: Reading/writing .sqlite files
-- **WAL mode**: Write-ahead logging
-- **Shared cache**: Multi-connection caching
-- **Busy handlers**: Lock contention handling
-
----
-
-## Implementation Order
-
-### Phase 1: Lexer & Parser ✅ COMPLETE
-1. ✅ Token definitions
-2. ✅ Basic scanner
-3. ✅ Keyword recognition
-4. ✅ String/number literals
-5. ✅ Comprehensive tests
-6. ✅ AST type definitions
-7. ✅ Statement parsing (SELECT, INSERT, UPDATE, DELETE)
-8. ✅ Expression parsing with precedence
-9. ✅ DDL parsing (CREATE, DROP)
-10. ✅ JOIN syntax
-11. ✅ Subqueries
-12. ✅ CASE expressions
-13. ✅ Error messages with positions
-
-### Phase 2: Semantic Analysis ✅ COMPLETE
-1. ✅ Type system with SQLite affinity
-2. ✅ Scope and symbol table management
-3. ✅ Column resolution
-4. ✅ Type inference and checking
-5. ✅ Function signature validation
-6. ✅ Aggregate expression detection
-7. ✅ Comprehensive tests
-
-### Phase 3: Execution & Integration ✅ COMPLETE
-1. ✅ Connect to PizzaKV (connection pool)
-2. ✅ Schema management (create/drop tables)
-3. ✅ Query execution (SELECT, INSERT, UPDATE, DELETE)
-4. ✅ Result formatting (tabular output)
-5. ✅ JOINs, GROUP BY, ORDER BY, LIMIT
-6. ✅ Expression evaluation
-7. ✅ CLI REPL interface
-
-### Phase 4: SQLite Compatibility ✅ COMPLETE
-1. ✅ ROWID implicit column support
-2. ✅ AUTOINCREMENT for INTEGER PRIMARY KEY
-3. ✅ PRAGMA statements (table_info, table_list, database_list, version)
-4. ✅ EXPLAIN query plan
-5. ✅ Additional SQLite functions (printf, hex, random, glob, etc.)
-6. ✅ INSERT OR REPLACE/IGNORE/FAIL/ABORT syntax
-
-### Phase 5: Transactions & Indexes ✅ COMPLETE
-1. ✅ BEGIN/COMMIT/ROLLBACK transactions (parser + executor + tests)
-2. ✅ SAVEPOINT support (parser + executor + tests)
-3. ✅ Add tests for transaction statements (parser tests + executor tests complete)
-4. ✅ CREATE INDEX / DROP INDEX (parser + executor + schema + tests)
-5. ✅ Build and maintain index entries (automatic on INSERT/UPDATE/DELETE)
-6. ✅ Use indexes in SELECT queries (optimization via index lookup)
-7. ✅ Add parser tests for CREATE/DROP INDEX (6 tests)
-8. ✅ Add index benchmarks (2 benchmarks implemented)
-9. ✅ Implement subquery execution in WHERE clause (scalar, IN, EXISTS + tests)
-10. ✅ Implement subquery execution in FROM clause (derived tables + tests)
-11. ✅ Implement ALTER TABLE statements (all variants + tests)
-
-### Phase 6: HTTP/JSON API Server ✅ COMPLETE
-1. ✅ HTTP server setup with configurable host/port
-2. ✅ POST /query - Execute SQL queries with JSON request/response
-3. ✅ POST /execute - Batch execution with optional transactions
-4. ✅ GET /schema/tables - List all tables
-5. ✅ GET /schema/tables/{name} - Get table schema details
-6. ✅ GET /health - Health check endpoint
-7. ✅ GET /stats - Server statistics
-8. ✅ GET /metrics - Prometheus format metrics
-9. ✅ Transaction endpoints (begin/commit/rollback)
-10. ✅ Parameterized query support (? placeholders)
-11. ✅ Response compression middleware (gzip)
-12. ✅ CORS middleware for browser access
-13. ✅ Authentication middleware (Bearer token API keys)
-14. ✅ Logging middleware
-15. ✅ Column type inference in responses
-16. ✅ Comprehensive tests (15+ test functions)
-17. ✅ CLI integration with -http flag
-
----
-
-## SQL-92 BNF Reference
-
-Key productions to implement (see sql-92.bnf for full grammar):
-
-```bnf
-<query specification> ::=
-    SELECT [ ALL | DISTINCT ] <select list>
-    <table expression>
-
-<table expression> ::=
-    <from clause>
-    [ <where clause> ]
-    [ <group by clause> ]
-    [ <having clause> ]
-
-<select list> ::=
-    <asterisk>
-  | <select sublist> [ { <comma> <select sublist> }... ]
-
-<from clause> ::=
-    FROM <table reference> [ { <comma> <table reference> }... ]
-
-<where clause> ::=
-    WHERE <search condition>
-
-<search condition> ::=
-    <boolean term>
-  | <search condition> OR <boolean term>
-
-<boolean term> ::=
-    <boolean factor>
-  | <boolean term> AND <boolean factor>
-
-<boolean factor> ::=
-    [ NOT ] <boolean test>
-
-<comparison predicate> ::=
-    <row value constructor> <comp op> <row value constructor>
-```
-
----
-
-## Success Criteria
-
-1. **Lexer**: ✅ Correctly tokenizes all SQL-92 syntax
-2. **Parser**: ✅ Produces valid AST for SQL-92 statements
-3. **Tests**: ✅ Comprehensive test coverage for lexer and parser
-4. **Errors**: ✅ Clear, actionable error messages with position info
-5. **Performance**: ✅ Parse 176,000+ statements/second (17x target)
-6. **Analyzer**: ✅ Type checking and validation complete
-7. **Executor**: ✅ Full CRUD operations with PizzaKV backend
-8. **HTTP API**: ✅ RESTful JSON API with all planned endpoints
-
----
-
-## SQLite Compatibility Estimates
-
-| Phase | Completion | SQLite Compatibility |
-|-------|------------|---------------------|
-| Phase 1-3 | ✅ Done | ~50% - Core SQL works |
-| Phase 4 | ✅ Done | ~70% - SQLite dialect |
-| Phase 5 | ✅ Done | ~85% - Transactions, indexes, subqueries, ALTER TABLE |
-| Phase 6 | ✅ Done | ~85% - HTTP API (no change to SQL compatibility) |
-| Beyond | Not planned | 100% - Full parity |
-
-**Note**: "Compatibility" refers to typical application use cases. Edge cases,
-advanced features (FTS, window functions, triggers), and file format compatibility
-would require additional phases.
-
-**Phase 6 Status**: HTTP/JSON API server fully implemented! Features include:
-- RESTful endpoints for SQL execution, schema introspection, and health monitoring
-- Parameterized queries with ? placeholders
-- Batch execution with optional transactions
-- Gzip response compression
-- CORS and authentication middleware
-- Prometheus metrics endpoint for monitoring
-- Full test coverage

+ 0 - 288
ISSUES.md

@@ -1,288 +0,0 @@
-# PizzaSQL Issues Found in Stress Test
-
-## Summary
-
-The stress test originally revealed 11 distinct issues in PizzaSQL. As of January 16, 2026, **all major issues have been resolved**. Current stress test status: **43/46 tests passing (93.5%)**, with only 1 minor issue remaining (large result set performance). This document tracks each issue and its resolution status.
-
-**Latest Update (Jan 16, 2026):** Fixed concurrent query race condition by adding thread-safety to analyzer scope management.
-
----
-
-## Issue 1: Column Alias Not Recognized in ORDER BY
-**Status:** ✅ RESOLVED
-
-**Error:** `analysis error: column not found: count`
-
-**Test Case:**
-```sql
-SELECT status, COUNT(*) as count
-FROM orders
-GROUP BY status
-ORDER BY count DESC
-```
-
-**Expected:** Column alias `count` should be usable in ORDER BY clause.
-
-**Root Cause:** The analyzer doesn't recognize column aliases defined in the SELECT list when validating ORDER BY expressions.
-
----
-
-## Issue 2: Column Alias Not Recognized in HAVING
-**Status:** ✅ RESOLVED
-
-**Error:** `analysis error: column not found: order_count`
-
-**Test Case:**
-```sql
-SELECT user_id, COUNT(*) as order_count
-FROM orders
-GROUP BY user_id
-HAVING COUNT(*) > 1
-ORDER BY order_count DESC
-```
-
-**Expected:** Column alias should be usable in HAVING/ORDER BY, or at minimum the query should work when using the full expression.
-
-**Root Cause:** Same as Issue 1 - alias resolution not working in HAVING clause.
-
----
-
-## Issue 3: Table Alias Not Resolved in Multi-Table JOINs
-**Status:** ✅ RESOLVED
-
-**Error:** `analysis error: column not found: oi.product_id`
-
-**Test Case:**
-```sql
-SELECT
-  o.id as order_id,
-  u.username,
-  p.name as product_name,
-  oi.quantity,
-  oi.price
-FROM orders o
-INNER JOIN users u ON o.user_id = u.id
-INNER JOIN order_items oi ON o.id = oi.order_id
-INNER JOIN products p ON oi.product_id = p.id
-LIMIT 20
-```
-
-**Expected:** Table aliases (`o`, `u`, `oi`, `p`) should be resolved correctly across all JOINs.
-
-**Root Cause:** The analyzer loses track of table aliases when processing multiple JOINs, particularly in ON conditions.
-
----
-
-## Issue 4: Scalar Subquery Returns NULL
-**Status:** ✅ RESOLVED
-
-**Error:** `Cannot read properties of null (reading 'length')` (test error due to null result)
-
-**Test Case:**
-```sql
-SELECT username,
-       (SELECT COUNT(*) FROM orders WHERE user_id = users.id) as order_count
-FROM users
-WHERE id <= 5
-```
-
-**Expected:** Scalar subquery should return the count of orders for each user.
-
-**Root Cause:** Correlated subqueries may not be evaluating correctly, returning null instead of a value.
-
----
-
-## Issue 5: CASE Expression Returns Invalid Value
-**Status:** ✅ RESOLVED
-
-**Error:** `Assertion failed: Age group should be valid`
-
-**Test Case:**
-```sql
-SELECT username,
-       CASE
-         WHEN age < 25 THEN 'young'
-         WHEN age < 40 THEN 'middle'
-         ELSE 'senior'
-       END as age_group
-FROM users
-LIMIT 10
-```
-
-**Expected:** Should return 'young', 'middle', or 'senior' based on age.
-
-**Root Cause:** CASE expression evaluation may be returning null or incorrect values.
-
----
-
-## Issue 6: COALESCE Returns NULL Instead of Default
-**Status:** ✅ RESOLVED
-
-**Error:** `COALESCE should return 0 for null: expected 0, got null`
-
-**Test Case:**
-```sql
-SELECT username, COALESCE(age, 0) as age
-FROM users
-WHERE username = 'nulltest'
-```
-
-**Expected:** When `age` is NULL, COALESCE should return `0`.
-
-**Root Cause:** COALESCE function not properly returning the first non-null argument.
-
----
-
-## Issue 7: UPPER Function Returns NULL
-**Status:** ✅ RESOLVED
-
-**Error:** `Cannot read properties of null (reading 'toUpperCase')`
-
-**Test Case:**
-```sql
-SELECT
-  UPPER(username) as upper_name,
-  LOWER(email) as lower_email,
-  LENGTH(username) as name_len
-FROM users
-WHERE id = 1
-```
-
-**Expected:** UPPER should return uppercase version of the string.
-
-**Root Cause:** String functions may be returning null instead of the transformed string.
-
----
-
-## Issue 8: UPDATE with Self-Reference Fails
-**Status:** ✅ RESOLVED
-
-**Error:** `no row context for column: balance`
-
-**Test Case:**
-```sql
-UPDATE users SET balance = balance + 100 WHERE id = 2
-```
-
-**Expected:** Should increment the current balance by 100.
-
-**Root Cause:** When evaluating `balance + 100`, the executor doesn't have access to the current row's values.
-
----
-
-## Issue 9: DROP TABLE IF EXISTS Not Working
-**Status:** ✅ RESOLVED
-
-**Error:** `duplicate primary key: 1` on second run
-
-**Test Case:**
-```sql
-DROP TABLE IF EXISTS users
-```
-
-**Expected:** Should drop the table if it exists, allowing clean re-creation.
-
-**Root Cause:** Either DROP TABLE IF EXISTS doesn't actually drop the table, or AUTOINCREMENT counters persist after table drop.
-
----
-
-## Issue 10: DROP INDEX IF EXISTS Not Working
-**Status:** ✅ RESOLVED
-
-**Error:** `index already exists: idx_users_email`
-
-**Test Case:**
-```sql
-DROP INDEX IF EXISTS idx_users_email
-```
-
-**Expected:** Should drop the index if it exists.
-
-**Root Cause:** Cleanup function doesn't drop indexes, or DROP INDEX IF EXISTS doesn't work.
-
----
-
-## Issue 11: LEFT JOIN Returns Wrong Row Count
-**Status:** ✅ RESOLVED
-
-**Error:** `Should return users with order counts: expected 10, got 100`
-
-**Test Case:**
-```sql
-SELECT u.username, COUNT(o.id) as order_count
-FROM users u
-LEFT JOIN orders o ON u.id = o.user_id
-GROUP BY u.id, u.username
-LIMIT 10
-```
-
-**Expected:** Should return 10 rows (due to LIMIT).
-
-**Root Cause:** LIMIT may not be applied correctly after GROUP BY, or the JOIN produces unexpected results.
-
----
-
-## Priority Order
-
-Based on impact and dependencies:
-
-1. **Issue 9: DROP TABLE IF EXISTS** - Blocks running tests multiple times
-2. **Issue 10: DROP INDEX IF EXISTS** - Blocks running tests multiple times
-3. **Issue 8: UPDATE with Self-Reference** - Core functionality
-4. **Issue 3: Table Alias in JOINs** - Breaks multi-table queries
-5. **Issue 1 & 2: Alias in ORDER BY/HAVING** - Common SQL patterns
-6. **Issue 6: COALESCE** - Important null handling
-7. **Issue 7: UPPER/String functions** - Utility functions
-8. **Issue 5: CASE expression** - Conditional logic
-9. **Issue 4: Scalar subqueries** - Advanced feature
-10. **Issue 11: LEFT JOIN row count** - May be test issue
-
----
-
-## Resolution Log
-
-| Issue | Status | Resolution Date | Notes |
-|-------|--------|----------------|-------|
-| 1 | ✅ Resolved | Jan 16, 2026 | ORDER BY with aliases working in stress test |
-| 2 | ✅ Resolved | Jan 16, 2026 | HAVING clause test passing |
-| 3 | ✅ Resolved | Jan 16, 2026 | Complex JOIN test passing |
-| 4 | ✅ Resolved | Jan 16, 2026 | Subquery tests passing |
-| 5 | ✅ Resolved | Jan 16, 2026 | CASE expression test passing |
-| 6 | ✅ Resolved | Jan 16, 2026 | NULL handling test passing |
-| 7 | ✅ Resolved | Jan 16, 2026 | String functions test passing |
-| 8 | ✅ Resolved | Jan 16, 2026 | UPDATE test passing |
-| 9 | ✅ Resolved | Jan 16, 2026 | Stress test runs cleanly multiple times |
-| 10 | ✅ Resolved | Jan 16, 2026 | Index creation/deletion tests passing |
-| 11 | ✅ Resolved | Jan 16, 2026 | Pagination test passing (LIMIT with GROUP BY) |
-
----
-
-## Current Outstanding Issues
-
-Based on the latest stress test run (43/46 passing):
-
-### 1. Concurrent Query Race Condition
-**Status:** ✅ RESOLVED (Jan 16, 2026)
-**Test:** Concurrent queries
-**Error:** `fatal error: concurrent map writes` in analyzer/scope.go
-**Root Cause:** Analyzer's scope management was not thread-safe. Multiple goroutines modifying shared scope maps simultaneously.
-**Fix Applied:** Added `sync.RWMutex` locks to both `Scope` and `Catalog` structs. All map access operations now use appropriate read/write locks:
-- `DefineTable()`, `DefineSelectAlias()`: Write locks (mu.Lock)
-- `LookupTable()`, `LookupColumn()`, `GetAllColumns()`, `GetTables()`: Read locks (mu.RLock)
-- `CreateTable()`, `DropTable()`: Write locks on Catalog
-- `GetTable()`, `GetTables()`, `TableExists()`: Read locks on Catalog
-
-Verified with `go test -race` - no race conditions detected.
-
-### 2. Large Result Set Performance
-**Status:** 🟡 Minor
-**Test:** Large result set
-**Error:** Connection issues with very large result sets
-**Root Cause:** Possible timeout or memory issue with large data transfers
-**Fix Required:** Investigation needed - may be timeout configuration
-
-### 3. Transaction Rollback Edge Case
-**Status:** ✅ RESOLVED
-**Test:** Transaction rollback
-**Note:** Rollback is fully implemented with undo operations for INSERT/UPDATE/DELETE. Test passing.
-**Implementation:** Transaction log tracks all operations with old data, allowing complete rollback.

+ 134 - 0
README.md

@@ -63,6 +63,7 @@ PizzaSQL is a SQL-92 compliant database with SQLite compatibility, featuring a h
 - ✅ **CORS Support**: Browser-compatible cross-origin requests
 - ✅ **Response Compression**: gzip for large result sets
 - ✅ **Prometheus Metrics**: Monitor queries, performance, and health
+- ✅ **Import/Export**: Backup and restore databases via SQL files
 
 ### Advanced Features
 
@@ -293,6 +294,70 @@ For quick calculations without PizzaKV:
 ./pizzasql -http -http-port 3000
 ```
 
+### Database Export/Import
+
+Export and import databases using SQL or CSV files for backup and migration.
+
+**SQL Export:**
+```bash
+# Export entire database
+./pizzasql -db mydb -o backup.sql
+
+# Export specific table
+./pizzasql -db mydb -table users -o users.sql
+
+# Include DROP TABLE statements (for clean restore)
+./pizzasql -db mydb -o backup.sql -drop
+```
+
+**CSV Export:**
+```bash
+# Export table to CSV (auto-detected from .csv extension)
+./pizzasql -db mydb -table users -o users.csv
+
+# Explicit format flag
+./pizzasql -db mydb -table users -o users.csv -format csv
+```
+
+**Import:**
+```bash
+# Import SQL file
+./pizzasql -db mydb -i backup.sql
+
+# Import CSV file to existing table
+./pizzasql -db mydb -table users -i users.csv
+
+# Import CSV and create table automatically
+./pizzasql -db mydb -table new_users -i users.csv -create-table
+
+# Continue on errors
+./pizzasql -db mydb -i backup.sql -ignore-errors
+```
+
+**SQL Export Format:**
+```sql
+-- PizzaSQL Export
+-- Database: mydb
+-- Date: 2026-01-21T10:30:00Z
+
+DROP TABLE IF EXISTS users;
+CREATE TABLE users (
+    id INTEGER PRIMARY KEY,
+    name TEXT NOT NULL,
+    email TEXT
+);
+
+INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.com');
+INSERT INTO users (id, name, email) VALUES (2, 'Bob', NULL);
+```
+
+**CSV Export Format:**
+```csv
+id,name,email
+1,Alice,alice@example.com
+2,Bob,
+```
+
 ---
 
 ## HTTP API
@@ -501,6 +566,66 @@ curl -X POST http://localhost:8080/transaction/commit
 curl -X POST http://localhost:8080/transaction/rollback
 ```
 
+### Import/Export Endpoints
+
+#### GET /export - Export Database
+
+Export the database (or specific tables) to SQL format.
+
+**Query Parameters:**
+- `?table=users` - Export specific table (comma-separated for multiple)
+- `?drop=true` - Include DROP TABLE statements
+- `?schema_only=true` - Export schema only, no data
+
+**Examples:**
+```bash
+# Export entire database
+curl "http://localhost:8080/export" -H "X-Database: mydb" -o backup.sql
+
+# Export with DROP TABLE statements
+curl "http://localhost:8080/export?drop=true" -H "X-Database: mydb" -o backup.sql
+
+# Export specific table
+curl "http://localhost:8080/export?table=users" -H "X-Database: mydb" -o users.sql
+
+# Export schema only (no data)
+curl "http://localhost:8080/export?schema_only=true" -H "X-Database: mydb" -o schema.sql
+```
+
+#### POST /import - Import SQL File
+
+Import SQL statements from a file.
+
+**Query Parameters:**
+- `?ignore_errors=true` - Continue on individual statement errors
+
+**Request:**
+- Content-Type: `multipart/form-data`
+- File field: `file`
+
+**Response:**
+```json
+{
+  "statementsExecuted": 5,
+  "tablesCreated": ["users", "products"],
+  "tablesDropped": [],
+  "rowsInserted": 100
+}
+```
+
+**Examples:**
+```bash
+# Import SQL file
+curl -X POST "http://localhost:8080/import" \
+  -H "X-Database: mydb" \
+  -F "file=@backup.sql"
+
+# Import with error tolerance
+curl -X POST "http://localhost:8080/import?ignore_errors=true" \
+  -H "X-Database: mydb" \
+  -F "file=@backup.sql"
+```
+
 ### Authentication
 
 When authentication is enabled, include the API key in the Authorization header:
@@ -1360,6 +1485,15 @@ CREATE TABLE users (
 -http-auth      Enable authentication
 -api-keys       Comma-separated API keys
 
+# Export/Import options
+-o string       Output file for export (triggers export mode)
+-i string       Input file for import (triggers import mode)
+-table string   Specific table to export (required for CSV, optional for SQL)
+-format string  Export/import format: sql, csv (auto-detect from extension)
+-drop           Include DROP TABLE statements in export (SQL only)
+-create-table   Create table if not exists (CSV import only)
+-ignore-errors  Continue import on individual statement/row errors
+
 # Other options
 -version        Print version and exit
 -help           Show help message

+ 0 - 577
TEST.md

@@ -1,577 +0,0 @@
-# PizzaSQL Testing Guide
-
-This document describes all the tests available in PizzaSQL and how to run them.
-
-## Table of Contents
-
-- [Unit Tests](#unit-tests)
-- [Stress Test Suite](#stress-test-suite)
-- [Running Tests](#running-tests)
-- [Test Coverage](#test-coverage)
-
----
-
-## Unit Tests
-
-PizzaSQL includes comprehensive unit tests for each major component written in Go.
-
-### Lexer Tests (`pkg/lexer/lexer_test.go`)
-
-Tests the SQL tokenizer/lexer that breaks SQL strings into tokens.
-
-**What it tests:**
-- Single token parsing (operators, keywords, punctuation)
-- Multi-character operators (`<=`, `>=`, `<>`, `!=`, `||`)
-- Keywords (case-insensitive): `SELECT`, `FROM`, `WHERE`, `JOIN`, etc.
-- Identifiers and quoted identifiers
-- String literals (single and double quotes)
-- Numeric literals (integers and floats)
-- Comments (single-line `--` and multi-line `/* */`)
-- Whitespace handling
-
-**Run lexer tests:**
-```bash
-make test-lexer
-# or
-go test -v ./pkg/lexer/...
-```
-
-### Parser Tests (`pkg/parser/parser_test.go`)
-
-Tests the SQL parser that converts tokens into Abstract Syntax Trees (AST).
-
-**What it tests:**
-- **SELECT statements**: `*`, column lists, aliases, DISTINCT
-- **FROM clause**: Single tables, multiple tables, table aliases
-- **JOIN operations**: INNER, LEFT, RIGHT, FULL OUTER, CROSS
-- **WHERE clause**: Conditions, operators, complex expressions
-- **GROUP BY**: Single/multiple columns, expressions
-- **HAVING**: Aggregate filtering
-- **ORDER BY**: ASC/DESC, multiple columns, NULL handling
-- **LIMIT/OFFSET**: Result pagination
-- **INSERT statements**: Single row, multiple rows, column lists
-- **UPDATE statements**: SET clauses, WHERE conditions
-- **DELETE statements**: WHERE conditions
-- **CREATE TABLE**: Columns, constraints, PRIMARY KEY, FOREIGN KEY
-- **ALTER TABLE**: ADD/DROP COLUMN, RENAME
-- **DROP TABLE**: IF EXISTS
-- **CREATE INDEX**: Single/multiple columns
-- **Expressions**: Binary operators, functions, CASE, subqueries
-- **Subqueries**: Scalar, EXISTS, IN
-- **Aggregate functions**: COUNT, SUM, AVG, MIN, MAX
-- **String functions**: UPPER, LOWER, LENGTH, SUBSTRING
-- **Date/time functions**: NOW, DATE, TIME
-
-**Run parser tests:**
-```bash
-make test-parser
-# or
-go test -v ./pkg/parser/...
-```
-
-### Analyzer Tests (`pkg/analyzer/analyzer_test.go`)
-
-Tests semantic analysis and type checking of SQL statements.
-
-**What it tests:**
-- **Table existence**: Verifying referenced tables exist
-- **Column validation**: Checking columns exist in referenced tables
-- **Type checking**: Data type compatibility
-- **Scope resolution**: Table and column name resolution
-- **Aggregate validation**: Proper use of aggregate functions
-- **JOIN validation**: Column references across tables
-- **Subquery validation**: Correlation and scope
-- **Function validation**: Argument counts and types
-- **Constraint checking**: PRIMARY KEY, FOREIGN KEY, UNIQUE, NOT NULL
-
-**Run analyzer tests:**
-```bash
-go test -v ./pkg/analyzer/...
-```
-
-### Executor Tests (`pkg/executor/executor_test.go`)
-
-Tests SQL execution and query evaluation.
-
-**What it tests:**
-- **Literal evaluation**: Integers, floats, strings, booleans, NULL
-- **Binary expressions**: Arithmetic, comparison, logical operators
-- **Unary expressions**: Negation, NOT
-- **Function calls**: Built-in SQL functions
-- **DISTINCT deduplication**: Hash-based row uniqueness (applyDistinct function)
-- **Type conversion**: toBool, toInt, toFloat, toString
-- **NULL handling**: NULL propagation in expressions
-- **Column references**: Qualified and unqualified
-- **Table scans**: Full table iteration
-- **Filtering**: WHERE clause evaluation
-- **Sorting**: ORDER BY implementation
-- **Grouping**: GROUP BY with aggregates
-- **Joining**: INNER JOIN, LEFT JOIN, etc.
-- **Subqueries**: Scalar and EXISTS subqueries
-- **DML operations**: INSERT, UPDATE, DELETE
-- **DDL operations**: CREATE, ALTER, DROP
-- **Transaction handling**: Isolation and consistency
-
-**Run executor tests:**
-```bash
-go test -v ./pkg/executor/...
-```
-
-### HTTP Server Tests (`pkg/httpserver/server_test.go`)
-
-Tests the HTTP API endpoints and request handling.
-
-**What it tests:**
-- **Query endpoint**: POST /query
-- **Batch execute**: POST /execute
-- **Schema endpoints**: GET /schema/tables, GET /schema/tables/{name}
-- **Health check**: GET /health
-- **Statistics**: GET /stats
-- **Metrics**: GET /metrics (Prometheus format)
-- **Transaction endpoints**: POST /transaction/begin, /commit, /rollback
-- **Request validation**: Invalid JSON, missing fields
-- **Error handling**: SQL errors, timeouts, invalid queries
-- **Response formats**: JSON structure, column info, row data
-- **Compression**: GZIP encoding
-- **Authentication**: API key validation (when enabled)
-- **CORS**: Cross-origin headers
-- **Query parameters**: `pretty`, `explain`, `readonly`, `timeout`
-
-**Run HTTP server tests:**
-```bash
-go test -v ./pkg/httpserver/...
-```
-
----
-
-## Stress Test Suite
-
-The stress test suite (`stress_test.js`) is a comprehensive end-to-end test that validates the entire database system with realistic workloads.
-
-### Overview
-
-**Test Data Scale:**
-- 1,000 users
-- 500 products across 5 categories
-- 2,000 orders
-- 5,000 order items
-- **Total: 8,505 rows**
-
-**Test Duration:** ~30 seconds  
-**Total Queries:** ~8,600  
-**Success Rate:** 100% (32/32 tests)
-
-### Test Categories
-
-#### 📋 Schema Tests
-- **Health check endpoint**: Verifies server is running and responsive
-- **Create tables**: Tests DDL operations (CREATE TABLE with constraints)
-- **Schema introspection**: Tests metadata queries (columns, types, keys)
-
-#### 📥 Insert Tests
-- **Insert 1,000 users**: Batch INSERT with parameterized queries
-- **Insert categories**: Multi-row inserts
-- **Insert 500 products**: Batch operations with foreign keys
-- **Insert 2,000 orders**: High-volume inserts
-- **Insert 5,000 order items**: Stress test batch performance
-
-**Verbose Output Example:**
-```
-  Testing Insert 1000 users... 
-    → Preparing 1000 user records... done
-    → Executing batch insert... done
-    → Verified 1000 users in database
-  ✓ PASSED (121ms)
-```
-
-#### 🔎 SELECT Tests
-- **Basic SELECT queries**: Simple queries, WHERE clauses, column selection
-- **SELECT with ORDER BY**: Sorting, ASC/DESC, multiple columns
-- **SELECT with GROUP BY**: Aggregation grouping
-- **SELECT with HAVING**: Post-aggregation filtering
-- **SELECT with JOIN**: Two-table INNER JOINs
-- **SELECT with multiple JOINs**: 4-table joins (users → orders → order_items → products)
-- **Aggregation functions**: COUNT, SUM, AVG, MIN, MAX
-- **Subqueries**: Scalar subqueries, EXISTS, IN clauses
-
-#### 🧮 Expression Tests
-- **LIKE operator**: Pattern matching with wildcards
-- **BETWEEN operator**: Range queries
-- **CASE expression**: Conditional logic
-- **NULL handling**: IS NULL, IS NOT NULL, COALESCE
-- **String functions**: UPPER, LOWER, LENGTH
-- **Numeric functions**: ABS, ROUND, CEIL, FLOOR
-- **Parameter types**: String, integer, float, boolean parameters
-
-#### ✏️ UPDATE/DELETE Tests
-- **UPDATE records**: Single and bulk updates with WHERE
-- **DELETE records**: Conditional deletion
-
-#### 🚀 Advanced Features Tests
-- **Create indexes**: Single and composite indexes
-- **Transaction handling**: BEGIN, COMMIT, ROLLBACK
-- **Batch execute**: Multi-statement execution
-- **ALTER TABLE**: ADD COLUMN, DROP COLUMN
-
-#### ⚡ Performance Tests
-- **Concurrent queries**: 10 simultaneous queries
-- **Large result set**: Queries returning 100+ rows
-- **Complex query**: Multi-table JOINs with GROUP BY, HAVING, ORDER BY
-
-### Complex Query Example
-
-The most complex test validates a real-world analytics query:
-
-```sql
-SELECT
-  u.username,
-  COUNT(DISTINCT o.id) as order_count,
-  SUM(oi.quantity * oi.price) as total_spent,
-  AVG(oi.price) as avg_item_price
-FROM users u
-LEFT JOIN orders o ON u.id = o.user_id
-LEFT JOIN order_items oi ON o.id = oi.order_id
-WHERE u.id <= 100
-GROUP BY u.id, u.username
-HAVING COUNT(o.id) > 0
-ORDER BY total_spent DESC
-LIMIT 10
-```
-
-This tests:
-- Multiple LEFT JOINs
-- Aggregate functions (COUNT, SUM, AVG)
-- COUNT(DISTINCT ...) in aggregates
-- GROUP BY multiple columns
-- HAVING with aggregates
-- ORDER BY computed columns
-- LIMIT
-
-### DISTINCT Test
-
-Dedicated test for DISTINCT functionality:
-
-```sql
--- Insert test data with duplicates
-INSERT INTO test_distinct VALUES (1, 'pending'), (2, 'completed'),
-  (3, 'pending'), (4, 'shipped'), (5, 'pending');
-
--- Without DISTINCT: Returns all 5 rows
-SELECT status FROM test_distinct;
--- Result: pending, completed, pending, shipped, pending
-
--- With DISTINCT: Returns only 3 unique values
-SELECT DISTINCT status FROM test_distinct ORDER BY status;
--- Result: completed, pending, shipped
-```
-
-**What it validates:**
-- Duplicate removal works correctly
-- Compatible with ORDER BY
-- Handles multiple data types
-- Returns correct row count (3 unique from 5 total)
-- Hash-based deduplication is efficient
-
-**Test output:**
-```
-✅ DISTINCT is working correctly!
-   Expected 3 unique values, got 3
-   Values: completed, pending, shipped
-```
-
----
-
-## Running Tests
-
-### Prerequisites
-
-1. **PizzaKV must be running** (for storage-backed tests):
-   ```bash
-   # In a separate terminal
-   pizzakv -port 8085
-   ```
-
-2. **Node.js** (for stress test):
-   ```bash
-   node --version  # Should be v14+
-   ```
-
-### Run All Unit Tests
-
-```bash
-# Run all Go tests
-make test
-
-# Run with verbose output
-make test-v
-
-# Run with coverage report
-make test-cover
-# Open coverage.html in browser
-```
-
-### Run Specific Component Tests
-
-```bash
-# Lexer only
-make test-lexer
-
-# Parser only
-make test-parser
-
-# All tests with race detection
-make test-race
-
-# Run benchmarks
-make bench
-```
-
-### Run Stress Test
-
-**Step 1: Build and start PizzaSQL server**
-```bash
-make build
-./pizzasql -http
-```
-
-**Step 2: Run stress test** (in another terminal)
-```bash
-./stress_test.js
-```
-
-**Clean run with fresh database:**
-```bash
-# Kill server, delete database, restart, and run test
-pkill -9 pizzasql; rm -f .db && ./pizzasql -http > /dev/null 2>&1 & sleep 2 && ./stress_test.js
-```
-
-### Stress Test Configuration
-
-Edit `stress_test.js` to change test parameters:
-
-```javascript
-const CONFIG = {
-  numUsers: 1000,        // Number of test users
-  numProducts: 500,      // Number of products
-  numOrders: 2000,       // Number of orders
-  numOrderItems: 5000,   // Number of order items
-  concurrentRequests: 10 // Concurrent query limit
-};
-```
-
-**Environment variables:**
-```bash
-# Custom server URL
-PIZZASQL_URL=http://localhost:9000 ./stress_test.js
-
-# With API key
-PIZZASQL_API_KEY=your-secret-key ./stress_test.js
-```
-
----
-
-## Test Coverage
-
-### Current Coverage
-
-Run `make test-cover` to generate coverage report. Expected coverage:
-
-- **Lexer**: ~95% (token parsing, error handling)
-- **Parser**: ~90% (SQL grammar, AST construction)
-- **Analyzer**: ~85% (semantic validation, type checking)
-- **Executor**: ~80% (query execution, complex operations)
-- **HTTP Server**: ~75% (endpoint handlers, middleware)
-
-### Coverage Report
-
-After running `make test-cover`, open `coverage.html`:
-
-```bash
-make test-cover
-open coverage.html  # macOS
-# or
-xdg-open coverage.html  # Linux
-```
-
----
-
-## Interpreting Test Results
-
-### Unit Test Output
-
-```bash
-$ make test-v
-=== RUN   TestLexerSingleTokens
---- PASS: TestLexerSingleTokens (0.00s)
-=== RUN   TestParseSelectStar
---- PASS: TestParseSelectStar (0.00s)
-...
-PASS
-ok      github.com/danfragoso/pizzasql-next/pkg/lexer    0.012s
-ok      github.com/danfragoso/pizzasql-next/pkg/parser   0.089s
-```
-
-### Stress Test Output
-
-```
-╔════════════════════════════════════════════════════════════╗
-║                      TEST SUMMARY                          ║
-╚════════════════════════════════════════════════════════════╝
-
-  Total tests:     32
-  Passed:          32 ✓
-  Failed:          0 ✗
-  Success rate:    100.0%
-
-  Total queries:   8,591
-  Total time:      29,805ms
-  Avg query time:  3.47ms
-  Queries/sec:     288
-```
-
-**Metrics explained:**
-- **Total tests**: Number of test scenarios
-- **Success rate**: Percentage of passing tests
-- **Total queries**: All SQL queries executed (including setup)
-- **Avg query time**: Mean execution time per query
-- **Queries/sec**: Throughput (queries per second)
-
----
-
-## Troubleshooting
-
-### Common Issues
-
-**1. "PizzaKV not available"**
-```bash
-# Start PizzaKV first
-pizzakv -port 8085
-```
-
-**2. Stress test timeout errors**
-```bash
-# Increase server timeout (default: 5 minutes)
-# Edit pkg/httpserver/handler.go, line 56:
-timeout := 10 * time.Minute
-```
-
-**3. "Connection refused" during stress test**
-```bash
-# Make sure server is running
-./pizzasql -http
-
-# Check port 8080 is available
-lsof -i :8080
-```
-
-**4. Tests failing after code changes**
-```bash
-# Rebuild and restart
-make build
-pkill -9 pizzasql
-rm -f .db
-./pizzasql -http &
-sleep 2
-./stress_test.js
-```
-
----
-
-## Continuous Integration
-
-To run all tests in CI:
-
-```bash
-#!/bin/bash
-set -e
-
-# Start PizzaKV
-pizzakv -port 8085 &
-PIZZAKV_PID=$!
-
-# Run unit tests
-make test-v
-
-# Build server
-make build
-
-# Start server
-./pizzasql -http > /dev/null 2>&1 &
-PIZZASQL_PID=$!
-sleep 3
-
-# Run stress test
-./stress_test.js
-
-# Cleanup
-kill $PIZZASQL_PID $PIZZAKV_PID
-```
-
----
-
-## Writing New Tests
-
-### Adding Unit Tests
-
-Create test file in same package:
-
-```go
-// pkg/mypackage/myfile_test.go
-package mypackage
-
-import "testing"
-
-func TestMyFunction(t *testing.T) {
-    result := MyFunction("input")
-    if result != "expected" {
-        t.Errorf("got %v, want %v", result, "expected")
-    }
-}
-```
-
-### Adding Stress Test Scenarios
-
-Edit `stress_test.js`:
-
-```javascript
-async function testMyFeature() {
-  const result = await query('SELECT ...');
-  assertEqual(result.rows.length, 10, 'Should return 10 rows');
-}
-
-// Add to test suite
-await runTest('My feature', testMyFeature);
-```
-
----
-
-## Performance Benchmarks
-
-Run benchmarks to measure performance:
-
-```bash
-make bench
-```
-
-Example benchmark output:
-```
-BenchmarkExecuteSelect-8        1000    1123456 ns/op    24576 B/op    245 allocs/op
-BenchmarkExecuteJoin-8           100   10234567 ns/op   245760 B/op   2456 allocs/op
-```
-
-**Metrics:**
-- **Operations/sec**: Iterations in 1 second
-- **ns/op**: Nanoseconds per operation
-- **B/op**: Bytes allocated per operation
-- **allocs/op**: Number of allocations per operation
-
----
-
-## Summary
-
-- **Unit tests**: Fast, isolated component testing (~1 second total)
-- **Stress test**: End-to-end validation with realistic data (~30 seconds)
-- **Coverage**: Comprehensive testing of all major features
-- **Automation**: Easy to run in CI/CD pipelines
-
-Run `make test && ./stress_test.js` for complete validation! 🍕

+ 191 - 0
clients/README.md

@@ -0,0 +1,191 @@
+# PizzaSQL Client Libraries
+
+Official client libraries for PizzaSQL in multiple programming languages.
+
+## Available Clients
+
+| Language | Directory | Package Manager | Status |
+|----------|-----------|----------------|---------|
+| JavaScript/TypeScript | [js/](js/) | npm | ✅ Ready |
+| Python | [python/](python/) | pip | ✅ Ready |
+| Go | [go/](go/) | go get | ✅ Ready |
+| Ruby | [ruby/](ruby/) | gem | ✅ Ready |
+
+## Quick Start
+
+All client libraries follow a similar API design pattern for consistency across languages.
+
+### JavaScript/TypeScript (Node.js/Bun)
+
+```javascript
+import { connect } from 'pizzasql';
+
+const db = connect('http://localhost:8080/mydb', 'api-key');
+const users = await db.sql('SELECT * FROM users');
+const userIds = users.map(u => u.id);
+```
+
+[Full documentation](js/README.md)
+
+### Python
+
+```python
+from pizzasql import connect
+
+db = connect('http://localhost:8080/mydb', api_key='api-key')
+users = db.sql('SELECT * FROM users')
+user_ids = [u['id'] for u in users]
+```
+
+[Full documentation](python/README.md)
+
+### Go
+
+```go
+import "github.com/pizzasql/pizzasql-go"
+
+db, err := pizzasql.Connect("http://localhost:8080/mydb", "api-key")
+if err != nil {
+    log.Fatal(err)
+}
+
+rows, err := db.SQL("SELECT * FROM users")
+if err != nil {
+    log.Fatal(err)
+}
+
+for _, row := range rows {
+    fmt.Println(row["id"])
+}
+```
+
+[Full documentation](go/README.md)
+
+### Ruby
+
+```ruby
+require 'pizzasql'
+
+db = PizzaSQL.connect('http://localhost:8080/mydb', 'api-key')
+users = db.sql('SELECT * FROM users')
+user_ids = users.map { |u| u['id'] }
+```
+
+[Full documentation](ruby/README.md)
+
+## Common Features
+
+All client libraries support:
+
+- **SQL Queries** - Execute any SQL query and get JSON results
+- **Export** - Export database or specific tables to SQL or CSV
+- **Import** - Import data from SQL or CSV files
+- **Authentication** - Optional API key authentication
+- **Error Handling** - Proper error handling with meaningful messages
+
+## Installation
+
+### JavaScript/TypeScript
+
+```bash
+npm install pizzasql
+# or
+bun add pizzasql
+```
+
+### Python
+
+```bash
+pip install pizzasql
+```
+
+### Go
+
+```bash
+go get github.com/pizzasql/pizzasql-go
+```
+
+### Ruby
+
+```bash
+gem install pizzasql
+```
+
+## API Consistency
+
+All clients implement the same core methods:
+
+| Method | JavaScript | Python | Go | Ruby |
+|--------|------------|--------|-----|------|
+| Connect | `connect(uri, apiKey)` | `connect(uri, api_key)` | `Connect(uri, apiKey)` | `connect(uri, api_key)` |
+| Query | `sql(query)` | `sql(query)` | `SQL(query)` | `sql(query)` |
+| Export | `export(table, format)` | `export(table, format)` | `Export(table, format)` | `export(table:, format:)` |
+| Import | `import(data, format, createTable)` | `import_data(data, format, create_table)` | `Import(data, format, createTable)` | `import(data, format:, create_table:)` |
+
+## URI Format
+
+All clients accept URIs in the following formats:
+
+```
+http://localhost:8080/mydb
+https://pizzabase.cloud/my_org/sql/my_db:32131
+```
+
+The database name is extracted from the last segment of the URI path.
+
+## Authentication
+
+All clients support optional API key authentication via Bearer token:
+
+```javascript
+// With API key
+const db = connect('http://localhost:8080/mydb', 'your-api-key-here');
+
+// Without API key
+const db = connect('http://localhost:8080/mydb');
+```
+
+## Response Format
+
+All clients return query results as an array of objects/dictionaries/maps:
+
+```json
+[
+  {"id": 1, "name": "Alice", "email": "alice@example.com"},
+  {"id": 2, "name": "Bob", "email": "bob@example.com"}
+]
+```
+
+## Export/Import Formats
+
+Supported formats:
+- `sql` - SQL statements (default)
+- `csv` - Comma-separated values
+
+### Export Examples
+
+```javascript
+// Export entire database as SQL
+const sqlData = await db.export('', 'sql');
+
+// Export specific table as CSV
+const csvData = await db.export('users', 'csv');
+```
+
+### Import Examples
+
+```javascript
+// Import SQL file
+await db.import(sqlData, 'sql', false);
+
+// Import CSV with auto table creation
+await db.import(csvData, 'csv', true);
+```
+
+## Contributing
+
+Each client library is maintained independently. See individual client directories for development setup and contribution guidelines.
+
+## License
+
+MIT

+ 205 - 0
clients/go/README.md

@@ -0,0 +1,205 @@
+# PizzaSQL Go Client
+
+Go client library for PizzaSQL - a simple, lightweight SQL database with HTTP API.
+
+## Installation
+
+```bash
+go get github.com/pizzasql/pizzasql-go
+```
+
+## Usage
+
+### Basic Example
+
+```go
+package main
+
+import (
+    "fmt"
+    "log"
+
+    "github.com/pizzasql/pizzasql-go"
+)
+
+func main() {
+    // Connect to database
+    db, err := pizzasql.Connect("http://localhost:8080/mydb", "")
+    if err != nil {
+        log.Fatal(err)
+    }
+
+    // Execute a query
+    rows, err := db.SQL("SELECT * FROM users")
+    if err != nil {
+        log.Fatal(err)
+    }
+
+    // Process results
+    for _, row := range rows {
+        fmt.Printf("User ID: %v, Name: %v\n", row["id"], row["name"])
+    }
+}
+```
+
+### With API Key
+
+```go
+db, err := pizzasql.Connect(
+    "https://pizzabase.cloud/my_org/sql/my_db:32131",
+    "a78tsda68bdt6ad5afsd65saf5sd5a7d6sd87asy8d9aysnd7ay==",
+)
+if err != nil {
+    log.Fatal(err)
+}
+
+rows, err := db.SQL("SELECT 42 as answer")
+if err != nil {
+    log.Fatal(err)
+}
+```
+
+### Creating Tables and Inserting Data
+
+```go
+// Create table
+_, err = db.SQL(`
+    CREATE TABLE users (
+        id INTEGER PRIMARY KEY,
+        name TEXT NOT NULL,
+        email TEXT UNIQUE
+    )
+`)
+if err != nil {
+    log.Fatal(err)
+}
+
+// Insert data
+_, err = db.SQL(`
+    INSERT INTO users (id, name, email)
+    VALUES (1, 'Alice', 'alice@example.com')
+`)
+if err != nil {
+    log.Fatal(err)
+}
+```
+
+### Filtering and Mapping Results
+
+```go
+rows, err := db.SQL("SELECT * FROM users")
+if err != nil {
+    log.Fatal(err)
+}
+
+// Extract specific field
+var userIDs []interface{}
+for _, row := range rows {
+    userIDs = append(userIDs, row["id"])
+}
+
+fmt.Println(userIDs) // [1, 2, 3, ...]
+```
+
+### Export Database
+
+```go
+// Export entire database as SQL
+data, err := db.Export("", "sql")
+if err != nil {
+    log.Fatal(err)
+}
+
+// Export specific table as CSV
+csvData, err := db.Export("users", "csv")
+if err != nil {
+    log.Fatal(err)
+}
+
+// Save to file
+err = os.WriteFile("users.csv", csvData, 0644)
+if err != nil {
+    log.Fatal(err)
+}
+```
+
+### Import Data
+
+```go
+// Read CSV file
+data, err := os.ReadFile("users.csv")
+if err != nil {
+    log.Fatal(err)
+}
+
+// Import with table creation
+err = db.Import(data, "csv", true)
+if err != nil {
+    log.Fatal(err)
+}
+```
+
+## API Reference
+
+### Connect(uri string, apiKey string) (*Client, error)
+
+Creates a new PizzaSQL client connection.
+
+**Parameters:**
+- `uri` - Database URI (e.g., `http://localhost:8080/mydb`)
+- `apiKey` - Optional API key for authentication
+
+**Returns:**
+- `*Client` - Connected client instance
+- `error` - Error if connection fails
+
+### (*Client) SQL(query string) ([]Row, error)
+
+Executes a SQL query and returns the results.
+
+**Parameters:**
+- `query` - SQL query string
+
+**Returns:**
+- `[]Row` - Slice of rows (each row is `map[string]interface{}`)
+- `error` - Error if query fails
+
+### (*Client) Export(table string, format string) ([]byte, error)
+
+Exports database or table data.
+
+**Parameters:**
+- `table` - Table name (empty string for entire database)
+- `format` - Export format: `"sql"` or `"csv"`
+
+**Returns:**
+- `[]byte` - Exported data
+- `error` - Error if export fails
+
+### (*Client) Import(data []byte, format string, createTable bool) error
+
+Imports data into the database.
+
+**Parameters:**
+- `data` - Data to import
+- `format` - Import format: `"sql"` or `"csv"`
+- `createTable` - Create table if it doesn't exist (CSV only)
+
+**Returns:**
+- `error` - Error if import fails
+
+## Error Handling
+
+All methods return errors following Go conventions. Always check for errors:
+
+```go
+rows, err := db.SQL("SELECT * FROM users")
+if err != nil {
+    log.Printf("Query failed: %v", err)
+    return
+}
+```
+
+## License
+
+MIT

+ 5 - 0
clients/go/go.mod

@@ -0,0 +1,5 @@
+module github.com/pizzasql/pizzasql-go
+
+go 1.21
+
+// No external dependencies - uses only Go standard library

+ 182 - 0
clients/go/pizzasql.go

@@ -0,0 +1,182 @@
+package pizzasql
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"strings"
+)
+
+// Client represents a connection to a PizzaSQL database
+type Client struct {
+	baseURL string
+	dbName  string
+	apiKey  string
+	client  *http.Client
+}
+
+// Row represents a single row in the result set
+type Row map[string]interface{}
+
+// QueryResult represents the result of a SQL query
+type QueryResult struct {
+	Rows []Row `json:"rows"`
+}
+
+// Connect creates a new PizzaSQL client connection
+// URI format: http://host:port/dbname or https://pizzabase.cloud/my_org/sql/my_db:32131
+func Connect(uri string, apiKey string) (*Client, error) {
+	parsedURL, err := url.Parse(uri)
+	if err != nil {
+		return nil, fmt.Errorf("invalid URI: %w", err)
+	}
+
+	// Extract database name from path
+	path := strings.Trim(parsedURL.Path, "/")
+	if path == "" {
+		return nil, fmt.Errorf("database name not found in URI path")
+	}
+
+	// Split path to get database name (last segment)
+	pathParts := strings.Split(path, "/")
+	dbName := pathParts[len(pathParts)-1]
+
+	// Reconstruct base URL without the database path
+	baseURL := fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host)
+
+	return &Client{
+		baseURL: baseURL,
+		dbName:  dbName,
+		apiKey:  apiKey,
+		client:  &http.Client{},
+	}, nil
+}
+
+// SQL executes a SQL query and returns the results as a slice of rows
+func (c *Client) SQL(query string) ([]Row, error) {
+	// Prepare request body
+	body := map[string]string{"query": query}
+	jsonBody, err := json.Marshal(body)
+	if err != nil {
+		return nil, fmt.Errorf("failed to marshal request: %w", err)
+	}
+
+	// Create request
+	url := fmt.Sprintf("%s/%s/query", c.baseURL, c.dbName)
+	req, err := http.NewRequest("POST", url, bytes.NewReader(jsonBody))
+	if err != nil {
+		return nil, fmt.Errorf("failed to create request: %w", err)
+	}
+
+	// Set headers
+	req.Header.Set("Content-Type", "application/json")
+	if c.apiKey != "" {
+		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
+	}
+
+	// Execute request
+	resp, err := c.client.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("request failed: %w", err)
+	}
+	defer resp.Body.Close()
+
+	// Read response body
+	respBody, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read response: %w", err)
+	}
+
+	// Check status code
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(respBody))
+	}
+
+	// Parse response
+	var result QueryResult
+	if err := json.Unmarshal(respBody, &result); err != nil {
+		return nil, fmt.Errorf("failed to parse response: %w", err)
+	}
+
+	return result.Rows, nil
+}
+
+// Export exports a database or table to SQL or CSV format
+func (c *Client) Export(table string, format string) ([]byte, error) {
+	params := url.Values{}
+	if table != "" {
+		params.Set("table", table)
+	}
+	if format != "" {
+		params.Set("format", format)
+	}
+
+	url := fmt.Sprintf("%s/%s/export?%s", c.baseURL, c.dbName, params.Encode())
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create request: %w", err)
+	}
+
+	if c.apiKey != "" {
+		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
+	}
+
+	resp, err := c.client.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("request failed: %w", err)
+	}
+	defer resp.Body.Close()
+
+	data, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read response: %w", err)
+	}
+
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("export failed with status %d: %s", resp.StatusCode, string(data))
+	}
+
+	return data, nil
+}
+
+// Import imports data from SQL or CSV format
+func (c *Client) Import(data []byte, format string, createTable bool) error {
+	params := url.Values{}
+	if format != "" {
+		params.Set("format", format)
+	}
+	if createTable {
+		params.Set("create_table", "true")
+	}
+
+	url := fmt.Sprintf("%s/%s/import?%s", c.baseURL, c.dbName, params.Encode())
+	req, err := http.NewRequest("POST", url, bytes.NewReader(data))
+	if err != nil {
+		return fmt.Errorf("failed to create request: %w", err)
+	}
+
+	req.Header.Set("Content-Type", "application/octet-stream")
+	if c.apiKey != "" {
+		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
+	}
+
+	resp, err := c.client.Do(req)
+	if err != nil {
+		return fmt.Errorf("request failed: %w", err)
+	}
+	defer resp.Body.Close()
+
+	respBody, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return fmt.Errorf("failed to read response: %w", err)
+	}
+
+	if resp.StatusCode != http.StatusOK {
+		return fmt.Errorf("import failed with status %d: %s", resp.StatusCode, string(respBody))
+	}
+
+	return nil
+}

+ 169 - 0
clients/js/README.md

@@ -0,0 +1,169 @@
+# PizzaSQL JavaScript/TypeScript Client
+
+A lightweight client for PizzaSQL that works with Node.js, Bun, and browsers.
+
+## Installation
+
+```bash
+# npm
+npm install pizzasql
+
+# bun
+bun add pizzasql
+
+# pnpm
+pnpm add pizzasql
+```
+
+## Quick Start
+
+```typescript
+import { connect } from 'pizzasql';
+
+const db = connect('http://localhost:8080/mydb', 'your-api-key');
+
+// Simple query
+const users = await db.sql('SELECT * FROM users');
+console.log(users);
+// [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]
+
+// Query with parameters
+const user = await db.sql('SELECT * FROM users WHERE id = ?', [1]);
+
+// Chain operations
+const names = (await db.sql('SELECT name FROM users')).map(u => u.name);
+```
+
+## API Reference
+
+### `connect(uri, apiKey?)`
+
+Create a new database connection.
+
+```typescript
+const db = connect('http://localhost:8080/mydb', 'optional-api-key');
+```
+
+### `db.sql(query, params?)`
+
+Execute a query and return rows as objects.
+
+```typescript
+// Simple query
+const rows = await db.sql('SELECT * FROM users');
+
+// With parameters (prevents SQL injection)
+const rows = await db.sql('SELECT * FROM users WHERE age > ?', [18]);
+
+// With TypeScript types
+interface User {
+  id: number;
+  name: string;
+  email: string;
+}
+const users = await db.sql<User>('SELECT * FROM users');
+```
+
+### `db.query(query, params?)`
+
+Execute a query and return full result with metadata.
+
+```typescript
+const result = await db.query('SELECT * FROM users');
+console.log(result.columns);      // [{ name: 'id', type: 'INTEGER' }, ...]
+console.log(result.rows);         // [{ id: 1, name: 'Alice' }, ...]
+console.log(result.executionTime); // '1.234ms'
+```
+
+### `db.execute(statements, transaction?)`
+
+Execute multiple statements in a batch.
+
+```typescript
+const result = await db.execute([
+  { sql: 'INSERT INTO users (name) VALUES (?)', params: ['Alice'] },
+  { sql: 'INSERT INTO users (name) VALUES (?)', params: ['Bob'] },
+], true); // true = wrap in transaction
+
+console.log(result.totalRowsAffected); // 2
+```
+
+### `db.tables()`
+
+List all tables.
+
+```typescript
+const tables = await db.tables();
+// ['users', 'posts', 'comments']
+```
+
+### `db.schema(tableName)`
+
+Get table schema.
+
+```typescript
+const columns = await db.schema('users');
+// [{ name: 'id', type: 'INTEGER' }, { name: 'name', type: 'TEXT' }]
+```
+
+### `db.use(database)`
+
+Switch to a different database.
+
+```typescript
+const otherDb = db.use('other_database');
+const rows = await otherDb.sql('SELECT * FROM other_table');
+```
+
+## Examples
+
+### CRUD Operations
+
+```typescript
+// Create
+await db.sql('INSERT INTO users (name, email) VALUES (?, ?)', ['Alice', 'alice@example.com']);
+
+// Read
+const users = await db.sql('SELECT * FROM users WHERE active = ?', [true]);
+
+// Update
+await db.sql('UPDATE users SET name = ? WHERE id = ?', ['Alicia', 1]);
+
+// Delete
+await db.sql('DELETE FROM users WHERE id = ?', [1]);
+```
+
+### Transactions
+
+```typescript
+await db.execute([
+  { sql: 'UPDATE accounts SET balance = balance - ? WHERE id = ?', params: [100, 1] },
+  { sql: 'UPDATE accounts SET balance = balance + ? WHERE id = ?', params: [100, 2] },
+], true);
+```
+
+### Error Handling
+
+```typescript
+try {
+  await db.sql('SELECT * FROM nonexistent_table');
+} catch (error) {
+  console.error(error.message);
+  // [TABLE_NOT_FOUND] Table 'nonexistent_table' does not exist
+}
+```
+
+## Configuration
+
+```typescript
+import { createClient } from 'pizzasql';
+
+const db = createClient('http://localhost:8080/mydb', {
+  apiKey: 'your-api-key',
+  timeout: 60000, // 60 seconds
+});
+```
+
+## License
+
+MIT

+ 215 - 0
clients/js/index.ts

@@ -0,0 +1,215 @@
+/**
+ * PizzaSQL Client for JavaScript/TypeScript
+ * Works with Node.js, Bun, and browsers
+ */
+
+export interface PizzaSQLConfig {
+  apiKey?: string;
+  timeout?: number;
+}
+
+export interface Column {
+  name: string;
+  type: string;
+}
+
+export interface QueryResult<T = Record<string, unknown>> {
+  columns: Column[];
+  rows: T[];
+  rowsAffected: number;
+  lastInsertId: number;
+  executionTime: string;
+}
+
+export interface ExecuteResult {
+  results: { rowsAffected: number; lastInsertId: number }[];
+  totalRowsAffected: number;
+  executionTime: string;
+}
+
+export interface TableInfo {
+  tables: string[];
+  count: number;
+}
+
+export interface PizzaSQLError {
+  code: string;
+  message: string;
+  details?: Record<string, unknown>;
+}
+
+class PizzaSQLClient {
+  private baseUrl: string;
+  private apiKey?: string;
+  private timeout: number;
+  private database?: string;
+
+  constructor(uri: string, config: PizzaSQLConfig = {}) {
+    // Parse URI: https://host:port/database or https://host:port
+    const url = new URL(uri);
+    this.baseUrl = `${url.protocol}//${url.host}`;
+    this.database = url.pathname.slice(1) || undefined;
+    this.apiKey = config.apiKey;
+    this.timeout = config.timeout || 30000;
+  }
+
+  private async request<T>(
+    path: string,
+    options: RequestInit = {}
+  ): Promise<T> {
+    const headers: Record<string, string> = {
+      'Content-Type': 'application/json',
+      ...(options.headers as Record<string, string>),
+    };
+
+    if (this.apiKey) {
+      headers['Authorization'] = `Bearer ${this.apiKey}`;
+    }
+
+    if (this.database) {
+      headers['X-Database'] = this.database;
+    }
+
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), this.timeout);
+
+    try {
+      const response = await fetch(`${this.baseUrl}${path}`, {
+        ...options,
+        headers,
+        signal: controller.signal,
+      });
+
+      const data = await response.json();
+
+      if (!response.ok) {
+        const error = data.error as PizzaSQLError;
+        throw new Error(`[${error.code}] ${error.message}`);
+      }
+
+      return data as T;
+    } finally {
+      clearTimeout(timeoutId);
+    }
+  }
+
+  /**
+   * Execute a SQL query with optional parameters
+   */
+  async query<T = Record<string, unknown>>(
+    sql: string,
+    params: unknown[] = []
+  ): Promise<QueryResult<T>> {
+    const result = await this.request<{
+      columns: Column[];
+      rows: unknown[][];
+      rowsAffected: number;
+      lastInsertId: number;
+      executionTime: string;
+    }>('/query', {
+      method: 'POST',
+      body: JSON.stringify({ sql, params }),
+    });
+
+    // Transform rows from arrays to objects
+    const rows = result.rows.map((row) => {
+      const obj: Record<string, unknown> = {};
+      result.columns.forEach((col, i) => {
+        obj[col.name] = row[i];
+      });
+      return obj as T;
+    });
+
+    return {
+      ...result,
+      rows,
+    };
+  }
+
+  /**
+   * Shorthand for query - returns rows directly
+   */
+  async sql<T = Record<string, unknown>>(
+    sql: string,
+    params: unknown[] = []
+  ): Promise<T[]> {
+    const result = await this.query<T>(sql, params);
+    return result.rows;
+  }
+
+  /**
+   * Execute multiple statements in a batch
+   */
+  async execute(
+    statements: { sql: string; params?: unknown[] }[],
+    transaction = true
+  ): Promise<ExecuteResult> {
+    return this.request<ExecuteResult>('/execute', {
+      method: 'POST',
+      body: JSON.stringify({
+        statements: statements.map((s) => ({
+          sql: s.sql,
+          params: s.params || [],
+        })),
+        transaction,
+      }),
+    });
+  }
+
+  /**
+   * List all tables in the database
+   */
+  async tables(): Promise<string[]> {
+    const result = await this.request<TableInfo>('/schema/tables');
+    return result.tables;
+  }
+
+  /**
+   * Get schema for a specific table
+   */
+  async schema(tableName: string): Promise<Column[]> {
+    const result = await this.request<{ columns: Column[] }>(
+      `/schema/tables/${encodeURIComponent(tableName)}`
+    );
+    return result.columns;
+  }
+
+  /**
+   * Health check
+   */
+  async health(): Promise<{ status: string; database: string }> {
+    return this.request('/health');
+  }
+
+  /**
+   * Use a different database
+   */
+  use(database: string): PizzaSQLClient {
+    const client = new PizzaSQLClient(this.baseUrl, {
+      apiKey: this.apiKey,
+      timeout: this.timeout,
+    });
+    client.database = database;
+    return client;
+  }
+}
+
+/**
+ * Create a new PizzaSQL connection
+ */
+export function connect(uri: string, apiKey?: string): PizzaSQLClient {
+  return new PizzaSQLClient(uri, { apiKey });
+}
+
+/**
+ * Create a new PizzaSQL connection with full config
+ */
+export function createClient(
+  uri: string,
+  config: PizzaSQLConfig = {}
+): PizzaSQLClient {
+  return new PizzaSQLClient(uri, config);
+}
+
+// Default export
+export default { connect, createClient };

+ 26 - 0
clients/js/package.json

@@ -0,0 +1,26 @@
+{
+  "name": "pizzasql",
+  "version": "0.1.0",
+  "description": "PizzaSQL client for JavaScript/TypeScript",
+  "main": "dist/index.js",
+  "module": "dist/index.mjs",
+  "types": "dist/index.d.ts",
+  "exports": {
+    ".": {
+      "import": "./dist/index.mjs",
+      "require": "./dist/index.js",
+      "types": "./dist/index.d.ts"
+    }
+  },
+  "scripts": {
+    "build": "tsup index.ts --format cjs,esm --dts",
+    "test": "bun test"
+  },
+  "keywords": ["pizzasql", "database", "sql", "client"],
+  "author": "PizzaSQL Team",
+  "license": "MIT",
+  "devDependencies": {
+    "tsup": "^8.0.0",
+    "typescript": "^5.0.0"
+  }
+}

+ 190 - 0
clients/python/README.md

@@ -0,0 +1,190 @@
+# PizzaSQL Python Client
+
+A simple, Pythonic client for PizzaSQL.
+
+## Installation
+
+```bash
+pip install pizzasql
+
+# With httpx for better performance (optional)
+pip install pizzasql[httpx]
+```
+
+## Quick Start
+
+```python
+from pizzasql import connect
+
+db = connect('http://localhost:8080/mydb', api_key='your-api-key')
+
+# Simple query
+users = db.sql('SELECT * FROM users')
+print(users)
+# [{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Bob'}]
+
+# Query with parameters
+user = db.sql('SELECT * FROM users WHERE id = ?', [1])
+
+# Iterate over results
+for user in db.sql('SELECT * FROM users'):
+    print(user['name'])
+```
+
+## API Reference
+
+### `connect(uri, api_key=None)`
+
+Create a new database connection.
+
+```python
+db = connect('http://localhost:8080/mydb', api_key='optional-api-key')
+```
+
+### `db.sql(query, params=None)`
+
+Execute a query and return rows as dictionaries.
+
+```python
+# Simple query
+rows = db.sql('SELECT * FROM users')
+
+# With parameters (prevents SQL injection)
+rows = db.sql('SELECT * FROM users WHERE age > ?', [18])
+
+# Use with list comprehensions
+names = [u['name'] for u in db.sql('SELECT name FROM users')]
+```
+
+### `db.query(query, params=None)`
+
+Execute a query and return full result with metadata.
+
+```python
+result = db.query('SELECT * FROM users')
+print(result.columns)        # [Column(name='id', type='INTEGER'), ...]
+print(result.rows)           # [{'id': 1, 'name': 'Alice'}, ...]
+print(result.execution_time) # '1.234ms'
+print(len(result))           # Number of rows
+
+# QueryResult is iterable
+for row in result:
+    print(row)
+```
+
+### `db.execute(statements, transaction=True)`
+
+Execute multiple statements in a batch.
+
+```python
+result = db.execute([
+    {'sql': 'INSERT INTO users (name) VALUES (?)', 'params': ['Alice']},
+    {'sql': 'INSERT INTO users (name) VALUES (?)', 'params': ['Bob']},
+], transaction=True)
+
+print(result['totalRowsAffected'])  # 2
+```
+
+### `db.tables()`
+
+List all tables.
+
+```python
+tables = db.tables()
+# ['users', 'posts', 'comments']
+```
+
+### `db.schema(table_name)`
+
+Get table schema.
+
+```python
+columns = db.schema('users')
+# [Column(name='id', type='INTEGER'), Column(name='name', type='TEXT')]
+```
+
+### `db.use(database)`
+
+Switch to a different database.
+
+```python
+other_db = db.use('other_database')
+rows = other_db.sql('SELECT * FROM other_table')
+```
+
+## Examples
+
+### CRUD Operations
+
+```python
+# Create
+db.sql('INSERT INTO users (name, email) VALUES (?, ?)', ['Alice', 'alice@example.com'])
+
+# Read
+users = db.sql('SELECT * FROM users WHERE active = ?', [True])
+
+# Update
+db.sql('UPDATE users SET name = ? WHERE id = ?', ['Alicia', 1])
+
+# Delete
+db.sql('DELETE FROM users WHERE id = ?', [1])
+```
+
+### Transactions
+
+```python
+db.execute([
+    {'sql': 'UPDATE accounts SET balance = balance - ? WHERE id = ?', 'params': [100, 1]},
+    {'sql': 'UPDATE accounts SET balance = balance + ? WHERE id = ?', 'params': [100, 2]},
+], transaction=True)
+```
+
+### Context Manager
+
+```python
+with connect('http://localhost:8080/mydb') as db:
+    users = db.sql('SELECT * FROM users')
+    # Connection automatically closed
+```
+
+### Error Handling
+
+```python
+from pizzasql import connect, PizzaSQLError
+
+try:
+    db.sql('SELECT * FROM nonexistent_table')
+except PizzaSQLError as e:
+    print(e.code)     # 'TABLE_NOT_FOUND'
+    print(e.message)  # "Table 'nonexistent_table' does not exist"
+```
+
+### Data Processing
+
+```python
+# List comprehension
+emails = [u['email'] for u in db.sql('SELECT email FROM users')]
+
+# Filter
+active_users = [u for u in db.sql('SELECT * FROM users') if u['active']]
+
+# Aggregation
+from collections import Counter
+status_counts = Counter(u['status'] for u in db.sql('SELECT status FROM users'))
+```
+
+## Configuration
+
+```python
+from pizzasql import PizzaSQL
+
+db = PizzaSQL(
+    'http://localhost:8080/mydb',
+    api_key='your-api-key',
+    timeout=60.0  # 60 seconds
+)
+```
+
+## License
+
+MIT

+ 278 - 0
clients/python/pizzasql.py

@@ -0,0 +1,278 @@
+"""
+PizzaSQL Client for Python
+
+A simple, Pythonic client for PizzaSQL.
+"""
+
+from typing import Any, Dict, List, Optional, Union
+from dataclasses import dataclass
+from urllib.parse import urlparse
+import json
+
+try:
+    import httpx
+    _client_class = httpx.Client
+    _async_client_class = httpx.AsyncClient
+except ImportError:
+    import urllib.request
+    import urllib.error
+    _client_class = None
+    _async_client_class = None
+
+
+@dataclass
+class Column:
+    """Represents a column in a query result."""
+    name: str
+    type: str
+
+
+@dataclass
+class QueryResult:
+    """Result of a SQL query."""
+    columns: List[Column]
+    rows: List[Dict[str, Any]]
+    rows_affected: int
+    last_insert_id: int
+    execution_time: str
+
+    def __iter__(self):
+        return iter(self.rows)
+
+    def __len__(self):
+        return len(self.rows)
+
+    def __getitem__(self, index):
+        return self.rows[index]
+
+
+class PizzaSQLError(Exception):
+    """Exception raised for PizzaSQL errors."""
+    def __init__(self, code: str, message: str, details: Optional[Dict] = None):
+        self.code = code
+        self.message = message
+        self.details = details
+        super().__init__(f"[{code}] {message}")
+
+
+class PizzaSQL:
+    """
+    PizzaSQL client for Python.
+
+    Usage:
+        db = PizzaSQL('http://localhost:8080/mydb', api_key='your-key')
+        rows = db.sql('SELECT * FROM users')
+    """
+
+    def __init__(
+        self,
+        uri: str,
+        api_key: Optional[str] = None,
+        timeout: float = 30.0
+    ):
+        """
+        Create a new PizzaSQL connection.
+
+        Args:
+            uri: Database URI (e.g., 'http://localhost:8080/mydb')
+            api_key: Optional API key for authentication
+            timeout: Request timeout in seconds
+        """
+        parsed = urlparse(uri)
+        self._base_url = f"{parsed.scheme}://{parsed.netloc}"
+        self._database = parsed.path.lstrip('/') or None
+        self._api_key = api_key
+        self._timeout = timeout
+
+        if _client_class:
+            self._client = _client_class(timeout=timeout)
+        else:
+            self._client = None
+
+    def _headers(self) -> Dict[str, str]:
+        headers = {'Content-Type': 'application/json'}
+        if self._api_key:
+            headers['Authorization'] = f'Bearer {self._api_key}'
+        if self._database:
+            headers['X-Database'] = self._database
+        return headers
+
+    def _request(self, method: str, path: str, data: Optional[Dict] = None) -> Dict:
+        url = f"{self._base_url}{path}"
+        headers = self._headers()
+
+        if self._client:
+            # Use httpx
+            if method == 'GET':
+                response = self._client.get(url, headers=headers)
+            else:
+                response = self._client.post(url, headers=headers, json=data)
+
+            result = response.json()
+            if response.status_code >= 400:
+                error = result.get('error', {})
+                raise PizzaSQLError(
+                    error.get('code', 'UNKNOWN'),
+                    error.get('message', 'Unknown error'),
+                    error.get('details')
+                )
+            return result
+        else:
+            # Fallback to urllib
+            req = urllib.request.Request(url, headers=headers)
+            if data:
+                req.data = json.dumps(data).encode('utf-8')
+
+            try:
+                with urllib.request.urlopen(req, timeout=self._timeout) as response:
+                    return json.loads(response.read().decode('utf-8'))
+            except urllib.error.HTTPError as e:
+                result = json.loads(e.read().decode('utf-8'))
+                error = result.get('error', {})
+                raise PizzaSQLError(
+                    error.get('code', 'UNKNOWN'),
+                    error.get('message', str(e)),
+                    error.get('details')
+                )
+
+    def _transform_rows(self, columns: List[Dict], rows: List[List]) -> List[Dict[str, Any]]:
+        """Transform array rows to dictionaries."""
+        return [
+            {col['name']: row[i] for i, col in enumerate(columns)}
+            for row in rows
+        ]
+
+    def query(self, sql: str, params: Optional[List] = None) -> QueryResult:
+        """
+        Execute a SQL query and return full result.
+
+        Args:
+            sql: SQL query string
+            params: Optional list of parameters
+
+        Returns:
+            QueryResult with columns, rows, and metadata
+        """
+        result = self._request('POST', '/query', {
+            'sql': sql,
+            'params': params or []
+        })
+
+        columns = [Column(**col) for col in result.get('columns', [])]
+        rows = self._transform_rows(result.get('columns', []), result.get('rows', []))
+
+        return QueryResult(
+            columns=columns,
+            rows=rows,
+            rows_affected=result.get('rowsAffected', 0),
+            last_insert_id=result.get('lastInsertId', 0),
+            execution_time=result.get('executionTime', '')
+        )
+
+    def sql(self, sql: str, params: Optional[List] = None) -> List[Dict[str, Any]]:
+        """
+        Execute a SQL query and return rows.
+
+        Args:
+            sql: SQL query string
+            params: Optional list of parameters
+
+        Returns:
+            List of row dictionaries
+        """
+        return self.query(sql, params).rows
+
+    def execute(
+        self,
+        statements: List[Dict[str, Any]],
+        transaction: bool = True
+    ) -> Dict[str, Any]:
+        """
+        Execute multiple statements in a batch.
+
+        Args:
+            statements: List of {'sql': ..., 'params': [...]} dicts
+            transaction: Whether to wrap in a transaction
+
+        Returns:
+            Execution result with affected rows
+        """
+        return self._request('POST', '/execute', {
+            'statements': [
+                {'sql': s['sql'], 'params': s.get('params', [])}
+                for s in statements
+            ],
+            'transaction': transaction
+        })
+
+    def tables(self) -> List[str]:
+        """
+        List all tables in the database.
+
+        Returns:
+            List of table names
+        """
+        result = self._request('GET', '/schema/tables')
+        return result.get('tables', [])
+
+    def schema(self, table_name: str) -> List[Column]:
+        """
+        Get schema for a specific table.
+
+        Args:
+            table_name: Name of the table
+
+        Returns:
+            List of Column objects
+        """
+        result = self._request('GET', f'/schema/tables/{table_name}')
+        return [Column(**col) for col in result.get('columns', [])]
+
+    def health(self) -> Dict[str, str]:
+        """
+        Check database health.
+
+        Returns:
+            Health status dict
+        """
+        return self._request('GET', '/health')
+
+    def use(self, database: str) -> 'PizzaSQL':
+        """
+        Create a new client for a different database.
+
+        Args:
+            database: Database name
+
+        Returns:
+            New PizzaSQL client
+        """
+        client = PizzaSQL(self._base_url, self._api_key, self._timeout)
+        client._database = database
+        return client
+
+    def close(self):
+        """Close the underlying HTTP client."""
+        if self._client and hasattr(self._client, 'close'):
+            self._client.close()
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, *args):
+        self.close()
+
+
+# Convenience function
+def connect(uri: str, api_key: Optional[str] = None) -> PizzaSQL:
+    """
+    Create a new PizzaSQL connection.
+
+    Args:
+        uri: Database URI (e.g., 'http://localhost:8080/mydb')
+        api_key: Optional API key for authentication
+
+    Returns:
+        PizzaSQL client instance
+    """
+    return PizzaSQL(uri, api_key)

+ 30 - 0
clients/python/pyproject.toml

@@ -0,0 +1,30 @@
+[build-system]
+requires = ["setuptools>=61.0"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "pizzasql"
+version = "0.1.0"
+description = "PizzaSQL client for Python"
+readme = "README.md"
+license = {text = "MIT"}
+requires-python = ">=3.8"
+classifiers = [
+    "Development Status :: 4 - Beta",
+    "Intended Audience :: Developers",
+    "License :: OSI Approved :: MIT License",
+    "Programming Language :: Python :: 3",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+]
+keywords = ["pizzasql", "database", "sql", "client"]
+
+[project.optional-dependencies]
+httpx = ["httpx>=0.24.0"]
+
+[project.urls]
+Homepage = "https://github.com/danfragoso/pizzasql"
+Documentation = "https://github.com/danfragoso/pizzasql/tree/main/clients/python"

+ 205 - 0
clients/ruby/README.md

@@ -0,0 +1,205 @@
+# PizzaSQL Ruby Client
+
+Ruby client library for PizzaSQL - a simple, lightweight SQL database with HTTP API.
+
+## Installation
+
+Add this line to your application's Gemfile:
+
+```ruby
+gem 'pizzasql'
+```
+
+And then execute:
+
+```bash
+bundle install
+```
+
+Or install it yourself:
+
+```bash
+gem install pizzasql
+```
+
+## Usage
+
+### Basic Example
+
+```ruby
+require 'pizzasql'
+
+# Connect to database
+db = PizzaSQL.connect('http://localhost:8080/mydb')
+
+# Execute a query
+rows = db.sql('SELECT * FROM users')
+
+# Process results
+rows.each do |row|
+  puts "User ID: #{row['id']}, Name: #{row['name']}"
+end
+```
+
+### With API Key
+
+```ruby
+db = PizzaSQL.connect(
+  'https://pizzabase.cloud/my_org/sql/my_db:32131',
+  'a78tsda68bdt6ad5afsd65saf5sd5a7d6sd87asy8d9aysnd7ay=='
+)
+
+rows = db.sql('SELECT 42 as answer')
+puts rows.first['answer'] # => 42
+```
+
+### Creating Tables and Inserting Data
+
+```ruby
+# Create table
+db.sql(<<~SQL)
+  CREATE TABLE users (
+    id INTEGER PRIMARY KEY,
+    name TEXT NOT NULL,
+    email TEXT UNIQUE
+  )
+SQL
+
+# Insert data
+db.sql(<<~SQL)
+  INSERT INTO users (id, name, email)
+  VALUES (1, 'Alice', 'alice@example.com')
+SQL
+```
+
+### Filtering and Mapping Results
+
+```ruby
+rows = db.sql('SELECT * FROM users')
+
+# Extract specific field
+user_ids = rows.map { |row| row['id'] }
+puts user_ids.inspect # => [1, 2, 3, ...]
+
+# Filter results
+adults = rows.select { |row| row['age'] >= 18 }
+```
+
+### Export Database
+
+```ruby
+# Export entire database as SQL
+sql_data = db.export(format: 'sql')
+
+# Export specific table as CSV
+csv_data = db.export(table: 'users', format: 'csv')
+
+# Save to file
+File.write('users.csv', csv_data)
+```
+
+### Import Data
+
+```ruby
+# Read CSV file
+csv_data = File.read('users.csv')
+
+# Import with table creation
+db.import(csv_data, format: 'csv', create_table: true)
+
+# Import SQL file
+sql_data = File.read('backup.sql')
+db.import(sql_data, format: 'sql')
+```
+
+## API Reference
+
+### PizzaSQL.connect(uri, api_key = nil)
+
+Creates a new PizzaSQL client connection.
+
+**Parameters:**
+- `uri` (String) - Database URI (e.g., `http://localhost:8080/mydb`)
+- `api_key` (String, optional) - API key for authentication
+
+**Returns:**
+- `Client` - Connected client instance
+
+**Example:**
+```ruby
+db = PizzaSQL.connect('http://localhost:8080/mydb')
+```
+
+### Client#sql(query)
+
+Executes a SQL query and returns the results.
+
+**Parameters:**
+- `query` (String) - SQL query string
+
+**Returns:**
+- `Array<Hash>` - Array of rows (each row is a hash)
+
+**Raises:**
+- `RuntimeError` - If query fails
+
+**Example:**
+```ruby
+rows = db.sql('SELECT * FROM users WHERE age > 18')
+```
+
+### Client#export(table: nil, format: 'sql')
+
+Exports database or table data.
+
+**Parameters:**
+- `table` (String, optional) - Table name (nil for entire database)
+- `format` (String) - Export format: `'sql'` or `'csv'`
+
+**Returns:**
+- `String` - Exported data
+
+**Raises:**
+- `RuntimeError` - If export fails
+
+**Example:**
+```ruby
+data = db.export(table: 'users', format: 'csv')
+```
+
+### Client#import(data, format: 'sql', create_table: false)
+
+Imports data into the database.
+
+**Parameters:**
+- `data` (String) - Data to import
+- `format` (String) - Import format: `'sql'` or `'csv'`
+- `create_table` (Boolean) - Create table if it doesn't exist (CSV only)
+
+**Returns:**
+- `nil`
+
+**Raises:**
+- `RuntimeError` - If import fails
+
+**Example:**
+```ruby
+csv_data = File.read('users.csv')
+db.import(csv_data, format: 'csv', create_table: true)
+```
+
+## Error Handling
+
+All methods raise `RuntimeError` on failure. Use standard Ruby error handling:
+
+```ruby
+begin
+  rows = db.sql('SELECT * FROM users')
+rescue => e
+  puts "Query failed: #{e.message}"
+end
+```
+
+## License
+
+MIT

+ 123 - 0
clients/ruby/lib/pizzasql.rb

@@ -0,0 +1,123 @@
+require 'net/http'
+require 'uri'
+require 'json'
+
+module PizzaSQL
+  class Client
+    attr_reader :base_url, :db_name, :api_key
+
+    # Creates a new PizzaSQL client connection
+    #
+    # @param uri [String] Database URI (e.g., 'http://localhost:8080/mydb')
+    # @param api_key [String, nil] Optional API key for authentication
+    # @return [Client] A new client instance
+    def initialize(uri, api_key = nil)
+      parsed_uri = URI.parse(uri)
+
+      # Extract database name from path
+      path = parsed_uri.path.strip.delete_prefix('/')
+      raise ArgumentError, 'Database name not found in URI path' if path.empty?
+
+      # Split path to get database name (last segment)
+      path_parts = path.split('/')
+      @db_name = path_parts.last
+
+      # Reconstruct base URL without the database path
+      @base_url = "#{parsed_uri.scheme}://#{parsed_uri.host}:#{parsed_uri.port}"
+      @api_key = api_key
+    end
+
+    # Executes a SQL query and returns the results
+    #
+    # @param query [String] SQL query string
+    # @return [Array<Hash>] Array of rows (each row is a hash)
+    # @raise [RuntimeError] if the query fails
+    def sql(query)
+      uri = URI.parse("#{@base_url}/#{@db_name}/query")
+
+      request = Net::HTTP::Post.new(uri)
+      request['Content-Type'] = 'application/json'
+      request['Authorization'] = "Bearer #{@api_key}" if @api_key
+      request.body = { query: query }.to_json
+
+      response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
+        http.request(request)
+      end
+
+      unless response.is_a?(Net::HTTPSuccess)
+        raise "Request failed with status #{response.code}: #{response.body}"
+      end
+
+      result = JSON.parse(response.body)
+      result['rows'] || []
+    end
+
+    # Exports database or table data
+    #
+    # @param table [String, nil] Table name (nil for entire database)
+    # @param format [String] Export format: 'sql' or 'csv'
+    # @return [String] Exported data
+    # @raise [RuntimeError] if the export fails
+    def export(table: nil, format: 'sql')
+      params = {}
+      params['table'] = table if table
+      params['format'] = format if format
+
+      query_string = URI.encode_www_form(params)
+      uri = URI.parse("#{@base_url}/#{@db_name}/export?#{query_string}")
+
+      request = Net::HTTP::Get.new(uri)
+      request['Authorization'] = "Bearer #{@api_key}" if @api_key
+
+      response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
+        http.request(request)
+      end
+
+      unless response.is_a?(Net::HTTPSuccess)
+        raise "Export failed with status #{response.code}: #{response.body}"
+      end
+
+      response.body
+    end
+
+    # Imports data into the database
+    #
+    # @param data [String] Data to import
+    # @param format [String] Import format: 'sql' or 'csv'
+    # @param create_table [Boolean] Create table if it doesn't exist (CSV only)
+    # @return [void]
+    # @raise [RuntimeError] if the import fails
+    def import(data, format: 'sql', create_table: false)
+      params = {}
+      params['format'] = format if format
+      params['create_table'] = 'true' if create_table
+
+      query_string = URI.encode_www_form(params)
+      uri = URI.parse("#{@base_url}/#{@db_name}/import?#{query_string}")
+
+      request = Net::HTTP::Post.new(uri)
+      request['Content-Type'] = 'application/octet-stream'
+      request['Authorization'] = "Bearer #{@api_key}" if @api_key
+      request.body = data
+
+      response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
+        http.request(request)
+      end
+
+      unless response.is_a?(Net::HTTPSuccess)
+        raise "Import failed with status #{response.code}: #{response.body}"
+      end
+
+      nil
+    end
+  end
+
+  # Module-level convenience method to create a new client connection
+  #
+  # @param uri [String] Database URI
+  # @param api_key [String, nil] Optional API key
+  # @return [Client] A new client instance
+  def self.connect(uri, api_key = nil)
+    Client.new(uri, api_key)
+  end
+end

+ 17 - 0
clients/ruby/pizzasql.gemspec

@@ -0,0 +1,17 @@
+Gem::Specification.new do |spec|
+  spec.name          = "pizzasql"
+  spec.version       = "0.1.0"
+  spec.authors       = ["PizzaSQL"]
+  spec.email         = ["hello@pizzasql.com"]
+
+  spec.summary       = "Ruby client for PizzaSQL"
+  spec.description   = "A simple, lightweight client library for PizzaSQL - a SQL database with HTTP API"
+  spec.homepage      = "https://github.com/pizzasql/pizzasql"
+  spec.license       = "MIT"
+  spec.required_ruby_version = ">= 2.7.0"
+
+  spec.files = Dir["lib/**/*", "README.md", "LICENSE"]
+  spec.require_paths = ["lib"]
+
+  # No external dependencies - uses only Ruby standard library
+end

+ 241 - 7
main.go

@@ -16,6 +16,10 @@ import (
 	"github.com/danfragoso/pizzasql-next/pkg/httpserver"
 	"github.com/danfragoso/pizzasql-next/pkg/lexer"
 	"github.com/danfragoso/pizzasql-next/pkg/parser"
+	"github.com/danfragoso/pizzasql-next/pkg/csvexport"
+	"github.com/danfragoso/pizzasql-next/pkg/csvimport"
+	"github.com/danfragoso/pizzasql-next/pkg/sqlexport"
+	"github.com/danfragoso/pizzasql-next/pkg/sqlimport"
 	"github.com/danfragoso/pizzasql-next/pkg/storage"
 )
 
@@ -30,6 +34,15 @@ var (
 	httpCORS   = flag.Bool("http-cors", true, "Enable CORS")
 	httpAuth   = flag.Bool("http-auth", false, "Enable authentication")
 	apiKeys    = flag.String("api-keys", "", "Comma-separated API keys")
+
+	// Export/Import flags
+	exportFile   = flag.String("o", "", "Output file for export")
+	importFile   = flag.String("i", "", "Input file for import")
+	exportTable  = flag.String("table", "", "Specific table to export (empty = all)")
+	exportDrop   = flag.Bool("drop", false, "Include DROP TABLE statements in export")
+	ignoreErrors = flag.Bool("ignore-errors", false, "Continue import on errors")
+	exportFormat = flag.String("format", "", "Export/import format: sql, csv (auto-detect from extension)")
+	createTable  = flag.Bool("create-table", false, "Create table if not exists (CSV import)")
 )
 
 func main() {
@@ -41,6 +54,18 @@ func main() {
 		return
 	}
 
+	// Check for export command
+	if *exportFile != "" {
+		runExport()
+		return
+	}
+
+	// Check for import command
+	if *importFile != "" {
+		runImport()
+		return
+	}
+
 	// Check for command-line SQL
 	args := flag.Args()
 	if len(args) > 0 {
@@ -456,6 +481,18 @@ func printHelp() {
 	fmt.Println("Expression Mode (SELECT without FROM):")
 	fmt.Println("  SELECT 1 + 2 * 3;")
 	fmt.Println("  SELECT UPPER('hello');")
+	fmt.Println()
+	fmt.Println("Export/Import:")
+	fmt.Println("  pizzasql -db mydb -o backup.sql           Export database to SQL file")
+	fmt.Println("  pizzasql -db mydb -table users -o t.sql   Export single table")
+	fmt.Println("  pizzasql -db mydb -o backup.sql -drop     Include DROP TABLE statements")
+	fmt.Println("  pizzasql -db mydb -i backup.sql           Import SQL file")
+	fmt.Println("  pizzasql -db mydb -i backup.sql -ignore-errors  Continue on errors")
+	fmt.Println()
+	fmt.Println("CSV Format:")
+	fmt.Println("  pizzasql -db mydb -table users -o users.csv         Export table to CSV")
+	fmt.Println("  pizzasql -db mydb -table users -i users.csv         Import CSV to table")
+	fmt.Println("  pizzasql -db mydb -table new -i data.csv -create-table  Create table from CSV")
 }
 
 func listTables(schema *storage.SchemaManager) {
@@ -475,20 +512,210 @@ func listTables(schema *storage.SchemaManager) {
 		fmt.Printf("  %s\n", t)
 	}
 }
-func runHTTPServer() {
+func runExport() {
+	// 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)
+		os.Exit(1)
+	}
+	defer pool.Close()
+
+	schema := storage.NewSchemaManager(pool, *database)
+	table := storage.NewTableManager(pool, schema, *database)
+
+	// Determine format from flag or file extension
+	format := strings.ToLower(*exportFormat)
+	if format == "" {
+		format = detectFileFormat(*exportFile)
+	}
+
+	switch format {
+	case "csv":
+		// CSV export requires a table name
+		if *exportTable == "" {
+			fmt.Fprintf(os.Stderr, "CSV export requires -table flag\n")
+			os.Exit(1)
+		}
+
+		csvOpts := csvexport.DefaultExportOptions()
+		csvOpts.Table = *exportTable
+
+		data, err := csvexport.ExportTableToBytes(schema, table, csvOpts)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "Export failed: %v\n", err)
+			os.Exit(1)
+		}
+
+		err = os.WriteFile(*exportFile, data, 0644)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to write file: %v\n", err)
+			os.Exit(1)
+		}
+
+		fmt.Printf("Exported table '%s' to %s (CSV)\n", *exportTable, *exportFile)
+
+	default: // sql, sqlite
+		// Configure export options
+		opts := sqlexport.ExportOptions{
+			IncludeData: true,
+			DropTables:  *exportDrop,
+		}
+
+		if *exportTable != "" {
+			opts.Tables = []string{*exportTable}
+		}
+
+		// Export database
+		sql, err := sqlexport.ExportDatabase(schema, table, opts)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "Export failed: %v\n", err)
+			os.Exit(1)
+		}
+
+		// Write to file
+		err = os.WriteFile(*exportFile, []byte(sql), 0644)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to write file: %v\n", err)
+			os.Exit(1)
+		}
+
+		fmt.Printf("Exported database '%s' to %s\n", *database, *exportFile)
+	}
+}
+
+func runImport() {
 	// 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 schema and executor
 	schema := storage.NewSchemaManager(pool, *database)
 	table := storage.NewTableManager(pool, schema, *database)
 	exec := executor.New(schema, table)
+	exec.SyncCatalog()
+
+	// Read file
+	data, err := os.ReadFile(*importFile)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Failed to read file: %v\n", err)
+		os.Exit(1)
+	}
+
+	// Determine format from flag or file extension
+	format := strings.ToLower(*exportFormat)
+	if format == "" {
+		format = detectFileFormat(*importFile)
+	}
+
+	switch format {
+	case "csv":
+		// CSV import requires a table name
+		if *exportTable == "" {
+			fmt.Fprintf(os.Stderr, "CSV import requires -table flag\n")
+			os.Exit(1)
+		}
+
+		csvOpts := csvimport.DefaultImportOptions()
+		csvOpts.TableName = *exportTable
+		csvOpts.IgnoreErrors = *ignoreErrors
+		csvOpts.CreateTable = *createTable
+
+		result, err := csvimport.ImportCSV(strings.NewReader(string(data)), schema, table, csvOpts)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "Import failed: %v\n", err)
+			if len(result.Errors) > 0 {
+				fmt.Fprintf(os.Stderr, "Errors:\n")
+				for _, e := range result.Errors {
+					fmt.Fprintf(os.Stderr, "  - %s\n", e)
+				}
+			}
+			os.Exit(1)
+		}
+
+		fmt.Printf("CSV import completed successfully\n")
+		fmt.Printf("  Rows imported: %d\n", result.RowsImported)
+		if result.RowsSkipped > 0 {
+			fmt.Printf("  Rows skipped: %d\n", result.RowsSkipped)
+		}
+		if result.TableCreated {
+			fmt.Printf("  Table created: %s\n", *exportTable)
+		}
+		if len(result.Errors) > 0 {
+			fmt.Printf("  Warnings/Errors: %d\n", len(result.Errors))
+			for _, e := range result.Errors {
+				fmt.Printf("    - %s\n", e)
+			}
+		}
+
+	default: // sql, sqlite
+		// Configure import options
+		opts := sqlimport.ImportOptions{
+			IgnoreErrors: *ignoreErrors,
+		}
+
+		// Import SQL
+		result, err := sqlimport.ImportSQL(exec, string(data), opts)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "Import failed: %v\n", err)
+			if len(result.Errors) > 0 {
+				fmt.Fprintf(os.Stderr, "Errors:\n")
+				for _, e := range result.Errors {
+					fmt.Fprintf(os.Stderr, "  - %s\n", e)
+				}
+			}
+			os.Exit(1)
+		}
+
+		fmt.Printf("Import completed successfully\n")
+		fmt.Printf("  Statements executed: %d\n", result.StatementsExecuted)
+		if len(result.TablesCreated) > 0 {
+			fmt.Printf("  Tables created: %s\n", strings.Join(result.TablesCreated, ", "))
+		}
+		if len(result.TablesDropped) > 0 {
+			fmt.Printf("  Tables dropped: %s\n", strings.Join(result.TablesDropped, ", "))
+		}
+		fmt.Printf("  Rows inserted: %d\n", result.RowsInserted)
+
+		if len(result.Errors) > 0 {
+			fmt.Printf("  Warnings/Errors: %d\n", len(result.Errors))
+			for _, e := range result.Errors {
+				fmt.Printf("    - %s\n", e)
+			}
+		}
+	}
+}
+
+func detectFileFormat(filename string) string {
+	lower := strings.ToLower(filename)
+	if strings.HasSuffix(lower, ".csv") {
+		return "csv"
+	}
+	if strings.HasSuffix(lower, ".db") || strings.HasSuffix(lower, ".sqlite") || strings.HasSuffix(lower, ".sqlite3") {
+		return "sqlite"
+	}
+	return "sql"
+}
+
+func runHTTPServer() {
+	// 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, // Auto-create databases on first access
+	}
+	dbManager := storage.NewDatabaseManager(pool, dbManagerConfig)
 
 	// Configure HTTP server
 	config := httpserver.DefaultConfig()
@@ -501,8 +728,8 @@ func runHTTPServer() {
 		config.APIKeys = strings.Split(*apiKeys, ",")
 	}
 
-	// Create and start server
-	server := httpserver.New(config, exec, schema)
+	// Create and start server with multi-database support
+	server := httpserver.NewWithDatabaseManager(config, dbManager)
 
 	// Handle graceful shutdown
 	stop := make(chan os.Signal, 1)
@@ -517,9 +744,12 @@ func runHTTPServer() {
 	}()
 
 	fmt.Printf("PizzaSQL HTTP server started on http://%s:%d\n", *httpHost, *httpPort)
-	fmt.Printf("Database: %s\n", *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")
@@ -532,9 +762,13 @@ func runHTTPServer() {
 	fmt.Println("  POST   /transaction/commit   - Commit transaction")
 	fmt.Println("  POST   /transaction/rollback - Rollback transaction")
 	fmt.Println()
-	fmt.Println("Example:")
+	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.Println("Press Ctrl+C to stop")
 
 	<-stop

BIN
pizzasql


+ 150 - 0
pkg/csvexport/export.go

@@ -0,0 +1,150 @@
+package csvexport
+
+import (
+	"bytes"
+	"encoding/csv"
+	"encoding/hex"
+	"fmt"
+	"io"
+	"sort"
+
+	"github.com/danfragoso/pizzasql-next/pkg/storage"
+)
+
+// ExportOptions configures CSV export behavior
+type ExportOptions struct {
+	Table         string // Required: specific table to export
+	IncludeHeader bool   // Include column names as first row (default: true)
+	NullValue     string // String representation of NULL (default: "")
+	Delimiter     rune   // CSV delimiter (default: ',')
+}
+
+// DefaultExportOptions returns sensible defaults
+func DefaultExportOptions() ExportOptions {
+	return ExportOptions{
+		IncludeHeader: true,
+		NullValue:     "",
+		Delimiter:     ',',
+	}
+}
+
+// ExportTable exports a single table to CSV format
+func ExportTable(w io.Writer, schema *storage.SchemaManager, table *storage.TableManager, opts ExportOptions) error {
+	if opts.Table == "" {
+		return fmt.Errorf("table name is required for CSV export")
+	}
+
+	// Get table schema
+	tableSchema, err := schema.GetSchema(opts.Table)
+	if err != nil {
+		return fmt.Errorf("failed to get schema for table %s: %w", opts.Table, err)
+	}
+
+	// Get all rows
+	rows, err := table.Select(opts.Table, nil)
+	if err != nil {
+		return fmt.Errorf("failed to select rows from table %s: %w", opts.Table, err)
+	}
+
+	// Create CSV writer
+	csvWriter := csv.NewWriter(w)
+	if opts.Delimiter != 0 {
+		csvWriter.Comma = opts.Delimiter
+	}
+	defer csvWriter.Flush()
+
+	// Get column names (excluding internal _rowid_)
+	var columns []string
+	for _, col := range tableSchema.Columns {
+		if col.Name != "_rowid_" {
+			columns = append(columns, col.Name)
+		}
+	}
+
+	// Write header if requested
+	if opts.IncludeHeader {
+		if err := csvWriter.Write(columns); err != nil {
+			return fmt.Errorf("failed to write CSV header: %w", err)
+		}
+	}
+
+	// Write data rows
+	for _, row := range rows {
+		record := make([]string, len(columns))
+		for i, colName := range columns {
+			value := row[colName]
+			record[i] = formatValue(value, opts.NullValue)
+		}
+		if err := csvWriter.Write(record); err != nil {
+			return fmt.Errorf("failed to write CSV row: %w", err)
+		}
+	}
+
+	return csvWriter.Error()
+}
+
+// ExportTableToBytes exports a single table and returns bytes
+func ExportTableToBytes(schema *storage.SchemaManager, table *storage.TableManager, opts ExportOptions) ([]byte, error) {
+	var buf bytes.Buffer
+	if err := ExportTable(&buf, schema, table, opts); err != nil {
+		return nil, err
+	}
+	return buf.Bytes(), nil
+}
+
+// ExportMultipleTables exports multiple tables as a map of table name to CSV bytes
+func ExportMultipleTables(schema *storage.SchemaManager, table *storage.TableManager, tables []string, opts ExportOptions) (map[string][]byte, error) {
+	// If no tables specified, export all
+	if len(tables) == 0 {
+		var err error
+		tables, err = schema.ListTables()
+		if err != nil {
+			return nil, fmt.Errorf("failed to list tables: %w", err)
+		}
+		sort.Strings(tables)
+	}
+
+	result := make(map[string][]byte)
+	for _, tableName := range tables {
+		tableOpts := opts
+		tableOpts.Table = tableName
+		data, err := ExportTableToBytes(schema, table, tableOpts)
+		if err != nil {
+			return nil, fmt.Errorf("failed to export table %s: %w", tableName, err)
+		}
+		result[tableName] = data
+	}
+
+	return result, nil
+}
+
+// formatValue converts a value to its CSV string representation
+func formatValue(value interface{}, nullValue string) string {
+	if value == nil {
+		return nullValue
+	}
+
+	switch v := value.(type) {
+	case string:
+		return v
+	case float64:
+		// Check if it's actually an integer
+		if v == float64(int64(v)) {
+			return fmt.Sprintf("%d", int64(v))
+		}
+		return fmt.Sprintf("%g", v)
+	case int64:
+		return fmt.Sprintf("%d", v)
+	case int:
+		return fmt.Sprintf("%d", v)
+	case bool:
+		if v {
+			return "1"
+		}
+		return "0"
+	case []byte:
+		return "0x" + hex.EncodeToString(v)
+	default:
+		return fmt.Sprintf("%v", v)
+	}
+}

+ 284 - 0
pkg/csvimport/import.go

@@ -0,0 +1,284 @@
+package csvimport
+
+import (
+	"encoding/csv"
+	"encoding/hex"
+	"fmt"
+	"io"
+	"strconv"
+	"strings"
+
+	"github.com/danfragoso/pizzasql-next/pkg/storage"
+)
+
+// ImportOptions configures CSV import behavior
+type ImportOptions struct {
+	TableName    string            // Target table name (required)
+	HasHeader    bool              // First row is header (default: true)
+	CreateTable  bool              // Create table if not exists
+	IgnoreErrors bool              // Continue on row errors
+	NullValue    string            // String that represents NULL (default: "")
+	Delimiter    rune              // CSV delimiter (default: ',')
+	ColumnTypes  map[string]string // Explicit column types for table creation
+}
+
+// DefaultImportOptions returns sensible defaults
+func DefaultImportOptions() ImportOptions {
+	return ImportOptions{
+		HasHeader:   true,
+		NullValue:   "",
+		Delimiter:   ',',
+		ColumnTypes: make(map[string]string),
+	}
+}
+
+// ImportResult contains import statistics
+type ImportResult struct {
+	RowsImported int64    `json:"rowsImported"`
+	RowsSkipped  int64    `json:"rowsSkipped"`
+	TableCreated bool     `json:"tableCreated"`
+	Errors       []string `json:"errors,omitempty"`
+}
+
+// ImportCSV imports CSV data into a table
+func ImportCSV(r io.Reader, schema *storage.SchemaManager, table *storage.TableManager, opts ImportOptions) (*ImportResult, error) {
+	if opts.TableName == "" {
+		return nil, fmt.Errorf("table name is required for CSV import")
+	}
+
+	result := &ImportResult{}
+
+	// Create CSV reader
+	csvReader := csv.NewReader(r)
+	if opts.Delimiter != 0 {
+		csvReader.Comma = opts.Delimiter
+	}
+	csvReader.FieldsPerRecord = -1 // Allow variable field count
+
+	// Read all records
+	records, err := csvReader.ReadAll()
+	if err != nil {
+		return result, fmt.Errorf("failed to read CSV: %w", err)
+	}
+
+	if len(records) == 0 {
+		return result, nil
+	}
+
+	// Determine column names
+	var columnNames []string
+	startRow := 0
+
+	if opts.HasHeader {
+		columnNames = records[0]
+		startRow = 1
+	} else {
+		// Generate column names if no header
+		for i := range records[0] {
+			columnNames = append(columnNames, fmt.Sprintf("column%d", i+1))
+		}
+	}
+
+	// Check if table exists
+	tableSchema, err := schema.GetSchema(opts.TableName)
+	tableExists := err == nil
+
+	if !tableExists {
+		if !opts.CreateTable {
+			return result, fmt.Errorf("table %s does not exist (use CreateTable option to auto-create)", opts.TableName)
+		}
+
+		// Create table with inferred schema
+		tableSchema = inferSchema(opts.TableName, columnNames, records[startRow:], opts.ColumnTypes)
+		if err := schema.CreateTable(tableSchema); err != nil {
+			return result, fmt.Errorf("failed to create table %s: %w", opts.TableName, err)
+		}
+		result.TableCreated = true
+	}
+
+	// Build column type map for parsing
+	columnTypeMap := make(map[string]string)
+	for _, col := range tableSchema.Columns {
+		columnTypeMap[strings.ToLower(col.Name)] = strings.ToUpper(col.Type)
+	}
+
+	// Import rows
+	for i := startRow; i < len(records); i++ {
+		record := records[i]
+		row := make(storage.Row)
+
+		for j, colName := range columnNames {
+			if j >= len(record) {
+				break
+			}
+
+			value := record[j]
+			colType := columnTypeMap[strings.ToLower(colName)]
+
+			// Handle NULL values
+			if value == opts.NullValue {
+				row[colName] = nil
+				continue
+			}
+
+			// Parse value based on column type
+			parsedValue, err := parseValue(value, colType)
+			if err != nil {
+				if opts.IgnoreErrors {
+					result.Errors = append(result.Errors, fmt.Sprintf("row %d, column %s: %v", i+1, colName, err))
+					row[colName] = value // Store as string
+				} else {
+					return result, fmt.Errorf("row %d, column %s: %w", i+1, colName, err)
+				}
+			} else {
+				row[colName] = parsedValue
+			}
+		}
+
+		// Insert row
+		if err := table.Insert(opts.TableName, row); err != nil {
+			if opts.IgnoreErrors {
+				result.Errors = append(result.Errors, fmt.Sprintf("row %d: %v", i+1, err))
+				result.RowsSkipped++
+			} else {
+				return result, fmt.Errorf("failed to insert row %d: %w", i+1, err)
+			}
+		} else {
+			result.RowsImported++
+		}
+	}
+
+	return result, nil
+}
+
+// inferSchema creates a schema based on column names and sample data
+func inferSchema(tableName string, columnNames []string, sampleData [][]string, explicitTypes map[string]string) *storage.Schema {
+	columns := make([]storage.Column, len(columnNames))
+
+	for i, name := range columnNames {
+		colType := "TEXT" // Default type
+
+		// Check for explicit type
+		if explicit, ok := explicitTypes[name]; ok {
+			colType = explicit
+		} else {
+			// Infer type from sample data
+			colType = inferColumnType(sampleData, i)
+		}
+
+		columns[i] = storage.Column{
+			Name:     name,
+			Type:     colType,
+			Nullable: true,
+		}
+	}
+
+	// Use first column as primary key if it looks like an ID
+	if len(columns) > 0 {
+		firstCol := strings.ToLower(columns[0].Name)
+		if firstCol == "id" || strings.HasSuffix(firstCol, "_id") || strings.HasSuffix(firstCol, "id") {
+			columns[0].PrimaryKey = true
+			columns[0].Nullable = false
+		}
+	}
+
+	return &storage.Schema{
+		Name:       tableName,
+		Columns:    columns,
+		PrimaryKey: columns[0].Name,
+	}
+}
+
+// inferColumnType infers the column type from sample data
+func inferColumnType(sampleData [][]string, colIndex int) string {
+	if len(sampleData) == 0 {
+		return "TEXT"
+	}
+
+	allInts := true
+	allFloats := true
+	hasData := false
+
+	for _, row := range sampleData {
+		if colIndex >= len(row) {
+			continue
+		}
+
+		value := strings.TrimSpace(row[colIndex])
+		if value == "" {
+			continue // Skip empty values for type inference
+		}
+
+		hasData = true
+
+		// Try parsing as integer
+		if _, err := strconv.ParseInt(value, 10, 64); err != nil {
+			allInts = false
+		}
+
+		// Try parsing as float
+		if _, err := strconv.ParseFloat(value, 64); err != nil {
+			allFloats = false
+		}
+	}
+
+	if !hasData {
+		return "TEXT"
+	}
+
+	if allInts {
+		return "INTEGER"
+	}
+	if allFloats {
+		return "REAL"
+	}
+	return "TEXT"
+}
+
+// parseValue parses a string value based on the column type
+func parseValue(value string, colType string) (interface{}, error) {
+	colType = strings.ToUpper(colType)
+
+	switch {
+	case strings.Contains(colType, "INT"):
+		// Handle INTEGER, INT, BIGINT, SMALLINT
+		i, err := strconv.ParseInt(value, 10, 64)
+		if err != nil {
+			return nil, fmt.Errorf("invalid integer value: %s", value)
+		}
+		return i, nil
+
+	case strings.Contains(colType, "REAL") || strings.Contains(colType, "FLOAT") || strings.Contains(colType, "DOUBLE"):
+		f, err := strconv.ParseFloat(value, 64)
+		if err != nil {
+			return nil, fmt.Errorf("invalid float value: %s", value)
+		}
+		return f, nil
+
+	case strings.Contains(colType, "BLOB"):
+		// Handle hex-encoded blob (0xABCD or just ABCD)
+		hexStr := value
+		if strings.HasPrefix(hexStr, "0x") || strings.HasPrefix(hexStr, "0X") {
+			hexStr = hexStr[2:]
+		}
+		data, err := hex.DecodeString(hexStr)
+		if err != nil {
+			return nil, fmt.Errorf("invalid hex blob value: %s", value)
+		}
+		return data, nil
+
+	case strings.Contains(colType, "BOOL"):
+		lower := strings.ToLower(value)
+		if lower == "1" || lower == "true" || lower == "yes" {
+			return true, nil
+		}
+		if lower == "0" || lower == "false" || lower == "no" {
+			return false, nil
+		}
+		return nil, fmt.Errorf("invalid boolean value: %s", value)
+
+	default:
+		// TEXT, VARCHAR, CHAR, etc.
+		return value, nil
+	}
+}

+ 29 - 6
pkg/executor/executor.go

@@ -84,6 +84,8 @@ func (e *Executor) SyncCatalog() error {
 		if err != nil {
 			continue
 		}
+		// Drop table from catalog if it exists, then recreate with updated schema
+		e.catalog.DropTable(tableName)
 		e.catalog.CreateTable(schema.ToAnalyzerTableInfo())
 	}
 
@@ -757,15 +759,27 @@ func (e *Executor) executeJoin(tableRef parser.TableRef, leftRows []storage.Row)
 func (e *Executor) mergeRows(left, right storage.Row, leftAlias, rightAlias string) storage.Row {
 	result := make(storage.Row)
 	for k, v := range left {
+		// Copy the key as-is (it might already be qualified)
 		result[k] = v
-		if leftAlias != "" {
+		// Only add qualified name if the key is NOT already qualified and we have an alias
+		if leftAlias != "" && !strings.Contains(k, ".") {
 			result[leftAlias+"."+k] = v
 		}
 	}
 	for k, v := range right {
-		result[k] = v
-		if rightAlias != "" {
-			result[rightAlias+"."+k] = v
+		// For unqualified names, only add if they don't already exist
+		// This prevents right table columns from overwriting left table columns
+		if !strings.Contains(k, ".") {
+			if _, exists := result[k]; !exists {
+				result[k] = v
+			}
+			// Add qualified name for right table
+			if rightAlias != "" {
+				result[rightAlias+"."+k] = v
+			}
+		} else {
+			// Already qualified, just copy it
+			result[k] = v
 		}
 	}
 	return result
@@ -1741,8 +1755,17 @@ func (e *Executor) evalExpr(expr parser.Expr, row storage.Row) (interface{}, err
 func (e *Executor) evalLiteral(lit *parser.LiteralExpr) (interface{}, error) {
 	switch lit.Type {
 	case lexer.TokenNumber:
-		if strings.Contains(lit.Value, ".") {
-			return strconv.ParseFloat(lit.Value, 64)
+		// Check for scientific notation (e.g., 1e+06) or decimal point
+		if strings.Contains(lit.Value, ".") || strings.ContainsAny(lit.Value, "eE") {
+			f, err := strconv.ParseFloat(lit.Value, 64)
+			if err != nil {
+				return nil, err
+			}
+			// If it's a whole number (no fractional part), return as int64
+			if f == float64(int64(f)) {
+				return int64(f), nil
+			}
+			return f, nil
 		}
 		return strconv.ParseInt(lit.Value, 10, 64)
 	case lexer.TokenString:

+ 82 - 0
pkg/executor/executor_join_test.go

@@ -0,0 +1,82 @@
+package executor
+
+import (
+	"testing"
+
+	"github.com/danfragoso/pizzasql-next/pkg/storage"
+)
+
+func TestMergeRowsDoesNotOverwriteColumns(t *testing.T) {
+	e := &Executor{}
+
+	// Create two rows with overlapping column names (like "id")
+	left := storage.Row{
+		"id":   "left-id-123",
+		"name": "LeftName",
+	}
+
+	right := storage.Row{
+		"id":    "right-id-456",
+		"value": "RightValue",
+	}
+
+	// Merge with aliases
+	merged := e.mergeRows(left, right, "o", "om")
+
+	// Check that both qualified names exist and are correct
+	if merged["o.id"] != "left-id-123" {
+		t.Errorf("Expected o.id = 'left-id-123', got %v", merged["o.id"])
+	}
+
+	if merged["om.id"] != "right-id-456" {
+		t.Errorf("Expected om.id = 'right-id-456', got %v", merged["om.id"])
+	}
+
+	// Check that the unqualified "id" is from the left table (first one wins)
+	if merged["id"] != "left-id-123" {
+		t.Errorf("Expected unqualified id = 'left-id-123' (from left table), got %v", merged["id"])
+	}
+
+	// Check other columns are present
+	if merged["o.name"] != "LeftName" {
+		t.Errorf("Expected o.name = 'LeftName', got %v", merged["o.name"])
+	}
+
+	if merged["om.value"] != "RightValue" {
+		t.Errorf("Expected om.value = 'RightValue', got %v", merged["om.value"])
+	}
+}
+
+func TestJoinConditionWithQualifiedNames(t *testing.T) {
+	e := &Executor{}
+
+	// Simulate two rows from different tables with the same column name "id"
+	orgRow := storage.Row{
+		"id":   "org-123",
+		"name": "Organization 1",
+	}
+
+	memberRow := storage.Row{
+		"id":     "member-456",
+		"org_id": "org-123", // This should match orgRow's id
+	}
+
+	// Merge with table aliases
+	merged := e.mergeRows(orgRow, memberRow, "o", "om")
+
+	// Verify that o.id and om.org_id have the correct values for comparison
+	// This is what the JOIN condition would use: o.id = om.org_id
+	if merged["o.id"] != "org-123" {
+		t.Errorf("Expected o.id = 'org-123', got %v", merged["o.id"])
+	}
+
+	if merged["om.org_id"] != "org-123" {
+		t.Errorf("Expected om.org_id = 'org-123', got %v", merged["om.org_id"])
+	}
+
+	// The key fix: om.org_id should NOT have been overwritten by the right table's "id"
+	// In the old buggy code, this would have been "member-456" instead of "org-123"
+	if merged["om.org_id"] == merged["om.id"] {
+		t.Log("✓ JOIN condition can correctly compare o.id with om.org_id")
+	}
+}

+ 375 - 19
pkg/httpserver/handler.go

@@ -3,13 +3,19 @@ package httpserver
 import (
 	"encoding/json"
 	"fmt"
+	"io"
+	"log"
 	"net/http"
 	"strings"
 	"sync/atomic"
 	"time"
 
+	"github.com/danfragoso/pizzasql-next/pkg/csvexport"
+	"github.com/danfragoso/pizzasql-next/pkg/csvimport"
 	"github.com/danfragoso/pizzasql-next/pkg/lexer"
 	"github.com/danfragoso/pizzasql-next/pkg/parser"
+	"github.com/danfragoso/pizzasql-next/pkg/sqlexport"
+	"github.com/danfragoso/pizzasql-next/pkg/sqlimport"
 )
 
 // QueryRequest represents a single query request.
@@ -47,6 +53,14 @@ func (s *Server) handleQuery(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	// Get database from X-Database header
+	dbName := r.Header.Get("X-Database")
+	exec, _, err := s.getExecutorForDatabase(dbName)
+	if err != nil {
+		writeError(w, http.StatusBadRequest, "DATABASE_ERROR", fmt.Sprintf("database not found: %s", dbName), nil)
+		return
+	}
+
 	// Check for pretty print
 	pretty := r.URL.Query().Get("pretty") == "true"
 	explain := r.URL.Query().Get("explain") == "true"
@@ -101,8 +115,8 @@ func (s *Server) handleQuery(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 
-		// Execute
-		result, err := s.executor.Execute(stmt)
+		// Execute using the database-specific executor
+		result, err := exec.Execute(stmt)
 		if err != nil {
 			errorChan <- &HTTPError{
 				Code:    "EXECUTION_ERROR",
@@ -116,11 +130,12 @@ func (s *Server) handleQuery(w http.ResponseWriter, r *http.Request) {
 
 		// Build response
 		resp := &QueryResponse{
-			Columns:       make([]ColumnInfo, len(result.Columns)),
-			Rows:          result.Rows,
-			RowsAffected:  result.RowsAffected,
-			LastInsertID:  result.LastInsertID,
-			ExecutionTime: duration.String(),
+			Columns:            make([]ColumnInfo, len(result.Columns)),
+			Rows:               result.Rows,
+			RowsAffected:       result.RowsAffected,
+			LastInsertID:       result.LastInsertID,
+			ExecutionTimeMicro: duration.Microseconds(),
+			RowsReturned:       len(result.Rows),
 		}
 
 		for i, col := range result.Columns {
@@ -135,6 +150,12 @@ func (s *Server) handleQuery(w http.ResponseWriter, r *http.Request) {
 			}
 		}
 
+		// Calculate bytes read (approximate size of the result set)
+		// This is the serialized JSON size of the rows data
+		if jsonBytes, err := json.Marshal(result.Rows); err == nil {
+			resp.BytesRead = int64(len(jsonBytes))
+		}
+
 		if explain {
 			resp.QueryPlan = []string{"Full table scan"} // TODO: Real query plan
 		}
@@ -180,6 +201,14 @@ func (s *Server) handleExecute(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	// Get database from X-Database header
+	dbName := r.Header.Get("X-Database")
+	exec, _, err := s.getExecutorForDatabase(dbName)
+	if err != nil {
+		writeError(w, http.StatusBadRequest, "DATABASE_ERROR", fmt.Sprintf("database not found: %s", dbName), nil)
+		return
+	}
+
 	pretty := r.URL.Query().Get("pretty") == "true"
 	start := time.Now()
 
@@ -190,7 +219,7 @@ func (s *Server) handleExecute(w http.ResponseWriter, r *http.Request) {
 		l := lexer.New("BEGIN")
 		p := parser.New(l)
 		stmt, _ := p.Parse()
-		s.executor.Execute(stmt)
+		exec.Execute(stmt)
 	}
 
 	var executeErr error
@@ -206,7 +235,7 @@ func (s *Server) handleExecute(w http.ResponseWriter, r *http.Request) {
 			break
 		}
 
-		result, err := s.executor.Execute(parsed)
+		result, err := exec.Execute(parsed)
 		if err != nil {
 			executeErr = err
 			break
@@ -225,7 +254,7 @@ func (s *Server) handleExecute(w http.ResponseWriter, r *http.Request) {
 			l := lexer.New("ROLLBACK")
 			p := parser.New(l)
 			stmt, _ := p.Parse()
-			s.executor.Execute(stmt)
+			exec.Execute(stmt)
 
 			writeError(w, http.StatusBadRequest, "TRANSACTION_ERROR", executeErr.Error(), nil)
 			return
@@ -234,7 +263,7 @@ func (s *Server) handleExecute(w http.ResponseWriter, r *http.Request) {
 			l := lexer.New("COMMIT")
 			p := parser.New(l)
 			stmt, _ := p.Parse()
-			s.executor.Execute(stmt)
+			exec.Execute(stmt)
 		}
 	} else if executeErr != nil {
 		writeError(w, http.StatusBadRequest, "EXECUTION_ERROR", executeErr.Error(), nil)
@@ -256,14 +285,35 @@ func (s *Server) handleSchemaTables(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	tables, err := s.schema.ListTables()
+	// Get database from X-Database header (trim whitespace)
+	dbName := strings.TrimSpace(r.Header.Get("X-Database"))
+
+	// Debug: Log the header value
+	log.Printf("[DEBUG] /schema/tables - X-Database header: %q", dbName)
+
+	_, schema, err := s.getExecutorForDatabase(dbName)
+	if err != nil {
+		writeError(w, http.StatusBadRequest, "DATABASE_ERROR", fmt.Sprintf("database not found: %s", dbName), nil)
+		return
+	}
+
+	// Debug: Log the actual database being used
+	actualDB := schema.GetDatabaseName()
+	log.Printf("[DEBUG] /schema/tables - Resolved to database: %q", actualDB)
+
+	tables, err := schema.ListTables()
 	if err != nil {
 		writeError(w, http.StatusInternalServerError, "SCHEMA_ERROR", err.Error(), nil)
 		return
 	}
 
+	log.Printf("[DEBUG] /schema/tables - Found %d tables in database %q", len(tables), actualDB)
+
+	// Include the actual database name and requested name in the response for verification
 	resp := map[string]interface{}{
-		"tables": tables,
+		"database":           actualDB,
+		"requested_database": dbName,
+		"tables":             tables,
 	}
 
 	pretty := r.URL.Query().Get("pretty") == "true"
@@ -277,6 +327,14 @@ func (s *Server) handleSchemaTable(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	// Get database from X-Database header
+	dbName := r.Header.Get("X-Database")
+	_, schemaManager, err := s.getExecutorForDatabase(dbName)
+	if err != nil {
+		writeError(w, http.StatusBadRequest, "DATABASE_ERROR", fmt.Sprintf("database not found: %s", dbName), nil)
+		return
+	}
+
 	// Extract table name from path
 	path := strings.TrimPrefix(r.URL.Path, "/schema/tables/")
 	tableName := strings.TrimSpace(path)
@@ -286,7 +344,7 @@ func (s *Server) handleSchemaTable(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	schema, err := s.schema.GetSchema(tableName)
+	schema, err := schemaManager.GetSchema(tableName)
 	if err != nil {
 		writeError(w, http.StatusNotFound, "TABLE_NOT_FOUND", fmt.Sprintf("Table '%s' not found", tableName), nil)
 		return
@@ -326,7 +384,14 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
 
 // handleStats handles GET /stats
 func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
-	tables, _ := s.schema.ListTables()
+	// Get database from X-Database header
+	dbName := r.Header.Get("X-Database")
+	_, schema, _ := s.getExecutorForDatabase(dbName)
+
+	var tables []string
+	if schema != nil {
+		tables, _ = schema.ListTables()
+	}
 
 	var avgQueryTime string
 	if s.stats.QueriesExecuted > 0 {
@@ -355,10 +420,18 @@ func (s *Server) handleTransactionBegin(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
+	// Get database from X-Database header
+	dbName := r.Header.Get("X-Database")
+	exec, _, err := s.getExecutorForDatabase(dbName)
+	if err != nil {
+		writeError(w, http.StatusBadRequest, "DATABASE_ERROR", fmt.Sprintf("database not found: %s", dbName), nil)
+		return
+	}
+
 	l := lexer.New("BEGIN")
 	p := parser.New(l)
 	stmt, _ := p.Parse()
-	_, err := s.executor.Execute(stmt)
+	_, err = exec.Execute(stmt)
 
 	if err != nil {
 		writeError(w, http.StatusInternalServerError, "TRANSACTION_ERROR", err.Error(), nil)
@@ -383,6 +456,14 @@ func (s *Server) handleTransactionCommit(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
+	// Get database from X-Database header
+	dbName := r.Header.Get("X-Database")
+	exec, _, err := s.getExecutorForDatabase(dbName)
+	if err != nil {
+		writeError(w, http.StatusBadRequest, "DATABASE_ERROR", fmt.Sprintf("database not found: %s", dbName), nil)
+		return
+	}
+
 	var req TransactionRequest
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		// Allow commit without transaction ID for simplicity
@@ -391,7 +472,7 @@ func (s *Server) handleTransactionCommit(w http.ResponseWriter, r *http.Request)
 	l := lexer.New("COMMIT")
 	p := parser.New(l)
 	stmt, _ := p.Parse()
-	_, err := s.executor.Execute(stmt)
+	_, err = exec.Execute(stmt)
 
 	if err != nil {
 		writeError(w, http.StatusInternalServerError, "TRANSACTION_ERROR", err.Error(), nil)
@@ -477,6 +558,14 @@ func (s *Server) handleTransactionRollback(w http.ResponseWriter, r *http.Reques
 		return
 	}
 
+	// Get database from X-Database header
+	dbName := r.Header.Get("X-Database")
+	exec, _, err := s.getExecutorForDatabase(dbName)
+	if err != nil {
+		writeError(w, http.StatusBadRequest, "DATABASE_ERROR", fmt.Sprintf("database not found: %s", dbName), nil)
+		return
+	}
+
 	var req TransactionRequest
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		// Allow rollback without transaction ID for simplicity
@@ -485,7 +574,7 @@ func (s *Server) handleTransactionRollback(w http.ResponseWriter, r *http.Reques
 	l := lexer.New("ROLLBACK")
 	p := parser.New(l)
 	stmt, _ := p.Parse()
-	_, err := s.executor.Execute(stmt)
+	_, err = exec.Execute(stmt)
 
 	if err != nil {
 		writeError(w, http.StatusInternalServerError, "TRANSACTION_ERROR", err.Error(), nil)
@@ -507,7 +596,15 @@ func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	tables, _ := s.schema.ListTables()
+	// Get database from X-Database header
+	dbName := r.Header.Get("X-Database")
+	_, schema, _ := s.getExecutorForDatabase(dbName)
+
+	var tables []string
+	if schema != nil {
+		tables, _ = schema.ListTables()
+	}
+
 	uptime := time.Since(s.stats.StartTime).Seconds()
 
 	queriesTotal := atomic.LoadInt64(&s.stats.QueriesExecuted)
@@ -542,3 +639,262 @@ func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request) {
 	fmt.Fprintf(w, "# TYPE pizzasql_info gauge\n")
 	fmt.Fprintf(w, "pizzasql_info{version=\"0.1.0\"} 1\n")
 }
+
+// handleExport handles GET /export
+func (s *Server) handleExport(w http.ResponseWriter, r *http.Request) {
+	if r.Method != http.MethodGet {
+		writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Only GET method is allowed", nil)
+		return
+	}
+
+	// Get database from X-Database header
+	dbName := strings.TrimSpace(r.Header.Get("X-Database"))
+	_, schema, err := s.getExecutorForDatabase(dbName)
+	if err != nil {
+		writeError(w, http.StatusBadRequest, "DATABASE_ERROR", fmt.Sprintf("database not found: %s", dbName), nil)
+		return
+	}
+
+	// Get the table manager from the database instance
+	dbInstance, err := s.dbManager.GetDatabase(dbName)
+	if err != nil {
+		writeError(w, http.StatusBadRequest, "DATABASE_ERROR", fmt.Sprintf("database not found: %s", dbName), nil)
+		return
+	}
+
+	// Get format parameter (default: sql)
+	format := strings.ToLower(r.URL.Query().Get("format"))
+	if format == "" {
+		format = "sql"
+	}
+
+	tableName := r.URL.Query().Get("table")
+
+	switch format {
+	case "csv":
+		// CSV export requires a single table
+		if tableName == "" {
+			writeError(w, http.StatusBadRequest, "TABLE_REQUIRED", "CSV export requires 'table' parameter", nil)
+			return
+		}
+
+		csvOpts := csvexport.DefaultExportOptions()
+		csvOpts.Table = tableName
+
+		data, err := csvexport.ExportTableToBytes(schema, dbInstance.Table, csvOpts)
+		if err != nil {
+			writeError(w, http.StatusInternalServerError, "EXPORT_ERROR", err.Error(), nil)
+			return
+		}
+
+		w.Header().Set("Content-Type", "text/csv")
+		w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.csv\"", tableName))
+		w.WriteHeader(http.StatusOK)
+		w.Write(data)
+
+	case "sql", "sqlite":
+		// SQL export (also used for SQLite-compatible export)
+		opts := sqlexport.DefaultExportOptions()
+
+		// Specific table(s) to export
+		if tableName != "" {
+			opts.Tables = strings.Split(tableName, ",")
+		}
+
+		// Include data (default: true)
+		if r.URL.Query().Get("schema_only") == "true" {
+			opts.IncludeData = false
+		}
+
+		// Include DROP TABLE statements
+		if r.URL.Query().Get("drop") == "true" {
+			opts.DropTables = true
+		}
+
+		// Generate SQL export
+		sql, err := sqlexport.ExportDatabase(schema, dbInstance.Table, opts)
+		if err != nil {
+			writeError(w, http.StatusInternalServerError, "EXPORT_ERROR", err.Error(), nil)
+			return
+		}
+
+		// Determine filename and content type
+		ext := "sql"
+		contentType := "application/sql"
+		if format == "sqlite" {
+			ext = "sql" // Still SQL text, but sqlite-compatible
+		}
+
+		filename := schema.GetDatabaseName() + "_export." + ext
+		if len(opts.Tables) == 1 {
+			filename = opts.Tables[0] + "_export." + ext
+		}
+
+		w.Header().Set("Content-Type", contentType)
+		w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(sql))
+
+	default:
+		writeError(w, http.StatusBadRequest, "INVALID_FORMAT",
+			fmt.Sprintf("Invalid format '%s'. Supported formats: sql, csv", format), nil)
+	}
+}
+
+// handleImport handles POST /import
+func (s *Server) handleImport(w http.ResponseWriter, r *http.Request) {
+	if r.Method != http.MethodPost {
+		writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Only POST method is allowed", nil)
+		return
+	}
+
+	// Get database from X-Database header
+	dbName := strings.TrimSpace(r.Header.Get("X-Database"))
+	exec, schema, err := s.getExecutorForDatabase(dbName)
+	if err != nil {
+		writeError(w, http.StatusBadRequest, "DATABASE_ERROR", fmt.Sprintf("database not found: %s", dbName), nil)
+		return
+	}
+
+	// Get the table manager from the database instance
+	dbInstance, err := s.dbManager.GetDatabase(dbName)
+	if err != nil {
+		writeError(w, http.StatusBadRequest, "DATABASE_ERROR", fmt.Sprintf("database not found: %s", dbName), nil)
+		return
+	}
+
+	// Get format parameter (default: sql, can be auto-detected)
+	format := strings.ToLower(r.URL.Query().Get("format"))
+	tableName := r.URL.Query().Get("table")
+	ignoreErrors := r.URL.Query().Get("ignore_errors") == "true"
+	createTable := r.URL.Query().Get("create_table") == "true"
+	pretty := r.URL.Query().Get("pretty") == "true"
+
+	// Check content type
+	contentType := r.Header.Get("Content-Type")
+
+	var fileContent []byte
+	var filename string
+
+	if strings.HasPrefix(contentType, "multipart/form-data") {
+		// Handle file upload
+		if err := r.ParseMultipartForm(32 << 20); err != nil { // 32MB max
+			writeError(w, http.StatusBadRequest, "INVALID_FORM", "Failed to parse multipart form: "+err.Error(), nil)
+			return
+		}
+
+		file, header, err := r.FormFile("file")
+		if err != nil {
+			writeError(w, http.StatusBadRequest, "MISSING_FILE", "No file uploaded. Use 'file' field name.", nil)
+			return
+		}
+		defer file.Close()
+		filename = header.Filename
+
+		// Read file content
+		fileContent, err = io.ReadAll(file)
+		if err != nil {
+			writeError(w, http.StatusBadRequest, "READ_ERROR", "Failed to read uploaded file: "+err.Error(), nil)
+			return
+		}
+
+	} else if strings.HasPrefix(contentType, "application/json") {
+		// Handle JSON body with SQL content
+		var req struct {
+			SQL string `json:"sql"`
+		}
+		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+			writeError(w, http.StatusBadRequest, "INVALID_JSON", "Invalid JSON in request body", nil)
+			return
+		}
+		fileContent = []byte(req.SQL)
+
+	} else if strings.HasPrefix(contentType, "text/plain") || strings.HasPrefix(contentType, "application/sql") || strings.HasPrefix(contentType, "text/csv") {
+		// Handle raw content in body
+		var err error
+		fileContent, err = io.ReadAll(r.Body)
+		if err != nil {
+			writeError(w, http.StatusBadRequest, "READ_ERROR", "Failed to read request body: "+err.Error(), nil)
+			return
+		}
+		// Auto-detect CSV from content type
+		if strings.HasPrefix(contentType, "text/csv") && format == "" {
+			format = "csv"
+		}
+
+	} else {
+		writeError(w, http.StatusBadRequest, "INVALID_CONTENT_TYPE",
+			"Content-Type must be multipart/form-data, application/json, text/plain, text/csv, or application/sql", nil)
+		return
+	}
+
+	if len(fileContent) == 0 {
+		writeError(w, http.StatusBadRequest, "EMPTY_CONTENT", "No content provided", nil)
+		return
+	}
+
+	// Auto-detect format from filename extension if not specified
+	if format == "" && filename != "" {
+		if strings.HasSuffix(strings.ToLower(filename), ".csv") {
+			format = "csv"
+		}
+	}
+	if format == "" {
+		format = "sql"
+	}
+
+	switch format {
+	case "csv":
+		// CSV import requires table name
+		if tableName == "" {
+			writeError(w, http.StatusBadRequest, "TABLE_REQUIRED", "CSV import requires 'table' parameter", nil)
+			return
+		}
+
+		csvOpts := csvimport.DefaultImportOptions()
+		csvOpts.TableName = tableName
+		csvOpts.IgnoreErrors = ignoreErrors
+		csvOpts.CreateTable = createTable
+
+		result, err := csvimport.ImportCSV(strings.NewReader(string(fileContent)), schema, dbInstance.Table, csvOpts)
+		if err != nil && !ignoreErrors {
+			writeError(w, http.StatusBadRequest, "IMPORT_ERROR", err.Error(), map[string]interface{}{
+				"rowsImported": result.RowsImported,
+				"rowsSkipped":  result.RowsSkipped,
+				"tableCreated": result.TableCreated,
+				"errors":       result.Errors,
+			})
+			return
+		}
+
+		// Sync catalog after import
+		exec.SyncCatalog()
+
+		writeJSON(w, http.StatusOK, result, pretty)
+
+	case "sql", "sqlite":
+		// SQL import
+		opts := sqlimport.DefaultImportOptions()
+		opts.IgnoreErrors = ignoreErrors
+
+		result, err := sqlimport.ImportSQL(exec, string(fileContent), opts)
+		if err != nil && !ignoreErrors {
+			writeError(w, http.StatusBadRequest, "IMPORT_ERROR", err.Error(), map[string]interface{}{
+				"statementsExecuted": result.StatementsExecuted,
+				"tablesCreated":      result.TablesCreated,
+				"rowsInserted":       result.RowsInserted,
+				"errors":             result.Errors,
+			})
+			return
+		}
+
+		// Sync catalog after import
+		exec.SyncCatalog()
+
+		writeJSON(w, http.StatusOK, result, pretty)
+
+	default:
+		writeError(w, http.StatusBadRequest, "INVALID_FORMAT",
+			fmt.Sprintf("Invalid format '%s'. Supported formats: sql, csv", format), nil)
+	}
+}

+ 1 - 1
pkg/httpserver/middleware.go

@@ -85,7 +85,7 @@ func (s *Server) corsMiddleware(next http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		w.Header().Set("Access-Control-Allow-Origin", "*")
 		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
-		w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
+		w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Database")
 
 		// Handle preflight
 		if r.Method == http.MethodOptions {

+ 16 - 6
pkg/httpserver/response.go

@@ -2,6 +2,7 @@ package httpserver
 
 import (
 	"encoding/json"
+	"log"
 	"net/http"
 )
 
@@ -13,12 +14,15 @@ type ColumnInfo struct {
 
 // QueryResponse represents a query response.
 type QueryResponse struct {
-	Columns       []ColumnInfo    `json:"columns"`
-	Rows          [][]interface{} `json:"rows"`
-	RowsAffected  int64           `json:"rowsAffected"`
-	LastInsertID  int64           `json:"lastInsertId"`
-	ExecutionTime string          `json:"executionTime"`
-	QueryPlan     []string        `json:"queryPlan,omitempty"`
+	Columns      []ColumnInfo    `json:"columns"`
+	Rows         [][]interface{} `json:"rows"`
+	RowsAffected int64           `json:"rowsAffected"`
+	LastInsertID int64           `json:"lastInsertId"`
+	QueryPlan    []string        `json:"queryPlan,omitempty"`
+	// Metrics for usage tracking and billing
+	BytesRead          int64 `json:"bytesRead"`          // Total bytes in the result set
+	RowsReturned       int   `json:"rowsReturned"`       // Number of rows returned
+	ExecutionTimeMicro int64 `json:"executionTimeMicro"` // Execution time in microseconds
 }
 
 // ExecuteResult represents a single execution result.
@@ -78,5 +82,11 @@ func writeError(w http.ResponseWriter, status int, code, message string, details
 			Details: details,
 		},
 	}
+	
+	// Log server errors (5xx status codes)
+	if status >= 500 {
+		log.Printf("ERROR [%d] %s: %s", status, code, message)
+	}
+	
 	writeJSON(w, status, resp, false)
 }

+ 112 - 14
pkg/httpserver/server.go

@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"log"
 	"net/http"
+	"sync"
 	"time"
 
 	"github.com/danfragoso/pizzasql-next/pkg/executor"
@@ -43,11 +44,16 @@ func DefaultConfig() *Config {
 
 // Server represents the HTTP API server.
 type Server struct {
-	config   *Config
-	executor *executor.Executor
-	schema   *storage.SchemaManager
-	server   *http.Server
-	stats    *Stats
+	config    *Config
+	executor  *executor.Executor  // Default executor (for backward compatibility)
+	schema    *storage.SchemaManager
+	dbManager *storage.DatabaseManager // Multi-database support
+	server    *http.Server
+	stats     *Stats
+
+	// Per-server executor cache for multi-database support
+	executorCache   map[string]*executor.Executor
+	executorCacheMu sync.RWMutex
 }
 
 // Stats tracks server statistics.
@@ -59,34 +65,76 @@ type Stats struct {
 }
 
 // New creates a new HTTP server.
+// Deprecated: Use NewWithDatabaseManager for multi-database support.
 func New(config *Config, exec *executor.Executor, schema *storage.SchemaManager) *Server {
 	if config == nil {
 		config = DefaultConfig()
 	}
 
 	s := &Server{
-		config:   config,
-		executor: exec,
-		schema:   schema,
+		config:        config,
+		executor:      exec,
+		schema:        schema,
+		executorCache: make(map[string]*executor.Executor),
 		stats: &Stats{
 			StartTime: time.Now(),
 		},
 	}
 
+	return s.init()
+}
+
+// NewWithDatabaseManager creates a new HTTP server with multi-database support.
+func NewWithDatabaseManager(config *Config, dbManager *storage.DatabaseManager) *Server {
+	if config == nil {
+		config = DefaultConfig()
+	}
+
+	// Initialize executor cache
+	execCache := make(map[string]*executor.Executor)
+
+	// Get the default database for backward compatibility
+	defaultDB, _ := dbManager.GetDatabase("")
+	var defaultExec *executor.Executor
+	var defaultSchema *storage.SchemaManager
+	if defaultDB != nil {
+		defaultExec = executor.New(defaultDB.Schema, defaultDB.Table)
+		defaultExec.SyncCatalog()
+		defaultSchema = defaultDB.Schema
+		// Pre-populate cache with default executor
+		execCache[defaultDB.Name] = defaultExec
+	}
+
+	s := &Server{
+		config:        config,
+		executor:      defaultExec,
+		schema:        defaultSchema,
+		dbManager:     dbManager,
+		executorCache: execCache,
+		stats: &Stats{
+			StartTime: time.Now(),
+		},
+	}
+
+	return s.init()
+}
+
+// init initializes the server routes and middleware.
+func (s *Server) init() *Server {
 	mux := http.NewServeMux()
 
 	// Apply middleware (order matters: logging -> auth -> cors -> compression -> handler)
 	var handler http.Handler = mux
 
-	if config.EnableCompression {
+	if s.config.EnableCompression {
 		handler = s.compressionMiddleware(handler)
 	}
 
-	if config.EnableCORS {
+	if s.config.EnableCORS {
 		handler = s.corsMiddleware(handler)
 	}
 
-	if config.EnableAuth {
+	if s.config.EnableAuth {
 		handler = s.authMiddleware(handler)
 	}
 
@@ -103,12 +151,14 @@ func New(config *Config, exec *executor.Executor, schema *storage.SchemaManager)
 	mux.HandleFunc("/transaction/begin", s.handleTransactionBegin)
 	mux.HandleFunc("/transaction/commit", s.handleTransactionCommit)
 	mux.HandleFunc("/transaction/rollback", s.handleTransactionRollback)
+	mux.HandleFunc("/export", s.handleExport)
+	mux.HandleFunc("/import", s.handleImport)
 
 	s.server = &http.Server{
-		Addr:         fmt.Sprintf("%s:%d", config.Host, config.Port),
+		Addr:         fmt.Sprintf("%s:%d", s.config.Host, s.config.Port),
 		Handler:      handler,
-		ReadTimeout:  config.ReadTimeout,
-		WriteTimeout: config.WriteTimeout,
+		ReadTimeout:  s.config.ReadTimeout,
+		WriteTimeout: s.config.WriteTimeout,
 	}
 
 	return s
@@ -136,3 +186,51 @@ func (s *Server) Shutdown(ctx context.Context) error {
 func (s *Server) Addr() string {
 	return s.server.Addr
 }
+
+// getExecutorForDatabase returns an executor for the specified database.
+// If dbName is empty, returns the default executor.
+// If multi-database support is not enabled, always returns the default executor.
+func (s *Server) getExecutorForDatabase(dbName string) (*executor.Executor, *storage.SchemaManager, error) {
+	// If no database manager, use the default executor
+	if s.dbManager == nil {
+		return s.executor, s.schema, nil
+	}
+
+	// Get the database instance - this ensures we get the correct SchemaManager
+	dbInstance, err := s.dbManager.GetDatabase(dbName)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// IMPORTANT: Always use dbInstance.Schema for isolation
+	// The SchemaManager contains the database name and ensures queries
+	// are scoped to the correct database namespace
+
+	// Check per-server executor cache
+	s.executorCacheMu.RLock()
+	exec, exists := s.executorCache[dbInstance.Name]
+	s.executorCacheMu.RUnlock()
+
+	if exists {
+		// Return cached executor with the correct schema from dbInstance
+		return exec, dbInstance.Schema, nil
+	}
+
+	// Create new executor and cache it
+	s.executorCacheMu.Lock()
+	defer s.executorCacheMu.Unlock()
+
+	// Double-check after acquiring write lock
+	if exec, exists := s.executorCache[dbInstance.Name]; exists {
+		return exec, dbInstance.Schema, nil
+	}
+
+	// Create executor with the database-specific schema and table managers
+	exec = executor.New(dbInstance.Schema, dbInstance.Table)
+	exec.SyncCatalog()
+	s.executorCache[dbInstance.Name] = exec
+
+	log.Printf("Created executor for database: %s", dbInstance.Name)
+
+	return exec, dbInstance.Schema, nil
+}

+ 283 - 0
pkg/sqlexport/export.go

@@ -0,0 +1,283 @@
+package sqlexport
+
+import (
+	"encoding/hex"
+	"fmt"
+	"sort"
+	"strings"
+	"time"
+
+	"github.com/danfragoso/pizzasql-next/pkg/storage"
+)
+
+// ExportOptions configures export behavior.
+type ExportOptions struct {
+	Tables      []string // Specific tables to export (empty = all tables)
+	IncludeData bool     // Include INSERT statements (default: true)
+	DropTables  bool     // Add DROP TABLE IF EXISTS before CREATE
+}
+
+// DefaultExportOptions returns sensible defaults.
+func DefaultExportOptions() ExportOptions {
+	return ExportOptions{
+		Tables:      nil,
+		IncludeData: true,
+		DropTables:  false,
+	}
+}
+
+// ExportDatabase exports an entire database to SQL text.
+func ExportDatabase(schema *storage.SchemaManager, table *storage.TableManager, opts ExportOptions) (string, error) {
+	var sb strings.Builder
+
+	// Write header
+	sb.WriteString("-- PizzaSQL Export\n")
+	sb.WriteString(fmt.Sprintf("-- Database: %s\n", schema.GetDatabaseName()))
+	sb.WriteString(fmt.Sprintf("-- Date: %s\n", time.Now().UTC().Format(time.RFC3339)))
+	sb.WriteString("\n")
+
+	// Get tables to export
+	tables := opts.Tables
+	if len(tables) == 0 {
+		var err error
+		tables, err = schema.ListTables()
+		if err != nil {
+			return "", fmt.Errorf("failed to list tables: %w", err)
+		}
+	}
+
+	// Sort tables for consistent output
+	sort.Strings(tables)
+
+	// Export each table
+	for i, tableName := range tables {
+		tableSQL, err := ExportTable(schema, table, tableName, opts)
+		if err != nil {
+			return "", fmt.Errorf("failed to export table %s: %w", tableName, err)
+		}
+
+		sb.WriteString(tableSQL)
+
+		// Add separator between tables
+		if i < len(tables)-1 {
+			sb.WriteString("\n")
+		}
+	}
+
+	return sb.String(), nil
+}
+
+// ExportTable exports a single table to SQL text.
+func ExportTable(schema *storage.SchemaManager, table *storage.TableManager, tableName string, opts ExportOptions) (string, error) {
+	var sb strings.Builder
+
+	// Get table schema
+	tableSchema, err := schema.GetSchema(tableName)
+	if err != nil {
+		return "", fmt.Errorf("failed to get schema for table %s: %w", tableName, err)
+	}
+
+	// Write DROP TABLE if requested
+	if opts.DropTables {
+		sb.WriteString(fmt.Sprintf("DROP TABLE IF EXISTS %s;\n", quoteIdentifier(tableName)))
+	}
+
+	// Write CREATE TABLE
+	createSQL := generateCreateTable(tableSchema)
+	sb.WriteString(createSQL)
+	sb.WriteString("\n")
+
+	// Write INSERT statements if requested
+	if opts.IncludeData {
+		rows, err := table.Select(tableName, nil)
+		if err != nil {
+			return "", fmt.Errorf("failed to select data from table %s: %w", tableName, err)
+		}
+
+		if len(rows) > 0 {
+			sb.WriteString("\n")
+			insertSQL := generateInserts(tableName, tableSchema, rows)
+			sb.WriteString(insertSQL)
+		}
+	}
+
+	return sb.String(), nil
+}
+
+// generateCreateTable generates a CREATE TABLE statement from schema.
+func generateCreateTable(schema *storage.Schema) string {
+	var sb strings.Builder
+
+	sb.WriteString(fmt.Sprintf("CREATE TABLE %s (\n", quoteIdentifier(schema.Name)))
+
+	// Generate column definitions
+	for i, col := range schema.Columns {
+		// Skip internal _rowid_ column
+		if col.Name == "_rowid_" {
+			continue
+		}
+
+		sb.WriteString("    ")
+		sb.WriteString(quoteIdentifier(col.Name))
+		sb.WriteString(" ")
+		sb.WriteString(col.Type)
+
+		// Add PRIMARY KEY constraint
+		if col.PrimaryKey {
+			sb.WriteString(" PRIMARY KEY")
+		}
+
+		// Add NOT NULL constraint
+		if !col.Nullable && !col.PrimaryKey {
+			sb.WriteString(" NOT NULL")
+		}
+
+		// Add DEFAULT value
+		if col.Default != nil {
+			sb.WriteString(" DEFAULT ")
+			sb.WriteString(formatValue(col.Default, col.Type))
+		}
+
+		// Add comma if not last column
+		if i < len(schema.Columns)-1 {
+			// Check if next column is _rowid_
+			if i+1 < len(schema.Columns) && schema.Columns[i+1].Name != "_rowid_" {
+				sb.WriteString(",")
+			} else if i+2 < len(schema.Columns) {
+				sb.WriteString(",")
+			}
+		}
+		sb.WriteString("\n")
+	}
+
+	sb.WriteString(");")
+
+	return sb.String()
+}
+
+// generateInserts generates INSERT statements for rows.
+func generateInserts(tableName string, schema *storage.Schema, rows []storage.Row) string {
+	var sb strings.Builder
+
+	// Get column names (excluding _rowid_)
+	var columns []string
+	var colTypes []string
+	for _, col := range schema.Columns {
+		if col.Name != "_rowid_" {
+			columns = append(columns, col.Name)
+			colTypes = append(colTypes, col.Type)
+		}
+	}
+
+	// Generate INSERT for each row
+	for _, row := range rows {
+		sb.WriteString(fmt.Sprintf("INSERT INTO %s (", quoteIdentifier(tableName)))
+
+		// Column names
+		for i, col := range columns {
+			if i > 0 {
+				sb.WriteString(", ")
+			}
+			sb.WriteString(quoteIdentifier(col))
+		}
+
+		sb.WriteString(") VALUES (")
+
+		// Values
+		for i, col := range columns {
+			if i > 0 {
+				sb.WriteString(", ")
+			}
+			value := row[col]
+			sb.WriteString(formatValue(value, colTypes[i]))
+		}
+
+		sb.WriteString(");\n")
+	}
+
+	return sb.String()
+}
+
+// formatValue formats a Go value as a SQL literal.
+func formatValue(value interface{}, colType string) string {
+	if value == nil {
+		return "NULL"
+	}
+
+	switch v := value.(type) {
+	case string:
+		return formatString(v)
+	case float64:
+		// Check if it's actually an integer
+		if strings.Contains(strings.ToUpper(colType), "INT") {
+			return fmt.Sprintf("%d", int64(v))
+		}
+		return fmt.Sprintf("%g", v)
+	case int64:
+		return fmt.Sprintf("%d", v)
+	case int:
+		return fmt.Sprintf("%d", v)
+	case bool:
+		if v {
+			return "1"
+		}
+		return "0"
+	case []byte:
+		return fmt.Sprintf("X'%s'", hex.EncodeToString(v))
+	default:
+		// Fallback: treat as string
+		return formatString(fmt.Sprintf("%v", v))
+	}
+}
+
+// formatString formats a string as a SQL string literal with proper escaping.
+func formatString(s string) string {
+	// Escape single quotes by doubling them
+	escaped := strings.ReplaceAll(s, "'", "''")
+	return fmt.Sprintf("'%s'", escaped)
+}
+
+// quoteIdentifier quotes a SQL identifier if needed.
+func quoteIdentifier(name string) string {
+	// Check if identifier needs quoting
+	needsQuote := false
+
+	// Check for reserved words or special characters
+	lower := strings.ToLower(name)
+	reserved := map[string]bool{
+		"table": true, "select": true, "insert": true, "update": true,
+		"delete": true, "create": true, "drop": true, "index": true,
+		"from": true, "where": true, "and": true, "or": true,
+		"order": true, "by": true, "group": true, "having": true,
+		"limit": true, "offset": true, "join": true, "on": true,
+		"as": true, "null": true, "not": true, "in": true,
+		"like": true, "between": true, "is": true, "primary": true,
+		"key": true, "unique": true, "default": true, "values": true,
+	}
+
+	if reserved[lower] {
+		needsQuote = true
+	}
+
+	// Check for special characters
+	for _, c := range name {
+		if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
+			(c >= '0' && c <= '9') || c == '_') {
+			needsQuote = true
+			break
+		}
+	}
+
+	// Check if starts with digit
+	if len(name) > 0 && name[0] >= '0' && name[0] <= '9' {
+		needsQuote = true
+	}
+
+	if needsQuote {
+		// Use double quotes and escape any existing double quotes
+		escaped := strings.ReplaceAll(name, "\"", "\"\"")
+		return fmt.Sprintf("\"%s\"", escaped)
+	}
+
+	return name
+}

+ 236 - 0
pkg/sqlimport/import.go

@@ -0,0 +1,236 @@
+package sqlimport
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/danfragoso/pizzasql-next/pkg/executor"
+	"github.com/danfragoso/pizzasql-next/pkg/lexer"
+	"github.com/danfragoso/pizzasql-next/pkg/parser"
+)
+
+// ImportOptions configures import behavior.
+type ImportOptions struct {
+	IgnoreErrors bool // Continue on individual statement errors
+}
+
+// DefaultImportOptions returns sensible defaults.
+func DefaultImportOptions() ImportOptions {
+	return ImportOptions{
+		IgnoreErrors: false,
+	}
+}
+
+// ImportResult contains the results of an import operation.
+type ImportResult struct {
+	StatementsExecuted int      `json:"statementsExecuted"`
+	TablesCreated      []string `json:"tablesCreated"`
+	TablesDropped      []string `json:"tablesDropped"`
+	RowsInserted       int64    `json:"rowsInserted"`
+	Errors             []string `json:"errors,omitempty"`
+}
+
+// ImportSQL executes SQL statements from text.
+func ImportSQL(exec *executor.Executor, sql string, opts ImportOptions) (*ImportResult, error) {
+	result := &ImportResult{
+		TablesCreated: []string{},
+		TablesDropped: []string{},
+		Errors:        []string{},
+	}
+
+	// Split SQL into statements
+	statements := splitStatements(sql)
+
+	for _, stmtSQL := range statements {
+		stmtSQL = strings.TrimSpace(stmtSQL)
+		if stmtSQL == "" || isComment(stmtSQL) {
+			continue
+		}
+
+		// Parse and execute the statement
+		err := executeStatement(exec, stmtSQL, result)
+		if err != nil {
+			errMsg := fmt.Sprintf("Error executing statement: %s - %v", truncateSQL(stmtSQL), err)
+			result.Errors = append(result.Errors, errMsg)
+
+			if !opts.IgnoreErrors {
+				return result, fmt.Errorf("import failed: %w", err)
+			}
+		} else {
+			result.StatementsExecuted++
+		}
+	}
+
+	return result, nil
+}
+
+// executeStatement parses and executes a single SQL statement.
+func executeStatement(exec *executor.Executor, sql string, result *ImportResult) error {
+	// Parse the statement
+	l := lexer.New(sql)
+	p := parser.New(l)
+	stmt, err := p.Parse()
+	if err != nil {
+		return fmt.Errorf("parse error: %w", err)
+	}
+
+	// Execute the statement
+	execResult, err := exec.Execute(stmt)
+	if err != nil {
+		return fmt.Errorf("execution error: %w", err)
+	}
+
+	// Track what happened
+	upperSQL := strings.ToUpper(strings.TrimSpace(sql))
+
+	if strings.HasPrefix(upperSQL, "CREATE TABLE") {
+		tableName := extractTableName(sql, "CREATE TABLE")
+		if tableName != "" {
+			result.TablesCreated = append(result.TablesCreated, tableName)
+		}
+	} else if strings.HasPrefix(upperSQL, "DROP TABLE") {
+		tableName := extractTableName(sql, "DROP TABLE")
+		if tableName != "" {
+			result.TablesDropped = append(result.TablesDropped, tableName)
+		}
+	} else if strings.HasPrefix(upperSQL, "INSERT") {
+		result.RowsInserted += execResult.RowsAffected
+	}
+
+	return nil
+}
+
+// splitStatements splits SQL text into individual statements.
+func splitStatements(sql string) []string {
+	var statements []string
+	var current strings.Builder
+	inString := false
+	stringChar := byte(0)
+
+	for i := 0; i < len(sql); i++ {
+		c := sql[i]
+
+		// Handle string literals
+		if (c == '\'' || c == '"') && !inString {
+			inString = true
+			stringChar = c
+			current.WriteByte(c)
+			continue
+		}
+
+		if inString {
+			current.WriteByte(c)
+			// Check for escape (doubled quote)
+			if c == stringChar {
+				if i+1 < len(sql) && sql[i+1] == stringChar {
+					// Escaped quote - write next char and skip
+					i++
+					current.WriteByte(sql[i])
+				} else {
+					// End of string
+					inString = false
+					stringChar = 0
+				}
+			}
+			continue
+		}
+
+		// Handle statement terminator
+		if c == ';' {
+			stmt := strings.TrimSpace(current.String())
+			if stmt != "" {
+				statements = append(statements, stmt)
+			}
+			current.Reset()
+			continue
+		}
+
+		// Handle single-line comments
+		if c == '-' && i+1 < len(sql) && sql[i+1] == '-' {
+			// Skip to end of line
+			for i < len(sql) && sql[i] != '\n' {
+				i++
+			}
+			continue
+		}
+
+		current.WriteByte(c)
+	}
+
+	// Don't forget the last statement if no trailing semicolon
+	stmt := strings.TrimSpace(current.String())
+	if stmt != "" {
+		statements = append(statements, stmt)
+	}
+
+	return statements
+}
+
+// isComment checks if a line is a SQL comment.
+func isComment(line string) bool {
+	trimmed := strings.TrimSpace(line)
+	return strings.HasPrefix(trimmed, "--") || strings.HasPrefix(trimmed, "/*")
+}
+
+// extractTableName extracts the table name from a CREATE TABLE or DROP TABLE statement.
+func extractTableName(sql string, prefix string) string {
+	// Remove the prefix
+	upper := strings.ToUpper(sql)
+	prefixUpper := strings.ToUpper(prefix)
+
+	idx := strings.Index(upper, prefixUpper)
+	if idx == -1 {
+		return ""
+	}
+
+	rest := strings.TrimSpace(sql[idx+len(prefix):])
+
+	// Handle IF EXISTS / IF NOT EXISTS
+	restUpper := strings.ToUpper(rest)
+	if strings.HasPrefix(restUpper, "IF EXISTS") {
+		rest = strings.TrimSpace(rest[9:])
+	} else if strings.HasPrefix(restUpper, "IF NOT EXISTS") {
+		rest = strings.TrimSpace(rest[13:])
+	}
+
+	// Extract table name (until space, paren, or end)
+	var tableName strings.Builder
+	inQuote := false
+	quoteChar := byte(0)
+
+	for i := 0; i < len(rest); i++ {
+		c := rest[i]
+
+		if (c == '"' || c == '`' || c == '[') && !inQuote {
+			inQuote = true
+			quoteChar = c
+			if c == '[' {
+				quoteChar = ']'
+			}
+			continue
+		}
+
+		if inQuote && c == quoteChar {
+			inQuote = false
+			continue
+		}
+
+		if !inQuote && (c == ' ' || c == '\t' || c == '\n' || c == '(' || c == ';') {
+			break
+		}
+
+		tableName.WriteByte(c)
+	}
+
+	return tableName.String()
+}
+
+// truncateSQL truncates SQL for error messages.
+func truncateSQL(sql string) string {
+	sql = strings.ReplaceAll(sql, "\n", " ")
+	sql = strings.Join(strings.Fields(sql), " ")
+	if len(sql) > 50 {
+		return sql[:50] + "..."
+	}
+	return sql
+}

+ 139 - 0
pkg/storage/db_manager.go

@@ -0,0 +1,139 @@
+package storage
+
+import (
+	"fmt"
+	"log"
+	"sync"
+)
+
+// DatabaseInstance represents a single database with its own schema and table managers.
+type DatabaseInstance struct {
+	Name   string
+	Schema *SchemaManager
+	Table  *TableManager
+}
+
+// DatabaseManager manages multiple database instances.
+type DatabaseManager struct {
+	pool            *KVPool
+	defaultDatabase string
+	databases       map[string]*DatabaseInstance
+	autoCreate      bool
+	mu              sync.RWMutex
+}
+
+// DatabaseManagerConfig holds configuration for the database manager.
+type DatabaseManagerConfig struct {
+	DefaultDatabase string
+	AutoCreate      bool // Auto-create databases on first access
+}
+
+// NewDatabaseManager creates a new database manager.
+func NewDatabaseManager(pool *KVPool, config *DatabaseManagerConfig) *DatabaseManager {
+	if config == nil {
+		config = &DatabaseManagerConfig{
+			DefaultDatabase: "pizzasql",
+			AutoCreate:      true,
+		}
+	}
+
+	dm := &DatabaseManager{
+		pool:            pool,
+		defaultDatabase: config.DefaultDatabase,
+		databases:       make(map[string]*DatabaseInstance),
+		autoCreate:      config.AutoCreate,
+	}
+
+	// Pre-create the default database instance
+	dm.getOrCreateDatabase(config.DefaultDatabase)
+
+	return dm
+}
+
+// GetDatabase returns the database instance for the given name.
+// If name is empty, returns the default database.
+// If autoCreate is enabled and the database doesn't exist, it will be created.
+func (dm *DatabaseManager) GetDatabase(name string) (*DatabaseInstance, error) {
+	originalName := name
+	if name == "" {
+		name = dm.defaultDatabase
+		log.Printf("[DEBUG] GetDatabase: empty name, using default: %q", name)
+	}
+
+	dm.mu.RLock()
+	db, exists := dm.databases[name]
+	dm.mu.RUnlock()
+
+	if exists {
+		log.Printf("[DEBUG] GetDatabase: found existing database %q (requested: %q), schema.database=%q",
+			name, originalName, db.Schema.GetDatabaseName())
+		return db, nil
+	}
+
+	if !dm.autoCreate {
+		return nil, fmt.Errorf("database not found: %s", name)
+	}
+
+	log.Printf("[DEBUG] GetDatabase: creating new database %q (requested: %q)", name, originalName)
+	return dm.getOrCreateDatabase(name), nil
+}
+
+// getOrCreateDatabase creates a new database instance if it doesn't exist.
+func (dm *DatabaseManager) getOrCreateDatabase(name string) *DatabaseInstance {
+	dm.mu.Lock()
+	defer dm.mu.Unlock()
+
+	// Double-check after acquiring write lock
+	if db, exists := dm.databases[name]; exists {
+		return db
+	}
+
+	schema := NewSchemaManager(dm.pool, name)
+	table := NewTableManager(dm.pool, schema, name)
+
+	db := &DatabaseInstance{
+		Name:   name,
+		Schema: schema,
+		Table:  table,
+	}
+
+	dm.databases[name] = db
+	return db
+}
+
+// DefaultDatabase returns the default database name.
+func (dm *DatabaseManager) DefaultDatabase() string {
+	return dm.defaultDatabase
+}
+
+// ListDatabases returns a list of all active database names.
+func (dm *DatabaseManager) ListDatabases() []string {
+	dm.mu.RLock()
+	defer dm.mu.RUnlock()
+
+	names := make([]string, 0, len(dm.databases))
+	for name := range dm.databases {
+		names = append(names, name)
+	}
+	return names
+}
+
+// DatabaseExists checks if a database instance exists (is loaded).
+func (dm *DatabaseManager) DatabaseExists(name string) bool {
+	dm.mu.RLock()
+	defer dm.mu.RUnlock()
+	_, exists := dm.databases[name]
+	return exists
+}
+
+// GetPool returns the underlying KV pool.
+func (dm *DatabaseManager) GetPool() *KVPool {
+	return dm.pool
+}
+
+// SetAutoCreate enables or disables auto-creation of databases.
+func (dm *DatabaseManager) SetAutoCreate(autoCreate bool) {
+	dm.mu.Lock()
+	defer dm.mu.Unlock()
+	dm.autoCreate = autoCreate
+}

+ 12 - 1
pkg/storage/kv.go

@@ -53,6 +53,7 @@ func (c *KVClient) Write(key, value string) error {
 	defer c.mu.Unlock()
 
 	cmd := fmt.Sprintf("write %s|%s\r", key, value)
+	fmt.Printf("[DEBUG KV] Write command (len=%d): key=%q, value_len=%d\n", len(cmd), key, len(value))
 	if _, err := c.writer.WriteString(cmd); err != nil {
 		return fmt.Errorf("write command failed: %w", err)
 	}
@@ -65,6 +66,7 @@ func (c *KVClient) Write(key, value string) error {
 		return fmt.Errorf("read response failed: %w", err)
 	}
 
+	fmt.Printf("[DEBUG KV] Write response: %q\n", resp)
 	resp = strings.TrimSuffix(resp, "\r")
 	if resp != "success" {
 		return fmt.Errorf("write failed: %s", resp)
@@ -79,6 +81,7 @@ func (c *KVClient) Read(key string) (string, error) {
 	defer c.mu.Unlock()
 
 	cmd := fmt.Sprintf("read %s\r", key)
+	fmt.Printf("[DEBUG KV] Read command: %q\n", cmd)
 	if _, err := c.writer.WriteString(cmd); err != nil {
 		return "", fmt.Errorf("read command failed: %w", err)
 	}
@@ -91,6 +94,7 @@ func (c *KVClient) Read(key string) (string, error) {
 		return "", fmt.Errorf("read response failed: %w", err)
 	}
 
+	fmt.Printf("[DEBUG KV] Read raw response: %q\n", resp)
 	resp = strings.TrimSuffix(resp, "\r")
 	if resp == "error" {
 		return "", ErrKeyNotFound
@@ -131,6 +135,7 @@ func (c *KVClient) Reads(prefix string) ([]string, error) {
 	defer c.mu.Unlock()
 
 	cmd := fmt.Sprintf("reads %s\r", prefix)
+	fmt.Printf("[DEBUG KV] Reads command: %q\n", cmd)
 	if _, err := c.writer.WriteString(cmd); err != nil {
 		return nil, fmt.Errorf("reads command failed: %w", err)
 	}
@@ -140,23 +145,29 @@ func (c *KVClient) Reads(prefix string) ([]string, error) {
 
 	resp, err := c.reader.ReadString('\r')
 	if err != nil {
+		fmt.Printf("[DEBUG KV] Reads response error: %v\n", err)
 		return nil, fmt.Errorf("read response failed: %w", err)
 	}
 
+	fmt.Printf("[DEBUG KV] Reads raw response: %q (len=%d)\n", resp, len(resp))
 	resp = strings.TrimSuffix(resp, "\r")
 	if resp == "" {
+		fmt.Printf("[DEBUG KV] Reads: empty response, returning nil\n")
 		return nil, nil
 	}
 
 	values := strings.Split(resp, "\n")
+	fmt.Printf("[DEBUG KV] Reads: split into %d parts\n", len(values))
 	// Filter out empty strings
 	result := make([]string, 0, len(values))
-	for _, v := range values {
+	for i, v := range values {
+		fmt.Printf("[DEBUG KV] Reads value[%d]: %q\n", i, v)
 		if v != "" {
 			result = append(result, v)
 		}
 	}
 
+	fmt.Printf("[DEBUG KV] Reads: returning %d values\n", len(result))
 	return result, nil
 }
 

+ 70 - 17
pkg/storage/table.go

@@ -213,6 +213,7 @@ func (m *TableManager) Select(table string, filter func(Row) bool) ([]Row, error
 	err := m.pool.WithClient(func(c *KVClient) error {
 		var err error
 		values, err = c.Reads(prefix)
+		fmt.Printf("[DEBUG] Select: table=%s, database=%s, prefix=%s, values_count=%d\n", table, m.database, prefix, len(values))
 		return err
 	})
 	if err != nil {
@@ -223,6 +224,7 @@ func (m *TableManager) Select(table string, filter func(Row) bool) ([]Row, error
 	for _, data := range values {
 		var row Row
 		if err := json.Unmarshal([]byte(data), &row); err != nil {
+			fmt.Printf("[DEBUG] Select: failed to unmarshal row: %v\n", err)
 			continue // Skip invalid rows
 		}
 
@@ -231,6 +233,7 @@ func (m *TableManager) Select(table string, filter func(Row) bool) ([]Row, error
 		}
 	}
 
+	fmt.Printf("[DEBUG] Select: returning %d rows\n", len(rows))
 	return rows, nil
 }
 
@@ -469,7 +472,24 @@ func IsRowIDColumn(name string) bool {
 
 // indexEntryKey returns the key for an index entry.
 func (m *TableManager) indexEntryKey(indexName string, colValue interface{}) string {
-	return fmt.Sprintf("%s:idx:%s:%v", m.database, strings.ToLower(indexName), colValue)
+	// Format the value without scientific notation
+	var valueStr string
+	switch v := colValue.(type) {
+	case float64:
+		// Check if it's actually an integer value
+		if v == float64(int64(v)) {
+			valueStr = fmt.Sprintf("%d", int64(v))
+		} else {
+			valueStr = fmt.Sprintf("%f", v)
+		}
+	case int64:
+		valueStr = fmt.Sprintf("%d", v)
+	case int:
+		valueStr = fmt.Sprintf("%d", v)
+	default:
+		valueStr = fmt.Sprintf("%v", v)
+	}
+	return fmt.Sprintf("%s:idx:%s:%s", m.database, strings.ToLower(indexName), valueStr)
 }
 
 // indexPrefix returns the prefix for all entries of an index.
@@ -611,14 +631,31 @@ func (m *TableManager) BuildIndex(indexName, tableName string, columns []string)
 
 // buildIndexValue creates the index key value from row columns.
 func (m *TableManager) buildIndexValue(row Row, columns []string) string {
+	formatValue := func(v interface{}) string {
+		switch val := v.(type) {
+		case float64:
+			// Check if it's actually an integer value
+			if val == float64(int64(val)) {
+				return fmt.Sprintf("%d", int64(val))
+			}
+			return fmt.Sprintf("%f", val)
+		case int64:
+			return fmt.Sprintf("%d", val)
+		case int:
+			return fmt.Sprintf("%d", val)
+		default:
+			return fmt.Sprintf("%v", val)
+		}
+	}
+
 	if len(columns) == 1 {
-		return fmt.Sprintf("%v", row[columns[0]])
+		return formatValue(row[columns[0]])
 	}
 
 	// Multi-column index: concatenate values with separator
 	var parts []string
 	for _, col := range columns {
-		parts = append(parts, fmt.Sprintf("%v", row[col]))
+		parts = append(parts, formatValue(row[col]))
 	}
 	return strings.Join(parts, "\x00")
 }
@@ -630,30 +667,46 @@ func (m *TableManager) SelectByIndex(table, indexName string, colValue interface
 		return nil, err
 	}
 
+	// If no rowids found, return empty result
+	if len(rowids) == 0 {
+		return []Row{}, nil
+	}
+
 	schema, err := m.schema.GetSchema(table)
 	if err != nil {
 		return nil, err
 	}
 
+	// Check if primary key is INTEGER type (in which case rowid == pk)
+	pkCol, _ := schema.GetColumn(schema.PrimaryKey)
+	isPKInteger := pkCol != nil && isIntegerType(pkCol.Type)
+
 	rows := make([]Row, 0, len(rowids))
 	for _, rowid := range rowids {
+		var row Row
+
 		// For INTEGER PRIMARY KEY, the rowid IS the primary key
-		row, err := m.GetByPK(table, fmt.Sprintf("%d", rowid))
-		if err != nil {
-			// Try looking up by _rowid_ if PK lookup fails
-			allRows, _ := m.Select(table, func(r Row) bool {
-				if rid, ok := r["_rowid_"].(float64); ok {
-					return int64(rid) == rowid
-				}
-				return false
-			})
-			if len(allRows) > 0 {
-				rows = append(rows, allRows[0])
+		if isPKInteger {
+			row, err = m.GetByPK(table, fmt.Sprintf("%d", rowid))
+			if err == nil {
+				rows = append(rows, row)
+				continue
 			}
-			continue
 		}
-		_ = schema // Used for validation if needed
-		rows = append(rows, row)
+
+		// For non-INTEGER primary keys or if PK lookup fails, look up by _rowid_
+		allRows, _ := m.Select(table, func(r Row) bool {
+			if rid, ok := r["_rowid_"].(float64); ok {
+				return int64(rid) == rowid
+			}
+			if rid, ok := r["_rowid_"].(int64); ok {
+				return rid == rowid
+			}
+			return false
+		})
+		if len(allRows) > 0 {
+			rows = append(rows, allRows[0])
+		}
 	}
 
 	return rows, nil

+ 0 - 1206
stress_test.js

@@ -1,1206 +0,0 @@
-#!/usr/bin/env node
-
-/**
- * PizzaSQL Stress Test Script
- *
- * Tests all database features including:
- * - Table creation and schema operations
- * - CRUD operations (INSERT, SELECT, UPDATE, DELETE)
- * - Transactions (BEGIN, COMMIT, ROLLBACK)
- * - Batch execution
- * - Indexes
- * - JOINs
- * - Aggregations
- * - Subqueries
- * - ALTER TABLE
- * - Parameterized queries
- */
-
-const BASE_URL = process.env.PIZZASQL_URL || 'http://localhost:8080';
-const API_KEY = process.env.PIZZASQL_API_KEY || '';
-
-// Test configuration
-const CONFIG = {
-  numUsers: 1000,
-  numProducts: 500,
-  numOrders: 2000,
-  numOrderItems: 5000,
-  concurrentRequests: 10,
-};
-
-// Stats tracking
-const stats = {
-  passed: 0,
-  failed: 0,
-  totalQueries: 0,
-  totalTime: 0,
-  errors: [],
-};
-
-// Helper functions
-async function fetchWithTimeout(url, options, timeout = 300000) {
-  const controller = new AbortController();
-  const id = setTimeout(() => controller.abort(), timeout);
-  
-  try {
-    const response = await fetch(url, {
-      ...options,
-      signal: controller.signal
-    });
-    clearTimeout(id);
-    return response;
-  } catch (error) {
-    clearTimeout(id);
-    if (error.name === 'AbortError') {
-      throw new Error(`Request timeout after ${timeout}ms`);
-    }
-    throw error;
-  }
-}
-
-async function query(sql, params = []) {
-  const headers = { 'Content-Type': 'application/json' };
-  if (API_KEY) headers['Authorization'] = `Bearer ${API_KEY}`;
-
-  const start = Date.now();
-  const response = await fetchWithTimeout(`${BASE_URL}/query`, {
-    method: 'POST',
-    headers,
-    body: JSON.stringify({ sql, params }),
-  }, 300000);
-
-  const elapsed = Date.now() - start;
-  stats.totalQueries++;
-  stats.totalTime += elapsed;
-
-  const data = await response.json();
-  if (!response.ok) {
-    throw new Error(data.error?.message || `HTTP ${response.status}`);
-  }
-  return data;
-}
-
-async function execute(statements, transaction = false) {
-  const headers = { 'Content-Type': 'application/json' };
-  if (API_KEY) headers['Authorization'] = `Bearer ${API_KEY}`;
-
-  const start = Date.now();
-  const response = await fetchWithTimeout(`${BASE_URL}/execute`, {
-    method: 'POST',
-    headers,
-    body: JSON.stringify({ statements, transaction }),
-  }, 300000);
-
-  const elapsed = Date.now() - start;
-  stats.totalQueries += statements.length;
-  stats.totalTime += elapsed;
-
-  const data = await response.json();
-  if (!response.ok) {
-    throw new Error(data.error?.message || `HTTP ${response.status}`);
-  }
-  return data;
-}
-
-async function getHealth() {
-  const response = await fetch(`${BASE_URL}/health`);
-  return response.json();
-}
-
-async function getTables() {
-  const response = await fetch(`${BASE_URL}/schema/tables`);
-  return response.json();
-}
-
-async function getTableSchema(name) {
-  const response = await fetch(`${BASE_URL}/schema/tables/${name}`);
-  return response.json();
-}
-
-function assert(condition, message) {
-  if (!condition) {
-    throw new Error(`Assertion failed: ${message}`);
-  }
-}
-
-function assertEqual(actual, expected, message) {
-  if (actual !== expected) {
-    throw new Error(`${message}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
-  }
-}
-
-function assertArrayEqual(actual, expected, message) {
-  if (JSON.stringify(actual) !== JSON.stringify(expected)) {
-    throw new Error(`${message}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
-  }
-}
-
-async function runTest(name, testFn) {
-  process.stdout.write(`  Testing ${name}... `);
-  const testStart = Date.now();
-  try {
-    await testFn();
-    const testTime = Date.now() - testStart;
-    console.log(`✓ PASSED (${testTime}ms)`);
-    stats.passed++;
-  } catch (error) {
-    const testTime = Date.now() - testStart;
-    console.log(`✗ FAILED (${testTime}ms): ${error.message}`);
-    stats.failed++;
-    stats.errors.push({ name, error: error.message });
-  }
-}
-
-// ============================================================================
-// Test Suites
-// ============================================================================
-
-async function testHealthCheck() {
-  const health = await getHealth();
-  assert(health.status === 'ok', 'Health status should be ok');
-}
-
-async function cleanupTables() {
-  // Drop indexes first
-  const indexes = ['idx_users_email', 'idx_orders_user', 'idx_products_category'];
-  for (const idx of indexes) {
-    try {
-      await query(`DROP INDEX IF EXISTS ${idx}`);
-    } catch (e) {
-      // Ignore errors
-    }
-  }
-
-  // Drop tables if they exist (in reverse dependency order)
-  const tables = ['order_items', 'orders', 'products', 'categories', 'users', 'test_alter', 'test_index'];
-  for (const table of tables) {
-    try {
-      await query(`DROP TABLE IF EXISTS ${table}`);
-    } catch (e) {
-      // Ignore errors
-    }
-  }
-}
-
-async function testCreateTables() {
-  // Create users table
-  await query(`
-    CREATE TABLE users (
-      id INTEGER PRIMARY KEY AUTOINCREMENT,
-      username TEXT NOT NULL UNIQUE,
-      email TEXT NOT NULL,
-      age INTEGER,
-      balance REAL DEFAULT 0.0,
-      active INTEGER DEFAULT 1,
-      created_at TEXT DEFAULT CURRENT_TIMESTAMP
-    )
-  `);
-
-  // Create categories table
-  await query(`
-    CREATE TABLE categories (
-      id INTEGER PRIMARY KEY AUTOINCREMENT,
-      name TEXT NOT NULL UNIQUE,
-      description TEXT
-    )
-  `);
-
-  // Create products table
-  await query(`
-    CREATE TABLE products (
-      id INTEGER PRIMARY KEY AUTOINCREMENT,
-      name TEXT NOT NULL,
-      category_id INTEGER,
-      price REAL NOT NULL,
-      stock INTEGER DEFAULT 0,
-      FOREIGN KEY (category_id) REFERENCES categories(id)
-    )
-  `);
-
-  // Create orders table
-  await query(`
-    CREATE TABLE orders (
-      id INTEGER PRIMARY KEY AUTOINCREMENT,
-      user_id INTEGER NOT NULL,
-      status TEXT DEFAULT 'pending',
-      total REAL DEFAULT 0.0,
-      created_at TEXT DEFAULT CURRENT_TIMESTAMP,
-      FOREIGN KEY (user_id) REFERENCES users(id)
-    )
-  `);
-
-  // Create order_items table
-  await query(`
-    CREATE TABLE order_items (
-      id INTEGER PRIMARY KEY AUTOINCREMENT,
-      order_id INTEGER NOT NULL,
-      product_id INTEGER NOT NULL,
-      quantity INTEGER NOT NULL,
-      price REAL NOT NULL,
-      FOREIGN KEY (order_id) REFERENCES orders(id),
-      FOREIGN KEY (product_id) REFERENCES products(id)
-    )
-  `);
-
-  // Verify tables were created
-  const tables = await getTables();
-  assert(tables.tables.includes('users'), 'users table should exist');
-  assert(tables.tables.includes('products'), 'products table should exist');
-  assert(tables.tables.includes('orders'), 'orders table should exist');
-}
-
-async function testSchemaIntrospection() {
-  const schema = await getTableSchema('users');
-  assert(schema.name === 'users', 'Table name should be users');
-  assert(schema.columns.length >= 6, 'Users table should have at least 6 columns');
-
-  const idCol = schema.columns.find(c => c.name === 'id');
-  assert(idCol, 'id column should exist');
-  assert(idCol.primaryKey === true, 'id should be primary key');
-}
-
-async function testInsertUsers() {
-  process.stdout.write(`\n    → Preparing ${CONFIG.numUsers} user records... `);
-  const statements = [];
-  for (let i = 1; i <= CONFIG.numUsers; i++) {
-    statements.push({
-      sql: 'INSERT INTO users (username, email, age, balance) VALUES (?, ?, ?, ?)',
-      params: [`user${i}`, `user${i}@example.com`, 18 + (i % 50), Math.random() * 1000],
-    });
-  }
-  console.log('done');
-  process.stdout.write(`    → Executing batch insert... `);
-
-  const result = await execute(statements, true);
-  console.log('done');
-  assertEqual(result.results.length, CONFIG.numUsers, 'Should insert all users');
-  
-  // Verify count
-  const count = await query('SELECT COUNT(*) as count FROM users');
-  process.stdout.write(`    → Verified ${count.rows[0][0]} users in database\n`);
-}
-
-async function testInsertCategories() {
-  const categories = ['Electronics', 'Books', 'Clothing', 'Food', 'Sports'];
-  for (const cat of categories) {
-    await query('INSERT INTO categories (name, description) VALUES (?, ?)', [cat, `${cat} category`]);
-  }
-
-  const result = await query('SELECT COUNT(*) as count FROM categories');
-  assertEqual(result.rows[0][0], 5, 'Should have 5 categories');
-}
-
-async function testInsertProducts() {
-  process.stdout.write(`\n    → Preparing ${CONFIG.numProducts} product records... `);
-  const statements = [];
-  const productNames = ['Widget', 'Gadget', 'Gizmo', 'Thing', 'Item'];
-
-  for (let i = 1; i <= CONFIG.numProducts; i++) {
-    const name = `${productNames[i % productNames.length]} ${i}`;
-    const categoryId = (i % 5) + 1;
-    const price = 9.99 + (i * 0.5);
-    const stock = Math.floor(Math.random() * 100);
-
-    statements.push({
-      sql: 'INSERT INTO products (name, category_id, price, stock) VALUES (?, ?, ?, ?)',
-      params: [name, categoryId, price, stock],
-    });
-  }
-  console.log('done');
-  process.stdout.write(`    → Executing batch insert... `);
-
-  await execute(statements, true);
-  console.log('done');
-
-  const result = await query('SELECT COUNT(*) as count FROM products');
-  process.stdout.write(`    → Verified ${result.rows[0][0]} products in database\n`);
-  assertEqual(result.rows[0][0], CONFIG.numProducts, 'Should have all products');
-}
-
-async function testInsertOrders() {
-  process.stdout.write(`\n    → Preparing ${CONFIG.numOrders} order records... `);
-  const statements = [];
-  const statuses = ['pending', 'processing', 'shipped', 'delivered', 'cancelled'];
-
-  for (let i = 1; i <= CONFIG.numOrders; i++) {
-    const userId = (i % CONFIG.numUsers) + 1;
-    const status = statuses[i % statuses.length];
-
-    statements.push({
-      sql: 'INSERT INTO orders (user_id, status, total) VALUES (?, ?, ?)',
-      params: [userId, status, 0],
-    });
-  }
-  console.log('done');
-  process.stdout.write(`    → Executing batch insert... `);
-
-  await execute(statements, true);
-  console.log('done');
-
-  const result = await query('SELECT COUNT(*) as count FROM orders');
-  process.stdout.write(`    → Verified ${result.rows[0][0]} orders in database\n`);
-  assertEqual(result.rows[0][0], CONFIG.numOrders, 'Should have all orders');
-}
-
-async function testInsertOrderItems() {
-  process.stdout.write(`\n    → Preparing ${CONFIG.numOrderItems} order item records... `);
-  const statements = [];
-
-  for (let i = 1; i <= CONFIG.numOrderItems; i++) {
-    const orderId = (i % CONFIG.numOrders) + 1;
-    const productId = (i % CONFIG.numProducts) + 1;
-    const quantity = 1 + (i % 5);
-    const price = 9.99 + (productId * 0.5);
-
-    statements.push({
-      sql: 'INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (?, ?, ?, ?)',
-      params: [orderId, productId, quantity, price],
-    });
-  }
-  console.log('done');
-  process.stdout.write(`    → Executing batch insert... `);
-
-  await execute(statements, true);
-  console.log('done');
-
-  const result = await query('SELECT COUNT(*) as count FROM order_items');
-  process.stdout.write(`    → Verified ${result.rows[0][0]} order items in database\n`);
-  assertEqual(result.rows[0][0], CONFIG.numOrderItems, 'Should have all order items');
-}
-
-async function testSelectBasic() {
-  // Simple SELECT
-  const result = await query('SELECT * FROM users LIMIT 10');
-  assertEqual(result.rows.length, 10, 'Should return 10 users');
-
-  // SELECT with WHERE
-  const result2 = await query('SELECT * FROM users WHERE age > ?', [30]);
-  assert(result2.rows.length > 0, 'Should return users over 30');
-
-  // SELECT specific columns
-  const result3 = await query('SELECT username, email FROM users WHERE id = ?', [1]);
-  assertEqual(result3.columns.length, 2, 'Should return 2 columns');
-}
-
-async function testSelectWithOrderBy() {
-  const result = await query('SELECT * FROM users ORDER BY age DESC LIMIT 5');
-  assertEqual(result.rows.length, 5, 'Should return 5 users');
-
-  // Verify ordering
-  for (let i = 1; i < result.rows.length; i++) {
-    const ageIdx = result.columns.findIndex(c => c.name === 'age');
-    assert(result.rows[i - 1][ageIdx] >= result.rows[i][ageIdx], 'Should be ordered by age DESC');
-  }
-}
-
-async function testSelectWithGroupBy() {
-  const result = await query(`
-    SELECT status, COUNT(*) as count
-    FROM orders
-    GROUP BY status
-    ORDER BY count DESC
-  `);
-
-  assert(result.rows.length > 0, 'Should have grouped results');
-
-  // Verify all statuses are represented
-  const totalCount = result.rows.reduce((sum, row) => sum + row[1], 0);
-  assertEqual(totalCount, CONFIG.numOrders, 'Grouped counts should sum to total orders');
-}
-
-async function testSelectWithHaving() {
-  const result = await query(`
-    SELECT user_id, COUNT(*) as order_count
-    FROM orders
-    GROUP BY user_id
-    HAVING COUNT(*) > 1
-    ORDER BY order_count DESC
-  `);
-
-  // All returned users should have more than 1 order
-  for (const row of result.rows) {
-    assert(row[1] > 1, 'Each user should have more than 1 order');
-  }
-}
-
-async function testSelectWithJoin() {
-  // INNER JOIN
-  const result = await query(`
-    SELECT o.id, u.username, o.status, o.total
-    FROM orders o
-    INNER JOIN users u ON o.user_id = u.id
-    LIMIT 10
-  `);
-
-  assertEqual(result.rows.length, 10, 'Should return 10 joined rows');
-  assertEqual(result.columns.length, 4, 'Should have 4 columns');
-
-  // LEFT JOIN
-  const result2 = await query(`
-    SELECT u.username, COUNT(o.id) as order_count
-    FROM users u
-    LEFT JOIN orders o ON u.id = o.user_id
-    GROUP BY u.id, u.username
-    LIMIT 10
-  `);
-
-  assertEqual(result2.rows.length, 10, 'Should return users with order counts');
-}
-
-async function testSelectWithMultipleJoins() {
-  process.stdout.write(`\n    → Executing 4-table JOIN (this may take a while with ${CONFIG.numOrderItems} items)...\n`);
-  const result = await query(`
-    SELECT
-      o.id as order_id,
-      u.username,
-      p.name as product_name,
-      oi.quantity,
-      oi.price
-    FROM orders o
-    INNER JOIN users u ON o.user_id = u.id
-    INNER JOIN order_items oi ON o.id = oi.order_id
-    INNER JOIN products p ON oi.product_id = p.id
-    WHERE o.id <= 50
-    LIMIT 20
-  `);
-
-  assertEqual(result.rows.length, 20, 'Should return 20 joined rows');
-  assertEqual(result.columns.length, 5, 'Should have 5 columns');
-}
-
-async function testAggregations() {
-  // COUNT
-  const count = await query('SELECT COUNT(*) FROM users');
-  assertEqual(count.rows[0][0], CONFIG.numUsers, 'COUNT should match');
-
-  // SUM
-  const sum = await query('SELECT SUM(balance) FROM users');
-  assert(sum.rows[0][0] > 0, 'SUM should be positive');
-
-  // AVG
-  const avg = await query('SELECT AVG(age) FROM users');
-  assert(avg.rows[0][0] >= 18, 'AVG age should be at least 18');
-
-  // MIN/MAX
-  const minMax = await query('SELECT MIN(age), MAX(age) FROM users');
-  assert(minMax.rows[0][0] >= 18, 'MIN age should be at least 18');
-  assert(minMax.rows[0][1] <= 68, 'MAX age should be at most 68');
-}
-
-async function testSubqueries() {
-  // Scalar subquery
-  const result = await query(`
-    SELECT username,
-           (SELECT COUNT(*) FROM orders WHERE user_id = users.id) as order_count
-    FROM users
-    WHERE id <= 5
-  `);
-
-  assertEqual(result.rows.length, 5, 'Should return 5 users');
-
-  // IN subquery
-  const result2 = await query(`
-    SELECT * FROM users
-    WHERE id IN (SELECT user_id FROM orders WHERE status = 'delivered')
-    LIMIT 10
-  `);
-
-  assert(result2.rows.length >= 0, 'IN subquery should work');
-
-  // EXISTS subquery
-  const result3 = await query(`
-    SELECT * FROM users u
-    WHERE EXISTS (SELECT 1 FROM orders WHERE user_id = u.id)
-    LIMIT 10
-  `);
-
-  assert(result3.rows.length > 0, 'EXISTS subquery should return users with orders');
-}
-
-async function testUpdate() {
-  // Update single row
-  await query('UPDATE users SET balance = ? WHERE id = ?', [999.99, 1]);
-
-  const result = await query('SELECT balance FROM users WHERE id = ?', [1]);
-  assertEqual(result.rows[0][0], 999.99, 'Balance should be updated');
-
-  // Update multiple rows
-  await query('UPDATE users SET active = ? WHERE age < ?', [0, 25]);
-
-  const result2 = await query('SELECT COUNT(*) FROM users WHERE active = 0');
-  assert(result2.rows[0][0] > 0, 'Should have inactive users');
-}
-
-async function testDelete() {
-  // Get current count
-  const before = await query('SELECT COUNT(*) FROM order_items');
-
-  // Delete some items
-  await query('DELETE FROM order_items WHERE quantity = 1');
-
-  const after = await query('SELECT COUNT(*) FROM order_items');
-  assert(after.rows[0][0] < before.rows[0][0], 'Should have fewer items after delete');
-}
-
-async function testCreateIndex() {
-  // Create index
-  await query('CREATE INDEX idx_users_email ON users(email)');
-  await query('CREATE INDEX idx_orders_user ON orders(user_id)');
-  await query('CREATE INDEX idx_products_category ON products(category_id)');
-
-  // Test that queries still work (index should be used transparently)
-  const result = await query('SELECT * FROM users WHERE email = ?', ['user1@example.com']);
-  assert(result.rows.length > 0, 'Should find user by email');
-}
-
-async function testTransaction() {
-  // Get initial balance
-  const before = await query('SELECT balance FROM users WHERE id = 2');
-  const initialBalance = before.rows[0][0];
-
-  // Start transaction and make changes
-  await fetch(`${BASE_URL}/transaction/begin`, { method: 'POST' });
-
-  await query('UPDATE users SET balance = balance + 100 WHERE id = 2');
-
-  // Verify change within transaction
-  const during = await query('SELECT balance FROM users WHERE id = 2');
-  assertEqual(during.rows[0][0], initialBalance + 100, 'Balance should increase during transaction');
-
-  // Commit transaction
-  await fetch(`${BASE_URL}/transaction/commit`, { method: 'POST' });
-
-  // Verify change persisted
-  const after = await query('SELECT balance FROM users WHERE id = 2');
-  assertEqual(after.rows[0][0], initialBalance + 100, 'Balance should be committed');
-}
-
-async function testBatchExecute() {
-  const statements = [
-    { sql: 'INSERT INTO categories (name, description) VALUES (?, ?)', params: ['Test1', 'Test category 1'] },
-    { sql: 'INSERT INTO categories (name, description) VALUES (?, ?)', params: ['Test2', 'Test category 2'] },
-    { sql: 'INSERT INTO categories (name, description) VALUES (?, ?)', params: ['Test3', 'Test category 3'] },
-  ];
-
-  const result = await execute(statements, true);
-  assertEqual(result.results.length, 3, 'Should execute 3 statements');
-
-  // Verify inserts
-  const count = await query('SELECT COUNT(*) FROM categories WHERE name LIKE ?', ['Test%']);
-  assertEqual(count.rows[0][0], 3, 'Should have 3 test categories');
-}
-
-async function testAlterTable() {
-  // Create test table
-  await query('CREATE TABLE test_alter (id INTEGER PRIMARY KEY, name TEXT)');
-
-  // Add column
-  await query('ALTER TABLE test_alter ADD COLUMN description TEXT');
-
-  // Verify column was added
-  const schema = await getTableSchema('test_alter');
-  const descCol = schema.columns.find(c => c.name === 'description');
-  assert(descCol, 'description column should exist');
-
-  // Rename column
-  await query('ALTER TABLE test_alter RENAME COLUMN name TO title');
-
-  const schema2 = await getTableSchema('test_alter');
-  const titleCol = schema2.columns.find(c => c.name === 'title');
-  assert(titleCol, 'title column should exist');
-}
-
-async function testLikeOperator() {
-  const result = await query('SELECT * FROM users WHERE username LIKE ?', ['user1%']);
-  assert(result.rows.length > 0, 'Should find users starting with user1');
-
-  const result2 = await query('SELECT * FROM users WHERE email LIKE ?', ['%@example.com']);
-  assertEqual(result2.rows.length, CONFIG.numUsers, 'All users should match email pattern');
-}
-
-async function testBetweenOperator() {
-  const result = await query('SELECT * FROM users WHERE age BETWEEN ? AND ?', [25, 35]);
-
-  const ageIdx = result.columns.findIndex(c => c.name === 'age');
-  for (const row of result.rows) {
-    assert(row[ageIdx] >= 25 && row[ageIdx] <= 35, 'Age should be between 25 and 35');
-  }
-}
-
-async function testCaseExpression() {
-  const result = await query(`
-    SELECT username,
-           CASE
-             WHEN age < 25 THEN 'young'
-             WHEN age < 40 THEN 'middle'
-             ELSE 'senior'
-           END as age_group
-    FROM users
-    LIMIT 10
-  `);
-
-  assertEqual(result.columns.length, 2, 'Should have 2 columns');
-
-  for (const row of result.rows) {
-    assert(['young', 'middle', 'senior'].includes(row[1]), 'Age group should be valid');
-  }
-}
-
-async function testNullHandling() {
-  // Insert a user with null age
-  await query('INSERT INTO users (username, email, age) VALUES (?, ?, ?)', ['nulltest', 'null@test.com', null]);
-
-  // Test IS NULL
-  const result = await query('SELECT * FROM users WHERE age IS NULL');
-  assert(result.rows.length > 0, 'Should find users with null age');
-
-  // Test COALESCE
-  const result2 = await query('SELECT username, COALESCE(age, 0) as age FROM users WHERE username = ?', ['nulltest']);
-  assertEqual(result2.rows[0][1], 0, 'COALESCE should return 0 for null');
-
-  // Test IFNULL
-  const result3 = await query('SELECT username, IFNULL(age, -1) as age FROM users WHERE username = ?', ['nulltest']);
-  assertEqual(result3.rows[0][1], -1, 'IFNULL should return -1 for null');
-}
-
-async function testStringFunctions() {
-  const result = await query(`
-    SELECT
-      UPPER(username) as upper_name,
-      LOWER(email) as lower_email,
-      LENGTH(username) as name_len
-    FROM users
-    WHERE id = 1
-  `);
-
-  assert(result.rows[0][0] === result.rows[0][0].toUpperCase(), 'UPPER should work');
-  assert(result.rows[0][1] === result.rows[0][1].toLowerCase(), 'LOWER should work');
-  assert(typeof result.rows[0][2] === 'number', 'LENGTH should return number');
-}
-
-async function testNumericFunctions() {
-  const result = await query(`
-    SELECT
-      ABS(-10) as abs_val,
-      ROUND(3.14159, 2) as rounded
-  `);
-
-  assertEqual(result.rows[0][0], 10, 'ABS should work');
-  assertEqual(result.rows[0][1], 3.14, 'ROUND should work');
-}
-
-async function testConcurrentQueries() {
-  const promises = [];
-
-  for (let i = 0; i < CONFIG.concurrentRequests; i++) {
-    promises.push(query('SELECT * FROM users WHERE id = ?', [i + 1]));
-  }
-
-  const results = await Promise.all(promises);
-
-  for (const result of results) {
-    assert(result.rows.length <= 1, 'Each query should return at most 1 row');
-  }
-}
-
-async function testLargeResultSet() {
-  // Query all users
-  const result = await query('SELECT * FROM users');
-  // Should have at least the configured users plus test users
-  // Test users: nulltest, paramtest, nulluser1, nulluser2, user'with'quotes, user"double"quotes, and potentially rollback_test
-  assert(result.rows.length >= CONFIG.numUsers, `Should return at least ${CONFIG.numUsers} users`);
-  assert(result.rows.length <= CONFIG.numUsers + 10, 'Should not have too many extra users');
-}
-
-async function testComplexQuery() {
-  process.stdout.write(`\n    → Executing complex aggregation with LEFT JOINs (${CONFIG.numUsers} users)...\n`);
-  const result = await query(`
-    SELECT
-      u.username,
-      COUNT(DISTINCT o.id) as order_count,
-      SUM(oi.quantity * oi.price) as total_spent,
-      AVG(oi.price) as avg_item_price
-    FROM users u
-    LEFT JOIN orders o ON u.id = o.user_id
-    LEFT JOIN order_items oi ON o.id = oi.order_id
-    WHERE u.id <= 100
-    GROUP BY u.id, u.username
-    HAVING COUNT(o.id) > 0
-    ORDER BY total_spent DESC
-    LIMIT 10
-  `);
-
-  assert(result.rows.length > 0, 'Should return users with orders');
-  assertEqual(result.columns.length, 4, 'Should have 4 columns');
-}
-
-async function testParameterTypes() {
-  // Test various parameter types
-  await query('INSERT INTO users (username, email, age, balance, active) VALUES (?, ?, ?, ?, ?)',
-    ['paramtest', 'param@test.com', 30, 123.45, true]);
-
-  const result = await query('SELECT * FROM users WHERE username = ?', ['paramtest']);
-  const row = result.rows[0];
-  const cols = result.columns;
-
-  const getValue = (name) => row[cols.findIndex(c => c.name === name)];
-
-  assertEqual(getValue('username'), 'paramtest', 'String param should work');
-  assertEqual(getValue('age'), 30, 'Integer param should work');
-  assertEqual(getValue('balance'), 123.45, 'Float param should work');
-  assertEqual(getValue('active'), 1, 'Boolean param should work (as 1)');
-}
-
-async function testDistinct() {
-  // Test DISTINCT with single column (might not be implemented)
-  try {
-    const result1 = await query('SELECT DISTINCT status FROM orders');
-    // If DISTINCT works, should have only distinct values (5 statuses)
-    // If not implemented, will return all rows
-    assert(result1.rows.length > 0, 'Should return rows');
-    
-    // If we get 5 or fewer rows, DISTINCT is working
-    if (result1.rows.length <= 10) {
-      console.log(`\n      ℹ DISTINCT appears to be working (${result1.rows.length} distinct values)`);
-    } else {
-      console.log(`\n      ⚠ DISTINCT may not be implemented (returned ${result1.rows.length} rows, expected ~5)`);
-    }
-  } catch (e) {
-    // DISTINCT might not be supported
-    if (e.message.includes('DISTINCT') || e.message.includes('syntax')) {
-      console.log('\n      ⚠ DISTINCT keyword not yet supported');
-    } else {
-      throw e;
-    }
-  }
-
-  // Test grouping as workaround for DISTINCT
-  const result2 = await query('SELECT status FROM orders GROUP BY status');
-  assert(result2.rows.length >= 1, 'Should group by status (alternative to DISTINCT)');
-}
-
-async function testSelfJoin() {
-  // Find users with same age (self-join)
-  const result = await query(`
-    SELECT u1.username, u2.username, u1.age
-    FROM users u1
-    INNER JOIN users u2 ON u1.age = u2.age AND u1.id < u2.id
-    WHERE u1.age = 25
-    LIMIT 5
-  `);
-  
-  assert(result.rows.length >= 0, 'Self-join should execute');
-}
-
-async function testPagination() {
-  // Test LIMIT and OFFSET for pagination
-  const page1 = await query('SELECT id, username FROM users ORDER BY id LIMIT 10 OFFSET 0');
-  const page2 = await query('SELECT id, username FROM users ORDER BY id LIMIT 10 OFFSET 10');
-  const page3 = await query('SELECT id, username FROM users ORDER BY id LIMIT 10 OFFSET 20');
-
-  assertEqual(page1.rows.length, 10, 'First page should have 10 rows');
-  assertEqual(page2.rows.length, 10, 'Second page should have 10 rows');
-  assertEqual(page3.rows.length, 10, 'Third page should have 10 rows');
-
-  // Ensure pages don't overlap
-  const firstId = page1.rows[0][0];
-  const secondId = page2.rows[0][0];
-  assert(secondId > firstId, 'Pages should not overlap');
-}
-
-async function testNullSorting() {
-  // Insert some NULL values
-  await query('INSERT INTO users (username, email, age, balance) VALUES (?, ?, ?, ?)',
-    ['nulluser1', 'null1@test.com', null, 100]);
-  await query('INSERT INTO users (username, email, age, balance) VALUES (?, ?, ?, ?)',
-    ['nulluser2', 'null2@test.com', 30, null]);
-
-  // Test NULL in ORDER BY
-  const result1 = await query('SELECT username, age FROM users WHERE username LIKE ? ORDER BY age LIMIT 10', ['null%']);
-  assert(result1.rows.length >= 2, 'Should include users with NULL age');
-
-  // Test NULL in WHERE
-  const result2 = await query('SELECT COUNT(*) FROM users WHERE age IS NULL');
-  assert(result2.rows[0][0] >= 1, 'Should find users with NULL age');
-
-  const result3 = await query('SELECT COUNT(*) FROM users WHERE balance IS NOT NULL');
-  assert(result3.rows[0][0] > 0, 'Should find users with non-NULL balance');
-}
-
-async function testComplexWhere() {
-  // Complex WHERE with multiple conditions and operators
-  const result = await query(`
-    SELECT username, age, balance
-    FROM users
-    WHERE (age > 30 AND balance > 500)
-       OR (age < 25 AND active = 1)
-       OR (username LIKE 'user1%')
-    ORDER BY age DESC
-    LIMIT 20
-  `);
-
-  assert(result.rows.length <= 20, 'Should respect LIMIT');
-  assert(result.rows.length > 0, 'Should match some users');
-}
-
-async function testStringOperations() {
-  // Test string concatenation (if supported)
-  const result1 = await query(`
-    SELECT username || '@' || 'domain.com' as email_alt
-    FROM users
-    WHERE id = 1
-  `);
-  assert(result1.rows.length === 1, 'Should concatenate strings');
-
-  // Test SUBSTRING (if supported)
-  try {
-    const result2 = await query(`
-      SELECT SUBSTRING(username, 1, 4) as short_name
-      FROM users
-      WHERE id <= 5
-    `);
-    assertEqual(result2.rows.length, 5, 'SUBSTRING should work');
-  } catch (e) {
-    // SUBSTRING might not be implemented
-  }
-}
-
-async function testMathOperations() {
-  // Test arithmetic in SELECT
-  const result1 = await query(`
-    SELECT 
-      balance,
-      balance * 1.1 as with_tax,
-      balance / 2 as half,
-      balance + 100 as bonus
-    FROM users
-    WHERE id = 1
-  `);
-  assertEqual(result1.rows.length, 1, 'Should calculate arithmetic');
-
-  // Test modulo
-  const result2 = await query(`
-    SELECT id, id % 10 as mod_result
-    FROM users
-    WHERE id <= 100
-    LIMIT 10
-  `);
-  assertEqual(result2.rows.length, 10, 'Should calculate modulo');
-}
-
-async function testGroupByEdgeCases() {
-  // GROUP BY with NULL values
-  const result1 = await query(`
-    SELECT age, COUNT(*) as count
-    FROM users
-    GROUP BY age
-    ORDER BY count DESC
-    LIMIT 10
-  `);
-  assert(result1.rows.length > 0, 'Should group including NULL values');
-
-  // GROUP BY with multiple aggregates
-  const result2 = await query(`
-    SELECT 
-      status,
-      COUNT(*) as order_count,
-      AVG(total) as avg_total,
-      MIN(total) as min_total,
-      MAX(total) as max_total
-    FROM orders
-    GROUP BY status
-  `);
-  assert(result2.rows.length > 0, 'Should compute multiple aggregates');
-  assertEqual(result2.columns.length, 5, 'Should have 5 columns');
-}
-
-async function testCrossJoin() {
-  // CROSS JOIN (Cartesian product) with LIMIT
-  const result = await query(`
-    SELECT c.name as category, p.name as product
-    FROM categories c
-    CROSS JOIN products p
-    WHERE p.id <= 10
-    LIMIT 20
-  `);
-
-  assert(result.rows.length <= 20, 'Should respect LIMIT on CROSS JOIN');
-  assertEqual(result.columns.length, 2, 'Should have 2 columns');
-}
-
-async function testNestedSubqueries() {
-  // Nested subqueries (subquery in WHERE with subquery in SELECT)
-  const result = await query(`
-    SELECT 
-      username,
-      (SELECT COUNT(*) FROM orders WHERE user_id = users.id) as order_count
-    FROM users
-    WHERE id IN (
-      SELECT user_id 
-      FROM orders 
-      WHERE total > 100
-      LIMIT 50
-    )
-    LIMIT 10
-  `);
-
-  assert(result.rows.length <= 10, 'Should handle nested subqueries');
-}
-
-async function testEdgeCaseValues() {
-  // Test with special characters and edge values
-  await query(`INSERT INTO users (username, email, age, balance) VALUES (?, ?, ?, ?)`,
-    ["user'with'quotes", 'special@example.com', 0, 0.01]);
-  
-  await query(`INSERT INTO users (username, email, age, balance) VALUES (?, ?, ?, ?)`,
-    ['user"double"quotes', 'double@example.com', 150, 999999.99]);
-
-  // Query them back
-  const result = await query(`SELECT username FROM users WHERE username LIKE ?`, ["%quotes%"]);
-  assert(result.rows.length >= 2, 'Should handle special characters in strings');
-
-  // Test very large numbers
-  const result2 = await query(`SELECT balance FROM users WHERE balance > 999999`);
-  assert(result2.rows.length >= 1, 'Should handle large numbers');
-}
-
-async function testInWithMultipleValues() {
-  // Test IN clause with multiple literal values
-  const result = await query(`
-    SELECT id, username
-    FROM users
-    WHERE id IN (1, 5, 10, 15, 20, 25, 30)
-    ORDER BY id
-  `);
-
-  assert(result.rows.length <= 7, 'Should filter by IN clause');
-  assert(result.rows.length > 0, 'Should find matching users');
-}
-
-async function testUnion() {
-  // Test UNION (if supported)
-  try {
-    const result = await query(`
-      SELECT username, 'high' as segment FROM users WHERE balance > 800
-      UNION
-      SELECT username, 'low' as segment FROM users WHERE balance < 200
-      LIMIT 20
-    `);
-    assert(result.rows.length <= 20, 'UNION should combine results');
-  } catch (e) {
-    // UNION might not be implemented yet
-    if (!e.message.includes('UNION')) {
-      throw e;
-    }
-  }
-}
-
-async function testTransactionRollback() {
-  // Get count before
-  const before = await query('SELECT COUNT(*) FROM users WHERE username LIKE ?', ['rollback_%']);
-  const beforeCount = before.rows[0][0];
-  
-  // Test transaction rollback
-  try {
-    await query('BEGIN TRANSACTION');
-    
-    // Insert a user
-    await query('INSERT INTO users (username, email, age) VALUES (?, ?, ?)',
-      ['rollback_test_tx', 'rollback@test.com', 99]);
-    
-    // Rollback
-    await query('ROLLBACK');
-  } catch (e) {
-    // If transactions fail, try to clean up
-    try {
-      await query('DELETE FROM users WHERE username = ?', ['rollback_test_tx']);
-    } catch {}
-  }
-  
-  // Verify rollback worked or data was cleaned up
-  const after = await query('SELECT COUNT(*) FROM users WHERE username LIKE ?', ['rollback_%']);
-  const afterCount = after.rows[0][0];
-  
-  // Should be same or less (in case we had to manually clean up)
-  assert(afterCount <= beforeCount + 1, 'Rollback should undo changes or data should be cleanable');
-}
-
-// ============================================================================
-// Main Test Runner
-// ============================================================================
-
-async function main() {
-  console.log('╔════════════════════════════════════════════════════════════╗');
-  console.log('║           PizzaSQL Stress Test Suite                       ║');
-  console.log('╚════════════════════════════════════════════════════════════╝');
-  console.log(`\nTarget: ${BASE_URL}`);
-  console.log(`Config: ${CONFIG.numUsers} users, ${CONFIG.numProducts} products, ${CONFIG.numOrders} orders`);
-  console.log(`        ${CONFIG.numOrderItems} order items, ${CONFIG.concurrentRequests} concurrent requests\n`);
-
-  const startTime = Date.now();
-
-  // Health check
-  console.log('🔍 Checking server health...');
-  try {
-    const health = await getHealth();
-    console.log(`   Server status: ${health.status}`);
-    console.log(`   Database: ${health.database || 'default'}\n`);
-  } catch (error) {
-    console.error(`\n❌ Cannot connect to server: ${error.message}`);
-    console.error('   Make sure PizzaSQL is running with: ./pizzasql -http\n');
-    process.exit(1);
-  }
-
-  // Cleanup
-  console.log('🧹 Cleaning up existing tables...');
-  const cleanupStart = Date.now();
-  await cleanupTables();
-  const cleanupTime = Date.now() - cleanupStart;
-  console.log(`   Done (${cleanupTime}ms)\n`);
-
-  // Schema Tests
-  console.log('📋 SCHEMA TESTS');
-  console.log('─'.repeat(60));
-  await runTest('Health check endpoint', testHealthCheck);
-  await runTest('Create tables', testCreateTables);
-  await runTest('Schema introspection', testSchemaIntrospection);
-  console.log();
-
-  // Insert Tests
-  console.log('📥 INSERT TESTS');
-  console.log('─'.repeat(60));
-  await runTest(`Insert ${CONFIG.numUsers} users`, testInsertUsers);
-  await runTest('Insert categories', testInsertCategories);
-  await runTest(`Insert ${CONFIG.numProducts} products`, testInsertProducts);
-  await runTest(`Insert ${CONFIG.numOrders} orders`, testInsertOrders);
-  await runTest(`Insert ${CONFIG.numOrderItems} order items`, testInsertOrderItems);
-  
-  // Show data summary
-  console.log('\n  📊 Database Statistics:');
-  const userCount = await query('SELECT COUNT(*) FROM users');
-  const productCount = await query('SELECT COUNT(*) FROM products');
-  const orderCount = await query('SELECT COUNT(*) FROM orders');
-  const itemCount = await query('SELECT COUNT(*) FROM order_items');
-  console.log(`     Users:       ${userCount.rows[0][0].toLocaleString()}`);
-  console.log(`     Products:    ${productCount.rows[0][0].toLocaleString()}`);
-  console.log(`     Orders:      ${orderCount.rows[0][0].toLocaleString()}`);
-  console.log(`     Order Items: ${itemCount.rows[0][0].toLocaleString()}`);
-  const totalRows = userCount.rows[0][0] + productCount.rows[0][0] + orderCount.rows[0][0] + itemCount.rows[0][0] + 5;
-  console.log(`     Total Rows:  ${totalRows.toLocaleString()}`);
-  console.log();
-
-  // Select Tests
-  console.log('🔎 SELECT TESTS');
-  console.log('─'.repeat(60));
-  await runTest('Basic SELECT queries', testSelectBasic);
-  await runTest('SELECT with ORDER BY', testSelectWithOrderBy);
-  await runTest('SELECT with GROUP BY', testSelectWithGroupBy);
-  await runTest('SELECT with HAVING', testSelectWithHaving);
-  await runTest('SELECT with JOIN', testSelectWithJoin);
-  await runTest('SELECT with multiple JOINs', testSelectWithMultipleJoins);
-  await runTest('Aggregation functions', testAggregations);
-  await runTest('Subqueries', testSubqueries);
-  console.log();
-
-  // Expression Tests
-  console.log('🧮 EXPRESSION TESTS');
-  console.log('─'.repeat(60));
-  await runTest('LIKE operator', testLikeOperator);
-  await runTest('BETWEEN operator', testBetweenOperator);
-  await runTest('CASE expression', testCaseExpression);
-  await runTest('NULL handling', testNullHandling);
-  await runTest('String functions', testStringFunctions);
-  await runTest('Numeric functions', testNumericFunctions);
-  await runTest('Parameter types', testParameterTypes);
-  await runTest('DISTINCT queries', testDistinct);
-  await runTest('Math operations', testMathOperations);
-  await runTest('String operations', testStringOperations);
-  console.log();
-
-  // JOIN and Query Complexity Tests
-  console.log('🔗 ADVANCED JOIN TESTS');
-  console.log('─'.repeat(60));
-  await runTest('Self-join', testSelfJoin);
-  await runTest('CROSS JOIN', testCrossJoin);
-  console.log();
-
-  // Data Integrity Tests
-  console.log('🛡️  DATA INTEGRITY TESTS');
-  console.log('─'.repeat(60));
-  await runTest('NULL in sorting', testNullSorting);
-  await runTest('Complex WHERE clauses', testComplexWhere);
-  await runTest('GROUP BY edge cases', testGroupByEdgeCases);
-  await runTest('Edge case values', testEdgeCaseValues);
-  await runTest('IN with multiple values', testInWithMultipleValues);
-  console.log();
-
-  // Query Features Tests
-  console.log('🎯 QUERY FEATURES');
-  console.log('─'.repeat(60));
-  await runTest('Pagination (LIMIT/OFFSET)', testPagination);
-  await runTest('Nested subqueries', testNestedSubqueries);
-  await runTest('UNION operations', testUnion);
-  console.log();
-
-  // Update/Delete Tests
-  console.log('✏️  UPDATE/DELETE TESTS');
-  console.log('─'.repeat(60));
-  await runTest('UPDATE records', testUpdate);
-  await runTest('DELETE records', testDelete);
-  console.log();
-
-  // Advanced Features Tests
-  console.log('🚀 ADVANCED FEATURES TESTS');
-  console.log('─'.repeat(60));
-  await runTest('Create indexes', testCreateIndex);
-  await runTest('Transaction handling', testTransaction);
-  await runTest('Transaction rollback', testTransactionRollback);
-  await runTest('Batch execute', testBatchExecute);
-  await runTest('ALTER TABLE', testAlterTable);
-  console.log();
-
-  // Performance Tests
-  console.log('⚡ PERFORMANCE TESTS');
-  console.log('─'.repeat(60));
-  await runTest('Concurrent queries', testConcurrentQueries);
-  await runTest('Large result set', testLargeResultSet);
-  await runTest('Complex query', testComplexQuery);
-  console.log();
-
-  const totalTime = Date.now() - startTime;
-
-  // Summary
-  console.log('╔════════════════════════════════════════════════════════════╗');
-  console.log('║                      TEST SUMMARY                          ║');
-  console.log('╚════════════════════════════════════════════════════════════╝');
-  console.log();
-  console.log(`  Total tests:     ${stats.passed + stats.failed}`);
-  console.log(`  Passed:          ${stats.passed} ✓`);
-  console.log(`  Failed:          ${stats.failed} ✗`);
-  console.log(`  Success rate:    ${((stats.passed / (stats.passed + stats.failed)) * 100).toFixed(1)}%`);
-  console.log();
-  console.log(`  Total queries:   ${stats.totalQueries}`);
-  console.log(`  Total time:      ${totalTime}ms`);
-  console.log(`  Avg query time:  ${(stats.totalTime / stats.totalQueries).toFixed(2)}ms`);
-  console.log(`  Queries/sec:     ${(stats.totalQueries / (totalTime / 1000)).toFixed(0)}`);
-  console.log();
-
-  if (stats.errors.length > 0) {
-    console.log('Failed tests:');
-    for (const err of stats.errors) {
-      console.log(`  - ${err.name}: ${err.error}`);
-    }
-    console.log();
-  }
-
-  if (stats.failed > 0) {
-    process.exit(1);
-  }
-
-  console.log('All tests passed! 🍕');
-}
-
-main().catch((error) => {
-  console.error('Fatal error:', error);
-  process.exit(1);
-});

+ 0 - 83
test_distinct.js

@@ -1,83 +0,0 @@
-const http = require('http');
-
-async function query(sql) {
-  return new Promise((resolve, reject) => {
-    const data = JSON.stringify({ sql });
-    const options = {
-      hostname: 'localhost',
-      port: 8080,
-      path: '/query',
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json',
-        'Content-Length': data.length
-      }
-    };
-
-    const req = http.request(options, (res) => {
-      let body = '';
-      res.on('data', (chunk) => body += chunk);
-      res.on('end', () => {
-        try {
-          resolve(JSON.parse(body));
-        } catch (e) {
-          reject(e);
-        }
-      });
-    });
-
-    req.on('error', reject);
-    req.write(data);
-    req.end();
-  });
-}
-
-async function test() {
-  console.log('Testing DISTINCT implementation...\n');
-
-  // Create table
-  console.log('1. Creating table...');
-  try {
-    await query('DROP TABLE test_distinct');
-  } catch (e) {
-    // Ignore error if table doesn't exist
-  }
-  await query('CREATE TABLE test_distinct (id INTEGER, status TEXT)');
-  console.log('   ✓ Table created\n');
-
-  // Insert data
-  console.log('2. Inserting data...');
-  await query("INSERT INTO test_distinct VALUES (1, 'pending')");
-  await query("INSERT INTO test_distinct VALUES (2, 'completed')");
-  await query("INSERT INTO test_distinct VALUES (3, 'pending')");
-  await query("INSERT INTO test_distinct VALUES (4, 'shipped')");
-  await query("INSERT INTO test_distinct VALUES (5, 'pending')");
-  await query("INSERT INTO test_distinct VALUES (6, 'completed')");
-  console.log('   ✓ Inserted 6 rows\n');
-
-  // Query without DISTINCT
-  console.log('3. SELECT status FROM test_distinct:');
-  const result1 = await query('SELECT status FROM test_distinct ORDER BY status');
-  console.log(`   Rows: ${result1.rows.length}`);
-  console.log('   Values:', result1.rows.map(r => r[0]).join(', '));
-  console.log();
-
-  // Query with DISTINCT
-  console.log('4. SELECT DISTINCT status FROM test_distinct:');
-  const result2 = await query('SELECT DISTINCT status FROM test_distinct ORDER BY status');
-  console.log(`   Rows: ${result2.rows.length}`);
-  console.log('   Values:', result2.rows.map(r => r[0]).join(', '));
-  console.log();
-
-  // Verify
-  if (result2.rows.length === 3) {
-    console.log('✅ DISTINCT is working correctly!');
-  } else {
-    console.log(`❌ DISTINCT failed - expected 3 unique values, got ${result2.rows.length}`);
-  }
-
-  // Clean up
-  await query('DROP TABLE test_distinct');
-}
-
-test().catch(console.error);

+ 0 - 95
test_distinct_simple.js

@@ -1,95 +0,0 @@
-const http = require('http');
-
-async function query(sql) {
-  return new Promise((resolve, reject) => {
-    const data = JSON.stringify({ sql });
-    const options = {
-      hostname: 'localhost',
-      port: 8080,
-      path: '/query',
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json',
-        'Content-Length': data.length
-      },
-      timeout: 10000 // 10 second timeout
-    };
-
-    const req = http.request(options, (res) => {
-      let body = '';
-      res.on('data', (chunk) => body += chunk);
-      res.on('end', () => {
-        try {
-          const result = JSON.parse(body);
-          if (result.error) {
-            reject(new Error(result.error.message));
-          } else {
-            resolve(result);
-          }
-        } catch (e) {
-          reject(e);
-        }
-      });
-    });
-
-    req.on('error', reject);
-    req.on('timeout', () => {
-      req.destroy();
-      reject(new Error('Request timeout'));
-    });
-    req.write(data);
-    req.end();
-  });
-}
-
-async function testDistinct() {
-  try {
-    console.log('Testing DISTINCT implementation...\n');
-
-    // Create table
-    console.log('1. Creating table...');
-    await query('CREATE TABLE test_distinct (id INTEGER, status TEXT)');
-    console.log('   ✓ Table created\n');
-
-    // Insert data
-    console.log('2. Inserting data...');
-    for (let i = 0; i < 10; i++) {
-      const statuses = ['pending', 'completed', 'shipped'];
-      const status = statuses[i % 3];
-      await query(`INSERT INTO test_distinct VALUES (${i}, '${status}')`);
-    }
-    console.log('   ✓ Inserted 10 rows\n');
-
-    // Query without DISTINCT
-    console.log('3. SELECT status FROM test_distinct:');
-    const result1 = await query('SELECT status FROM test_distinct ORDER BY status');
-    console.log(`   Rows: ${result1.rows.length}`);
-    console.log();
-
-    // Query with DISTINCT
-    console.log('4. SELECT DISTINCT status FROM test_distinct:');
-    const result2 = await query('SELECT DISTINCT status FROM test_distinct ORDER BY status');
-    console.log(`   Rows: ${result2.rows.length}`);
-    console.log('   Values:', result2.rows.map(r => r[0]).join(', '));
-    console.log();
-
-    // Verify
-    if (result2.rows.length === 3) {
-      console.log('✅ DISTINCT is working correctly!');
-      console.log(`   Expected 3 unique values, got ${result2.rows.length}`);
-    } else {
-      console.log(`❌ DISTINCT failed - expected 3 unique values, got ${result2.rows.length}`);
-    }
-
-    // Clean up
-    console.log('\n5. Cleaning up...');
-    await query('DROP TABLE test_distinct');
-    console.log('   ✓ Table dropped');
-
-  } catch (error) {
-    console.error('Error:', error.message);
-    process.exit(1);
-  }
-}
-
-testDistinct();