Skip to content

Commit

Permalink
[MAJOR] Generative Sequelie (#1)
Browse files Browse the repository at this point in the history
* feat(sequelie): add `sequelie.Sql` type

* feat(sequelie): add golang codegen

* feat(readme): update todo

* feat(cli): adds the codegen cli

* feat(readme): add information about the cli

* feat(readme): add table of contents

* feat(gitignore): ignore .exe files

* feat(gitignore): ignore binaries

* feat(examples): update examples

* feat(readme): replace `GetAndTransform` for `Get().Interpolate()`
  • Loading branch information
ShindouMihou authored Jun 13, 2023
1 parent a1ae0ad commit 272a752
Show file tree
Hide file tree
Showing 12 changed files with 250 additions and 26 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
.idea/
.idea/
.sequelie/
*.exe
.binaries/
52 changes: 47 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ To learn by code, you can review our examples:
- [using `books.get`](examples/get)
- [using `books.get_with_field`](examples/get-with-field)

#### 📚 Table of Contents
- [`Installation`](#-installation)
- [`Creating Sequelie Files`](#-creating-sequelie-files)
- [`Declarations`](#-declarations)
- [`Literal Interpolation`](#-literal-interpolation)
- [`Reusing Queries`](#-reusing-queries)
- [`Codegen`](#-codegen)
- [`TODO`](#-todo)

#### 📦 Installation
```shell
go install github.com/ShindouMihou/sequelie
Expand Down Expand Up @@ -118,7 +127,7 @@ To use literal interpolation, you can use the `{&key}` placeholder, such as the
SELECT * FROM {$$TABLE} WHERE {&field} = $1
```
```go
query := sequelie.GetAndTransform("books.get_with_field", sequelie.Map{"field":"id"})
query := sequelie.Get("books.get_with_field").Interpolate(sequelie.Map{"field":"id"})
```

Additionally, Sequelie can automatically handle marshaling the data into JSON by adding the following
Expand Down Expand Up @@ -177,12 +186,45 @@ SELECT * FROM {$$TABLE} AND {$INSERT:articles.get}
You can view the full example of this in:
- [`examples/articles.sql`](examples/articles.sql)

##### 🚃 Codegen

Sequelie includes support for transpiling Sequelie files into Golang files, reducing the start-up time of application.
To perform code generation with Sequelie, there are two methods available:
1. [`Manual Codegen`](#manual-codegen): used when you don't want to use the cli.
2. [`CLI Codegen`](https://github.com/ShindouMihou/sequelie/releases): used when you want to use the cli.

###### Manual Codegen

Sequelie exposes the `generate` function that is used to export all the queries into Golang files under the `.sequelie/`
folder, you can run the following line to have it exported:
```go
// REMINDER: You need to have Sequelie read the queries first using the `.Read` functions.
sequelie.Generate()
```

##### CLI Codegen

Sequelie also offers a CLI tool that can handle the generations, you can download the binaries from:
- [`GitHub Releases`](https://github.com/ShindouMihou/sequelie/releases)

There is only one command in the CLI and that is `generate`:
```shell
# By default, if there is no `-d` or `--directory` parameter, it will use the `./` or working directory
# and search for Sequelie files recursively, this can be more expensive and time-consuming as it has to
# traverse through many folders.
sequelie generate

# It is recommended to specify where your Sequelie files are using the `-d` or `--directory` parameter.
# An example using the current repository would be:
sequelie generate -d examples/
```

##### 🎉 TODO
In light of the future of the library, here are the planned features of Sequelie that will one day transform the
Golang SQL ORM-less ecosystems.
- [ ] Generative Sequelie
- [ ] Generating `.go` files containing the SQL queries.
- [ ] Adding `sequelie.Sql` type to support direct interpolation in `.go` generated files.
- [ ] Adding a CLI to compile, or generate, the SQL files into `.go`
- [x] Generative Sequelie
- [x] Generating `.go` files containing the SQL queries.
- [x] Adding `sequelie.Sql` type to support direct interpolation in `.go` generated files.
- [x] Adding a CLI to compile, or generate, the SQL files into `.go`
- [ ] Operative Sequelie
- [ ] Supporting deferring of `Insert Operator`-enabled queries when the dependencies are not initialized.
46 changes: 46 additions & 0 deletions app/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package main

import (
"github.com/urfave/cli/v2"
"log"
"os"
"sequelie"
)

func main() {
app := &cli.App{
Name: "sequelie",
Description: "Off-loading your SQL queries from your Golang code, now includes codegen!",
Commands: []*cli.Command{
{
Name: "generate",
Aliases: []string{"g"},
Description: "Generates Golang files containing SQL queries from Sequelie.",
Action: func(context *cli.Context) error {
dir := context.String("directory")
if dir == "" {
dir = "./"
}
if err := sequelie.ReadDirectory(dir); err != nil {
return err
}
if err := sequelie.Generate(); err != nil {
return err
}
log.Println("[SQL] Generated all Golang files under the \".sequelie\" directory.")
return nil
},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "directory",
Aliases: []string{"d"},
Usage: "Reads and generates all the Sequelie files from the specific directory (recursive).",
},
},
},
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
2 changes: 1 addition & 1 deletion container.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"sync"
)

var store = make(map[string]string)
var store = make(map[string]*Query)

func readDirs(directories []string, options *Options) error {
var waitGroup sync.WaitGroup
Expand Down
2 changes: 1 addition & 1 deletion examples/get-with-field/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func main() {
}
// In this example, we are retrieving the "books.get_with_field" query from the books.sql and
// making it basically like "books.get" as an example.
query := sequelie.GetAndTransform("books.get_with_field", sequelie.Map{"field": "id"})
query := sequelie.Get("books.get_with_field").Interpolate(sequelie.Map{"field": "id"})
rows, err := sequel.Query(query, 0)
if err != nil {
log.Fatal("failed to get rows in postgres: ", err)
Expand Down
2 changes: 1 addition & 1 deletion examples/get/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func main() {
}
// In this example, we are retrieving the "books.get" query from the books.sql and uses
// the database/sql lib to handle the insertion of the id's value.
rows, err := sequel.Query(sequelie.Get("books.get"), 0)
rows, err := sequel.Query(sequelie.Get("books.get").String(), 0)
if err != nil {
log.Fatal("failed to get rows in postgres: ", err)
return
Expand Down
106 changes: 106 additions & 0 deletions generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package sequelie

import (
"bytes"
"errors"
"github.com/dave/jennifer/jen"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"os"
"strings"
"sync"
)

var titleCaser = cases.Title(language.AmericanEnglish, cases.NoLower)
var importSequelie = []byte("import \"sequelie\"")

func renderQuery(key string, q *Query) string {
separatorIndex := strings.Index(key, ".")
if separatorIndex == -1 {
panic("cannot find the separator for the key")
}
token := key[separatorIndex+1:]
token = strings.ReplaceAll(token, "_", " ")
token = titleCaser.String(token)
token = strings.ReplaceAll(token, " ", "")

return jen.Var().
Id(token+"Query").
Op("=").
Qual("sequelie", "Query").
Call(jen.Lit(q.String())).
GoString()
}

func getPackage(key string) string {
separatorIndex := strings.Index(key, ".")
if separatorIndex == -1 {
panic("cannot find the separator for the key")
}
return strings.ToLower(key[:separatorIndex])
}

func flatten() map[string][][]byte {
var t = make(map[string][][]byte)
for k, v := range store {
pkg := getPackage(k)
t[pkg] = append(t[pkg], []byte(renderQuery(k, v)))
}
return t
}

func render(pkg string, declarations [][]byte) []byte {
t := [][]byte{
[]byte("package " + pkg),
{},
importSequelie,
{},
}
t = append(t, declarations...)
return bytes.Join(t, newLineBytes)
}

func Generate() error {
t := flatten()

var waitGroup sync.WaitGroup
var errs []error

for pkg, declarations := range t {
pkg := pkg
declarations := declarations

waitGroup.Add(1)
go func() {
defer waitGroup.Done()
rendered := render(pkg, declarations)
err := os.MkdirAll(".sequelie/"+pkg, os.ModePerm)
if err != nil {
errs = append(errs, err)
return
}
f, err := os.Create(".sequelie/" + pkg + "/queries.go")
if err != nil {
errs = append(errs, err)
return
}
defer func(f *os.File) {
err := f.Close()
if err != nil {
errs = append(errs, err)
return
}
}(f)
_, err = f.Write(rendered)
if err != nil {
errs = append(errs, err)
return
}
}()
}
waitGroup.Wait()
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
9 changes: 9 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
module sequelie

go 1.18

require (
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/dave/jennifer v1.6.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/urfave/cli/v2 v2.25.6 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/text v0.10.0 // indirect
)
6 changes: 3 additions & 3 deletions reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ type localOptions struct {
Operators bool
}

func (reader *iReader) read(file string, m map[string]string, options *Options) error {
func (reader *iReader) read(file string, m map[string]*Query, options *Options) error {
f, err := os.Open(file)
if err != nil {
return err
Expand All @@ -63,7 +63,7 @@ func (reader *iReader) read(file string, m map[string]string, options *Options)
var push = func() error {
if address != nil {
mutex.Lock()
m[string(*address)] = strings.TrimSpace(builder.String())
m[string(*address)] = ptr(Query(strings.TrimSpace(builder.String())))
mutex.Unlock()

builder.Reset()
Expand Down Expand Up @@ -95,7 +95,7 @@ func (reader *iReader) read(file string, m map[string]string, options *Options)
v, e := m[string(name)]
mutex.RUnlock()
if e {
line = bytes.Replace(line, clause, []byte(v), 1)
line = bytes.Replace(line, clause, v.Bytes(), 1)
} else {
options.Logger.Println(
"ERR sequelie couldn't insert ", string(name), " into ", string(*address),
Expand Down
6 changes: 1 addition & 5 deletions sequelie.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,7 @@ func ReadDirectoryWithSettings(dir string, settings *Options) error {
return readDir(dir, settings)
}

func GetAndTransform(address string, transformers Map) string {
return transform(Get(address), transformers, &Settings)
}

func Get(address string) string {
func Get(address string) *Query {
q, ex := store[address]
if !ex {
panic("cannot find any query with the address " + address)
Expand Down
25 changes: 16 additions & 9 deletions sequelie_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package sequelie

import (
"fmt"

"testing"
)

Expand Down Expand Up @@ -47,14 +46,14 @@ func TestReadDir(t *testing.T) {
}

func TestGet(t *testing.T) {
v := Get("books.get")
v := Get("books.get").String()
if v != "SELECT * FROM books WHERE id = $1" {
t.Error("books.get does not match desired value, instead got:", v)
}
}

func TestGetAndTransformMarshalString(t *testing.T) {
v := GetAndTransform("books.test", Map{
v := Get("books.test").Interpolate(Map{
"field": "id",
"value": &marshalStringStruct{"world"},
})
Expand All @@ -64,7 +63,7 @@ func TestGetAndTransformMarshalString(t *testing.T) {
}

func TestGetAndTransformString(t *testing.T) {
v := GetAndTransform("books.test", Map{
v := Get("books.test").Interpolate(Map{
"field": "id",
"value": &stringStruct{"world"},
})
Expand All @@ -74,7 +73,7 @@ func TestGetAndTransformString(t *testing.T) {
}

func TestGetAndTransformMarshalSequelie(t *testing.T) {
v := GetAndTransform("books.test", Map{
v := Get("books.test").Interpolate(Map{
"field": "id",
"value": &sequelieMarshalStruct{"world"},
})
Expand All @@ -84,7 +83,7 @@ func TestGetAndTransformMarshalSequelie(t *testing.T) {
}

func TestGetAndTransformMarshalJson(t *testing.T) {
v := GetAndTransform("books.test", Map{
v := Get("books.test").Interpolate(Map{
"field": "id",
"value": &marshalJsonStruct{"world"},
})
Expand All @@ -94,16 +93,24 @@ func TestGetAndTransformMarshalJson(t *testing.T) {
}

func TestGetDeclaration(t *testing.T) {
v := Get("books.get_romance_books")
v := Get("books.get_romance_books").String()
if v != "SELECT * FROM books WHERE category = 'romance'" {
t.Error("books.get_romance_books does not match desired value, instead got: ", v)
}
v = Get("articles.reuse")
if v != "SELECT * FROM articles AND "+Get("articles.get") {
v = Get("articles.reuse").String()
if v != "SELECT * FROM articles AND "+Get("articles.get").String() {
t.Error("articles.reuse does not match desired value, instead got: ", v)
}
}

func TestGenerate(t *testing.T) {
err := Generate()
if err != nil {
t.Error(err)
return
}
}

func BenchmarkReadDirectory(b *testing.B) {
for i := 0; i < b.N; i++ {
err := ReadDirectory("examples/")
Expand Down
Loading

0 comments on commit 272a752

Please sign in to comment.