#258: Alias can be an arbitrary string. (#259)

* Fixed space issues with expressions

* Alias can now be an arbitrary string

* Alias can now be an arbitrary string (fixed)

* Alias now automatically applied to expressions

* Ignore .run

* Fixed issue with TestRun not logging correctly to testing.T

* Fiddling with sqlite3 temp file closing

* Re-enable tests
This commit is contained in:
Neil O'Toole 2023-06-18 00:05:09 -06:00 committed by GitHub
parent 44d27207f8
commit 2ba633fc2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 322 additions and 178 deletions

2
.gitignore vendored
View File

@ -24,7 +24,7 @@ _testmain.go
*.prof
.DS_Store
/.run
/.idea
/grammar/build
*/~test$*

View File

@ -25,7 +25,7 @@ to SLQ (`sq`'s query language).
# Now
$ sq '.actor | where(.actor_id <= 2)'
```
- Column-only queries are now permissible. This has the neat side effect
- [#256]: Column-only queries are now possible. This has the neat side effect
that `sq` can now be used as a calculator.
```shell
@ -33,11 +33,13 @@ to SLQ (`sq`'s query language).
1+2
3
```
You probably want to use `--no-header` (`-H`):
You may want to use `--no-header` (`-H`) when using `sq` as a calculator.
```shell
$ sq -H 1+2
3
$ sq -H '(1+2)*3'
9
```
@ -59,7 +61,22 @@ to SLQ (`sq`'s query language).
PENELOPE 3
NICK 3
```
- [#258]: Column aliases can now be arbitrary strings, instead of only a
valid identifier.
```shell
# Previously only valid identifier allowed
$ sq '.actor | .first_name:given_name | .[0:2]'
given_name
PENELOPE
NICK
# Now, any arbitrary string can be used
$ sq '.actor | .first_name:"Given Name" | .[0:2]'
Given Name
PENELOPE
NICK
```
## [v0.37.1] - 2023-06-15
@ -580,6 +597,8 @@ make working with lots of sources much easier.
[#244]: https://github.com/neilotoole/sq/issues/244
[#252]: https://github.com/neilotoole/sq/issues/252
[#254]: https://github.com/neilotoole/sq/issues/254
[#256]: https://github.com/neilotoole/sq/issues/256
[#258]: https://github.com/neilotoole/sq/issues/258
[v0.15.2]: https://github.com/neilotoole/sq/releases/tag/v0.15.2
[v0.15.3]: https://github.com/neilotoole/sq/compare/v0.15.2...v0.15.3

View File

@ -50,6 +50,10 @@ func New(ctx context.Context, t testing.TB, from *TestRun) *TestRun {
ctx = context.Background()
}
if !lg.InContext(ctx) {
ctx = lg.NewContext(ctx, slogt.New(t))
}
tr := &TestRun{T: t, Context: ctx}
var cfgStore config.Store
@ -68,10 +72,6 @@ func New(ctx context.Context, t testing.TB, from *TestRun) *TestRun {
//
// If cfgStore is nil, a new one is created in a temp dir.
func newRun(ctx context.Context, t testing.TB, cfgStore config.Store) (ru *run.Run, out, errOut *bytes.Buffer) {
if !lg.InContext(ctx) {
ctx = lg.NewContext(ctx, slogt.New(t))
}
out = &bytes.Buffer{}
errOut = &bytes.Buffer{}

View File

@ -876,6 +876,10 @@ func NewScratchSource(ctx context.Context, name string) (src *source.Source, cln
return nil, cleanFn, err
}
// REVISIT: This mechanism is janky: should we be keeping the file open?
// Probably not.
lg.WarnIfCloseError(log, "Close scratch file", f)
log.Debug("Created sqlite3 scratchdb data file", lga.Path, f.Name())
src = &source.Source{
@ -887,7 +891,10 @@ func NewScratchSource(ctx context.Context, name string) (src *source.Source, cln
fn := func() error {
log.Debug("Deleting sqlite3 scratchdb data file", lga.Src, src, lga.Path, f.Name())
if cleanFn != nil {
return cleanFn()
cleanErr := cleanFn()
if cleanErr != nil {
log.Warn("Error cleaning scratch source", lga.Err, cleanErr)
}
}
return nil
}

View File

@ -168,7 +168,7 @@ selector: NAME (NAME)?;
// - ."actor".first_name
selectorElement: (selector) (alias)?;
alias: ALIAS_RESERVED | ':' (ARG | ID);
alias: ALIAS_RESERVED | ':' (ARG | ID | STRING);
// The grammar has problems dealing with "reserved" lexer tokens.
// Basically, there's a problem with using "column:KEYWORD".
// ALIAS_RESERVED is a hack to deal with those cases.

View File

@ -3,6 +3,8 @@ package ast
import (
"strings"
"github.com/neilotoole/sq/libsq/core/stringz"
"github.com/neilotoole/sq/libsq/ast/internal/slq"
)
@ -15,6 +17,8 @@ func (v *parseTreeVisitor) VisitAlias(ctx *slq.AliasContext) any {
var alias string
if ctx.ID() != nil {
alias = ctx.ID().GetText()
} else if ctx.STRING() != nil {
alias = stringz.StripDoubleQuote(ctx.STRING().GetText())
}
switch node := v.cur.(type) {

View File

@ -1,6 +1,12 @@
package ast
import "github.com/neilotoole/sq/libsq/ast/internal/slq"
import (
"strings"
"github.com/neilotoole/sq/libsq/core/stringz"
"github.com/neilotoole/sq/libsq/ast/internal/slq"
)
var (
_ Node = (*ExprElementNode)(nil)
@ -103,6 +109,13 @@ func (v *parseTreeVisitor) VisitExprElement(ctx *slq.ExprElementContext) interfa
return e
}
}
if node.alias == "" {
node.alias = ctx.GetText()
node.alias = stringz.StripDoubleQuote(node.alias)
node.alias = strings.TrimPrefix(node.alias, "_")
}
return v.cur.AddChild(node)
}

View File

@ -1,6 +1,8 @@
package ast
import (
"strings"
"github.com/neilotoole/sq/libsq/ast/internal/slq"
)
@ -132,9 +134,7 @@ func (v *parseTreeVisitor) VisitFunc(ctx *slq.FuncContext) any {
if node.alias == "" {
node.alias = ctx.GetText()
if node.alias[0] == '_' {
node.alias = node.alias[1:]
}
node.alias = strings.TrimPrefix(node.alias, "_")
}
return v.cur.AddChild(node)

File diff suppressed because one or more lines are too long

View File

@ -86,65 +86,65 @@ func slqParserInit() {
12, 25, 277, 9, 25, 1, 26, 1, 26, 1, 27, 1, 27, 1, 27, 0, 1, 50, 28, 0,
2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38,
40, 42, 44, 46, 48, 50, 52, 54, 0, 9, 1, 0, 40, 45, 2, 0, 3, 6, 20, 20,
1, 0, 23, 24, 2, 0, 27, 27, 29, 29, 2, 0, 2, 2, 12, 13, 1, 0, 14, 16, 1,
0, 40, 43, 3, 0, 28, 28, 38, 39, 48, 48, 2, 0, 18, 19, 23, 24, 306, 0,
59, 1, 0, 0, 0, 2, 80, 1, 0, 0, 0, 4, 88, 1, 0, 0, 0, 6, 108, 1, 0, 0,
0, 8, 110, 1, 0, 0, 0, 10, 112, 1, 0, 0, 0, 12, 116, 1, 0, 0, 0, 14, 131,
1, 0, 0, 0, 16, 133, 1, 0, 0, 0, 18, 143, 1, 0, 0, 0, 20, 145, 1, 0, 0,
0, 22, 147, 1, 0, 0, 0, 24, 158, 1, 0, 0, 0, 26, 167, 1, 0, 0, 0, 28, 169,
1, 0, 0, 0, 30, 181, 1, 0, 0, 0, 32, 185, 1, 0, 0, 0, 34, 197, 1, 0, 0,
0, 36, 201, 1, 0, 0, 0, 38, 208, 1, 0, 0, 0, 40, 210, 1, 0, 0, 0, 42, 212,
1, 0, 0, 0, 44, 215, 1, 0, 0, 0, 46, 217, 1, 0, 0, 0, 48, 230, 1, 0, 0,
0, 50, 246, 1, 0, 0, 0, 52, 278, 1, 0, 0, 0, 54, 280, 1, 0, 0, 0, 56, 58,
5, 1, 0, 0, 57, 56, 1, 0, 0, 0, 58, 61, 1, 0, 0, 0, 59, 57, 1, 0, 0, 0,
59, 60, 1, 0, 0, 0, 60, 62, 1, 0, 0, 0, 61, 59, 1, 0, 0, 0, 62, 71, 3,
2, 1, 0, 63, 65, 5, 1, 0, 0, 64, 63, 1, 0, 0, 0, 65, 66, 1, 0, 0, 0, 66,
64, 1, 0, 0, 0, 66, 67, 1, 0, 0, 0, 67, 68, 1, 0, 0, 0, 68, 70, 3, 2, 1,
0, 69, 64, 1, 0, 0, 0, 70, 73, 1, 0, 0, 0, 71, 69, 1, 0, 0, 0, 71, 72,
1, 0, 0, 0, 72, 77, 1, 0, 0, 0, 73, 71, 1, 0, 0, 0, 74, 76, 5, 1, 0, 0,
75, 74, 1, 0, 0, 0, 76, 79, 1, 0, 0, 0, 77, 75, 1, 0, 0, 0, 77, 78, 1,
0, 0, 0, 78, 1, 1, 0, 0, 0, 79, 77, 1, 0, 0, 0, 80, 85, 3, 4, 2, 0, 81,
82, 5, 36, 0, 0, 82, 84, 3, 4, 2, 0, 83, 81, 1, 0, 0, 0, 84, 87, 1, 0,
0, 0, 85, 83, 1, 0, 0, 0, 85, 86, 1, 0, 0, 0, 86, 3, 1, 0, 0, 0, 87, 85,
1, 0, 0, 0, 88, 93, 3, 6, 3, 0, 89, 90, 5, 35, 0, 0, 90, 92, 3, 6, 3, 0,
91, 89, 1, 0, 0, 0, 92, 95, 1, 0, 0, 0, 93, 91, 1, 0, 0, 0, 93, 94, 1,
0, 0, 0, 94, 5, 1, 0, 0, 0, 95, 93, 1, 0, 0, 0, 96, 109, 3, 42, 21, 0,
97, 109, 3, 44, 22, 0, 98, 109, 3, 36, 18, 0, 99, 109, 3, 16, 8, 0, 100,
109, 3, 28, 14, 0, 101, 109, 3, 32, 16, 0, 102, 109, 3, 46, 23, 0, 103,
109, 3, 20, 10, 0, 104, 109, 3, 22, 11, 0, 105, 109, 3, 24, 12, 0, 106,
109, 3, 10, 5, 0, 107, 109, 3, 48, 24, 0, 108, 96, 1, 0, 0, 0, 108, 97,
1, 0, 0, 0, 108, 98, 1, 0, 0, 0, 108, 99, 1, 0, 0, 0, 108, 100, 1, 0, 0,
0, 108, 101, 1, 0, 0, 0, 108, 102, 1, 0, 0, 0, 108, 103, 1, 0, 0, 0, 108,
104, 1, 0, 0, 0, 108, 105, 1, 0, 0, 0, 108, 106, 1, 0, 0, 0, 108, 107,
1, 0, 0, 0, 109, 7, 1, 0, 0, 0, 110, 111, 7, 0, 0, 0, 111, 9, 1, 0, 0,
0, 112, 114, 3, 12, 6, 0, 113, 115, 3, 38, 19, 0, 114, 113, 1, 0, 0, 0,
114, 115, 1, 0, 0, 0, 115, 11, 1, 0, 0, 0, 116, 117, 3, 14, 7, 0, 117,
127, 5, 31, 0, 0, 118, 123, 3, 50, 25, 0, 119, 120, 5, 35, 0, 0, 120, 122,
3, 50, 25, 0, 121, 119, 1, 0, 0, 0, 122, 125, 1, 0, 0, 0, 123, 121, 1,
0, 0, 0, 123, 124, 1, 0, 0, 0, 124, 128, 1, 0, 0, 0, 125, 123, 1, 0, 0,
0, 126, 128, 5, 2, 0, 0, 127, 118, 1, 0, 0, 0, 127, 126, 1, 0, 0, 0, 127,
128, 1, 0, 0, 0, 128, 129, 1, 0, 0, 0, 129, 130, 5, 32, 0, 0, 130, 13,
1, 0, 0, 0, 131, 132, 7, 1, 0, 0, 132, 15, 1, 0, 0, 0, 133, 134, 5, 7,
0, 0, 134, 135, 5, 31, 0, 0, 135, 136, 3, 18, 9, 0, 136, 137, 5, 32, 0,
0, 137, 17, 1, 0, 0, 0, 138, 139, 3, 34, 17, 0, 139, 140, 3, 8, 4, 0, 140,
141, 3, 34, 17, 0, 141, 144, 1, 0, 0, 0, 142, 144, 3, 34, 17, 0, 143, 138,
1, 0, 0, 0, 143, 142, 1, 0, 0, 0, 144, 19, 1, 0, 0, 0, 145, 146, 5, 8,
0, 0, 146, 21, 1, 0, 0, 0, 147, 153, 5, 9, 0, 0, 148, 150, 5, 31, 0, 0,
149, 151, 3, 34, 17, 0, 150, 149, 1, 0, 0, 0, 150, 151, 1, 0, 0, 0, 151,
152, 1, 0, 0, 0, 152, 154, 5, 32, 0, 0, 153, 148, 1, 0, 0, 0, 153, 154,
1, 0, 0, 0, 154, 156, 1, 0, 0, 0, 155, 157, 3, 38, 19, 0, 156, 155, 1,
0, 0, 0, 156, 157, 1, 0, 0, 0, 157, 23, 1, 0, 0, 0, 158, 159, 5, 21, 0,
0, 159, 161, 5, 31, 0, 0, 160, 162, 3, 50, 25, 0, 161, 160, 1, 0, 0, 0,
161, 162, 1, 0, 0, 0, 162, 163, 1, 0, 0, 0, 163, 164, 5, 32, 0, 0, 164,
25, 1, 0, 0, 0, 165, 168, 3, 34, 17, 0, 166, 168, 3, 12, 6, 0, 167, 165,
1, 0, 0, 0, 167, 166, 1, 0, 0, 0, 168, 27, 1, 0, 0, 0, 169, 170, 5, 22,
0, 0, 170, 171, 5, 31, 0, 0, 171, 176, 3, 26, 13, 0, 172, 173, 5, 35, 0,
0, 173, 175, 3, 26, 13, 0, 174, 172, 1, 0, 0, 0, 175, 178, 1, 0, 0, 0,
176, 174, 1, 0, 0, 0, 176, 177, 1, 0, 0, 0, 177, 179, 1, 0, 0, 0, 178,
176, 1, 0, 0, 0, 179, 180, 5, 32, 0, 0, 180, 29, 1, 0, 0, 0, 181, 183,
3, 34, 17, 0, 182, 184, 7, 2, 0, 0, 183, 182, 1, 0, 0, 0, 183, 184, 1,
0, 0, 0, 184, 31, 1, 0, 0, 0, 185, 186, 5, 25, 0, 0, 186, 187, 5, 31, 0,
0, 187, 192, 3, 30, 15, 0, 188, 189, 5, 35, 0, 0, 189, 191, 3, 30, 15,
1, 0, 23, 24, 3, 0, 27, 27, 29, 29, 48, 48, 2, 0, 2, 2, 12, 13, 1, 0, 14,
16, 1, 0, 40, 43, 3, 0, 28, 28, 38, 39, 48, 48, 2, 0, 18, 19, 23, 24, 306,
0, 59, 1, 0, 0, 0, 2, 80, 1, 0, 0, 0, 4, 88, 1, 0, 0, 0, 6, 108, 1, 0,
0, 0, 8, 110, 1, 0, 0, 0, 10, 112, 1, 0, 0, 0, 12, 116, 1, 0, 0, 0, 14,
131, 1, 0, 0, 0, 16, 133, 1, 0, 0, 0, 18, 143, 1, 0, 0, 0, 20, 145, 1,
0, 0, 0, 22, 147, 1, 0, 0, 0, 24, 158, 1, 0, 0, 0, 26, 167, 1, 0, 0, 0,
28, 169, 1, 0, 0, 0, 30, 181, 1, 0, 0, 0, 32, 185, 1, 0, 0, 0, 34, 197,
1, 0, 0, 0, 36, 201, 1, 0, 0, 0, 38, 208, 1, 0, 0, 0, 40, 210, 1, 0, 0,
0, 42, 212, 1, 0, 0, 0, 44, 215, 1, 0, 0, 0, 46, 217, 1, 0, 0, 0, 48, 230,
1, 0, 0, 0, 50, 246, 1, 0, 0, 0, 52, 278, 1, 0, 0, 0, 54, 280, 1, 0, 0,
0, 56, 58, 5, 1, 0, 0, 57, 56, 1, 0, 0, 0, 58, 61, 1, 0, 0, 0, 59, 57,
1, 0, 0, 0, 59, 60, 1, 0, 0, 0, 60, 62, 1, 0, 0, 0, 61, 59, 1, 0, 0, 0,
62, 71, 3, 2, 1, 0, 63, 65, 5, 1, 0, 0, 64, 63, 1, 0, 0, 0, 65, 66, 1,
0, 0, 0, 66, 64, 1, 0, 0, 0, 66, 67, 1, 0, 0, 0, 67, 68, 1, 0, 0, 0, 68,
70, 3, 2, 1, 0, 69, 64, 1, 0, 0, 0, 70, 73, 1, 0, 0, 0, 71, 69, 1, 0, 0,
0, 71, 72, 1, 0, 0, 0, 72, 77, 1, 0, 0, 0, 73, 71, 1, 0, 0, 0, 74, 76,
5, 1, 0, 0, 75, 74, 1, 0, 0, 0, 76, 79, 1, 0, 0, 0, 77, 75, 1, 0, 0, 0,
77, 78, 1, 0, 0, 0, 78, 1, 1, 0, 0, 0, 79, 77, 1, 0, 0, 0, 80, 85, 3, 4,
2, 0, 81, 82, 5, 36, 0, 0, 82, 84, 3, 4, 2, 0, 83, 81, 1, 0, 0, 0, 84,
87, 1, 0, 0, 0, 85, 83, 1, 0, 0, 0, 85, 86, 1, 0, 0, 0, 86, 3, 1, 0, 0,
0, 87, 85, 1, 0, 0, 0, 88, 93, 3, 6, 3, 0, 89, 90, 5, 35, 0, 0, 90, 92,
3, 6, 3, 0, 91, 89, 1, 0, 0, 0, 92, 95, 1, 0, 0, 0, 93, 91, 1, 0, 0, 0,
93, 94, 1, 0, 0, 0, 94, 5, 1, 0, 0, 0, 95, 93, 1, 0, 0, 0, 96, 109, 3,
42, 21, 0, 97, 109, 3, 44, 22, 0, 98, 109, 3, 36, 18, 0, 99, 109, 3, 16,
8, 0, 100, 109, 3, 28, 14, 0, 101, 109, 3, 32, 16, 0, 102, 109, 3, 46,
23, 0, 103, 109, 3, 20, 10, 0, 104, 109, 3, 22, 11, 0, 105, 109, 3, 24,
12, 0, 106, 109, 3, 10, 5, 0, 107, 109, 3, 48, 24, 0, 108, 96, 1, 0, 0,
0, 108, 97, 1, 0, 0, 0, 108, 98, 1, 0, 0, 0, 108, 99, 1, 0, 0, 0, 108,
100, 1, 0, 0, 0, 108, 101, 1, 0, 0, 0, 108, 102, 1, 0, 0, 0, 108, 103,
1, 0, 0, 0, 108, 104, 1, 0, 0, 0, 108, 105, 1, 0, 0, 0, 108, 106, 1, 0,
0, 0, 108, 107, 1, 0, 0, 0, 109, 7, 1, 0, 0, 0, 110, 111, 7, 0, 0, 0, 111,
9, 1, 0, 0, 0, 112, 114, 3, 12, 6, 0, 113, 115, 3, 38, 19, 0, 114, 113,
1, 0, 0, 0, 114, 115, 1, 0, 0, 0, 115, 11, 1, 0, 0, 0, 116, 117, 3, 14,
7, 0, 117, 127, 5, 31, 0, 0, 118, 123, 3, 50, 25, 0, 119, 120, 5, 35, 0,
0, 120, 122, 3, 50, 25, 0, 121, 119, 1, 0, 0, 0, 122, 125, 1, 0, 0, 0,
123, 121, 1, 0, 0, 0, 123, 124, 1, 0, 0, 0, 124, 128, 1, 0, 0, 0, 125,
123, 1, 0, 0, 0, 126, 128, 5, 2, 0, 0, 127, 118, 1, 0, 0, 0, 127, 126,
1, 0, 0, 0, 127, 128, 1, 0, 0, 0, 128, 129, 1, 0, 0, 0, 129, 130, 5, 32,
0, 0, 130, 13, 1, 0, 0, 0, 131, 132, 7, 1, 0, 0, 132, 15, 1, 0, 0, 0, 133,
134, 5, 7, 0, 0, 134, 135, 5, 31, 0, 0, 135, 136, 3, 18, 9, 0, 136, 137,
5, 32, 0, 0, 137, 17, 1, 0, 0, 0, 138, 139, 3, 34, 17, 0, 139, 140, 3,
8, 4, 0, 140, 141, 3, 34, 17, 0, 141, 144, 1, 0, 0, 0, 142, 144, 3, 34,
17, 0, 143, 138, 1, 0, 0, 0, 143, 142, 1, 0, 0, 0, 144, 19, 1, 0, 0, 0,
145, 146, 5, 8, 0, 0, 146, 21, 1, 0, 0, 0, 147, 153, 5, 9, 0, 0, 148, 150,
5, 31, 0, 0, 149, 151, 3, 34, 17, 0, 150, 149, 1, 0, 0, 0, 150, 151, 1,
0, 0, 0, 151, 152, 1, 0, 0, 0, 152, 154, 5, 32, 0, 0, 153, 148, 1, 0, 0,
0, 153, 154, 1, 0, 0, 0, 154, 156, 1, 0, 0, 0, 155, 157, 3, 38, 19, 0,
156, 155, 1, 0, 0, 0, 156, 157, 1, 0, 0, 0, 157, 23, 1, 0, 0, 0, 158, 159,
5, 21, 0, 0, 159, 161, 5, 31, 0, 0, 160, 162, 3, 50, 25, 0, 161, 160, 1,
0, 0, 0, 161, 162, 1, 0, 0, 0, 162, 163, 1, 0, 0, 0, 163, 164, 5, 32, 0,
0, 164, 25, 1, 0, 0, 0, 165, 168, 3, 34, 17, 0, 166, 168, 3, 12, 6, 0,
167, 165, 1, 0, 0, 0, 167, 166, 1, 0, 0, 0, 168, 27, 1, 0, 0, 0, 169, 170,
5, 22, 0, 0, 170, 171, 5, 31, 0, 0, 171, 176, 3, 26, 13, 0, 172, 173, 5,
35, 0, 0, 173, 175, 3, 26, 13, 0, 174, 172, 1, 0, 0, 0, 175, 178, 1, 0,
0, 0, 176, 174, 1, 0, 0, 0, 176, 177, 1, 0, 0, 0, 177, 179, 1, 0, 0, 0,
178, 176, 1, 0, 0, 0, 179, 180, 5, 32, 0, 0, 180, 29, 1, 0, 0, 0, 181,
183, 3, 34, 17, 0, 182, 184, 7, 2, 0, 0, 183, 182, 1, 0, 0, 0, 183, 184,
1, 0, 0, 0, 184, 31, 1, 0, 0, 0, 185, 186, 5, 25, 0, 0, 186, 187, 5, 31,
0, 0, 187, 192, 3, 30, 15, 0, 188, 189, 5, 35, 0, 0, 189, 191, 3, 30, 15,
0, 190, 188, 1, 0, 0, 0, 191, 194, 1, 0, 0, 0, 192, 190, 1, 0, 0, 0, 192,
193, 1, 0, 0, 0, 193, 195, 1, 0, 0, 0, 194, 192, 1, 0, 0, 0, 195, 196,
5, 32, 0, 0, 196, 33, 1, 0, 0, 0, 197, 199, 5, 46, 0, 0, 198, 200, 5, 46,
@ -3626,6 +3626,7 @@ type IAliasContext interface {
COLON() antlr.TerminalNode
ARG() antlr.TerminalNode
ID() antlr.TerminalNode
STRING() antlr.TerminalNode
// IsAliasContext differentiates from other interfaces.
IsAliasContext()
@ -3674,6 +3675,10 @@ func (s *AliasContext) ID() antlr.TerminalNode {
return s.GetToken(SLQParserID, 0)
}
func (s *AliasContext) STRING() antlr.TerminalNode {
return s.GetToken(SLQParserSTRING, 0)
}
func (s *AliasContext) GetRuleContext() antlr.RuleContext {
return s
}
@ -3749,7 +3754,7 @@ func (p *SLQParser) Alias() (localctx IAliasContext) {
p.SetState(207)
_la = p.GetTokenStream().LA(1)
if !(_la == SLQParserARG || _la == SLQParserID) {
if !((int64(_la) & ^0x3f) == 0 && ((int64(1)<<_la)&281475647799296) != 0) {
p.GetErrorHandler().RecoverInline(p)
} else {
p.GetErrorHandler().ReportMatch(p)

View File

@ -18,11 +18,8 @@ func doExpr(rc *Context, expr *ast.ExprNode) (string, error) {
if expr.HasParens() {
sb.WriteRune('(')
}
for i, child := range expr.Children() {
if i > 0 {
sb.WriteRune(sp)
}
for _, child := range expr.Children() {
switch child := child.(type) {
case *ast.TblColSelectorNode, *ast.ColSelectorNode:
val, err := renderSelectorNode(rc.Dialect, child)

View File

@ -27,5 +27,17 @@ func doOperator(rc *Context, op *ast.OperatorNode) (string, error) {
}
// By default, just return the operator unchanged.
if operatorHasSpace(val) {
val = " " + val + " "
}
return val, nil
}
func operatorHasSpace(op string) bool {
switch op {
case "-", "+", "*", "/":
return false
}
return true
}

View File

@ -4,6 +4,8 @@ import (
"fmt"
"strings"
"github.com/neilotoole/sq/libsq/core/stringz"
"github.com/antlr/antlr4/runtime/Go/antlr/v4"
"github.com/neilotoole/sq/libsq/ast/internal/slq"
@ -17,7 +19,12 @@ func (v *parseTreeVisitor) VisitSelectorElement(ctx *slq.SelectorElementContext)
}
if aliasCtx := ctx.Alias(); aliasCtx != nil {
node.alias = ctx.Alias().ID().GetText()
if aliasCtx.ID() != nil {
node.alias = aliasCtx.ID().GetText()
}
if aliasCtx.STRING() != nil {
node.alias = stringz.StripDoubleQuote(aliasCtx.STRING().GetText())
}
}
if err := v.cur.AddChild(node); err != nil {

View File

@ -438,6 +438,19 @@ func DoubleQuote(s string) string {
return sb.String()
}
// StripDoubleQuote strips double quotes from s,
// or returns s unchanged if it is not correctly double-quoted.
func StripDoubleQuote(s string) string {
if len(s) < 2 {
return s
}
if s[0] == '"' && s[len(s)-1] == '"' {
return s[1 : len(s)-1]
}
return s
}
// BacktickQuote backtick-quotes (and escapes) s.
//
// hello `world` --> `hello ``world```

View File

@ -325,6 +325,29 @@ func TestDoubleQuote(t *testing.T) {
}
}
func TestStripDoubleQuote(t *testing.T) {
testCases := []struct {
in string
want string
}{
{in: ``, want: ``},
{in: `"`, want: `"`},
{in: `""`, want: ``},
{in: `"a`, want: `"a`},
{in: `"a"`, want: `a`},
{in: `"abc"`, want: `abc`},
{in: `"hello "" world"`, want: `hello "" world`},
}
for i, tc := range testCases {
tc := tc
t.Run(tutil.Name(i, tc.in), func(t *testing.T) {
got := stringz.StripDoubleQuote(tc.in)
require.Equal(t, tc.want, got)
})
}
}
func TestBacktickQuote(t *testing.T) {
testCases := []struct {
in string

View File

@ -40,8 +40,7 @@ func (ng *engine) prepare(ctx context.Context, qm *queryModel) error {
return errz.Errorf("unknown ast.Tabler %T: %s", node, node)
}
rndr := ng.targetDB.SQLDriver().Renderer()
// rndr := ng.rc.Renderer
rndr := ng.rc.Renderer
if frags.Columns, err = rndr.SelectCols(ng.rc, qm.Cols); err != nil {
return err

View File

@ -5,12 +5,10 @@ import (
"github.com/neilotoole/sq/drivers/mysql"
"github.com/neilotoole/sq/libsq/source"
_ "github.com/mattn/go-sqlite3"
)
//nolint:exhaustive,lll
//nolint:exhaustive
func TestQuery_args(t *testing.T) {
testCases := []queryTestCase{
{
@ -18,7 +16,7 @@ func TestQuery_args(t *testing.T) {
in: `@sakila | .actor | where(.first_name == $first)`,
args: map[string]string{"first": "TOM"},
wantSQL: `SELECT * FROM "actor" WHERE "first_name" = 'TOM'`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `actor` WHERE `first_name` = 'TOM'"},
override: driverMap{mysql.Type: "SELECT * FROM `actor` WHERE `first_name` = 'TOM'"},
wantRecCount: 2,
},
{
@ -26,7 +24,7 @@ func TestQuery_args(t *testing.T) {
in: `@sakila | .actor | where(.first_name == $first && .last_name == $last)`,
args: map[string]string{"first": "TOM", "last": "MIRANDA"},
wantSQL: `SELECT * FROM "actor" WHERE "first_name" = 'TOM' AND "last_name" = 'MIRANDA'`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `actor` WHERE `first_name` = 'TOM' AND `last_name` = 'MIRANDA'"},
override: driverMap{mysql.Type: "SELECT * FROM `actor` WHERE `first_name` = 'TOM' AND `last_name` = 'MIRANDA'"},
wantRecCount: 1,
},
{
@ -34,7 +32,7 @@ func TestQuery_args(t *testing.T) {
in: `@sakila | .actor | where(.actor_id == int($id))`,
args: map[string]string{"id": "1"},
wantSQL: `SELECT * FROM "actor" WHERE "actor_id" = 1`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `actor` WHERE `actor_id` = 1"},
override: driverMap{mysql.Type: "SELECT * FROM `actor` WHERE `actor_id` = 1"},
skip: true, // Skip until we implement casting, e.g. .actor_id == int($id)
wantRecCount: 1,
},

View File

@ -7,8 +7,6 @@ import (
"github.com/neilotoole/sq/drivers/mysql"
"github.com/neilotoole/sq/libsq/source"
_ "github.com/mattn/go-sqlite3"
)
@ -19,14 +17,14 @@ func TestQuery_cols(t *testing.T) {
name: "cols",
in: `@sakila | .actor | .first_name, .last_name`,
wantSQL: `SELECT "first_name", "last_name" FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT `first_name`, `last_name` FROM `actor`"},
override: driverMap{mysql.Type: "SELECT `first_name`, `last_name` FROM `actor`"},
wantRecCount: sakila.TblActorCount,
},
{
name: "cols-whitespace-single-col",
in: `@sakila | .actor | ."first name"`,
wantSQL: `SELECT "first name" FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT `first name` FROM `actor`"},
override: driverMap{mysql.Type: "SELECT `first name` FROM `actor`"},
wantRecCount: sakila.TblActorCount,
skipExec: true,
},
@ -34,44 +32,50 @@ func TestQuery_cols(t *testing.T) {
name: "cols-whitespace-multiple-cols",
in: `@sakila | .actor | .actor_id, ."first name", ."last name"`,
wantSQL: `SELECT "actor_id", "first name", "last name" FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT `actor_id`, `first name`, `last name` FROM `actor`"},
override: driverMap{mysql.Type: "SELECT `actor_id`, `first name`, `last name` FROM `actor`"},
wantRecCount: sakila.TblActorCount,
skipExec: true,
},
{
name: "table-whitespace",
in: `@sakila | ."film actor"`,
wantSQL: `SELECT * FROM "film actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `film actor`"},
override: driverMap{mysql.Type: "SELECT * FROM `film actor`"},
skipExec: true,
},
{
name: "cols-aliases",
in: `@sakila | .actor | .first_name:given_name, .last_name:family_name`,
wantSQL: `SELECT "first_name" AS "given_name", "last_name" AS "family_name" FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT `first_name` AS `given_name`, `last_name` AS `family_name` FROM `actor`"},
override: driverMap{mysql.Type: "SELECT `first_name` AS `given_name`, `last_name` AS `family_name` FROM `actor`"},
wantRecCount: sakila.TblActorCount,
},
{
name: "cols-aliases-whitespace",
in: `@sakila | .actor | .first_name:"Given Name", .last_name:family_name`,
wantSQL: `SELECT "first_name" AS "Given Name", "last_name" AS "family_name" FROM "actor"`,
override: driverMap{mysql.Type: "SELECT `first_name` AS `Given Name`, `last_name` AS `family_name` FROM `actor`"},
wantRecCount: sakila.TblActorCount,
},
{
name: "handle-table/cols",
in: `@sakila.actor | .first_name, .last_name`,
wantSQL: `SELECT "first_name", "last_name" FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT `first_name`, `last_name` FROM `actor`"},
override: driverMap{mysql.Type: "SELECT `first_name`, `last_name` FROM `actor`"},
wantRecCount: sakila.TblActorCount,
},
{
name: "cols-select-literal-value",
in: `@sakila.actor | .first_name, "xxx", .last_name`,
wantSQL: `SELECT "first_name", 'xxx', "last_name" FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT `first_name`, 'xxx', `last_name` FROM `actor`"},
wantSQL: `SELECT "first_name", 'xxx' AS "xxx", "last_name" FROM "actor"`,
override: driverMap{mysql.Type: "SELECT `first_name`, 'xxx' AS `xxx`, `last_name` FROM `actor`"},
wantRecCount: sakila.TblActorCount,
},
{
name: "select/literal",
in: `@sakila.actor | .first_name, 5`,
wantSQL: `SELECT "first_name", 5 FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT `first_name`, 5 FROM `actor`"},
wantSQL: `SELECT "first_name", 5 AS "5" FROM "actor"`,
override: driverMap{mysql.Type: "SELECT `first_name`, 5 AS `5` FROM `actor`"},
wantRecCount: sakila.TblActorCount,
},
}

View File

@ -5,8 +5,6 @@ import (
"github.com/neilotoole/sq/drivers/mysql"
"github.com/neilotoole/sq/libsq/source"
_ "github.com/mattn/go-sqlite3"
)
@ -17,35 +15,35 @@ func TestQuery_count(t *testing.T) {
name: "alias",
in: `@sakila | .actor | count:quantity`,
wantSQL: `SELECT count(*) AS "quantity" FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT count(*) AS `quantity` FROM `actor`"},
override: driverMap{mysql.Type: "SELECT count(*) AS `quantity` FROM `actor`"},
wantRecCount: 1,
},
{
name: "count-same-alias",
in: `@sakila | .actor | count:count`,
wantSQL: `SELECT count(*) AS "count" FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT count(*) AS `count` FROM `actor`"},
override: driverMap{mysql.Type: "SELECT count(*) AS `count` FROM `actor`"},
wantRecCount: 1,
},
{
name: "whitespace-col",
in: `@sakila | .actor | count(."first name")`,
wantSQL: `SELECT count("first name") FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT count(`first name`) FROM `actor`"},
override: driverMap{mysql.Type: "SELECT count(`first name`) FROM `actor`"},
skipExec: true,
},
{
name: "select-handle-table",
in: `@sakila.actor | count`,
wantSQL: `SELECT count(*) AS "count" FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT count(*) AS `count` FROM `actor`"},
override: driverMap{mysql.Type: "SELECT count(*) AS `count` FROM `actor`"},
wantRecCount: 1,
},
{
name: "select-handle-table-ws-selector",
in: `@sakila.actor | count(."first name")`,
wantSQL: `SELECT count("first name") FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT count(`first name`) FROM `actor`"},
override: driverMap{mysql.Type: "SELECT count(`first name`) FROM `actor`"},
skipExec: true,
},
{
@ -54,21 +52,21 @@ func TestQuery_count(t *testing.T) {
// being a reserved word (unique).
in: `@sakila | .actor | count:unique`,
wantSQL: `SELECT count(*) AS "unique" FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT count(*) AS `unique` FROM `actor`"},
override: driverMap{mysql.Type: "SELECT count(*) AS `unique` FROM `actor`"},
wantRecCount: 1,
},
{
name: "no-parens-no-args-with-alias-arbitrary",
in: `@sakila | .actor | count:something_123`,
wantSQL: `SELECT count(*) AS "something_123" FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT count(*) AS `something_123` FROM `actor`"},
override: driverMap{mysql.Type: "SELECT count(*) AS `something_123` FROM `actor`"},
wantRecCount: 1,
},
{
name: "parens-no-args",
in: `@sakila | .actor | count()`,
wantSQL: `SELECT count(*) AS "count" FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT count(*) AS `count` FROM `actor`"},
override: driverMap{mysql.Type: "SELECT count(*) AS `count` FROM `actor`"},
wantRecCount: 1,
},
{
@ -80,7 +78,7 @@ func TestQuery_count(t *testing.T) {
name: "single-selector",
in: `@sakila | .actor | count(.first_name)`,
wantSQL: `SELECT count("first_name") FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT count(`first_name`) FROM `actor`"},
override: driverMap{mysql.Type: "SELECT count(`first_name`) FROM `actor`"},
wantRecCount: 1,
},
{
@ -92,7 +90,7 @@ func TestQuery_count(t *testing.T) {
name: "count/no-parens-no-args",
in: `@sakila | .actor | count`,
wantSQL: `SELECT count(*) AS "count" FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT count(*) AS `count` FROM `actor`"},
override: driverMap{mysql.Type: "SELECT count(*) AS `count` FROM `actor`"},
wantRecCount: 1,
},
{
@ -101,7 +99,7 @@ func TestQuery_count(t *testing.T) {
// being a reserved word (count).
in: `@sakila | .actor | count:count`,
wantSQL: `SELECT count(*) AS "count" FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT count(*) AS `count` FROM `actor`"},
override: driverMap{mysql.Type: "SELECT count(*) AS `count` FROM `actor`"},
wantRecCount: 1,
},
}

View File

@ -3,44 +3,44 @@ package libsq_test
import (
"testing"
"github.com/neilotoole/sq/testh/tutil"
"github.com/neilotoole/sq/testh/sakila"
"github.com/neilotoole/sq/drivers/mysql"
"github.com/neilotoole/sq/libsq/source"
_ "github.com/mattn/go-sqlite3"
)
//nolint:exhaustive,lll
//nolint:exhaustive
func TestQuery_expr_where(t *testing.T) {
testCases := []queryTestCase{
{
name: "literal/string",
in: `@sakila | .actor | where(.first_name == "TOM")`,
wantSQL: `SELECT * FROM "actor" WHERE "first_name" = 'TOM'`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `actor` WHERE `first_name` = 'TOM'"},
override: driverMap{mysql.Type: "SELECT * FROM `actor` WHERE `first_name` = 'TOM'"},
wantRecCount: 2,
},
{
name: "literal/two-strings",
in: `@sakila | .actor | where(.first_name == "TOM" && .last_name == "MIRANDA")`,
wantSQL: `SELECT * FROM "actor" WHERE "first_name" = 'TOM' AND "last_name" = 'MIRANDA'`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `actor` WHERE `first_name` = 'TOM' AND `last_name` = 'MIRANDA'"},
override: driverMap{mysql.Type: "SELECT * FROM `actor` WHERE `first_name` = 'TOM' AND `last_name` = 'MIRANDA'"},
wantRecCount: 1,
},
{
name: "literal/integer",
in: `@sakila | .actor | where(.actor_id == 1)`,
wantSQL: `SELECT * FROM "actor" WHERE "actor_id" = 1`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `actor` WHERE `actor_id` = 1"},
override: driverMap{mysql.Type: "SELECT * FROM `actor` WHERE `actor_id` = 1"},
wantRecCount: 1,
},
{
name: "is_null",
in: `@sakila | .address | where(.postal_code == null)`,
wantSQL: `SELECT * FROM "address" WHERE "postal_code" IS NULL`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `address` WHERE `postal_code` IS NULL"},
override: driverMap{mysql.Type: "SELECT * FROM `address` WHERE `postal_code` IS NULL"},
wantRecCount: 4,
// skipExec because mysql sakila db doesn't have the same null values.
// This is a bug in the dataset.
@ -50,7 +50,7 @@ func TestQuery_expr_where(t *testing.T) {
name: "is_not_null",
in: `@sakila | .address | where(.postal_code != null)`,
wantSQL: `SELECT * FROM "address" WHERE "postal_code" IS NOT NULL`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `address` WHERE `postal_code` IS NOT NULL"},
override: driverMap{mysql.Type: "SELECT * FROM `address` WHERE `postal_code` IS NOT NULL"},
wantRecCount: 599,
// skipExec because mysql sakila db doesn't have the same null values.
// This is a bug in the dataset.
@ -72,63 +72,72 @@ func TestQuery_expr_literal(t *testing.T) {
{
name: "table/col_and_literal",
in: `@sakila | .actor | .first_name, 1`,
wantSQL: `SELECT "first_name", 1 FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT `first_name`, 1 FROM `actor`"},
wantSQL: `SELECT "first_name", 1 AS "1" FROM "actor"`,
override: driverMap{mysql.Type: "SELECT `first_name`, 1 AS `1` FROM `actor`"},
wantRecCount: sakila.TblActorCount,
sinkFns: []SinkTestFunc{assertSinkColValue(1, int64(1))},
},
{
name: "table/literal",
in: `@sakila | .actor | 1`,
wantSQL: `SELECT 1 FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT 1 FROM `actor`"},
wantSQL: `SELECT 1 AS "1" FROM "actor"`,
override: driverMap{mysql.Type: "SELECT 1 AS `1` FROM `actor`"},
wantRecCount: sakila.TblActorCount,
sinkFns: []SinkTestFunc{assertSinkColValue(0, int64(1))},
},
{
name: "no-table/literal",
in: `@sakila | 1`,
wantSQL: `SELECT 1`,
wantSQL: `SELECT 1 AS "1"`,
override: driverMap{mysql.Type: "SELECT 1 AS `1`"},
wantRecCount: 1,
sinkFns: []SinkTestFunc{assertSinkColValue(0, int64(1))},
},
{
name: "no-table/literal_addition",
in: `@sakila | 1+1`,
wantSQL: `SELECT 1+1 AS "1+1"`,
override: driverMap{mysql.Type: "SELECT 1+1 AS `1+1`"},
wantRecCount: 1,
sinkFns: []SinkTestFunc{assertSinkColValue(0, int64(2))},
},
{
name: "table/literal_parens",
in: `@sakila | .actor | (1)`,
wantSQL: `SELECT (1) FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT (1) FROM `actor`"},
wantSQL: `SELECT (1) AS "(1)" FROM "actor"`,
override: driverMap{mysql.Type: "SELECT (1) AS `(1)` FROM `actor`"},
wantRecCount: sakila.TblActorCount,
sinkFns: []SinkTestFunc{assertSinkColValue(0, int64(1))},
},
{
name: "table/addition",
in: `@sakila | .actor | 1+2`,
wantSQL: `SELECT 1 + 2 FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT 1 + 2 FROM `actor`"},
wantSQL: `SELECT 1+2 AS "1+2" FROM "actor"`,
override: driverMap{mysql.Type: "SELECT 1+2 AS `1+2` FROM `actor`"},
wantRecCount: sakila.TblActorCount,
sinkFns: []SinkTestFunc{assertSinkColValue(0, int64(3))},
},
{
name: "table/addition_whitespace",
in: `@sakila | .actor | 1+ 2 + 3`,
wantSQL: `SELECT 1 + 2 + 3 FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT 1 + 2 + 3 FROM `actor`"},
wantSQL: `SELECT 1+2+3 AS "1+2+3" FROM "actor"`,
override: driverMap{mysql.Type: "SELECT 1+2+3 AS `1+2+3` FROM `actor`"},
wantRecCount: sakila.TblActorCount,
sinkFns: []SinkTestFunc{assertSinkColValue(0, int64(6))},
},
{
name: "table/math_parens",
in: `@sakila | .actor | (1+ 2) * 3`,
wantSQL: `SELECT (1 + 2) * 3 FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT (1 + 2) * 3 FROM `actor`"},
in: `@sakila | .actor | ((2+2) * 3)`,
wantSQL: `SELECT ((2+2)*3) AS "((2+2)*3)" FROM "actor"`,
override: driverMap{mysql.Type: "SELECT ((2+2)*3) AS `((2+2)*3)` FROM `actor`"},
wantRecCount: sakila.TblActorCount,
sinkFns: []SinkTestFunc{assertSinkColValue(0, int64(9))},
sinkFns: []SinkTestFunc{assertSinkColValue(0, int64(12))},
},
{
name: "table/literal_alias",
in: `@sakila | .actor | 1:total`,
wantSQL: `SELECT 1 AS "total" FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT 1 AS `total` FROM `actor`"},
override: driverMap{mysql.Type: "SELECT 1 AS `total` FROM `actor`"},
wantRecCount: sakila.TblActorCount,
sinkFns: []SinkTestFunc{
assertSinkColName(0, "total"),
@ -138,8 +147,8 @@ func TestQuery_expr_literal(t *testing.T) {
{
name: "table/addition_alias",
in: `@sakila | .actor | (1+2):total`,
wantSQL: `SELECT (1 + 2) AS "total" FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT (1 + 2) AS `total` FROM `actor`"},
wantSQL: `SELECT (1+2) AS "total" FROM "actor"`,
override: driverMap{mysql.Type: "SELECT (1+2) AS `total` FROM `actor`"},
sinkFns: []SinkTestFunc{
assertSinkColValue(0, int64(3)),
assertSinkColName(0, "total"),
@ -148,9 +157,9 @@ func TestQuery_expr_literal(t *testing.T) {
},
}
for _, tc := range testCases {
for i, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Run(tutil.Name(i, tc.name), func(t *testing.T) {
execQueryTestCase(t, tc)
})
}

33
libsq/query_func_test.go Normal file
View File

@ -0,0 +1,33 @@
package libsq_test
import (
"testing"
"github.com/neilotoole/sq/drivers/mysql"
_ "github.com/mattn/go-sqlite3"
)
//nolint:exhaustive
func TestQuery_func(t *testing.T) {
testCases := []queryTestCase{
{
name: "max",
in: `@sakila | .actor | max(.actor_id)`,
wantSQL: `SELECT max("actor_id") AS "max(.actor_id)" FROM "actor"`,
override: driverMap{mysql.Type: "SELECT max(`actor_id`) AS `max(.actor_id)` FROM `actor`"},
wantRecCount: 1,
sinkFns: []SinkTestFunc{
assertSinkColName(0, "max(.actor_id)"),
assertSinkColValue(0, int64(200)),
},
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
execQueryTestCase(t, tc)
})
}
}

View File

@ -19,14 +19,14 @@ func TestQuery_groupby(t *testing.T) {
name: "group_by/single-term",
in: `@sakila | .payment | .customer_id, sum(.amount) | group_by(.customer_id)`,
wantSQL: `SELECT "customer_id", sum("amount") AS "sum(.amount)" FROM "payment" GROUP BY "customer_id"`,
override: map[source.DriverType]string{mysql.Type: "SELECT `customer_id`, sum(`amount`) AS `sum(.amount)` FROM `payment` GROUP BY `customer_id`"},
override: driverMap{mysql.Type: "SELECT `customer_id`, sum(`amount`) AS `sum(.amount)` FROM `payment` GROUP BY `customer_id`"},
wantRecCount: 599,
},
{
name: "group_by/multiple_terms",
in: `@sakila | .payment | .customer_id, .staff_id, sum(.amount) | group_by(.customer_id, .staff_id)`,
wantSQL: `SELECT "customer_id", "staff_id", sum("amount") AS "sum(.amount)" FROM "payment" GROUP BY "customer_id", "staff_id"`,
override: map[source.DriverType]string{mysql.Type: "SELECT `customer_id`, `staff_id`, sum(`amount`) AS `sum(.amount)` FROM `payment` GROUP BY `customer_id`, `staff_id`"},
override: driverMap{mysql.Type: "SELECT `customer_id`, `staff_id`, sum(`amount`) AS `sum(.amount)` FROM `payment` GROUP BY `customer_id`, `staff_id`"},
wantRecCount: 1198,
},
{

View File

@ -7,8 +7,6 @@ import (
"github.com/neilotoole/sq/drivers/mysql"
"github.com/neilotoole/sq/libsq/source"
_ "github.com/mattn/go-sqlite3"
)
@ -19,21 +17,21 @@ func TestQuery_join(t *testing.T) {
name: "join/single-selector",
in: `@sakila | .actor, .film_actor | join(.actor_id)`,
wantSQL: `SELECT * FROM "actor" INNER JOIN "film_actor" ON "actor"."actor_id" = "film_actor"."actor_id"`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `actor` INNER JOIN `film_actor` ON `actor`.`actor_id` = `film_actor`.`actor_id`"},
override: driverMap{mysql.Type: "SELECT * FROM `actor` INNER JOIN `film_actor` ON `actor`.`actor_id` = `film_actor`.`actor_id`"},
wantRecCount: sakila.TblFilmActorCount,
},
{
name: "join/fq-table-cols-equal",
in: `@sakila | .actor, .film_actor | join(.film_actor.actor_id == .actor.actor_id)`,
wantSQL: `SELECT * FROM "actor" INNER JOIN "film_actor" ON "film_actor"."actor_id" = "actor"."actor_id"`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `actor` INNER JOIN `film_actor` ON `film_actor`.`actor_id` = `actor`.`actor_id`"},
override: driverMap{mysql.Type: "SELECT * FROM `actor` INNER JOIN `film_actor` ON `film_actor`.`actor_id` = `actor`.`actor_id`"},
wantRecCount: sakila.TblFilmActorCount,
},
{
name: "join/fq-table-cols-equal-whitespace",
in: `@sakila | .actor, ."film actor" | join(."film actor".actor_id == .actor.actor_id)`,
wantSQL: `SELECT * FROM "actor" INNER JOIN "film actor" ON "film actor"."actor_id" = "actor"."actor_id"`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `actor` INNER JOIN `film actor` ON `film actor`.`actor_id` = `actor`.`actor_id`"},
override: driverMap{mysql.Type: "SELECT * FROM `actor` INNER JOIN `film actor` ON `film actor`.`actor_id` = `actor`.`actor_id`"},
skipExec: true,
},
}

View File

@ -18,7 +18,11 @@ func TestQuery_no_source(t *testing.T) {
want string
wantErr bool
}{
{"1+2", "SELECT 1 + 2", false},
{"1+2", `SELECT 1+2 AS "1+2"`, false},
{"(1+ 2) * 3", `SELECT (1+2)*3 AS "(1+2)*3"`, false},
{"(1+ 2) * 3", `SELECT (1+2)*3 AS "(1+2)*3"`, false},
{`1:"the number"`, `SELECT 1 AS "the number"`, false},
{`1:thenumber`, `SELECT 1 AS "thenumber"`, false},
}
for i, tc := range testCases {
@ -36,7 +40,7 @@ func TestQuery_no_source(t *testing.T) {
ScratchDBOpener: dbases,
}
gotSQL, gotErr := libsq.SLQ2SQL(th.Context, qc, "1+2")
gotSQL, gotErr := libsq.SLQ2SQL(th.Context, qc, tc.in)
if tc.wantErr {
require.Error(t, gotErr)
return

View File

@ -7,54 +7,52 @@ import (
"github.com/neilotoole/sq/drivers/mysql"
"github.com/neilotoole/sq/libsq/source"
_ "github.com/mattn/go-sqlite3"
)
//nolint:exhaustive,lll
//nolint:exhaustive
func TestQuery_orderby(t *testing.T) {
testCases := []queryTestCase{
{
name: "order_by/single-element",
in: `@sakila | .actor | order_by(.first_name)`,
wantSQL: `SELECT * FROM "actor" ORDER BY "first_name"`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `actor` ORDER BY `first_name`"},
override: driverMap{mysql.Type: "SELECT * FROM `actor` ORDER BY `first_name`"},
wantRecCount: sakila.TblActorCount,
},
{
name: "order_by/single-element-table-selector",
in: `@sakila | .actor | order_by(.actor.first_name)`,
wantSQL: `SELECT * FROM "actor" ORDER BY "actor"."first_name"`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `actor` ORDER BY `actor`.`first_name`"},
override: driverMap{mysql.Type: "SELECT * FROM `actor` ORDER BY `actor`.`first_name`"},
wantRecCount: sakila.TblActorCount,
},
{
name: "order_by/single-element-asc",
in: `@sakila | .actor | order_by(.first_name+)`,
wantSQL: `SELECT * FROM "actor" ORDER BY "first_name" ASC`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `actor` ORDER BY `first_name` ASC"},
override: driverMap{mysql.Type: "SELECT * FROM `actor` ORDER BY `first_name` ASC"},
wantRecCount: sakila.TblActorCount,
},
{
name: "order_by/single-element-desc",
in: `@sakila | .actor | order_by(.first_name-)`,
wantSQL: `SELECT * FROM "actor" ORDER BY "first_name" DESC`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `actor` ORDER BY `first_name` DESC"},
override: driverMap{mysql.Type: "SELECT * FROM `actor` ORDER BY `first_name` DESC"},
wantRecCount: sakila.TblActorCount,
},
{
name: "order_by/multiple-elements",
in: `@sakila | .actor | order_by(.first_name+, .last_name-)`,
wantSQL: `SELECT * FROM "actor" ORDER BY "first_name" ASC, "last_name" DESC`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `actor` ORDER BY `first_name` ASC, `last_name` DESC"},
override: driverMap{mysql.Type: "SELECT * FROM `actor` ORDER BY `first_name` ASC, `last_name` DESC"},
wantRecCount: sakila.TblActorCount,
},
{
name: "order_by/synonym-sort-by",
in: `@sakila | .actor | sort_by(.first_name)`,
wantSQL: `SELECT * FROM "actor" ORDER BY "first_name"`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `actor` ORDER BY `first_name`"},
override: driverMap{mysql.Type: "SELECT * FROM `actor` ORDER BY `first_name`"},
wantRecCount: sakila.TblActorCount,
},
{

View File

@ -19,6 +19,10 @@ import (
"github.com/neilotoole/sq/testh/sakila"
)
// driverMap is a map of source.DriverType to a string.
// It is used to specify a string for a specific driver.
type driverMap map[source.DriverType]string
// queryTestCase is used to test libsq's rendering of SLQ into SQL.
// It is probably the most important test struct in the codebase.
type queryTestCase struct {
@ -46,7 +50,7 @@ type queryTestCase struct {
// override allows an alternative "wantSQL" for a specific driver type.
// For example, MySQL uses backtick as the quote char, so it needs
// a separate wantSQL string.
override map[source.DriverType]string
override driverMap
// onlyFor indicates that this test should only run on sources of
// the specified types. When empty, the test is executed on all types.

View File

@ -7,8 +7,6 @@ import (
"github.com/neilotoole/sq/drivers/mysql"
"github.com/neilotoole/sq/libsq/source"
_ "github.com/mattn/go-sqlite3"
)
@ -19,21 +17,21 @@ func TestQuery_unique(t *testing.T) {
name: "unique/single-col",
in: `@sakila | .actor | .first_name | unique`,
wantSQL: `SELECT DISTINCT "first_name" FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT DISTINCT `first_name` FROM `actor`"},
override: driverMap{mysql.Type: "SELECT DISTINCT `first_name` FROM `actor`"},
wantRecCount: 128,
},
{
name: "unique/no-col",
in: `@sakila | .actor | unique`,
wantSQL: `SELECT DISTINCT * FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT DISTINCT * FROM `actor`"},
override: driverMap{mysql.Type: "SELECT DISTINCT * FROM `actor`"},
wantRecCount: sakila.TblActorCount,
},
{
name: "unique/no-col",
in: `@sakila | .actor | unique`,
wantSQL: `SELECT DISTINCT * FROM "actor"`,
override: map[source.DriverType]string{mysql.Type: "SELECT DISTINCT * FROM `actor`"},
override: driverMap{mysql.Type: "SELECT DISTINCT * FROM `actor`"},
wantRecCount: sakila.TblActorCount,
},
}

View File

@ -5,8 +5,6 @@ import (
"github.com/neilotoole/sq/drivers/mysql"
"github.com/neilotoole/sq/libsq/source"
_ "github.com/mattn/go-sqlite3"
)
@ -17,42 +15,42 @@ func TestQuery_where(t *testing.T) {
name: "operator/eq",
in: `@sakila | .actor | where(.actor_id == 100)`,
wantSQL: `SELECT * FROM "actor" WHERE "actor_id" = 100`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `actor` WHERE `actor_id` = 100"},
override: driverMap{mysql.Type: "SELECT * FROM `actor` WHERE `actor_id` = 100"},
wantRecCount: 1,
},
{
name: "operator/ne",
in: `@sakila | .actor | where(.actor_id != 100)`,
wantSQL: `SELECT * FROM "actor" WHERE "actor_id" != 100`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `actor` WHERE `actor_id` != 100"},
override: driverMap{mysql.Type: "SELECT * FROM `actor` WHERE `actor_id` != 100"},
wantRecCount: 199,
},
{
name: "operator/lt",
in: `@sakila | .actor | where(.actor_id < 100)`,
wantSQL: `SELECT * FROM "actor" WHERE "actor_id" < 100`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `actor` WHERE `actor_id` < 100"},
override: driverMap{mysql.Type: "SELECT * FROM `actor` WHERE `actor_id` < 100"},
wantRecCount: 99,
},
{
name: "operator/lte",
in: `@sakila | .actor | where(.actor_id <= 100)`,
wantSQL: `SELECT * FROM "actor" WHERE "actor_id" <= 100`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `actor` WHERE `actor_id` <= 100"},
override: driverMap{mysql.Type: "SELECT * FROM `actor` WHERE `actor_id` <= 100"},
wantRecCount: 100,
},
{
name: "operator/gt",
in: `@sakila | .actor | where(.actor_id > 100)`,
wantSQL: `SELECT * FROM "actor" WHERE "actor_id" > 100`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `actor` WHERE `actor_id` > 100"},
override: driverMap{mysql.Type: "SELECT * FROM `actor` WHERE `actor_id` > 100"},
wantRecCount: 100,
},
{
name: "operator/gte",
in: `@sakila | .actor | where(.actor_id >= 100)`,
wantSQL: `SELECT * FROM "actor" WHERE "actor_id" >= 100`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `actor` WHERE `actor_id` >= 100"},
override: driverMap{mysql.Type: "SELECT * FROM `actor` WHERE `actor_id` >= 100"},
wantRecCount: 101,
},
{
@ -65,28 +63,28 @@ func TestQuery_where(t *testing.T) {
name: "select_alias",
in: `@sakila | .actor | select(.actor_id == 1)`,
wantSQL: `SELECT * FROM "actor" WHERE "actor_id" = 1`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `actor` WHERE `actor_id` = 1"},
override: driverMap{mysql.Type: "SELECT * FROM `actor` WHERE `actor_id` = 1"},
wantRecCount: 1,
},
{
name: "where_compound_1",
in: `@sakila | .actor | where(.actor_id >= 100 && .actor_id < 150)`,
wantSQL: `SELECT * FROM "actor" WHERE "actor_id" >= 100 AND "actor_id" < 150`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `actor` WHERE `actor_id` >= 100 AND `actor_id` < 150"},
override: driverMap{mysql.Type: "SELECT * FROM `actor` WHERE `actor_id` >= 100 AND `actor_id` < 150"},
wantRecCount: 50,
},
{
name: "where_compound_2",
in: `@sakila | .actor | where(.actor_id >= 100 || (.actor_id < 150 && .first_name == "TOM"))`,
wantSQL: `SELECT * FROM "actor" WHERE "actor_id" >= 100 OR ("actor_id" < 150 AND "first_name" = 'TOM')`,
override: map[source.DriverType]string{mysql.Type: "SELECT * FROM `actor` WHERE `actor_id` >= 100 OR (`actor_id` < 150 AND `first_name` = 'TOM')"},
override: driverMap{mysql.Type: "SELECT * FROM `actor` WHERE `actor_id` >= 100 OR (`actor_id` < 150 AND `first_name` = 'TOM')"},
wantRecCount: 103,
},
{
name: "where_using_col_alias",
in: `@sakila | .actor | .first_name:given_name | where(.given_name == "TOM")`,
wantSQL: `SELECT "first_name" AS "given_name" FROM "actor" WHERE "given_name" = 'TOM'`,
override: map[source.DriverType]string{mysql.Type: "SELECT `first_name` AS `given_name` FROM `actor` WHERE `given_name` = 'TOM'"},
override: driverMap{mysql.Type: "SELECT `first_name` AS `given_name` FROM `actor` WHERE `given_name` = 'TOM'"},
wantRecCount: 2,
// Skip because this only works on SQLite, not the other SQL databases.
// I'm not sure if this will ever be implemented. Perhaps sq could look at

View File

@ -535,7 +535,10 @@ func TempDirFile(filename string) (dir string, f *os.File, cleanFn func() error,
}
cleanFn = func() error {
return errz.Append(f.Close(), os.RemoveAll(dir))
closeErr := f.Close()
removeErr := os.RemoveAll(dir)
return errz.Append(closeErr, removeErr)
}
return dir, f, cleanFn, nil