feat: initial

This commit is contained in:
Reinaldy Rafli 2021-06-24 14:43:28 +07:00
commit 82c04c6290
15 changed files with 454 additions and 0 deletions

100
.github/COMMIT_CONVENTION.md vendored Normal file
View File

@ -0,0 +1,100 @@
> [conventional-changelog](https://github.com/ajoslin/conventional-changelog) [angular](https://github.com/angular/angular) preset
## Angular Convention
Angular's [commit message guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit).
### Examples
Appears under "Features" header, pencil subheader:
```
feat(pencil): add 'graphiteWidth' option
```
Appears under "Bug Fixes" header, graphite subheader, with a link to issue #28:
```
fix(graphite): stop graphite breaking when width < 0.1
Closes #28
```
Appears under "Performance Improvements" header, and under "Breaking Changes" with the breaking change explanation:
```
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.
```
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.
```
revert: feat(pencil): add 'graphiteWidth' option
This reverts commit 667ecc1654a317a13331b17617d973392f415f02.
```
### Commit Message Format
A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**:
```
<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
```
The **header** is mandatory and the **scope** of the header is optional.
### Revert
If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit. In the body it should say: `This reverts commit <hash>.`, where the hash is the SHA of the commit being reverted.
### Type
If the prefix is `feat`, `fix` or `perf`, it will appear in the changelog. However if there is any [BREAKING CHANGE](#footer), the commit will always appear in the changelog.
Other prefixes are up to your discretion. Suggested prefixes are `build`, `ci`, `docs` ,`style`, `refactor`, and `test` for non-changelog related tasks.
Details regarding these types can be found in the official [Angular Contributing Guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#type).
### Scope
The scope could be anything specifying place of the commit change. For example `$location`,
`$browser`, `$compile`, `$rootScope`, `ngHref`, `ngClick`, `ngView`, etc...
### Subject
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
### Body
Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes".
The body should include the motivation for the change and contrast this with previous behavior.
### Footer
The footer should contain any information about **Breaking Changes** and is also the place to
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).
[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#

23
.github/workflow/build.yml vendored Normal file
View File

@ -0,0 +1,23 @@
name: Build test
on: [pull_request, push]
jobs:
build-test:
name: Build test
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
go-version: [1.15.x, 1.16.x]
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Build
run: go build ./

23
.github/workflow/coverage.yml vendored Normal file
View File

@ -0,0 +1,23 @@
name: Test and coverage
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 2
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.16.x
- name: Run coverage
run: go test -v -race -coverprofile=coverage.out -covermode=atomic -failfast
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1

15
.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021-present Reinaldy Rafli and Bob collaborators
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

27
README.md Normal file
View File

@ -0,0 +1,27 @@
# Bob - SQL Query Builder
I really need a create table SQL builder, and I can't find one. So, like everything else, I made one. Heavily inspired by [Squirrel](https://github.com/Masterminds/squirrel) and [Knex](https://knexjs.org/).
Oh, and of course, heavily inspired by Bob the Builder.
## Usage
It's not ready for production (yet). But, the API is probably close to how you'd do things on Squirrel.
```go
import "github.com/aldy505/bob"
func main() {
sql, _, err := bob.CreateTable("users").
Columns("id", "email", "name", "password", "date").
Types("varchar(36)", "varchar(255)", "varchar(255)", "text", "date").
Primary("id").
Unique("email")
ToSql()
if err != nil {
log.Fatal(err)
}
// process SQL with whatever you like it
}
```

28
append.go Normal file
View File

@ -0,0 +1,28 @@
package bob
import "io"
func AppendToSql(parts []BobBuilder, w io.Writer, sep string, args []interface{}) ([]interface{}, error) {
for i, p := range parts {
partSql, partArgs, err := p.ToSql()
if err != nil {
return nil, err
} else if len(partSql) == 0 {
continue
}
if i > 0 {
_, err := io.WriteString(w, sep)
if err != nil {
return nil, err
}
}
_, err = io.WriteString(w, partSql)
if err != nil {
return nil, err
}
args = append(args, partArgs...)
}
return args, nil
}

19
bob.go Normal file
View File

@ -0,0 +1,19 @@
package bob
import "github.com/lann/builder"
type BobBuilderType builder.Builder
type BobBuilder interface {
ToSql() (string, []interface{}, error)
}
func (b BobBuilderType) CreateTable(table string) CreateBuilder {
return CreateBuilder(b).Name(table)
}
var BobStmtBuilder = BobBuilderType(builder.EmptyBuilder)
func CreateTable(table string) CreateBuilder {
return BobStmtBuilder.CreateTable(table)
}

99
create.go Normal file
View File

@ -0,0 +1,99 @@
package bob
import (
"bytes"
"errors"
"strings"
"github.com/aldy505/bob/util"
"github.com/lann/builder"
)
type CreateBuilder builder.Builder
type createData struct {
TableName string
Schema string
Columns []string
Types []string
Primary string
Unique string
NotNull []string
}
func init() {
builder.Register(CreateBuilder{}, createData{})
}
func (b CreateBuilder) Name(name string) CreateBuilder {
return builder.Set(b, "TableName", name).(CreateBuilder)
}
func (b CreateBuilder) WithSchema(name string) CreateBuilder {
return builder.Set(b, "Schema", name).(CreateBuilder)
}
func (b CreateBuilder) Columns(cols ...string) CreateBuilder {
return builder.Set(b, "Columns", cols).(CreateBuilder)
}
func (b CreateBuilder) Types(types ...string) CreateBuilder {
return builder.Set(b, "Types", types).(CreateBuilder)
}
func (b CreateBuilder) Primary(column string) CreateBuilder {
return builder.Set(b, "Primary", column).(CreateBuilder)
}
func (b CreateBuilder) Unique(column string) CreateBuilder {
return builder.Set(b, "Unique", column).(CreateBuilder)
}
func (b CreateBuilder) ToSql() (string, []interface{}, error) {
data := builder.GetStruct(b).(createData)
return data.ToSql()
}
func (d *createData) ToSql() (sqlStr string, args []interface{}, err error) {
if len(d.TableName) == 0 {
err = errors.New("create statements must specify a table")
return
}
if (len(d.Columns) != len(d.Types)) && len(d.Columns) > 0 {
err = errors.New("columns and types should have equal length")
return
}
sql := &bytes.Buffer{}
sql.WriteString("CREATE TABLE ")
if d.Schema != "" {
sql.WriteString("`" + d.Schema + "`.")
}
sql.WriteString("`" + d.TableName + "`")
sql.WriteString(" ")
var columnTypes []string
for i := 0; i < len(d.Columns); i++ {
columnTypes = append(columnTypes, "`"+d.Columns[i]+"` "+d.Types[i])
}
sql.WriteString("(")
sql.WriteString(strings.Join(columnTypes, ", "))
sql.WriteString(");")
if d.Primary != "" && util.IsIn(d.Columns, d.Primary) {
sql.WriteString(" ")
sql.WriteString("ALTER TABLE `" + d.TableName + "` ADD PRIMARY KEY (`" + d.Primary + "`);")
}
if d.Unique != "" && util.IsIn(d.Columns, d.Unique) {
sql.WriteString(" ")
sql.WriteString("ALTER TABLE `" + d.TableName + "` ADD UNIQUE (`" + d.Unique + "`);")
}
sqlStr = sql.String()
return
}

36
create_test.go Normal file
View File

@ -0,0 +1,36 @@
package bob_test
import (
"testing"
"github.com/aldy505/bob"
)
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()
if err != nil {
t.Fatal(err.Error())
}
result := "CREATE TABLE `users` (`name` varchar(255), `password` text, `date` date);"
if sql != result {
t.Fatal("sql is not equal to result:", sql)
}
})
t.Run("should return correct sql with primary key and unique key", func(t *testing.T) {
sql, _, err := bob.CreateTable("users").
Columns("id", "name", "email", "password", "date").
Types("uuid", "varchar(255)", "varchar(255)", "text", "date").
Primary("id").
Unique("email").
ToSql()
if err != nil {
t.Fatal(err.Error())
}
result := "CREATE TABLE `users` (`id` uuid, `name` varchar(255), `password` text, `date` date); ALTER TABLE `users` ADD PRIMARY KEY (`id`); ALTER TABLE `users` ADD UNIQUE (`email`)"
if sql != result {
t.Fatal("sql is not equal to result:", sql)
}
})
}

8
go.mod Normal file
View File

@ -0,0 +1,8 @@
module github.com/aldy505/bob
go 1.16
require (
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
)

4
go.sum Normal file
View File

@ -0,0 +1,4 @@
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=

25
has.go Normal file
View File

@ -0,0 +1,25 @@
package bob
import "github.com/lann/builder"
// TODO - The whole file is a todo
// Meant to find two things: HasTable and HasColumn(s)
type HasBuilder builder.Builder
type hasData struct {
Name string
Placeholder PlaceholderFormat
}
func init() {
builder.Register(HasBuilder{}, hasData{})
}
func (h HasBuilder) HasTable(table string) HasBuilder {
return builder.Set(h, "Name", table).(HasBuilder)
}
func (h HasBuilder) PlaceholderFormat(f PlaceholderFormat) HasBuilder {
return builder.Set(h, "Placeholder", f).(HasBuilder)
}

7
placeholder.go Normal file
View File

@ -0,0 +1,7 @@
package bob
// TODO
type PlaceholderFormat interface {
ReplacePlaceholders(sql string) (string, error)
}

19
util/util.go Normal file
View File

@ -0,0 +1,19 @@
package util
func IsIn(arr []string, value string) bool {
for _, item := range arr {
if item == value {
return true
}
}
return false
}
func FindPosition(arr []string, value string) int {
for i, item := range arr {
if item == value {
return i
}
}
return -1
}