diff --git a/.github/COMMIT_CONVENTION.md b/.github/COMMIT_CONVENTION.md index dc54917..e424d9c 100644 --- a/.github/COMMIT_CONVENTION.md +++ b/.github/COMMIT_CONVENTION.md @@ -8,13 +8,13 @@ Angular's [commit message guidelines](https://github.com/angular/angular/blob/ma Appears under "Features" header, pencil subheader: -``` +```xml feat(pencil): add 'graphiteWidth' option ``` Appears under "Bug Fixes" header, graphite subheader, with a link to issue #28: -``` +```xml fix(graphite): stop graphite breaking when width < 0.1 Closes #28 @@ -22,7 +22,7 @@ Closes #28 Appears under "Performance Improvements" header, and under "Breaking Changes" with the breaking change explanation: -``` +```xml perf(pencil): remove graphiteWidth option BREAKING CHANGE: The graphiteWidth option has been removed. The default graphite width of 10mm is always used for performance reason. @@ -30,7 +30,7 @@ BREAKING CHANGE: The graphiteWidth option has been removed. The default graphite The following commit and commit `667ecc1` do not appear in the changelog if they are under the same release. If not, the revert commit appears under the "Reverts" header. -``` +```xml revert: feat(pencil): add 'graphiteWidth' option This reverts commit 667ecc1654a317a13331b17617d973392f415f02. @@ -40,7 +40,7 @@ This reverts commit 667ecc1654a317a13331b17617d973392f415f02. A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**: -``` +```xml (): @@ -71,9 +71,9 @@ The scope could be anything specifying place of the commit change. For example ` The subject contains succinct description of the change: -* use the imperative, present tense: "change" not "changed" nor "changes" -* don't capitalize first letter -* no dot (.) at the end + * use the imperative, present tense: "change" not "changed" nor "changes" + * don't capitalize first letter + * no dot (.) at the end ### Body @@ -87,14 +87,6 @@ reference GitHub issues that this commit **Closes**. **Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this. -A detailed explanation can be found in this [document](#commit-message-format). +A detailed explanation can be found in this [document][commit-message-format]. -[npm-image]: https://badge.fury.io/js/conventional-changelog-angular.svg -[npm-url]: https://npmjs.org/package/conventional-changelog-angular -[travis-image]: https://travis-ci.org/conventional-changelog/conventional-changelog-angular.svg?branch=master -[travis-url]: https://travis-ci.org/conventional-changelog/conventional-changelog-angular -[daviddm-image]: https://david-dm.org/conventional-changelog/conventional-changelog-angular.svg?theme=shields.io -[daviddm-url]: https://david-dm.org/conventional-changelog/conventional-changelog-angular -[coveralls-image]: https://coveralls.io/repos/conventional-changelog/conventional-changelog-angular/badge.svg -[coveralls-url]: https://coveralls.io/r/conventional-changelog/conventional-changelog-angular [commit-message-format]: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit# \ No newline at end of file diff --git a/README.md b/README.md index b0d0265..42a8ac9 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ func main() { Types("varchar(36)", "varchar(255)", "varchar(255)", "text", "date"). Primary("id"). Unique("email") - ToSql() + ToSQL() if err != nil { log.Fatal(err) } diff --git a/append.go b/append.go index 0710745..a46a69d 100644 --- a/append.go +++ b/append.go @@ -2,12 +2,13 @@ package bob import "io" -func appendToSql(parts []BobBuilder, w io.Writer, sep string, args []interface{}) ([]interface{}, error) { +// appendToSQL - Documentation coming soon +func appendToSQL(parts []BobBuilder, w io.Writer, sep string, args []interface{}) ([]interface{}, error) { for i, p := range parts { - partSql, partArgs, err := p.ToSql() + partSQL, partArgs, err := p.ToSQL() if err != nil { return nil, err - } else if len(partSql) == 0 { + } else if len(partSQL) == 0 { continue } @@ -18,7 +19,7 @@ func appendToSql(parts []BobBuilder, w io.Writer, sep string, args []interface{} } } - _, err = io.WriteString(w, partSql) + _, err = io.WriteString(w, partSQL) if err != nil { return nil, err } diff --git a/bob.go b/bob.go index 804e273..caee221 100644 --- a/bob.go +++ b/bob.go @@ -2,34 +2,43 @@ package bob import "github.com/lann/builder" +// BobBuilderType is the type for BobBuilder type BobBuilderType builder.Builder +// BobBuilder interface wraps the ToSQL method type BobBuilder interface { - ToSql() (string, []interface{}, error) + ToSQL() (string, []interface{}, error) } +// CreateTable creates a table with CreateBuilder interface func (b BobBuilderType) CreateTable(table string) CreateBuilder { return CreateBuilder(b).Name(table) } +// HasTable checks if a table exists with HasBuilder interface func (b BobBuilderType) HasTable(table string) HasBuilder { return HasBuilder(b).HasTable(table) } +// HasColumn checks if a column exists with HasBuilder interface func (b BobBuilderType) HasColumn(column string) HasBuilder { return HasBuilder(b).HasColumn(column) } +// BobStmtBuilder is the parent builder for BobBuilderType var BobStmtBuilder = BobBuilderType(builder.EmptyBuilder) +// CreateTable creates a table with CreateBuilder interface func CreateTable(table string) CreateBuilder { return BobStmtBuilder.CreateTable(table) } +// HasTable checks if a table exists with HasBuilder interface func HasTable(table string) HasBuilder { return BobStmtBuilder.HasTable(table) } +// HasColumn checks if a column exists with HasBuilder interface func HasColumn(col string) HasBuilder { return BobStmtBuilder.HasColumn(col) } diff --git a/create.go b/create.go index 3679731..556aae0 100644 --- a/create.go +++ b/create.go @@ -25,36 +25,44 @@ func init() { builder.Register(CreateBuilder{}, createData{}) } +// Name sets the table name func (b CreateBuilder) Name(name string) CreateBuilder { return builder.Set(b, "TableName", name).(CreateBuilder) } +// WithSchema specifies the schema to be used when using the schema-building commands. func (b CreateBuilder) WithSchema(name string) CreateBuilder { return builder.Set(b, "Schema", name).(CreateBuilder) } +// Columns sets the column names func (b CreateBuilder) Columns(cols ...string) CreateBuilder { return builder.Set(b, "Columns", cols).(CreateBuilder) } +// Types set a type for certain column func (b CreateBuilder) Types(types ...string) CreateBuilder { return builder.Set(b, "Types", types).(CreateBuilder) } +// Primary will set that column as the primary key for a table. func (b CreateBuilder) Primary(column string) CreateBuilder { return builder.Set(b, "Primary", column).(CreateBuilder) } +// Unique adds an unique index to a table over the given columns. func (b CreateBuilder) Unique(column string) CreateBuilder { return builder.Set(b, "Unique", column).(CreateBuilder) } -func (b CreateBuilder) ToSql() (string, []interface{}, error) { +// ToSQL returns 3 variables filled out with the correct values based on bindings, etc. +func (b CreateBuilder) ToSQL() (string, []interface{}, error) { data := builder.GetStruct(b).(createData) - return data.ToSql() + return data.ToSQL() } -func (d *createData) ToSql() (sqlStr string, args []interface{}, err error) { +// ToSQL returns 3 variables filled out with the correct values based on bindings, etc. +func (d *createData) ToSQL() (sqlStr string, args []interface{}, err error) { if len(d.TableName) == 0 || d.TableName == "" { err = errors.New("create statements must specify a table") return diff --git a/create_test.go b/create_test.go index 8f48a68..fdd217a 100644 --- a/create_test.go +++ b/create_test.go @@ -8,7 +8,7 @@ import ( func TestCreate(t *testing.T) { t.Run("should return correct sql string with basic columns and types", func(t *testing.T) { - sql, _, err := bob.CreateTable("users").Columns("name", "password", "date").Types("varchar(255)", "text", "date").ToSql() + sql, _, err := bob.CreateTable("users").Columns("name", "password", "date").Types("varchar(255)", "text", "date").ToSQL() if err != nil { t.Fatal(err.Error()) } @@ -24,7 +24,7 @@ func TestCreate(t *testing.T) { Types("uuid", "varchar(255)", "varchar(255)", "text", "date"). Primary("id"). Unique("email"). - ToSql() + ToSQL() if err != nil { t.Fatal(err.Error()) } @@ -35,7 +35,7 @@ func TestCreate(t *testing.T) { }) t.Run("should be able to have a schema name", func(t *testing.T) { - sql, _, err := bob.CreateTable("users").WithSchema("private").Columns("name", "password", "date").Types("varchar(255)", "text", "date").ToSql() + sql, _, err := bob.CreateTable("users").WithSchema("private").Columns("name", "password", "date").Types("varchar(255)", "text", "date").ToSQL() if err != nil { t.Fatal(err.Error()) } @@ -49,28 +49,28 @@ func TestCreate(t *testing.T) { _, _, err := bob.CreateTable("users"). Columns("id", "name", "email", "password", "date"). Types("uuid", "varchar(255)", "varchar(255)", "date"). - ToSql() + ToSQL() if err.Error() != "columns and types should have equal length" { t.Fatal("should throw an error, it didn't:", err.Error()) } }) t.Run("should emit error on empty table name", func(t *testing.T) { - _, _, err := bob.CreateTable("").Columns("name").Types("text").ToSql() + _, _, err := bob.CreateTable("").Columns("name").Types("text").ToSQL() if err.Error() != "create statements must specify a table" { t.Fatal("should throw an error, it didn't:", err.Error()) } }) t.Run("should emit error for primary key not in columns", func(t *testing.T) { - _, _, err := bob.CreateTable("users").Columns("name").Types("text").Primary("id").ToSql() + _, _, err := bob.CreateTable("users").Columns("name").Types("text").Primary("id").ToSQL() if err.Error() != "supplied primary column name doesn't exists on columns" { t.Fatal("should throw an error, it didn't:", err.Error()) } }) t.Run("should emit error for unique key not in columns", func(t *testing.T) { - _, _, err := bob.CreateTable("users").Columns("name").Types("text").Unique("id").ToSql() + _, _, err := bob.CreateTable("users").Columns("name").Types("text").Unique("id").ToSQL() if err.Error() != "supplied unique column name doesn't exists on columns" { t.Fatal("should throw an error, it didn't:", err.Error()) } diff --git a/has.go b/has.go index 1e0051b..deccb71 100644 --- a/has.go +++ b/has.go @@ -23,28 +23,34 @@ func init() { builder.Register(HasBuilder{}, hasData{}) } +// HasTable checks for a table's existence by tableName, resolving with a boolean to signal if the table exists. func (h HasBuilder) HasTable(table string) HasBuilder { return builder.Set(h, "Name", table).(HasBuilder) } +// HasColumn checks if a column exists in the current table, resolves the promise with a boolean, true if the column exists, false otherwise. func (h HasBuilder) HasColumn(column string) HasBuilder { return builder.Set(h, "Column", column).(HasBuilder) } +// WithSchema specifies the schema to be used when using the schema-building commands. func (h HasBuilder) WithSchema(schema string) HasBuilder { return builder.Set(h, "Schema", schema).(HasBuilder) } +// PlaceholderFormat changes the default placeholder (?) to desired placeholder. func (h HasBuilder) PlaceholderFormat(f string) HasBuilder { return builder.Set(h, "Placeholder", f).(HasBuilder) } -func (h HasBuilder) ToSql() (string, []interface{}, error) { +// ToSQL returns 3 variables filled out with the correct values based on bindings, etc. +func (h HasBuilder) ToSQL() (string, []interface{}, error) { data := builder.GetStruct(h).(hasData) - return data.ToSql() + return data.ToSQL() } -func (d *hasData) ToSql() (sqlStr string, args []interface{}, err error) { +// ToSQL returns 3 variables filled out with the correct values based on bindings, etc. +func (d *hasData) ToSQL() (sqlStr string, args []interface{}, err error) { sql := &bytes.Buffer{} if d.Name == "" { err = errors.New("has statement should have a table name") diff --git a/has_test.go b/has_test.go index 4c4ad46..9ce92a1 100644 --- a/has_test.go +++ b/has_test.go @@ -10,7 +10,7 @@ import ( func TestHas(t *testing.T) { t.Run("should be able to create a hasTable query", func(t *testing.T) { - sql, args, err := bob.HasTable("users").ToSql() + sql, args, err := bob.HasTable("users").ToSQL() if err != nil { t.Fatal(err.Error()) } @@ -26,7 +26,7 @@ func TestHas(t *testing.T) { }) t.Run("should be able to create a hasColumn query", func(t *testing.T) { - sql, args, err := bob.HasTable("users").HasColumn("name").ToSql() + sql, args, err := bob.HasTable("users").HasColumn("name").ToSQL() if err != nil { t.Fatal(err.Error()) } @@ -40,4 +40,59 @@ func TestHas(t *testing.T) { t.Fatal("args is not equal with argsResult:", args) } }) + + t.Run("should be able to create a hasColumn query (but reversed)", func(t *testing.T) { + sql, args, err := bob.HasColumn("name").HasTable("users").ToSQL() + if err != nil { + t.Fatal(err.Error()) + } + + result := "SELECT * FROM information_schema.columns WHERE table_name = ? AND column_name = ? AND table_schema = current_schema();" + if sql != result { + t.Fatal("sql is not equal with result:", sql) + } + + if len(args) != 2 { + t.Fatal("args is not equal with argsResult:", args) + } + }) + + t.Run("should be able to create a hasTable query with schema", func(t *testing.T) { + sql, args, err := bob.HasTable("users").WithSchema("private").ToSQL() + if err != nil { + t.Fatal(err.Error()) + } + + result := "SELECT * FROM information_schema.tables WHERE table_name = ? AND table_schema = ?;" + if sql != result { + t.Fatal("sql is not equal with result:", sql) + } + + if len(args) != 2 { + t.Fatal("args is not equal with argsResult:", args) + } + }) + + t.Run("should be able to have a different placeholder", func(t *testing.T) { + sql, args, err := bob.HasTable("users").HasColumn("name").PlaceholderFormat(bob.Dollar).ToSQL() + if err != nil { + t.Fatal(err.Error()) + } + + result := "SELECT * FROM information_schema.columns WHERE table_name = $1 AND column_name = $2 AND table_schema = current_schema();" + if sql != result { + t.Fatal("sql is not equal with result:", sql) + } + + if len(args) != 2 { + t.Fatal("args is not equal with argsResult:", args) + } + }) + + t.Run("should expect an error for no table name", func(t *testing.T) { + _, _, err := bob.HasTable("").ToSQL() + if err.Error() != "has statement should have a table name" { + t.Fatal("error is different:", err.Error()) + } + }) } diff --git a/placeholder.go b/placeholder.go index dd45abc..e812c7c 100644 --- a/placeholder.go +++ b/placeholder.go @@ -1,22 +1,40 @@ package bob -import "strings" - -const ( - Question = "?" - Dollar = "$" - Colon = ":" - AtP = "@p" +import ( + "strconv" + "strings" ) +const ( + // Question is the format used in MySQL + Question = "?" + // Dollar is the format used in PostgreSQL + Dollar = "$" + // Colon is the format used in Oracle Database, but here I implemented it wrong. + // I will either fix it or remove it in the future. + Colon = ":" + // AtP comes in the documentation of Squirrel but I don't know what database uses it. + AtP = "@p" +) + +// PlaceholderFormat is an interface for placeholder formattings. type PlaceholderFormat interface { ReplacePlaceholders(sql string) (string, error) } -// TODO - test this one +// ReplacePlaceholder converts default placeholder format to a specific format. func ReplacePlaceholder(sql string, format string) string { if format == "" { format = Question } + + if format == Dollar || format == Colon { + separate := strings.SplitAfter(sql, "?") + for i := 0; i < len(separate); i++ { + separate[i] = strings.Replace(separate[i], "?", format+strconv.Itoa(i+1), 1) + } + return strings.Join(separate, "") + } + return strings.ReplaceAll(sql, "?", format) } diff --git a/placeholder_test.go b/placeholder_test.go new file mode 100644 index 0000000..01075a5 --- /dev/null +++ b/placeholder_test.go @@ -0,0 +1,39 @@ +package bob_test + +import ( + "testing" + + "github.com/aldy505/bob" +) + +func TestReplacePlaceholder(t *testing.T) { + t.Run("should be able to replace placeholder to dollar", func(t *testing.T) { + sql := "INSERT INTO table_name (`col1`, `col2`, `col3`) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?);" + result := bob.ReplacePlaceholder(sql, bob.Dollar) + should := "INSERT INTO table_name (`col1`, `col2`, `col3`) VALUES ($1, $2, $3), ($4, $5, $6), ($7, $8, $9);" + + if result != should { + t.Fatal("result string doesn't match:", result) + } + }) + + t.Run("should be able to replace placeholder to colon", func(t *testing.T) { + sql := "INSERT INTO table_name (`col1`, `col2`, `col3`) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?);" + result := bob.ReplacePlaceholder(sql, bob.Colon) + should := "INSERT INTO table_name (`col1`, `col2`, `col3`) VALUES (:1, :2, :3), (:4, :5, :6), (:7, :8, :9);" + + if result != should { + t.Fatal("result string doesn't match:", result) + } + }) + + t.Run("should be able to replace placeholder to @p", func(t *testing.T) { + sql := "INSERT INTO table_name (`col1`, `col2`, `col3`) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?);" + result := bob.ReplacePlaceholder(sql, bob.AtP) + should := "INSERT INTO table_name (`col1`, `col2`, `col3`) VALUES (@p, @p, @p), (@p, @p, @p), (@p, @p, @p);" + + if result != should { + t.Fatal("result string doesn't match:", result) + } + }) +} diff --git a/util/util.go b/util/util.go index 542b03e..f1b27fb 100644 --- a/util/util.go +++ b/util/util.go @@ -1,5 +1,6 @@ package util +// IsIn checks if an array have a value func IsIn(arr []string, value string) bool { for _, item := range arr { if item == value { @@ -9,6 +10,7 @@ func IsIn(arr []string, value string) bool { return false } +// FindPosition search for value position on an array func FindPosition(arr []string, value string) int { for i, item := range arr { if item == value {