Skip to content

Commit d0fd0c9

Browse files
committed
initial commit
0 parents  commit d0fd0c9

28 files changed

Lines changed: 3428 additions & 0 deletions

.envrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
source_url "https://raw.githubusercontent.com/cachix/devenv/95f329d49a8a5289d31e0982652f7058a189bfca/direnvrc" "sha256-d+8cBpDfDBj41inrADaJt+bDWhOktwslgoP5YiGJ1v0="
2+
3+
use devenv

.github/workflows/ci.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
on: [push, pull_request]
2+
name: Test
3+
jobs:
4+
test:
5+
runs-on: ubuntu-latest
6+
steps:
7+
- uses: actions/checkout@v4
8+
- uses: cachix/install-nix-action@v26
9+
- uses: cachix/cachix-action@v14
10+
with:
11+
name: devenv
12+
- name: Install devenv.sh
13+
run: nix profile install nixpkgs#devenv
14+
- name: Build the devenv shell and run any pre-commit hooks
15+
run: devenv test

.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Devenv
2+
.devenv*
3+
devenv.local.nix
4+
5+
# direnv
6+
.direnv
7+
8+
# pre-commit
9+
.pre-commit-config.yaml
10+
11+
coverage.*
12+
bin/

Readme.md

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# MangoSQL
2+
3+
## Description
4+
5+
MangoSQL is a fresh and juicy SQL code generator.
6+
7+
1. You provide your database schema (.sql files)
8+
2. You run MangoSQL cli to generate a client with type-safe interfaces and queries
9+
3. You write application code that calls the generated code
10+
11+
This is not an ORM, and you can easily inspect the code generated.
12+
This is inspired by [SQLC](https://github.com/sqlc-dev/sqlc) but pushes the idea farther by supporting relations and dynamic queries.
13+
14+
## Features
15+
16+
* **Convenient**: Generate all the structs for you
17+
* **Flexible**: Provide a way to run dynamic queries (pagination, search, ...)
18+
* **Time Saver**: All the basic queries (CRUD) are auto-generated from your schema, nothing to declare
19+
* **Performant**: Support batching out of the box
20+
* **Safe**: All the SQL queries use prepared statement
21+
* **Consistency**: Easy to use transaction API
22+
23+
## Status
24+
25+
This is WIP, features are still not complete and may change
26+
27+
Goals:
28+
* Handle custom relations (join, aggregations, ...)
29+
* Handle views
30+
* Support Mysql/MariaDB/Sqlite3
31+
* Better CLI
32+
33+
## Example
34+
35+
So let's see what it means in reality. For the following:
36+
```sql
37+
CREATE TABLE users (
38+
id UUID PRIMARY KEY,
39+
email VARCHAR(64) NOT NULL,
40+
name VARCHAR(64) NOT NULL,
41+
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
42+
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
43+
deleted_at TIMESTAMP DEFAULT NULL
44+
);
45+
```
46+
47+
Execute the following command to automatically generate a `database/client.go`
48+
```sh
49+
mangosql --output=database schema.sql
50+
```
51+
52+
This is all you need to do, now the client can be used in your code
53+
```go
54+
sqlDb, err := sqlx.Connect("postgres", "Postgres Connection URL")
55+
if err != nil {
56+
panic(err)
57+
}
58+
59+
db := database.New(sqlDb)
60+
61+
// then you can use it and make queries or transactions
62+
63+
// Handle crud operation
64+
user, err := db.User.Create(database.UserCreate{
65+
Name: "user1",
66+
Email: "user1@email.com"
67+
})
68+
69+
// Handle transactions
70+
err := db.Transaction(func(tx *database.DBClient) error {
71+
// ...
72+
})
73+
74+
// Handle dynamic clause (filters, pagination, ...)
75+
users, err := db.User.Where(func(cond database.SelectBuilder) database.SelectBuilder {
76+
return cond.Where("name ILIKE $1", "%user%").Offset(0).Limit(20)
77+
})
78+
79+
// Handle Batching
80+
ids, err := db.User.UpsertMany([]database.UserUpdate{
81+
{Email: "usernew@localhost", Name: "usernew"}, // this entry will be inserted
82+
{Id: id, Email: "user1-updated", Name: "user1-updated"}, // this entry will be updated
83+
})
84+
85+
// ...
86+
```
87+
88+
## API
89+
90+
Here is the list of all the autogenerated methods for your tables:
91+
92+
## Getters
93+
* db.{Table}.Count()
94+
* db.{Table}.CountWhere(condition)
95+
* db.{Table}.All(offset, limit)
96+
* db.{Table}.Where(condition)
97+
* db.{Table}.GetById(id)
98+
99+
## Mutations
100+
* db.{Table}.Create(input)
101+
* db.{Table}.Update(input)
102+
* db.{Table}.Upsert(input)
103+
* db.{Table}.DeleteSoft(id)
104+
* db.{Table}.DeleteHard(id)
105+
106+
## Batch Mutations
107+
* db.{Table}.CreateMany(inputs)
108+
* db.{Table}.UpdateMany(inputs)
109+
* db.{Table}.UpsertMany(inputs)

cmd/mangosql/mangosql.go

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"fmt"
7+
"go/format"
8+
"log"
9+
"os"
10+
"path"
11+
"regexp"
12+
"strings"
13+
14+
"github.com/kefniark/mango-sql/internal"
15+
"github.com/urfave/cli/v2"
16+
)
17+
18+
func main() {
19+
app := &cli.App{
20+
Name: "mangosql",
21+
HelpName: "MangoSQL",
22+
Usage: "Generate a SQL Client from a SQL file or folder of SQL migrations",
23+
UsageText: `Syntax: mangosql [options] <source folder>
24+
Example: mangosql --output db/file.go db/schema.sql`,
25+
Suggest: true,
26+
Flags: []cli.Flag{
27+
&cli.StringFlag{
28+
Name: "output",
29+
Value: "database/client.go",
30+
Usage: "Output file",
31+
},
32+
&cli.StringFlag{
33+
Name: "package",
34+
Value: "database",
35+
Usage: "Go Package",
36+
},
37+
},
38+
Action: func(ctx *cli.Context) error {
39+
if ctx.NArg() <= 0 {
40+
return fmt.Errorf("missing source folder")
41+
}
42+
43+
name := ctx.Args().Get(0)
44+
return generate(GenerateOptions{
45+
Src: name,
46+
Output: ctx.String("output"),
47+
Package: ctx.String("package"),
48+
})
49+
},
50+
}
51+
52+
if err := app.Run(os.Args); err != nil {
53+
log.Fatal(err)
54+
}
55+
}
56+
57+
type GenerateOptions struct {
58+
Src string
59+
Output string
60+
Package string
61+
}
62+
63+
func generate(opts GenerateOptions) error {
64+
stat, err := os.Stat(opts.Output)
65+
if err != nil {
66+
return err
67+
}
68+
69+
var sql string
70+
if stat.IsDir() {
71+
sql = parseMigrationFolder(opts.Src)
72+
} else {
73+
data, err := os.ReadFile(opts.Src)
74+
if err != nil {
75+
return err
76+
}
77+
sql = string(data)
78+
}
79+
80+
schema, err := internal.ParseSchema(sql)
81+
if err != nil {
82+
return err
83+
}
84+
85+
var b bytes.Buffer
86+
contents := bufio.NewWriter(&b)
87+
88+
if err = internal.Generate(schema, contents, opts.Package); err != nil {
89+
return err
90+
}
91+
92+
folder := path.Dir(opts.Output)
93+
file := path.Base(opts.Output)
94+
95+
stat, err = os.Stat(opts.Output)
96+
if err == nil && stat.IsDir() {
97+
folder = opts.Output
98+
file = "client.go"
99+
}
100+
101+
if err = os.MkdirAll(folder, os.ModeAppend); err != nil {
102+
return err
103+
}
104+
105+
f, err := os.Create(path.Join(folder, file))
106+
if err != nil {
107+
return err
108+
}
109+
110+
defer f.Close()
111+
112+
contents.Flush()
113+
114+
formatted, err := format.Source([]byte((b.String())))
115+
if err != nil {
116+
return err
117+
}
118+
119+
_, err = f.WriteString(string(formatted))
120+
fmt.Printf("Generated %s\n", path.Join(folder, file))
121+
122+
return err
123+
}
124+
125+
func parseMigrationFolder(folderName string) string {
126+
entries, err := os.ReadDir(folderName)
127+
if err != nil {
128+
panic(err)
129+
}
130+
131+
sql := []string{}
132+
for _, entries := range entries {
133+
if entries.IsDir() {
134+
continue
135+
}
136+
137+
fileName := path.Join(folderName, entries.Name())
138+
data, err := os.ReadFile(fileName)
139+
if err != nil {
140+
panic(err)
141+
}
142+
143+
entry := parseMigrationFile(string(data))
144+
sql = append(sql, strings.TrimSpace(entry))
145+
}
146+
147+
return strings.Join(sql, "\n")
148+
}
149+
150+
var parseGooseMetaUp = regexp.MustCompile(`-- \+goose Up`)
151+
var parseGooseMetaDown = regexp.MustCompile(`-- \+goose Down`)
152+
153+
func parseMigrationFile(content string) string {
154+
up := parseGooseMetaUp.FindStringIndex(content)
155+
down := parseGooseMetaDown.FindStringIndex(content)
156+
157+
if len(up) == 0 {
158+
return ""
159+
}
160+
161+
if len(down) == 0 {
162+
return content[up[1]:]
163+
}
164+
165+
if up[0] < down[0] {
166+
return content[up[1]:down[0]]
167+
}
168+
169+
return content[down[1]:up[0]]
170+
}

0 commit comments

Comments
 (0)