Skip to content

Commit a5419fe

Browse files
committed
Implement <smart-column> parsing
1 parent 19db09b commit a5419fe

File tree

5 files changed

+251
-27
lines changed

5 files changed

+251
-27
lines changed

cmd/sqlcup/main.go

Lines changed: 110 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
package main
33

44
import (
5+
_ "embed"
56
"errors"
67
"flag"
78
"fmt"
@@ -19,7 +20,10 @@ var (
1920
onlyFlag = flag.String("only", "", "Limit output to 'schema' or 'queries'")
2021
)
2122

22-
var errBadArgument = errors.New("bad argument")
23+
var (
24+
errBadArgument = errors.New("bad argument")
25+
errInvalidSmartColumn = fmt.Errorf("%w: invalid <smart-column>", errBadArgument)
26+
)
2327

2428
func main() {
2529
flag.CommandLine.SetOutput(os.Stdout)
@@ -35,34 +39,13 @@ func main() {
3539
}
3640
}
3741

42+
//go:embed usage.txt
43+
var usage string
44+
3845
//goland:noinspection GoUnhandledErrorResult
3946
func printUsage() {
4047
w := flag.CommandLine.Output()
41-
fmt.Fprintln(w, "sqlcup - generate SQL statements for sqlc (https://sqlc.dev)")
42-
fmt.Fprintln(w)
43-
fmt.Fprintln(w, "Synopsis:")
44-
fmt.Fprintln(w, " sqlcup [options] <name> <column> ...")
45-
fmt.Fprintln(w)
46-
fmt.Fprintln(w, "Description:")
47-
fmt.Fprintln(w, " sqlcup prints SQL statements to stdout. The <name> argument given to sqlcup")
48-
fmt.Fprintln(w, " must be of the form <singular>/<plural> where <singular> is the name of the")
49-
fmt.Fprintln(w, " Go struct and <plural> is the name of the database table.")
50-
fmt.Fprintln(w, " sqlcup capitalizes those names where required.")
51-
fmt.Fprintln(w)
52-
fmt.Fprintln(w, " Each <column> arguments given to sqlcup defines a database column and must")
53-
fmt.Fprintln(w, " be of the form <name>:<type>[:<constraint>]. <name>, <type> and the")
54-
fmt.Fprintln(w, " optional <constraint> are used to generate a CREATE TABLE statement.")
55-
fmt.Fprintln(w, " In addition, <name> also appears in the SQL queries. sqlcup never")
56-
fmt.Fprintln(w, " capitalizes those names.")
57-
fmt.Fprintln(w)
58-
fmt.Fprintln(w, " If any part of a <column> contains a space, it may be necessary to add")
59-
fmt.Fprintln(w, " quotes or escape those spaces, depending on the user's shell.")
60-
fmt.Fprintln(w)
61-
fmt.Fprintln(w, "Example:")
62-
fmt.Fprintln(w, " sqlcup author/authors \"id:INTEGER:PRIMARY KEY\" \"name:text:NOT NULL\" bio:text")
63-
fmt.Fprintln(w, " sqlcup --order-by name user/users \"id:INTEGER:PRIMARY KEY\" name:text")
64-
fmt.Fprintln(w)
65-
fmt.Fprintln(w, "Options:")
48+
fmt.Fprintln(w, usage)
6649
flag.PrintDefaults()
6750
}
6851

@@ -106,9 +89,109 @@ type scaffoldCommandArgs struct {
10689
}
10790

10891
func parseColumnDefinition(s string) (column, error) {
92+
var (
93+
plainColumn = strings.Contains(s, ":")
94+
smartColumn = strings.Contains(s, "#")
95+
)
96+
if plainColumn && smartColumn {
97+
return column{}, fmt.Errorf("%w: invalid <column>: '%s' contains both plain and smart separators", errBadArgument, s)
98+
}
99+
if plainColumn {
100+
return parsePlainColumnDefinition(s)
101+
} else if smartColumn {
102+
return parseSmartColumnDefinition(s)
103+
}
104+
return column{}, fmt.Errorf("%w: invalid <column>: '%s', expected <smart-column> or <plain-column>", errBadArgument, s)
105+
}
106+
107+
func parseSmartColumnDefinition(s string) (column, error) {
108+
if s == "#id" {
109+
return column{
110+
ID: true,
111+
Name: "id",
112+
Type: "INTEGER",
113+
Constraint: "PRIMARY KEY",
114+
}, nil
115+
}
116+
117+
name, rest, _ := strings.Cut(s, "#")
118+
if name == "" {
119+
return column{}, fmt.Errorf("%w: '%s', missing <name>", errInvalidSmartColumn, s)
120+
}
121+
122+
var (
123+
colType string
124+
id bool
125+
null bool
126+
unique bool
127+
)
128+
tags := strings.Split(rest, "#")
129+
for _, tag := range tags {
130+
switch tag {
131+
case "id":
132+
id = true
133+
case "null":
134+
null = true
135+
case "unique":
136+
unique = true
137+
case "float":
138+
colType = "FLOAT"
139+
case "double":
140+
colType = "DOUBLE"
141+
case "datetime":
142+
colType = "DATETIME"
143+
case "text":
144+
colType = "TEXT"
145+
case "int":
146+
colType = "INTEGER"
147+
case "blob":
148+
colType = "BLOB"
149+
default:
150+
return column{}, fmt.Errorf("%w: '%s': unknown <tag> #%s", errInvalidSmartColumn, s, tag)
151+
}
152+
}
153+
if id {
154+
if unique || null {
155+
return column{}, fmt.Errorf("%w: '%s', cannot combine #id with #unique or #null", errInvalidSmartColumn, s)
156+
}
157+
if colType == "" {
158+
colType = "INTEGER"
159+
}
160+
// sqlite special case
161+
var constraint = "PRIMARY KEY"
162+
if colType != "INTEGER" {
163+
constraint = "NOT NULL " + constraint
164+
}
165+
return column{
166+
Name: name,
167+
Type: colType,
168+
Constraint: constraint,
169+
ID: true,
170+
}, nil
171+
}
172+
173+
if colType == "" {
174+
return column{}, fmt.Errorf("%w: '%s' missing column type", errInvalidSmartColumn, s)
175+
}
176+
constraint := ""
177+
if !null {
178+
constraint += " NOT NULL"
179+
}
180+
if unique {
181+
constraint += " UNIQUE"
182+
}
183+
return column{
184+
Name: name,
185+
Type: colType,
186+
Constraint: strings.TrimSpace(constraint),
187+
ID: false,
188+
}, nil
189+
}
190+
191+
func parsePlainColumnDefinition(s string) (column, error) {
109192
parts := strings.Split(s, ":")
110193
if len(parts) < 2 || len(parts) > 3 {
111-
return column{}, fmt.Errorf("%w: invalid <column>: '%s', expected '<name>:<type>' or '<name>:<type>:<constraint>'", errBadArgument, s)
194+
return column{}, fmt.Errorf("%w: invalid <plain-column>: '%s', expected '<name>:<type>' or '<name>:<type>:<constraint>'", errBadArgument, s)
112195
}
113196
col := column{
114197
ID: strings.ToLower(parts[0]) == *idColumnFlag,

cmd/sqlcup/main_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package main
2+
3+
import (
4+
"github.com/google/go-cmp/cmp"
5+
"testing"
6+
)
7+
8+
var smartColTests = map[string]struct {
9+
col column
10+
err error
11+
}{
12+
"#id": {
13+
col: column{
14+
Name: "id",
15+
Type: "INTEGER",
16+
Constraint: "PRIMARY KEY",
17+
ID: true,
18+
},
19+
},
20+
"col_id#id": {
21+
col: column{
22+
Name: "col_id",
23+
Type: "INTEGER",
24+
Constraint: "PRIMARY KEY",
25+
ID: true,
26+
},
27+
},
28+
"primary_key#text#id": {
29+
col: column{
30+
Name: "primary_key",
31+
Type: "TEXT",
32+
Constraint: "NOT NULL PRIMARY KEY",
33+
ID: true,
34+
},
35+
},
36+
"col#text": {
37+
col: column{
38+
Name: "col",
39+
Type: "TEXT",
40+
Constraint: "NOT NULL",
41+
ID: false,
42+
},
43+
},
44+
"col#text#null": {
45+
col: column{
46+
Name: "col",
47+
Type: "TEXT",
48+
Constraint: "",
49+
ID: false,
50+
},
51+
},
52+
"col#text#unique": {
53+
col: column{
54+
Name: "col",
55+
Type: "TEXT",
56+
Constraint: "NOT NULL UNIQUE",
57+
ID: false,
58+
},
59+
},
60+
"col#int": {
61+
col: column{
62+
Name: "col",
63+
Type: "INTEGER",
64+
Constraint: "NOT NULL",
65+
ID: false,
66+
},
67+
},
68+
"col#datetime": {
69+
col: column{
70+
Name: "col",
71+
Type: "DATETIME",
72+
Constraint: "NOT NULL",
73+
ID: false,
74+
},
75+
err: nil,
76+
},
77+
}
78+
79+
func TestParseSmartColumnDefinition(t *testing.T) {
80+
for def, want := range smartColTests {
81+
t.Run(def, func(t *testing.T) {
82+
got, err := parseSmartColumnDefinition(def)
83+
if diff := cmp.Diff(want.err, err); diff != "" {
84+
t.Errorf("parseSmartColumnDefinition(\"%s\") returned wrong error: diff -want +got\n%s", def, diff)
85+
}
86+
if diff := cmp.Diff(want.col, got); diff != "" {
87+
t.Errorf("parseSmartColumnDefinition(\"%s\") returned wrong column: diff -want +got\n%s", def, diff)
88+
}
89+
})
90+
}
91+
}

cmd/sqlcup/usage.txt

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
sqlcup - generate SQL statements for sqlc (https://sqlc.dev)
2+
3+
Synopsis:
4+
sqlcup [options] <name> <column> ...
5+
6+
Description:
7+
sqlcup prints SQL statements to stdout. The <name> argument given to sqlcup
8+
must be of the form <singular>/<plural> where <singular> is the name of the
9+
Go struct and <plural> is the name of the database table.
10+
sqlcup capitalizes those names where required.
11+
12+
Each column argument given to sqlcup defines a database column and must
13+
be either a <plain-column> or a <smart-column>:
14+
15+
A <plain-column> must be of the form <name>:<type>[:<constraint>]. <name>,
16+
<type> and the optional <constraint> are used to generate a CREATE TABLE
17+
statement. In addition, <name> also appears in SQL queries. sqlcup never
18+
capitalizes those names. To use <tag> you need to define a <smart-column>.
19+
20+
A <smart-column> is a shortcut for common column definitions. It must be of
21+
the form <name>#<tag>#<tag>...
22+
23+
A <tag> adds either a data type or a constraint to a <smart-column>.
24+
25+
#id
26+
Make this column the primary key. Omitting column type and <name>
27+
for an #id column creates an INTEGER PRIMARY KEY named 'id'.
28+
29+
#text, #int, #float, #double, #datetime, #blob
30+
Set the column type.
31+
32+
#unique
33+
Add a UNIQUE constraint.
34+
35+
#null
36+
Omit NOT NULL constraint.
37+
38+
If any part of a <column> contains a space, it may be necessary to add
39+
quotes or escape those spaces, depending on the user's shell.
40+
41+
Example:
42+
sqlcup author/authors "id:INTEGER:PRIMARY KEY" "name:text:NOT NULL" bio:text
43+
sqlcup --order-by name user/users "id:INTEGER:PRIMARY KEY" name:text
44+
sqlcup author/authors #id name#text#unique bio#text#null
45+
46+
Options:

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
module github.com/ngrash/sqlcup
22

33
go 1.18
4+
5+
require github.com/google/go-cmp v0.5.9 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
2+
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=

0 commit comments

Comments
 (0)