diff --git a/delete.go b/delete.go index 284ec23..048e356 100644 --- a/delete.go +++ b/delete.go @@ -5,23 +5,23 @@ import ( ) // https://www.sqlite.org/lang_delete.html -type DeleteQuery struct { +type DeleteStatement struct { table SelectTable where []Where args []any } -func (q *DeleteQuery) Args() []any { - return q.args +func (s *DeleteStatement) Args() []any { + return s.args } -func (q *DeleteQuery) IsValid() bool { - if !q.table.IsValid() { +func (s *DeleteStatement) IsValid() bool { + if !s.table.IsValid() { return false } - for _, w := range q.where { + for _, w := range s.where { if !w.IsValid() { return false } @@ -30,13 +30,13 @@ func (q *DeleteQuery) IsValid() bool { return false } -func (q *DeleteQuery) String() string { - strs := []string{"delete", q.table.String()} +func (s *DeleteStatement) String() string { + strs := []string{"delete", s.table.String()} - if len(q.where) > 0 { + if len(s.where) > 0 { strs = append(strs, "where") - for _, w := range q.where { + for _, w := range s.where { strs = append(strs, w.String()) } } @@ -44,26 +44,26 @@ func (q *DeleteQuery) String() string { return strings.Join(strs, " ") } -func (q *DeleteQuery) Where(def string, args ...any) *DeleteQuery { - q.where = append(q.where, Where(def)) +func (s *DeleteStatement) Where(def string, args ...any) *DeleteStatement { + s.where = append(s.where, Where(def)) if len(args) > 0 { - if q.args == nil { - q.args = []any{} + if s.args == nil { + s.args = []any{} } - q.args = append(q.args, args...) + s.args = append(s.args, args...) } - return q + return s } -func Delete(from string) *DeleteQuery { - q := &DeleteQuery{ +func Delete(from string) *DeleteStatement { + s := &DeleteStatement{ table: From(from), where: []Where{}, } - return q + return s } diff --git a/delete_test.go b/delete_test.go index ad51ed6..b2f35b0 100644 --- a/delete_test.go +++ b/delete_test.go @@ -8,7 +8,7 @@ import ( func TestDelete(t *testing.T) { type TestCase struct { - In *DeleteQuery + In *DeleteStatement Args []any String string } @@ -24,7 +24,7 @@ func TestDelete(t *testing.T) { }, { In: Delete("customer as c"), - String: "select c.id, c.name from customer as c", + String: "delete from customer as c", }, { In: Delete("customer").Where("id = ?", 1234), diff --git a/insert.go b/insert.go new file mode 100644 index 0000000..b2f4eee --- /dev/null +++ b/insert.go @@ -0,0 +1,120 @@ +package sqimple + +import ( + "fmt" + "slices" + "strings" +) + +// https://www.sqlite.org/lang_delete.html +type InsertStatement struct { + table InsertTable + or string + + columns []string + values []Values + + selectValues *SelectQuery +} + +func (s *InsertStatement) Args() []any { + if s.selectValues != nil { + return s.selectValues.Args() + } + + args := []any{} + for _, row := range s.values { + for _, column := range s.columns { + args = append(args, row[column]) + } + } + + return args +} + +func (s *InsertStatement) IsValid() bool { + if !s.table.IsValid() { + return false + } + + return false +} + +func (s *InsertStatement) Columns(columns ...string) *InsertStatement { + if s.columns == nil { + s.columns = []string{} + } + + for _, column := range columns { + if !slices.Contains(s.columns, column) { + s.columns = append(s.columns, columns...) + } + } + + return s +} + +func (s *InsertStatement) Or(or string) *InsertStatement { + s.or = or + return s +} + +func (s *InsertStatement) Select(q *SelectQuery) *InsertStatement { + s.selectValues = q + + return s +} + +func (s *InsertStatement) String() string { + strs := []string{"insert"} + + if s.or != "" { + strs = append(strs, "or", s.or) + } + + strs = append(strs, s.table.String()) + + if len(s.columns) > 0 { + strs = append(strs, fmt.Sprintf("(%s)", strings.Join(s.columns, ", "))) + } + + if s.selectValues != nil { + strs = append(strs, s.selectValues.String()) + } else { + strs = append(strs, "values") + + valueStrs := []string{} + for range s.values { + marks := []string{} + for range s.columns { + marks = append(marks, "?") + } + + valueStrs = append(valueStrs, fmt.Sprintf("(%s)", strings.Join(marks, ", "))) + } + + strs = append(strs, strings.Join(valueStrs, ", ")) + } + + return strings.Join(strs, " ") +} + +func (s *InsertStatement) Values(values ...Values) *InsertStatement { + if s.values == nil { + s.values = []Values{} + } + + for _, row := range values { + s.values = append(s.values, row) + } + + return s +} + +func Insert(into string) *InsertStatement { + s := &InsertStatement{ + table: Into(into), + } + + return s +} diff --git a/insert_test.go b/insert_test.go new file mode 100644 index 0000000..df4a989 --- /dev/null +++ b/insert_test.go @@ -0,0 +1,40 @@ +package sqimple + +import ( + "testing" + + "github.com/alecthomas/assert/v2" +) + +func TestInsert(t *testing.T) { + type TestCase struct { + In *InsertStatement + Args []any + String string + } + + testCases := []TestCase{ + { + In: Insert("customer").Columns("name").Values(Values{"name": "Adam"}), + Args: []any{"Adam"}, + String: "insert into customer (name) values (?)", + }, + { + In: Insert("customer").Columns("name").Values(Values{"name": "Adam"}, Values{"name": "Bernard"}), + Args: []any{"Adam", "Bernard"}, + String: "insert into customer (name) values (?), (?)", + }, + { + In: Insert("customer").Or("update").Columns("id", "name").Values(Values{"id": 1234, "name": "Adam"}), + Args: []any{1234, "Adam"}, + String: "insert or update into customer (id, name) values (?, ?)", + }, + } + + 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") + }) + } +} diff --git a/table.go b/table.go index aa6e0db..1bb731b 100644 --- a/table.go +++ b/table.go @@ -6,7 +6,46 @@ import ( "strings" ) -var selectTableRegexp = regexp.MustCompile("^(from|left join|inner join|right join) ([^ ]+)( (as) ([^ ]+))?( (on) ([^ ]+) (=|!=|>|>=|<|<=) ([^ ]+)( (and) ([^ ]+) (=|!=|>|>=|<|<=) ([^ ]+))*)?$") +var ( + insertTableRegexp = regexp.MustCompile("^into ([^ ]+)( (as) ([^ ]+))?$") + selectTableRegexp = regexp.MustCompile("^(from|left join|inner join|right join) ([^ ]+)( (as) ([^ ]+))?( (on) ([^ ]+) (=|!=|>|>=|<|<=) ([^ ]+)( (and) ([^ ]+) (=|!=|>|>=|<|<=) ([^ ]+))*)?$") +) + +type InsertTable string + +func (t InsertTable) IsValid() bool { + return insertTableRegexp.MatchString(string(t)) +} + +func (t InsertTable) Parse() (table, as string, ok bool) { + if insertTableRegexp.MatchString(string(t)) { + ok = true + + result := insertTableRegexp.FindAllStringSubmatch(string(t), -1) + + for _, r := range result { + table = r[1] + + if r[3] == "as" { + as = r[4] + } + } + } + + return +} + +func (t InsertTable) String() string { + return string(t) +} + +func Into(def string) InsertTable { + if strings.Index(def, "into ") == 0 { + return InsertTable(def) + } + + return InsertTable(fmt.Sprintf("into %s", def)) +} type SelectTable string diff --git a/table_test.go b/table_test.go index 15a1aa8..d606192 100644 --- a/table_test.go +++ b/table_test.go @@ -6,6 +6,40 @@ import ( "github.com/alecthomas/assert/v2" ) +func TestInsertTable(t *testing.T) { + type TestCase struct { + In string + IsValid bool + Table string + As string + } + + testCases := []TestCase{ + { + In: "into customer", + IsValid: true, + Table: "customer", + }, + { + In: "into customer as c", + IsValid: true, + Table: "customer", + As: "c", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.In, func(t *testing.T) { + it := InsertTable(testCase.In) + table, as, ok := it.Parse() + + assert.Equal(t, testCase.IsValid, ok, "incorrect test case") + assert.Equal(t, testCase.Table, table, "parsed table incorrectly") + assert.Equal(t, testCase.As, as, "parsed as incorrectly") + }) + } +} + func TestSelectTable(t *testing.T) { type TestCase struct { In string diff --git a/values.go b/values.go new file mode 100644 index 0000000..f510113 --- /dev/null +++ b/values.go @@ -0,0 +1,3 @@ +package sqimple + +type Values map[string]any