tinysql

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 1, 2025 License: AGPL-3.0 Imports: 4 Imported by: 0

README

TinySQL

DOI Go Report Card

TinySQL is a lightweight, educational SQL database engine written in pure Go. It implements a comprehensive subset of SQL features using only Go's standard library, making it perfect for learning database internals and for applications that need a simple embedded SQL database.

Quick start

Install
go get github.com/SimonWaldherr/tinySQL@latest
Use the engine directly
package main

import (
    "context"
    "fmt"
    tsql "github.com/SimonWaldherr/tinySQL"
)

func main() {
    db := tsql.NewDB()

    p := tsql.NewParser(`CREATE TABLE users (id INT, name TEXT)`)
    st, _ := p.ParseStatement()
    tsql.Execute(context.Background(), db, "default", st)

    p = tsql.NewParser(`INSERT INTO users VALUES (1, 'Alice')`)
    st, _ = p.ParseStatement()
    tsql.Execute(context.Background(), db, "default", st)

    p = tsql.NewParser(`SELECT id, name FROM users`)
    st, _ = p.ParseStatement()
    rs, _ := tsql.Execute(context.Background(), db, "default", st)

    for _, row := range rs.Rows {
        fmt.Println(tsql.GetVal(row, "id"), tsql.GetVal(row, "name"))
    }
}
Use with database/sql
package main

import (
    "database/sql"
    "fmt"
    _ "github.com/SimonWaldherr/tinySQL/internal/driver"
)

func main() {
    db, _ := sql.Open("tinysql", "mem://?tenant=default")
    defer db.Close()

    db.Exec(`CREATE TABLE t (id INT, name TEXT)`)
    db.Exec(`INSERT INTO t VALUES (?, ?)`, 1, "Alice")

    row := db.QueryRow(`SELECT name FROM t WHERE id = ?`, 1)
    var name string
    _ = row.Scan(&name)
    fmt.Println(name)
}

Run tests

# no cache
go test ./... -count=1

# with coverage output
go test -coverprofile=coverage.out ./...

Goals (and non-goals)

  • Lightweight, educational SQL engine in pure Go
  • Useful for embeddings, demos, and learning database internals
  • Not intended as a production-grade relational database

Requirements

  • Go 1.25+ (see go.mod)

DSN (Data Source Name) Format

When using the database/sql driver:

  • In-memory database: mem://?tenant=<tenant_name>
  • File-based database: file:/path/to/db.dat?tenant=<tenant_name>&autosave=1

Parameters:

  • tenant - Tenant name for multi-tenancy (required)
  • autosave - Auto-save to file (optional, for file-based databases)

Limitations

TinySQL is designed for educational purposes

Testing

Run the test suite:

# Run all tests
go test ./...

# Run tests with verbose output
go test -v ./...

# Run tests multiple times to check consistency
go test -v -count=3 ./...

Contributing

This is an educational project. Contributions that improve code clarity, add comprehensive examples, or enhance the learning experience are welcome.

Educational Goals

TinySQL demonstrates:

  • SQL parsing and AST construction
  • Query execution and optimization basics
  • Database storage concepts
  • Go's database/sql driver interface
  • 3-valued logic (NULL semantics)
  • JSON data handling in SQL
  • Multi-tenancy patterns

Perfect for computer science students, developers learning database internals, or anyone who wants to understand how SQL databases work under the hood.

Documentation

Overview

Example

Example demonstrates the usage of the TinySQL engine

package main

import (
	"context"
	"fmt"
	"strings"

	tsql "github.com/SimonWaldherr/tinySQL"
)

func main() {
	db := tsql.NewDB()

	dedent := func(s string) string {
		trimmed := strings.TrimSpace(s)
		if !strings.Contains(trimmed, "\n") {
			return trimmed
		}
		lines := strings.Split(trimmed, "\n")
		indent := -1
		for _, line := range lines[1:] {
			if strings.TrimSpace(line) == "" {
				continue
			}
			leading := len(line) - len(strings.TrimLeft(line, " \t"))
			if indent == -1 || leading < indent {
				indent = leading
			}
		}
		if indent > 0 {
			for i := 1; i < len(lines); i++ {
				if strings.TrimSpace(lines[i]) == "" {
					lines[i] = ""
					continue
				}
				if len(lines[i]) >= indent {
					lines[i] = lines[i][indent:]
				}
			}
		}
		for i, line := range lines {
			lines[i] = strings.TrimRight(line, " \t")
		}
		return strings.Join(lines, "\n")
	}

	run := func(sql string) {
		display := dedent(sql)
		fmt.Println("SQL>", display)
		p := tsql.NewParser(sql)
		st, err := p.ParseStatement()
		if err != nil {
			fmt.Println("ERR:", err)
			fmt.Println()
			return
		}
		rs, err := tsql.Execute(context.Background(), db, "default", st)
		if err != nil {
			fmt.Println("ERR:", err)
			fmt.Println()
			return
		}
		if rs == nil {
			fmt.Println()
			return
		}
		if len(rs.Rows) == 1 && len(rs.Cols) == 1 && (rs.Cols[0] == "updated" || rs.Cols[0] == "deleted") {
			if val, ok := tsql.GetVal(rs.Rows[0], rs.Cols[0]); ok {
				fmt.Printf("%s: %v\n\n", rs.Cols[0], val)
				return
			}
		}
		displayCols := make([]string, len(rs.Cols))
		for i, col := range rs.Cols {
			parts := strings.Split(col, ".")
			displayCols[i] = parts[len(parts)-1]
		}
		fmt.Println(strings.Join(displayCols, " | "))
		for _, row := range rs.Rows {
			cells := make([]string, len(rs.Cols))
			for i, col := range rs.Cols {
				if v, ok := tsql.GetVal(row, col); ok {
					cells[i] = fmt.Sprint(v)
				} else {
					cells[i] = ""
				}
			}
			fmt.Println(strings.Join(cells, " | "))
		}
		fmt.Println()
	}

	// --- Create table and seed data ---
	run(`CREATE TABLE users (
		id INT,
		name TEXT,
		active BOOL,
		score INT
	)`)

	run(`INSERT INTO users (id, name, active, score) VALUES (1, 'Alice', true, 40)`)
	run(`INSERT INTO users (id, name, active, score) VALUES (2, 'Bob', false, 25)`)
	run(`INSERT INTO users (id, name, active, score) VALUES (3, 'Carol', true, 30)`)

	// --- Basic reads ---
	run(`SELECT id, name, active, score FROM users ORDER BY id`)
	run(`SELECT name, score FROM users WHERE active = true ORDER BY score DESC`)

	// --- Update a row ---
	run(`UPDATE users SET score = 50 WHERE name = 'Bob'`)
	run(`SELECT name, score FROM users ORDER BY id`)

	// --- Aggregate summary ---
	run(`SELECT COUNT(*) AS total_users, SUM(score) AS total_score FROM users`)

	// --- Delete inactive rows ---
	run(`DELETE FROM users WHERE active = false`)
	run(`SELECT name FROM users ORDER BY id`)

}
Output:

SQL> CREATE TABLE users (
	id INT,
	name TEXT,
	active BOOL,
	score INT
)

SQL> INSERT INTO users (id, name, active, score) VALUES (1, 'Alice', true, 40)

SQL> INSERT INTO users (id, name, active, score) VALUES (2, 'Bob', false, 25)

SQL> INSERT INTO users (id, name, active, score) VALUES (3, 'Carol', true, 30)

SQL> SELECT id, name, active, score FROM users ORDER BY id
id | name | active | score
1 | Alice | true | 40
2 | Bob | false | 25
3 | Carol | true | 30

SQL> SELECT name, score FROM users WHERE active = true ORDER BY score DESC
name | score
Alice | 40
Carol | 30

SQL> UPDATE users SET score = 50 WHERE name = 'Bob'
updated: 1

SQL> SELECT name, score FROM users ORDER BY id
name | score
Alice | 40
Bob | 50
Carol | 30

SQL> SELECT COUNT(*) AS total_users, SUM(score) AS total_score FROM users
total_users | total_score
3 | 120

SQL> DELETE FROM users WHERE active = false
deleted: 1

SQL> SELECT name FROM users ORDER BY id
name
Alice
Carol

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Compile

func Compile(cache *engine.QueryCache, sql string) (*engine.CompiledQuery, error)

Compile parses and caches a SQL query for reuse (like regexp.Compile)

func Execute

func Execute(ctx context.Context, db *storage.DB, tenant string, stmt engine.Statement) (*engine.ResultSet, error)

Execute executes a SQL statement

func ExecuteCompiled

func ExecuteCompiled(ctx context.Context, db *storage.DB, tenant string, compiled *engine.CompiledQuery) (*engine.ResultSet, error)

ExecuteCompiled executes a compiled query

func GetVal

func GetVal(row engine.Row, name string) (any, bool)

GetVal retrieves a value from a row by column name

func LoadFromFile

func LoadFromFile(filename string) (*storage.DB, error)

LoadFromFile loads a database from a GOB file

func MustCompile

func MustCompile(cache *engine.QueryCache, sql string) *engine.CompiledQuery

MustCompile is like Compile but panics on error (like regexp.MustCompile)

func NewDB

func NewDB() *storage.DB

NewDB creates a new database instance

func NewParser

func NewParser(sql string) *engine.Parser

NewParser creates a new SQL parser

func NewQueryCache

func NewQueryCache(maxSize int) *engine.QueryCache

NewQueryCache creates a new query cache for compiling and reusing queries

func SaveToFile

func SaveToFile(db *storage.DB, filename string) error

SaveToFile saves the database to a GOB file for persistence

Types

This section is empty.

Directories

Path Synopsis
cmd
demo command
repl command
server command
wasm_browser command
wasm_node command
internal
driver
Package driver implements a database/sql driver for tinySQL.
Package driver implements a database/sql driver for tinySQL.
engine
Package engine provides SQL parsing, planning, and execution for tinySQL.
Package engine provides SQL parsing, planning, and execution for tinySQL.
storage
Package storage provides the durable data structures for tinySQL.
Package storage provides the durable data structures for tinySQL.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL