initial commit

This commit is contained in:
2026-07-07 17:25:02 +01:00
commit daff6a8f09
12 changed files with 562 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
package simql
import (
"regexp"
)
var columnRegexp = regexp.MustCompile("^([^ ]+)( as ([^ ]+))?$")
type Column string
func (c Column) IsValid() bool {
return columnRegexp.MatchString(string(c))
}
func (c Column) Parse() (string, string, bool) {
if columnRegexp.MatchString(string(c)) {
result := columnRegexp.FindAllStringSubmatch(string(c), -1)
for _, r := range result {
column, as := r[1], r[3]
return column, as, true
}
}
return "", "", false
}
func (c Column) String() string {
return string(c)
}
+42
View File
@@ -0,0 +1,42 @@
package simql
import (
"testing"
"github.com/alecthomas/assert/v2"
)
func TestColumn(t *testing.T) {
type TestCase struct {
In string
IsValid bool
Column string
As string
}
testCases := []TestCase{
// Column only
{In: "*", IsValid: true, Column: "*"},
{In: "email", IsValid: true, Column: "email"},
{In: "customer.*", IsValid: true, Column: "customer.*"},
{In: "customer.email", IsValid: true, Column: "customer.email"},
// Column and alias
{In: "customer.email as email", IsValid: true, Column: "customer.email", As: "email"},
// Invalid format (spaces)
{In: "customer.email as email"},
{In: " customer.email as email"},
{In: "also as broken "},
// Invalid format (disallowed characters) - TODO
}
for _, testCase := range testCases {
t.Run(testCase.In, func(t *testing.T) {
c := Column(testCase.In)
column, as, ok := c.Parse()
assert.Equal(t, testCase.IsValid, ok, "incorrect test case")
assert.Equal(t, testCase.Column, column, "parsed column incorrectly")
assert.Equal(t, testCase.As, as, "parsed as incorrectly")
})
}
}
+10
View File
@@ -0,0 +1,10 @@
module code.aneur.in/go/simql
go 1.25.6
require github.com/alecthomas/assert/v2 v2.11.0
require (
github.com/alecthomas/repr v0.4.0 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
)
+6
View File
@@ -0,0 +1,6 @@
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
+41
View File
@@ -0,0 +1,41 @@
package simql
import (
"fmt"
"strings"
)
type OnClause struct {
column string
operator string
value any
and []*OnClause
}
func (o *OnClause) And(column, operator string, value any) *OnClause {
o.and = append(o.and, On(column, operator, value))
return o
}
func (o *OnClause) String() string {
strs := []string{fmt.Sprintf("%s %s %s", o.column, o.operator, o.value)}
for _, next := range o.and {
strs = append(strs, "and", next.String())
}
return strings.Join(strs, " ")
}
func On(column, operator string, value any) *OnClause {
o := &OnClause{
column: column,
operator: operator,
value: value,
and: []*OnClause{},
}
return o
}
+6
View File
@@ -0,0 +1,6 @@
package simql
type Query interface {
Args() []any
String() string
}
+147
View File
@@ -0,0 +1,147 @@
package simql
import (
"fmt"
"strings"
)
type SelectQuery struct {
columns []Column
// (from|join type), table, as, A, operator, B
tables []SelectTable
where []Where
args []any
limit int
offset int
}
func (s *SelectQuery) Args() []any {
return s.args
}
func (s *SelectQuery) Column(def string) *SelectQuery {
s.columns = append(s.columns, Column(def))
return s
}
func (s *SelectQuery) Columns(defs ...string) *SelectQuery {
for _, def := range defs {
s.columns = append(s.columns, Column(def))
}
return s
}
func (s *SelectQuery) IsValid() bool {
for _, c := range s.columns {
if !c.IsValid() {
return false
}
}
for _, t := range s.tables {
if !t.IsValid() {
return false
}
}
for _, w := range s.where {
if !w.IsValid() {
return false
}
}
return false
}
func (s *SelectQuery) LeftJoin(def string) *SelectQuery {
s.tables = append(s.tables, LeftJoin(def))
return s
}
func (s *SelectQuery) Limit(limit int) *SelectQuery {
s.limit = limit
return s
}
func (s *SelectQuery) InnerJoin(def string) *SelectQuery {
s.tables = append(s.tables, InnerJoin(def))
return s
}
func (s *SelectQuery) RightJoin(def string) *SelectQuery {
s.tables = append(s.tables, RightJoin(def))
return s
}
func (s *SelectQuery) Offset(limit int) *SelectQuery {
s.limit = limit
return s
}
func (s *SelectQuery) String() string {
strs := []string{"select"}
if len(s.columns) > 0 {
for i, column := range s.columns {
if i < len(s.columns)-1 {
strs = append(strs, fmt.Sprintf("%s,", column.String()))
} else {
strs = append(strs, column.String())
}
}
} else {
strs = append(strs, "*")
}
for _, table := range s.tables {
strs = append(strs, table.String())
}
if len(s.where) > 0 {
strs = append(strs, "where")
for _, w := range s.where {
strs = append(strs, w.String())
}
}
if s.limit > 0 {
if s.offset > 0 {
strs = append(strs, fmt.Sprintf("limit %d, %d", s.offset, s.limit))
} else {
strs = append(strs, fmt.Sprintf("limit %d", s.limit))
}
}
return strings.Join(strs, " ")
}
func (s *SelectQuery) Where(def string, args ...any) *SelectQuery {
s.where = append(s.where, Where(def))
if len(args) > 0 {
if s.args == nil {
s.args = []any{}
}
s.args = append(s.args, args...)
}
return s
}
func Select(from string) *SelectQuery {
s := &SelectQuery{
columns: []Column{},
tables: []SelectTable{From(from)},
where: []Where{},
}
return s
}
+56
View File
@@ -0,0 +1,56 @@
package simql
import (
"testing"
"github.com/alecthomas/assert/v2"
)
func TestSelect(t *testing.T) {
type TestCase struct {
In *SelectQuery
Args []any
String string
}
testCases := []TestCase{
{
In: Select("customer"),
String: "select * from customer",
},
{
In: Select("customer as c"),
String: "select * from customer as c",
},
{
In: Select("customer as c").Columns("c.id", "c.name"),
String: "select c.id, c.name from customer as c",
},
{
In: Select("customer as c").InnerJoin("invoices as i on i.customer_id = c.id").Columns("c.name", "i.date"),
String: "select c.name, i.date from customer as c inner join invoices as i on i.customer_id = c.id",
},
{
In: Select("customer").Where("id = ?", 1234),
Args: []any{1234},
String: "select * from customer where id = ?",
},
{
In: Select("customer").Where("id = ?", 1234).Where("and amount > ?", 1000),
Args: []any{1234, 1000},
String: "select * from customer where id = ? and amount > ?",
},
{
In: Select("customer").Where("id = ?", 1234).Columns("name", "email"),
Args: []any{1234},
String: "select name, email from customer where id = ?",
},
}
for _, testCase := range testCases {
t.Run(testCase.String, func(t *testing.T) {
assert.Equal(t, testCase.Args, testCase.In.Args(), "collected arguments incorrectly")
assert.Equal(t, testCase.String, testCase.In.String(), "stringified incorrectly")
})
}
}
+83
View File
@@ -0,0 +1,83 @@
package simql
import (
"fmt"
"regexp"
"strings"
)
var selectTableRegexp = regexp.MustCompile("^(from|left join|inner join|right join) ([^ ]+)( (as) ([^ ]+))?( (on) ([^ ]+) (=|!=|>|>=|<|<=) ([^ ]+)( (and) ([^ ]+) (=|!=|>|>=|<|<=) ([^ ]+))*)?$")
type SelectTable string
func (t SelectTable) IsValid() bool {
return selectTableRegexp.MatchString(string(t))
}
func (t SelectTable) Parse() (selectType, table, as string, on *OnClause, ok bool) {
if selectTableRegexp.MatchString(string(t)) {
ok = true
result := selectTableRegexp.FindAllStringSubmatch(string(t), -1)
for i, r := range result {
// for j, c := range r {
// fmt.Println(i, j, c)
// }
if i == 0 {
selectType, table = r[1], r[2]
if r[4] == "as" {
as = r[5]
}
if r[7] == "on" {
on = On(r[8], r[9], r[10])
}
}
if r[12] == "and" {
on.And(r[13], r[14], r[15])
}
}
}
return
}
func (t SelectTable) String() string {
return string(t)
}
func From(def string) SelectTable {
if strings.Index(def, "from ") == 0 {
return SelectTable(def)
}
return SelectTable(fmt.Sprintf("from %s", def))
}
func InnerJoin(def string) SelectTable {
if strings.Index(def, "inner join ") == 0 {
return SelectTable(def)
}
return SelectTable(fmt.Sprintf("inner join %s", def))
}
func LeftJoin(def string) SelectTable {
if strings.Index(def, "left join ") == 0 {
return SelectTable(def)
}
return SelectTable(fmt.Sprintf("left join %s", def))
}
func RightJoin(def string) SelectTable {
if strings.Index(def, "right join ") == 0 {
return SelectTable(def)
}
return SelectTable(fmt.Sprintf("right join %s", def))
}
+72
View File
@@ -0,0 +1,72 @@
package simql
import (
"testing"
"github.com/alecthomas/assert/v2"
)
func TestSelectTable(t *testing.T) {
type TestCase struct {
In string
IsValid bool
SelectType string
Table string
As string
On *OnClause
}
testCases := []TestCase{
{
In: "from customer",
IsValid: true,
SelectType: "from",
Table: "customer",
},
{
In: "right join invoice on invoice.customer_id = customer.id",
IsValid: true,
SelectType: "right join",
Table: "invoice",
On: On("invoice.customer_id", "=", "customer.id"),
},
{
In: "inner join invoice as i on i.customer_id = c.id",
IsValid: true,
SelectType: "inner join",
Table: "invoice",
As: "i",
On: On("i.customer_id", "=", "c.id"),
},
{
In: "left join invoice as i on i.customer_id = c.id and i.paid = false",
IsValid: true,
SelectType: "left join",
Table: "invoice",
As: "i",
On: On("i.customer_id", "=", "c.id").And("i.paid", "=", "false"),
},
// Currently failing - need to fix bug with 3+ on statements
{
In: "left join invoice as i on i.customer_id = c.id and i.paid = false and i.amount > 1000",
IsValid: true,
SelectType: "left join",
Table: "invoice",
As: "i",
On: On("i.customer_id", "=", "c.id").And("i.paid", "=", "false").And("i.amount", ">", "1000"),
},
}
for _, testCase := range testCases {
t.Run(testCase.In, func(t *testing.T) {
st := SelectTable(testCase.In)
selectType, table, as, on, ok := st.Parse()
assert.Equal(t, testCase.IsValid, ok, "incorrect test case")
assert.Equal(t, testCase.SelectType, selectType, "parsed select type incorrectly")
assert.Equal(t, testCase.Table, table, "parsed table incorrectly")
assert.Equal(t, testCase.As, as, "parsed as incorrectly")
assert.Equal(t, testCase.On, on, "parsed on incorrectly")
})
}
}
+32
View File
@@ -0,0 +1,32 @@
package simql
import "regexp"
var whereRegexp = regexp.MustCompile("^((and|or) )?([^ ]+) (=|!=|>|>=|<|<=) ([^ ]+)?$")
type Where string
func (w Where) IsValid() bool {
return whereRegexp.MatchString(string(w))
}
func (w Where) Parse() (glue, column, operator string, value any, ok bool) {
if whereRegexp.MatchString(string(w)) {
ok = true
result := whereRegexp.FindAllStringSubmatch(string(w), -1)
for _, r := range result {
glue, column, operator, value = r[2], r[3], r[4], r[5]
if value == "?" {
value = nil
}
}
}
return
}
func (w Where) String() string {
return string(w)
}
+38
View File
@@ -0,0 +1,38 @@
package simql
import (
"testing"
"github.com/alecthomas/assert/v2"
)
func TestWhere(t *testing.T) {
type TestCase struct {
In string
IsValid bool
Glue string
Column string
Operator string
Value any
}
testCases := []TestCase{
{In: "id = ?", IsValid: true, Column: "id", Operator: "="},
{In: "id = 1234", IsValid: true, Column: "id", Operator: "=", Value: "1234"},
{In: "and id = ?", IsValid: true, Glue: "and", Column: "id", Operator: "="},
{In: "or id = ?", IsValid: true, Glue: "or", Column: "id", Operator: "="},
}
for _, testCase := range testCases {
t.Run(testCase.In, func(t *testing.T) {
w := Where(testCase.In)
glue, column, operator, value, ok := w.Parse()
assert.Equal(t, testCase.IsValid, ok, "incorrect test case")
assert.Equal(t, testCase.Glue, glue, "parsed glue incorrectly")
assert.Equal(t, testCase.Column, column, "parsed column incorrectly")
assert.Equal(t, testCase.Operator, operator, "parsed operator incorrectly")
assert.Equal(t, testCase.Value, value, "parsed value incorrectly")
})
}
}