Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
module github.com/aybavs/sql-query-engine

go 1.22
go 1.25.0

require modernc.org/sqlite v1.56.0

require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/sys v0.47.0 // indirect
modernc.org/libc v1.74.4 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
50 changes: 50 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo=
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI=
modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.74.4 h1:fX1Omw4o2/1C2iRkkIsrQTasJQldLhRmuPreXLoWs9k=
modernc.org/libc v1.74.4/go.mod h1:eeQAS9W3sZeKYMFubydxJpII9ybHWshk+7or7bLG9co=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.56.0 h1:/D8e2RfFqoy/Zc6PuC76U28zFwmI/sYx1Kjm4yEn9e0=
modernc.org/sqlite v1.56.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
22 changes: 21 additions & 1 deletion internal/catalog/catalog.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
// Package catalog holds table schemas and resolves table/column references.
package catalog

import "github.com/aybavs/sql-query-engine/internal/value"
import (
"sort"

"github.com/aybavs/sql-query-engine/internal/value"
)

type Column struct {
Name string
Expand Down Expand Up @@ -35,3 +39,19 @@ func (c *Catalog) Table(name string) (*Table, bool) {
t, ok := c.tables[name]
return t, ok
}

// Tables returns every registered table, ordered by name so callers that
// iterate the catalog behave deterministically.
func (c *Catalog) Tables() []*Table {
names := make([]string, 0, len(c.tables))
for n := range c.tables {
names = append(names, n)
}
sort.Strings(names)

out := make([]*Table, 0, len(names))
for _, n := range names {
out = append(out, c.tables[n])
}
return out
}
10 changes: 10 additions & 0 deletions internal/catalog/catalog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,13 @@ func TestCatalogLookup(t *testing.T) {
t.Fatal("unknown column should not resolve")
}
}

func TestTablesIsSortedAndComplete(t *testing.T) {
c := New()
c.Add(&Table{Name: "users"})
c.Add(&Table{Name: "orders"})
got := c.Tables()
if len(got) != 2 || got[0].Name != "orders" || got[1].Name != "users" {
t.Fatalf("Tables() = %v, want [orders users]", got)
}
}
128 changes: 128 additions & 0 deletions internal/difftest/oracle.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Package difftest compares this engine against SQLite on generated queries.
// It is test-only: nothing outside its own tests imports it, so the SQLite
// driver never enters the engine's build.
package difftest

import (
"database/sql"
"fmt"
"path/filepath"
"strings"

"github.com/aybavs/sql-query-engine/internal/catalog"
"github.com/aybavs/sql-query-engine/internal/csv"
"github.com/aybavs/sql-query-engine/internal/value"

_ "modernc.org/sqlite"
)

// Row is one result row of driver values (int64, float64, string, or nil).
type Row []any

// Oracle is an in-memory SQLite database loaded with the same fixture data as
// the engine under test, used as the source of truth for query results.
type Oracle struct{ db *sql.DB }

func NewOracle(cat *catalog.Catalog, dataDir string) (*Oracle, error) {
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
return nil, err
}
o := &Oracle{db: db}
for _, t := range cat.Tables() {
if err := o.load(t, dataDir); err != nil {
db.Close()
return nil, err
}
}
return o, nil
}

func (o *Oracle) load(t *catalog.Table, dataDir string) error {
cols := make([]string, len(t.Columns))
for i, c := range t.Columns {
cols[i] = c.Name + " " + sqliteType(c.Type)
}
create := fmt.Sprintf("CREATE TABLE %s (%s)", t.Name, strings.Join(cols, ", "))
if _, err := o.db.Exec(create); err != nil {
return fmt.Errorf("create %s: %w", t.Name, err)
}

rows, err := csv.Read(filepath.Join(dataDir, t.File), t.Columns)
if err != nil {
return err
}
placeholders := strings.TrimSuffix(strings.Repeat("?,", len(t.Columns)), ",")
insert := fmt.Sprintf("INSERT INTO %s VALUES (%s)", t.Name, placeholders)
for _, r := range rows {
args := make([]any, len(r))
for i, v := range r {
args[i] = driverValue(v)
}
if _, err := o.db.Exec(insert, args...); err != nil {
return fmt.Errorf("insert into %s: %w", t.Name, err)
}
}
return nil
}

// driverValue converts an engine value into what the SQLite driver expects,
// preserving NULL rather than substituting a zero value.
func driverValue(v value.Value) any {
if v.IsNull() {
return nil
}
switch v.Type {
case value.TInt:
return v.I
case value.TFloat:
return v.F
case value.TBool:
return v.B
default:
return v.S
}
}

func sqliteType(t value.Type) string {
switch t {
case value.TInt:
return "INTEGER"
case value.TFloat:
return "REAL"
case value.TBool:
return "INTEGER"
default:
return "TEXT"
}
}

// Query runs a statement and returns its rows as driver values.
func (o *Oracle) Query(query string) ([]Row, error) {
rs, err := o.db.Query(query)
if err != nil {
return nil, err
}
defer rs.Close()

cols, err := rs.Columns()
if err != nil {
return nil, err
}

var out []Row
for rs.Next() {
cells := make([]any, len(cols))
ptrs := make([]any, len(cols))
for i := range cells {
ptrs[i] = &cells[i]
}
if err := rs.Scan(ptrs...); err != nil {
return nil, err
}
out = append(out, Row(cells))
}
return out, rs.Err()
}

func (o *Oracle) Close() error { return o.db.Close() }
73 changes: 73 additions & 0 deletions internal/difftest/oracle_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package difftest

import (
"os"
"path/filepath"
"testing"

"github.com/aybavs/sql-query-engine/internal/catalog"
"github.com/aybavs/sql-query-engine/internal/value"
)

func fixture(t *testing.T) (*catalog.Catalog, string) {
t.Helper()
dir := t.TempDir()
os.WriteFile(filepath.Join(dir, "users.csv"), []byte("1,alice,30\n2,bob,\n"), 0o644)
cat := catalog.New()
cat.Add(&catalog.Table{Name: "users", File: "users.csv", Columns: []catalog.Column{
{Name: "id", Type: value.TInt},
{Name: "name", Type: value.TText},
{Name: "age", Type: value.TInt},
}})
return cat, dir
}

func TestOracleLoadsAndQueries(t *testing.T) {
cat, dir := fixture(t)
o, err := NewOracle(cat, dir)
if err != nil {
t.Fatalf("NewOracle: %v", err)
}
defer o.Close()

rows, err := o.Query("SELECT name FROM users ORDER BY name")
if err != nil {
t.Fatalf("Query: %v", err)
}
if len(rows) != 2 {
t.Fatalf("rows = %v, want 2", rows)
}
if s, ok := rows[0][0].(string); !ok || s != "alice" {
t.Fatalf("first row = %v, want alice", rows[0])
}
}

func TestOracleKeepsEmptyFieldsNull(t *testing.T) {
cat, dir := fixture(t)
o, err := NewOracle(cat, dir)
if err != nil {
t.Fatal(err)
}
defer o.Close()

rows, err := o.Query("SELECT age FROM users WHERE name = 'bob'")
if err != nil {
t.Fatal(err)
}
if len(rows) != 1 || rows[0][0] != nil {
t.Fatalf("bob's age = %v, want NULL", rows)
}
}

func TestOracleReportsInvalidQuery(t *testing.T) {
cat, dir := fixture(t)
o, err := NewOracle(cat, dir)
if err != nil {
t.Fatal(err)
}
defer o.Close()

if _, err := o.Query("SELECT nope FROM users"); err == nil {
t.Fatal("expected an error for an unknown column")
}
}
Loading