Skip to content

Commit 13309f3

Browse files
committed
Improve documentation and error messages
1 parent 84f55c4 commit 13309f3

File tree

3 files changed

+92
-53
lines changed

3 files changed

+92
-53
lines changed

README.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
## Installation
66

77
```
8-
$ go install github.com/ngrash/sqlcup/cmd/[email protected].0
8+
$ go install github.com/ngrash/sqlcup/cmd/[email protected].1
99
```
1010

1111
## Usage
@@ -15,13 +15,12 @@ $ sqlcup -help
1515
sqlcup - generate SQL statements for sqlc (https://sqlc.dev)
1616
1717
Synopsis:
18-
sqlcup [options] <name> <column> ...
18+
sqlcup [options] <entity-name> <column> ...
1919
2020
Description:
21-
sqlcup prints SQL statements to stdout. The <name> argument given to sqlcup
22-
must be of the form <singular>/<plural> where <singular> is the name of the
23-
Go struct and <plural> is the name of the database table.
24-
sqlcup capitalizes those names where required.
21+
sqlcup prints SQL statements to stdout. The <entity-name> argument must be
22+
of the form <singular-name>/<plural-name>. sqlcup capitalizes those names
23+
where necessary.
2524
2625
Each column argument given to sqlcup defines a database column and must
2726
be either a <plain-column> or a <smart-column>:
@@ -32,12 +31,14 @@ Description:
3231
capitalizes those names. To use <tag> you need to define a <smart-column>.
3332
3433
A <smart-column> is a shortcut for common column definitions. It must be of
35-
the form <name>@<tag>@<tag>...
34+
the form [<name>]<tag>... where <name> is only optional for the special case
35+
when the <smart-column> consists of the single <tag> @id. A <smart-column> is
36+
not nullable unless @null is present.
3637
3738
A <tag> adds either a data type or a constraint to a <smart-column>.
3839
3940
@id
40-
Make this column the primary key. Omitting column type and <name>
41+
Make this column the primary key. Omitting <type> and <name>
4142
for an @id column creates an INTEGER PRIMARY KEY named 'id'.
4243
4344
@text, @int, @float, @double, @datetime, @blob
@@ -47,10 +48,10 @@ Description:
4748
Add a UNIQUE constraint.
4849
4950
@null
50-
Omit NOT NULL constraint.
51+
Omit the default NOT NULL constraint.
5152
5253
If any part of a <column> contains a space, it may be necessary to add
53-
quotes or escape those spaces, depending on the user's shell.
54+
quotes or otherwise escape those spaces, depending on the user's shell.
5455
5556
Example:
5657
sqlcup author/authors "id:INTEGER:PRIMARY KEY" "name:text:NOT NULL" bio:text
@@ -68,7 +69,6 @@ Options:
6869
Limit output to 'schema' or 'queries'
6970
-order-by string
7071
Include ORDER BY in 'SELECT *' statement
71-
7272
```
7373

7474
## Example

cmd/sqlcup/main.go

Lines changed: 71 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,6 @@ import (
1212
"unicode"
1313
)
1414

15-
const (
16-
plainColumnSep = ":"
17-
smartColumnSep = "@"
18-
)
19-
2015
var (
2116
noExistsClauseFlag = flag.Bool("no-exists-clause", false, "Omit IF NOT EXISTS in CREATE TABLE statements")
2217
idColumnFlag = flag.String("id-column", "id", "Name of the column that identifies a row")
@@ -25,41 +20,84 @@ var (
2520
onlyFlag = flag.String("only", "", "Limit output to 'schema' or 'queries'")
2621
)
2722

23+
const (
24+
plainColumnSep = ":"
25+
smartColumnSep = "@"
26+
)
27+
28+
const (
29+
exitCodeBadArgument = 1
30+
exitCodeInternalError = 2
31+
)
32+
2833
var (
2934
errBadArgument = errors.New("bad argument")
3035
errInvalidSmartColumn = fmt.Errorf("%w: invalid <smart-column>", errBadArgument)
3136
)
3237

33-
func main() {
34-
flag.CommandLine.SetOutput(os.Stdout)
35-
flag.Usage = printUsage
36-
flag.Parse()
38+
// usage contains the inline documentation for sqlcup.
39+
//go:embed usage.txt
40+
var usage string
3741

38-
if err := run(); err != nil {
39-
_, _ = fmt.Fprintf(os.Stderr, "%s: %s\n", os.Args[0], err)
40-
if errors.Is(err, errBadArgument) {
41-
flag.Usage()
42+
func main() {
43+
// Suppress error logs from flag package while parsing flags.
44+
flag.CommandLine.SetOutput(io.Discard)
45+
// With flag.ContinueOnError we prevent Parse from calling os.Exit on error and instead show our own error message.
46+
flag.CommandLine.Init(os.Args[0], flag.ContinueOnError)
47+
if err := flag.CommandLine.Parse(os.Args[1:]); err != nil {
48+
if err == flag.ErrHelp {
49+
printHelp()
50+
os.Exit(0)
4251
}
43-
os.Exit(1)
52+
fatalUsageError(err)
53+
}
54+
55+
sca, err := parseScaffoldCommandArgs(flag.CommandLine.Args())
56+
if err != nil {
57+
exitWithError(err)
58+
}
59+
60+
err = scaffoldCommand(sca)
61+
if err != nil {
62+
exitWithError(err)
4463
}
4564
}
4665

47-
//go:embed usage.txt
48-
var usage string
66+
// fatalUsageError writes the inline help to os.Stdout and the err to os.Stderr, then calls os.Exit(1).
67+
//goland:noinspection GoUnhandledErrorResult
68+
func fatalUsageError(err error) {
69+
printHelp()
70+
71+
// Write error message to stderr.
72+
fmt.Fprintf(os.Stderr, "%s: %s\n", os.Args[0], err)
73+
74+
// Exit process with non-zero status code to indicate failure to the calling process.
75+
os.Exit(exitCodeBadArgument)
76+
}
4977

5078
//goland:noinspection GoUnhandledErrorResult
51-
func printUsage() {
52-
w := flag.CommandLine.Output()
53-
fmt.Fprintln(w, usage)
54-
flag.PrintDefaults()
79+
func printHelp() {
80+
// Write usage documentation for sqlcup to stdout.
81+
fmt.Fprintln(os.Stdout, usage)
82+
83+
// Write flag documentation and defaults to stdout.
84+
flag.CommandLine.SetOutput(os.Stdout)
85+
flag.CommandLine.PrintDefaults()
86+
flag.CommandLine.SetOutput(io.Discard)
87+
fmt.Fprintln(os.Stdout)
5588
}
5689

57-
func run() error {
58-
sca, err := parseScaffoldCommandArgs(flag.Args())
59-
if err != nil {
60-
return err
90+
// exitWithError prints err to os.Stderr and calls os.Exit.
91+
// If err is (or wraps) errBadArgument, inline documentation is written to os.Stdout.
92+
//goland:noinspection GoUnhandledErrorResult
93+
func exitWithError(err error) {
94+
if errors.Is(err, errBadArgument) {
95+
fatalUsageError(err)
96+
} else {
97+
// This is not a user error, so we don't write inline help.
98+
fmt.Fprintf(os.Stderr, "%s: %s\n", os.Args[0], err)
99+
os.Exit(exitCodeInternalError)
61100
}
62-
return scaffoldCommand(sca)
63101
}
64102

65103
type column struct {
@@ -152,7 +190,7 @@ func parseSmartColumnDefinition(s string) (column, error) {
152190
case "blob":
153191
colType = "BLOB"
154192
default:
155-
return column{}, fmt.Errorf("%w: '%s': unknown <tag> #%s", errInvalidSmartColumn, s, tag)
193+
return column{}, fmt.Errorf("%w: '%s', unknown <tag> #%s", errInvalidSmartColumn, s, tag)
156194
}
157195
}
158196
if id {
@@ -176,7 +214,7 @@ func parseSmartColumnDefinition(s string) (column, error) {
176214
}
177215

178216
if colType == "" {
179-
return column{}, fmt.Errorf("%w: '%s' missing column type", errInvalidSmartColumn, s)
217+
return column{}, fmt.Errorf("%w: '%s', missing column type", errInvalidSmartColumn, s)
180218
}
181219
constraint := ""
182220
if !null {
@@ -195,8 +233,8 @@ func parseSmartColumnDefinition(s string) (column, error) {
195233

196234
func parsePlainColumnDefinition(s string) (column, error) {
197235
parts := strings.Split(s, ":")
198-
if len(parts) < 2 || len(parts) > 3 {
199-
return column{}, fmt.Errorf("%w: invalid <plain-column>: '%s', expected '<name>:<type>' or '<name>:<type>:<constraint>'", errBadArgument, s)
236+
if len(parts) < 2 || len(parts) > 3 || parts[0] == "" {
237+
return column{}, fmt.Errorf("%w: invalid <plain-column>: '%s', expected '<name>:<type>[:<constraint>]'", errBadArgument, s)
200238
}
201239
col := column{
202240
ID: strings.ToLower(parts[0]) == *idColumnFlag,
@@ -331,14 +369,14 @@ func writeSchema(w io.Writer, args *scaffoldCommandArgs) {
331369
fmt.Fprintf(w, ");")
332370
}
333371

334-
//goland:noinspection GoUnhandledErrorResult
372+
//goland:noinspection GoUnhandledErrorResult,SqlNoDataSourceInspection
335373
func writeGetQuery(w io.Writer, args *scaffoldCommandArgs) {
336374
fmt.Fprintf(w, "-- name: Get%s :one\n", args.SingularEntity)
337375
fmt.Fprintf(w, "SELECT * FROM %s\n", args.Table)
338376
fmt.Fprintf(w, "WHERE %s = ? LIMIT 1;", args.IDColumn.Name)
339377
}
340378

341-
//goland:noinspection GoUnhandledErrorResult
379+
//goland:noinspection GoUnhandledErrorResult,SqlNoDataSourceInspection
342380
func writeListQuery(w io.Writer, args *scaffoldCommandArgs) {
343381
fmt.Fprintf(w, "-- name: List%s :many\n", args.PluralEntity)
344382
fmt.Fprintf(w, "SELECT * FROM %s", args.Table)
@@ -349,7 +387,7 @@ func writeListQuery(w io.Writer, args *scaffoldCommandArgs) {
349387
}
350388
}
351389

352-
//goland:noinspection GoUnhandledErrorResult
390+
//goland:noinspection GoUnhandledErrorResult,SqlNoDataSourceInspection
353391
func writeCreateQuery(w io.Writer, args *scaffoldCommandArgs) {
354392
fmt.Fprintf(w, "-- name: Create%s :one\n", args.SingularEntity)
355393
fmt.Fprintf(w, "INSERT INTO %s (\n", args.Table)
@@ -375,7 +413,7 @@ func writeCreateQuery(w io.Writer, args *scaffoldCommandArgs) {
375413
fmt.Fprintf(w, "RETURNING *;")
376414
}
377415

378-
//goland:noinspection GoUnhandledErrorResult
416+
//goland:noinspection GoUnhandledErrorResult,SqlNoDataSourceInspection
379417
func writeDeleteQuery(w io.Writer, args *scaffoldCommandArgs) {
380418
fmt.Fprintf(w, "-- name: Delete%s :exec\n", args.SingularEntity)
381419
fmt.Fprintf(w, "DELETE FROM %s\n", args.Table)

cmd/sqlcup/usage.txt

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
sqlcup - generate SQL statements for sqlc (https://sqlc.dev)
22

33
Synopsis:
4-
sqlcup [options] <name> <column> ...
4+
sqlcup [options] <entity-name> <column> ...
55

66
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.
7+
sqlcup prints SQL statements to stdout. The <entity-name> argument must be
8+
of the form <singular-name>/<plural-name>. sqlcup capitalizes those names
9+
where necessary.
1110

1211
Each column argument given to sqlcup defines a database column and must
1312
be either a <plain-column> or a <smart-column>:
@@ -18,12 +17,14 @@ Description:
1817
capitalizes those names. To use <tag> you need to define a <smart-column>.
1918

2019
A <smart-column> is a shortcut for common column definitions. It must be of
21-
the form <name>@<tag>@<tag>...
20+
the form [<name>]<tag>... where <name> is only optional for the special case
21+
when the <smart-column> consists of the single <tag> @id. A <smart-column> is
22+
not nullable unless @null is present.
2223

2324
A <tag> adds either a data type or a constraint to a <smart-column>.
2425

2526
@id
26-
Make this column the primary key. Omitting column type and <name>
27+
Make this column the primary key. Omitting <type> and <name>
2728
for an @id column creates an INTEGER PRIMARY KEY named 'id'.
2829

2930
@text, @int, @float, @double, @datetime, @blob
@@ -33,10 +34,10 @@ Description:
3334
Add a UNIQUE constraint.
3435

3536
@null
36-
Omit NOT NULL constraint.
37+
Omit the default NOT NULL constraint.
3738

3839
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+
quotes or otherwise escape those spaces, depending on the user's shell.
4041

4142
Example:
4243
sqlcup author/authors "id:INTEGER:PRIMARY KEY" "name:text:NOT NULL" bio:text

0 commit comments

Comments
 (0)