commit c2e8f78d0c0f9c599a9e0ce369f25ae93144e1f5 Author: David J. Allen Date: Sat Aug 31 19:07:08 2024 -0600 Initial commit diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3c5c214 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/davidallendj/artisan + +go 1.22.6 diff --git a/main.go b/main.go new file mode 100644 index 0000000..a083fbf --- /dev/null +++ b/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "fmt" + + artisan "github.com/davidallendj/artisan/pkg/artisan" +) + +func main() { + // the builder is always the starting point with artisan + var ( + b = artisan.Builder{} + name = "david" + count = 1 + ) + + // do 'create' with two different ways to add columns + fmt.Printf("create.1: %s\n", b.Create("test").WithAttribute(artisan.TABLE).AddColumns(artisan.Columns{ + "count": artisan.Integer{}, + "name": artisan.Text{}, + }).Build()) + fmt.Printf("create.2: %s\n", b.Create("test").AddColumn("count", artisan.Integer{}).AddColumn("name", artisan.Text{}).Build()) + fmt.Printf("create.3: %s\n", b.Create("test").AddColumns(artisan.Columns{"count": artisan.Integer{}, "name": artisan.Text{}}).Build()) + + // do 'insert' with two different ways to add values + fmt.Printf("insert.1: %s\n", b.Insert("test").AddValue("count", count).Build()) + fmt.Printf("insert.2: %s\n", b.Insert("test").AddValues(artisan.Values{"count": count, "name": name}).Build()) + + // do 'select' both with and without 'where' clause + fmt.Printf("select.1: %s\n", b.Select().From("test").Where(artisan.IsEqual("name", name)).Build()) + fmt.Printf("select.2: %s\n", b.Select(artisan.AllColumns()).From("test").Build()) + fmt.Printf("select.3: %s\n", b.Select("count", "name").From("test").Where("count>10").Build()) + fmt.Printf("select.4: %s\n", b.Select("name").WithAttribute(artisan.DISTINCT).From("test").Where(artisan.IsGreaterThanOrEqual("count", 10)).Build()) + + // do 'update' to set existing values + fmt.Printf("update.1: %s\n", b.Update("test").Set(artisan.Values{"count": 10, "name": "joe"}).Where("count>3").Build()) + fmt.Printf("update.2: %s\n", b.Update("test").Set(artisan.Values{"count": 10, "name": "joe"}).Where(artisan.IsGreaterThan("count", 3)).OrderBy("count").Limit(10).Offset(2).Build()) + + // do 'delete' and delete a single and multiple records + fmt.Printf("delete.1: %s\n", b.Delete("test").Where(artisan.IsLessThanOrEqual("count", 1)).Build()) + + // do 'drop' to remove a table + fmt.Printf("drop.1: %s\n", b.Drop("test").Build()) + fmt.Printf("drop.2: %s\n", b.Drop("test").IfExists().Build()) +} diff --git a/pkg/artisan/builder.go b/pkg/artisan/builder.go new file mode 100644 index 0000000..bc003d2 --- /dev/null +++ b/pkg/artisan/builder.go @@ -0,0 +1,54 @@ +package artisan + +import ( + "fmt" + + sql "github.com/davidallendj/artisan/pkg/artisan/driver" +) + +type state interface { + GetBuilder() *Builder +} + +type Builder struct { + Driver sql.Driver + stmt string + err error +} + +type fromClause struct{ builder *Builder } +type whereClause struct{ builder *Builder } + +type tablename string +type conditional string + +func IsGreaterThan[T any, U any](column T, value U) conditional { + return conditional(fmt.Sprintf("%v>%v", column, ConvertValue(value))) +} +func IsGreaterThanOrEqual[T any, U any](column T, value U) conditional { + return conditional(fmt.Sprintf("%v>=%v", column, ConvertValue(value))) +} +func IsLessThan[T any, U any](column T, value U) conditional { + return conditional(fmt.Sprintf("%v<%v", column, ConvertValue(value))) +} +func IsLessThanOrEqual[T any, U any](column T, value U) conditional { + return conditional(fmt.Sprintf("%v<=%v", column, ConvertValue(value))) +} +func IsEqual[T any, U any](column T, value U) conditional { + return conditional(fmt.Sprintf("%v=%v", column, ConvertValue(value))) +} + +func (b *Builder) Build() string { + var s []byte = make([]byte, len(b.stmt)) + copy(s, b.stmt) + b.Reset() + return string(s) +} + +func (b *Builder) Error() error { + return b.err +} + +func (b *Builder) Reset() { + b.stmt = "" +} diff --git a/pkg/artisan/create.go b/pkg/artisan/create.go new file mode 100644 index 0000000..c9dce13 --- /dev/null +++ b/pkg/artisan/create.go @@ -0,0 +1,111 @@ +package artisan + +import ( + "fmt" + "strings" +) + +const ( + TABLE createAttribute = iota + TRIGGER + INDEX + VIEW +) + +type createAttribute int +type createColumns struct { + builder *Builder + tableName string +} +type create struct { + builder *Builder + columns []Column +} +type createOptions struct { + attribute createAttribute +} +type CreateOption func(*createOptions) + +func getCreateOptions(opts ...CreateOption) *createOptions { + co := &createOptions{} + for _, opt := range opts { + opt(co) + } + return co +} + +func (b *Builder) Create(tableName string) *createColumns { + // always reset string if calling create + b.Reset() + b.stmt += fmt.Sprintf("CREATE TABLE %s", tableName) + + return &createColumns{builder: b, tableName: tableName} +} + +func (c *createColumns) WithAttribute(attribute createAttribute) *createColumns { + switch attribute { + case TRIGGER: + // b.stmt += fmt.Sprintf("CREATE TRIGGER %s") + /* TODO */ + case VIEW: + // b.stmt += fmt.Sprintf("CREATE VIEW %s") + /* TODO */ + case INDEX: + /* TODO */ + case TABLE: + c.builder.stmt = fmt.Sprintf("CREATE TABLE %s", c.tableName) + default: + c.builder.stmt = fmt.Sprintf("CREATE TABLE %s", c.tableName) + // TODO: do reflection and add to stmt based on *supported* data type + } + return c +} + +func (c *createColumns) AddColumn(name string, typ Type) *create { + cc := &create{builder: c.builder} + return cc.AddColumn(name, typ) +} + +func (c *create) AddColumn(name string, typ Type) *create { + c.columns = append(c.columns, Column{name, typ}) + return c +} + +func (c *createColumns) AddColumns(columns Columns) *create { + cc := &create{builder: c.builder} + return cc.AddColumns(columns) +} + +func (c *create) AddColumns(columns Columns) *create { + for k, v := range columns { + c.columns = append(c.columns, Column{k, v}) + } + return c +} + +func (c *create) Build() string { + var columns, values string + for _, col := range c.columns { + columns += fmt.Sprintf("%s %s, ", col.Name, col.Type.Name()) + } + + // trim off trailing delimiter + columns = strings.TrimRight(columns, ", ") + values = strings.TrimRight(values, ", ") + + return fmt.Sprintf("%s(%s);", c.builder.stmt, columns) +} + +// func WithColumn(name string, _type Type) CreateOption { +// return func(co *createOptions) { +// co.columns = append(co.columns, Column{name: name, _type: _type}) +// } +// } + +// func WithColumns(columns Columns) CreateOption { +// return func(co *createOptions) { +// for name, _type := range columns { +// WithColumn(name, _type) +// } +// } +// } diff --git a/pkg/artisan/delete.go b/pkg/artisan/delete.go new file mode 100644 index 0000000..c659712 --- /dev/null +++ b/pkg/artisan/delete.go @@ -0,0 +1,18 @@ +package artisan + +import "fmt" + +type deleteClause struct{ builder *Builder } + +func (b *Builder) Delete(tableName string) *deleteClause { + b.stmt = fmt.Sprintf("DELETE FROM %s", tableName) + return &deleteClause{builder: b} +} + +func (s *deleteClause) Where(condition conditional) *whereClause { + s.builder.stmt += fmt.Sprintf(" WHERE %s", condition) + return &whereClause{builder: s.builder} +} + +// defined in `select.go` +// func (s *whereClause) Build() string diff --git a/pkg/artisan/driver/driver.go b/pkg/artisan/driver/driver.go new file mode 100644 index 0000000..1ec012e --- /dev/null +++ b/pkg/artisan/driver/driver.go @@ -0,0 +1,5 @@ +package sql + +type Driver interface { + Build() string +} diff --git a/pkg/artisan/drop.go b/pkg/artisan/drop.go new file mode 100644 index 0000000..d326ee0 --- /dev/null +++ b/pkg/artisan/drop.go @@ -0,0 +1,30 @@ +package artisan + +import "fmt" + +type drop struct { + builder *Builder +} +type dropOptional struct { + tableName string + builder *Builder +} + +func (b *Builder) Drop(tableName string) *dropOptional { + b.Reset() + b.stmt += fmt.Sprintf("DROP TABLE %s", tableName) + return &dropOptional{builder: b, tableName: tableName} +} + +func (d *dropOptional) IfExists() *drop { + d.builder.stmt = fmt.Sprintf("DROP TABLE IF EXISTS %s", d.tableName) + return &drop{builder: d.builder} +} + +func (d *dropOptional) Build() string { + return d.builder.stmt + ";" +} + +func (d *drop) Build() string { + return d.builder.stmt + ";" +} diff --git a/pkg/artisan/insert.go b/pkg/artisan/insert.go new file mode 100644 index 0000000..8b70529 --- /dev/null +++ b/pkg/artisan/insert.go @@ -0,0 +1,57 @@ +package artisan + +import ( + "fmt" + "strings" +) + +type insertClause struct { + builder *Builder + columns []Column +} +type insertColumns struct { + builder *Builder + tableName string +} + +func (b *Builder) Insert(tableName string) *insertColumns { + b.Reset() + b.stmt += fmt.Sprintf("INSERT INTO %s", tableName) + + return &insertColumns{builder: b} +} + +func (i *insertColumns) AddValue(name string, value any) *insertClause { + ic := &insertClause{builder: i.builder} + return ic.AddValue(name, value) +} + +func (i *insertClause) AddValue(name string, value any) *insertClause { + i.columns = append(i.columns, Column{name, ConvertValue(value)}) + return i +} + +func (i *insertColumns) AddValues(values Values) *insertClause { + ic := &insertClause{builder: i.builder} + return ic.AddValues(values) +} + +func (i *insertClause) AddValues(values Values) *insertClause { + for name, value := range values { + i.columns = append(i.columns, Column{name, ConvertValue(value)}) + } + return i +} + +func (i *insertClause) Build() string { + var columns, values string + for _, col := range i.columns { + columns += fmt.Sprintf("%s, ", col.Name) + values += fmt.Sprintf("%s, ", col.Type.Value()) + } + + // trim off trailing delimiter + columns = strings.TrimRight(columns, ", ") + values = strings.TrimRight(values, ", ") + return fmt.Sprintf("%s(%s) VALUES(%s);", i.builder.stmt, columns, values) +} diff --git a/pkg/artisan/select.go b/pkg/artisan/select.go new file mode 100644 index 0000000..4602d2c --- /dev/null +++ b/pkg/artisan/select.go @@ -0,0 +1,61 @@ +package artisan + +import ( + "fmt" + "strings" +) + +const ( + NONE selectAttribute = iota + DISTINCT + ALL +) + +type selectAttribute int +type selectArg string +type selectClause struct{ builder *Builder } +type selectOptional struct{ builder *Builder } + +// Select() initiates a new SQL using a builder object. Calling this function +// will call the builders Reset() function and clear the current statement. +func (b *Builder) Select(what ...selectArg) *selectOptional { + b.Reset() + if len(what) == 0 { + b.stmt += "SELECT *" + } else { + var output string + for _, s := range what { + output += string(s) + ", " + } + output = strings.TrimRight(output, ", ") + b.stmt += fmt.Sprintf("SELECT %s", output) + } + return &selectOptional{builder: b} +} + +func (s *selectOptional) WithAttribute(attribute selectAttribute) *selectClause { + switch attribute { + case NONE: + s.builder.stmt += "" + case DISTINCT: + s.builder.stmt += " DISTINCT" + case ALL: + s.builder.stmt += " ALL" + default: + } + return &selectClause{builder: s.builder} +} + +func (s *selectOptional) From(table tablename) *fromClause { + sc := &selectClause{builder: s.builder} + return sc.From(table) +} + +func (s *selectClause) From(table tablename) *fromClause { + s.builder.stmt += fmt.Sprintf(" FROM %s", table) + return &fromClause{builder: s.builder} +} + +func (s *fromClause) Build() string { + return s.builder.stmt + ";" +} diff --git a/pkg/artisan/type.go b/pkg/artisan/type.go new file mode 100644 index 0000000..d3ad64f --- /dev/null +++ b/pkg/artisan/type.go @@ -0,0 +1,63 @@ +package artisan + +import ( + "fmt" + "reflect" +) + +type Column struct { + Name string + Type Type +} +type Columns map[string]Type +type Values map[string]any + +// define abstract interfaces +type Type interface { + Name() string + Value() string +} + +// define functions +func AllColumns() selectArg { + return "*" +} + +// define concrete SQLite types +type Base struct{ value string } +type Text struct{ Base } +type Integer struct{ Base } +type Real struct{ Base } +type Bool struct{ Base } + +// define type name/value functions +func (b Base) Value() string { return b.value } +func (Base) Name() string { return "TEXT" } +func (Text) Name() string { return "TEXT" } +func (Integer) Name() string { return "INTEGER" } +func (Real) Name() string { return "REAL" } +func (Bool) Name() string { return "BOOLEAN" } + +// define string interface functions for printing +func (t Text) String() string { return t.Value() } +func (i Integer) String() string { return i.Value() } +func (r Real) String() string { return r.Value() } + +type IntegerType interface{ ~int | ~int32 | ~int64 } + +func ConvertValue(value any) Type { + s := fmt.Sprint(value) + switch reflect.TypeOf(value).Kind() { + case reflect.String: + return Text{Base{fmt.Sprintf("\"%s\"", s)}} + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return Integer{Base{s}} + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return Integer{Base{s}} + case reflect.Float32, reflect.Float64: + return Real{Base{s}} + case reflect.Bool: + return Bool{Base{s}} + } + return Base{s} +} diff --git a/pkg/artisan/update.go b/pkg/artisan/update.go new file mode 100644 index 0000000..51a33b9 --- /dev/null +++ b/pkg/artisan/update.go @@ -0,0 +1,43 @@ +package artisan + +import ( + "fmt" + "strings" +) + +type updateClause struct{ builder *Builder } +type setClause struct{ builder *Builder } + +func (b *Builder) Update(table tablename) *updateClause { + b.stmt = fmt.Sprintf("UPDATE %s", table) + return &updateClause{builder: b} +} + +func (s *updateClause) Set(values Values) *setClause { + columns := "" + for name, value := range values { + columns += fmt.Sprintf("%s = %v, ", name, value) + } + s.builder.stmt += strings.TrimRight(columns, ", ") + return &setClause{builder: s.builder} +} + +func (s *setClause) Where(condition conditional) *whereClause { + s.builder.stmt += fmt.Sprintf(" WHERE %s", condition) + return &whereClause{builder: s.builder} +} + +func (s *whereClause) OrderBy(column string) *whereClause { + s.builder.stmt += fmt.Sprintf(" ORDER BY %s", column) + return s +} + +func (s *whereClause) Limit(rowcount int) *whereClause { + s.builder.stmt += fmt.Sprintf(" LIMIT %d", rowcount) + return s +} + +func (s *whereClause) Offset(offset int) *whereClause { + s.builder.stmt += fmt.Sprintf(" OFFSET %d", offset) + return s +} diff --git a/pkg/artisan/where.go b/pkg/artisan/where.go new file mode 100644 index 0000000..31611ec --- /dev/null +++ b/pkg/artisan/where.go @@ -0,0 +1,12 @@ +package artisan + +import "fmt" + +func (s *fromClause) Where(condition conditional) *whereClause { + s.builder.stmt += fmt.Sprintf(" WHERE %s", condition) + return &whereClause{builder: s.builder} +} + +func (s *whereClause) Build() string { + return s.builder.stmt + ";" +}