initial commit
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package simql
|
||||
|
||||
type Query interface {
|
||||
Args() []any
|
||||
String() string
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user