mirror of
https://github.com/neilotoole/sq.git
synced 2024-12-25 01:04:55 +03:00
Feature/173 args (#183)
- Implement --arg feature - Refactor sqlbuilder package (now called "render"). - Bug fixes, especially around expressions.
This commit is contained in:
parent
801dd69218
commit
9a1c6a7d09
11
cli/cli.go
11
cli/cli.go
@ -158,15 +158,20 @@ func ExecuteWith(ctx context.Context, rc *RunContext, args []string) error {
|
||||
// look like: [query, arg1, arg2] -- noting that SetArgs
|
||||
// doesn't want the first args element.
|
||||
effectiveArgs := append([]string{"slq"}, args...)
|
||||
if effectiveArgs, err = preprocessFlagArgVars(effectiveArgs); err != nil {
|
||||
return err
|
||||
}
|
||||
rootCmd.SetArgs(effectiveArgs)
|
||||
} else {
|
||||
if cmd.Name() == rootCmd.Name() {
|
||||
// Not sure why we have two paths to this, but it appears
|
||||
// that we've found the root cmd again, so again
|
||||
// we redirect to "slq" cmd.
|
||||
|
||||
a := append([]string{"slq"}, args...)
|
||||
rootCmd.SetArgs(a)
|
||||
effectiveArgs := append([]string{"slq"}, args...)
|
||||
if effectiveArgs, err = preprocessFlagArgVars(effectiveArgs); err != nil {
|
||||
return err
|
||||
}
|
||||
rootCmd.SetArgs(effectiveArgs)
|
||||
} else {
|
||||
// It's just a normal command like "sq ls" or such.
|
||||
|
||||
|
159
cli/cmd_slq.go
159
cli/cmd_slq.go
@ -5,6 +5,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/lg/lgm"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/lg"
|
||||
@ -25,26 +27,38 @@ func newSLQCmd() *cobra.Command {
|
||||
Short: "Execute SLQ query",
|
||||
// This command is hidden, because it is effectively the root cmd.
|
||||
Hidden: true,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: execSLQ,
|
||||
ValidArgsFunction: completeSLQ,
|
||||
}
|
||||
|
||||
addQueryCmdFlags(cmd)
|
||||
|
||||
// Explicitly flagVersion because people like to do "sq --version"
|
||||
cmd.Flags().StringArray(flagArg, nil, flagArgUsage)
|
||||
|
||||
// Explicitly add flagVersion because people like to do "sq --version"
|
||||
// as much as "sq version".
|
||||
cmd.Flags().Bool(flagVersion, false, flagVersionUsage)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func execSLQ(cmd *cobra.Command, _ []string) error {
|
||||
rc := RunContextFrom(cmd.Context())
|
||||
// execSLQ is sq's core command.
|
||||
func execSLQ(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
msg := "no query"
|
||||
if cmdFlagChanged(cmd, flagArg) {
|
||||
msg += fmt.Sprintf(": maybe check flag --%s usage", flagArg)
|
||||
}
|
||||
|
||||
return errz.New(msg)
|
||||
}
|
||||
|
||||
ctx := cmd.Context()
|
||||
rc := RunContextFrom(ctx)
|
||||
srcs := rc.Config.Sources
|
||||
|
||||
// check if there's input on stdin
|
||||
src, err := checkStdinSource(cmd.Context(), rc)
|
||||
src, err := checkStdinSource(ctx, rc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -53,15 +67,13 @@ func execSLQ(cmd *cobra.Command, _ []string) error {
|
||||
// We have a valid source on stdin.
|
||||
|
||||
// Add the source to the set.
|
||||
err = srcs.Add(src)
|
||||
if err != nil {
|
||||
if err = srcs.Add(src); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the stdin pipe data source as the active source,
|
||||
// as it's commonly the only data source the user is acting upon.
|
||||
_, err = srcs.SetActive(src.Handle)
|
||||
if err != nil {
|
||||
if _, err = srcs.SetActive(src.Handle); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
@ -74,10 +86,15 @@ func execSLQ(cmd *cobra.Command, _ []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
mArgs, err := extractFlagArgsValues(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !cmdFlagChanged(cmd, flagInsert) {
|
||||
// The user didn't specify the --insert=@src.tbl flag,
|
||||
// so we just want to print the records.
|
||||
return execSLQPrint(cmd.Context(), rc)
|
||||
return execSLQPrint(ctx, rc, mArgs)
|
||||
}
|
||||
|
||||
// Instead of printing the records, they will be
|
||||
@ -101,12 +118,14 @@ func execSLQ(cmd *cobra.Command, _ []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return execSLQInsert(cmd.Context(), rc, destSrc, destTbl)
|
||||
return execSLQInsert(ctx, rc, mArgs, destSrc, destTbl)
|
||||
}
|
||||
|
||||
// execSQLInsert executes the SLQ and inserts resulting records
|
||||
// into destTbl in destSrc.
|
||||
func execSLQInsert(ctx context.Context, rc *RunContext, destSrc *source.Source, destTbl string) error {
|
||||
func execSLQInsert(ctx context.Context, rc *RunContext, mArgs map[string]string,
|
||||
destSrc *source.Source, destTbl string,
|
||||
) error {
|
||||
args, srcs, dbases := rc.Args, rc.Config.Sources, rc.databases
|
||||
slq, err := preprocessUserSLQ(ctx, rc, args)
|
||||
if err != nil {
|
||||
@ -138,7 +157,7 @@ func execSLQInsert(ctx context.Context, rc *RunContext, destSrc *source.Source,
|
||||
Sources: srcs,
|
||||
DBOpener: rc.databases,
|
||||
JoinDBOpener: rc.databases,
|
||||
Args: nil,
|
||||
Args: mArgs,
|
||||
}
|
||||
|
||||
execErr := libsq.ExecuteSLQ(ctx, qc, slq, inserter)
|
||||
@ -156,7 +175,7 @@ func execSLQInsert(ctx context.Context, rc *RunContext, destSrc *source.Source,
|
||||
}
|
||||
|
||||
// execSLQPrint executes the SLQ query, and prints output to writer.
|
||||
func execSLQPrint(ctx context.Context, rc *RunContext) error {
|
||||
func execSLQPrint(ctx context.Context, rc *RunContext, mArgs map[string]string) error {
|
||||
slq, err := preprocessUserSLQ(ctx, rc, rc.Args)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -166,7 +185,7 @@ func execSLQPrint(ctx context.Context, rc *RunContext) error {
|
||||
Sources: rc.Config.Sources,
|
||||
DBOpener: rc.databases,
|
||||
JoinDBOpener: rc.databases,
|
||||
Args: nil,
|
||||
Args: mArgs,
|
||||
}
|
||||
|
||||
recw := output.NewRecordWriterAdapter(rc.writers.recordw)
|
||||
@ -341,3 +360,113 @@ func addQueryCmdFlags(cmd *cobra.Command) {
|
||||
|
||||
cmd.Flags().StringP(flagSrcOptions, "", "", flagQuerySrcOptionsUsage)
|
||||
}
|
||||
|
||||
// extractFlagArgsValues returns a map {key:value} of predefined variables
|
||||
// as supplied via --arg. For example:
|
||||
//
|
||||
// sq --arg name TOM '.actor | .first_name == $name'
|
||||
//
|
||||
// See preprocessFlagArgVars.
|
||||
func extractFlagArgsValues(cmd *cobra.Command) (map[string]string, error) {
|
||||
if !cmdFlagChanged(cmd, flagArg) {
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
arr, err := cmd.Flags().GetStringArray(flagArg)
|
||||
if err != nil {
|
||||
return nil, errz.Err(err)
|
||||
}
|
||||
|
||||
if len(arr) == 0 {
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
mArgs := map[string]string{}
|
||||
for _, kv := range arr {
|
||||
k, v, ok := strings.Cut(kv, ":")
|
||||
if !ok || k == "" {
|
||||
return nil, errz.Errorf("invalid --%s value", flagArg)
|
||||
}
|
||||
|
||||
if _, ok := mArgs[k]; ok {
|
||||
// If the key already exists, don't overwrite. This mimics jq's
|
||||
// behavior.
|
||||
|
||||
log := lg.FromContext(cmd.Context())
|
||||
log.With("arg", k).Warn("Double use of --arg key; using first value.")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
mArgs[k] = v
|
||||
}
|
||||
|
||||
return mArgs, nil
|
||||
}
|
||||
|
||||
// preprocessFlagArgVars is a hack to support the predefined
|
||||
// variables "--arg" mechanism. We implement the mechanism in alignment
|
||||
// with how jq does it: "--arg name value".
|
||||
// See: https://stedolan.github.io/jq/manual/v1.6/
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// sq --arg first TOM --arg last MIRANDA '.actor | .first_name == $first && .last_name == $last'
|
||||
//
|
||||
// However, cobra (or rather, pflag) doesn't support this type of flag input.
|
||||
// So, we have a hack. In the example above, the two elements "first" and "TOM"
|
||||
// are concatenated into a single flag value "first:TOM". Thus, the returned
|
||||
// slice will be shorter.
|
||||
//
|
||||
// This function needs to be called before cobra/pflag starts processing
|
||||
// the program args.
|
||||
//
|
||||
// Any code making use of flagArg will need to deconstruct the flag value.
|
||||
// Specifically, that means extractFlagArgsValues.
|
||||
func preprocessFlagArgVars(args []string) ([]string, error) {
|
||||
const flg = "--" + flagArg
|
||||
|
||||
if len(args) == 0 {
|
||||
return args, nil
|
||||
}
|
||||
|
||||
if !slices.Contains(args, flg) {
|
||||
return args, nil
|
||||
}
|
||||
|
||||
rez := make([]string, 0, len(args))
|
||||
|
||||
var i int
|
||||
for i = 0; i < len(args); {
|
||||
if args[i] == flg {
|
||||
val, err := extractFlagArgsSingleArg(args[i:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rez = append(rez, flg)
|
||||
rez = append(rez, val)
|
||||
i += 3
|
||||
continue
|
||||
}
|
||||
|
||||
rez = append(rez, args[i])
|
||||
i++
|
||||
}
|
||||
|
||||
return rez, nil
|
||||
}
|
||||
|
||||
// args will look like ["--arg", "key", "value", "--other-flag"].
|
||||
// The function will return "key:value".
|
||||
// See preprocessFlagArgVars.
|
||||
func extractFlagArgsSingleArg(args []string) (string, error) {
|
||||
if len(args) < 3 {
|
||||
return "", errz.Errorf("invalid %s flag: must be '--%s key value'", flagArg, flagArg)
|
||||
}
|
||||
|
||||
if err := stringz.ValidIdent(args[1]); err != nil {
|
||||
return "", errz.Errorf("invalid --%s key: %s", flagArg, args[1])
|
||||
}
|
||||
|
||||
return args[1] + ":" + args[2], nil
|
||||
}
|
||||
|
@ -112,6 +112,9 @@ const (
|
||||
|
||||
flagSkipVerify = "skip-verify"
|
||||
flagSkipVerifyUsage = "Don't ping source before adding it"
|
||||
|
||||
flagArg = "arg"
|
||||
flagArgUsage = "Set a string value to a variable"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -1 +1,87 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_preprocessFlagArgVars(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
in []string
|
||||
want []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
in: []string{},
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
name: "no flags",
|
||||
in: []string{".actor"},
|
||||
want: []string{".actor"},
|
||||
},
|
||||
{
|
||||
name: "non-arg flag",
|
||||
in: []string{"--json", ".actor"},
|
||||
want: []string{"--json", ".actor"},
|
||||
},
|
||||
{
|
||||
name: "non-arg flag with value",
|
||||
in: []string{"--json", "true", ".actor"},
|
||||
want: []string{"--json", "true", ".actor"},
|
||||
},
|
||||
{
|
||||
name: "single arg flag",
|
||||
in: []string{"--arg", "name", "TOM", ".actor"},
|
||||
want: []string{"--arg", "name:TOM", ".actor"},
|
||||
},
|
||||
{
|
||||
name: "invalid arg name",
|
||||
in: []string{"--arg", "na me", "TOM", ".actor"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid arg name (with colon)",
|
||||
in: []string{"--arg", "na:me", "TOM", ".actor"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "colon in value",
|
||||
in: []string{"--arg", "name", "T:OM", ".actor"},
|
||||
want: []string{"--arg", "name:T:OM", ".actor"},
|
||||
},
|
||||
{
|
||||
name: "single arg flag with whitespace",
|
||||
in: []string{"--arg", "name", "TOM DOWD", ".actor"},
|
||||
want: []string{"--arg", "name:TOM DOWD", ".actor"},
|
||||
},
|
||||
{
|
||||
name: "two arg flags",
|
||||
in: []string{"--arg", "name", "TOM", "--arg", "eyes", "blue", ".actor"},
|
||||
want: []string{"--arg", "name:TOM", "--arg", "eyes:blue", ".actor"},
|
||||
},
|
||||
{
|
||||
name: "two arg flags with interspersed flag",
|
||||
in: []string{"--arg", "name", "TOM", "--json", "true", "--arg", "eyes", "blue", ".actor"},
|
||||
want: []string{"--arg", "name:TOM", "--json", "true", "--arg", "eyes:blue", ".actor"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, gotErr := preprocessFlagArgVars(tc.in)
|
||||
if tc.wantErr {
|
||||
t.Log(gotErr.Error())
|
||||
require.Error(t, gotErr)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, gotErr)
|
||||
require.EqualValues(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/driver/dialect"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/lg/lga"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/lg/lgm"
|
||||
@ -18,7 +20,7 @@ import (
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"github.com/xo/dburl"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/ast/sqlbuilder"
|
||||
"github.com/neilotoole/sq/libsq/ast/render"
|
||||
"github.com/neilotoole/sq/libsq/core/errz"
|
||||
"github.com/neilotoole/sq/libsq/core/kind"
|
||||
"github.com/neilotoole/sq/libsq/core/sqlmodel"
|
||||
@ -70,13 +72,15 @@ func (d *driveri) DriverMetadata() driver.Metadata {
|
||||
}
|
||||
|
||||
// Dialect implements driver.Driver.
|
||||
func (d *driveri) Dialect() driver.Dialect {
|
||||
return driver.Dialect{
|
||||
func (d *driveri) Dialect() dialect.Dialect {
|
||||
return dialect.Dialect{
|
||||
Type: Type,
|
||||
Placeholders: placeholders,
|
||||
Quote: '`',
|
||||
IdentQuote: '`',
|
||||
Enquote: stringz.BacktickQuote,
|
||||
IntBool: true,
|
||||
MaxBatchValues: 250,
|
||||
Ops: dialect.DefaultOps(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,9 +92,10 @@ func placeholders(numCols, numRows int) string {
|
||||
return strings.Join(rows, driver.Comma)
|
||||
}
|
||||
|
||||
// SQLBuilder implements driver.SQLDriver.
|
||||
func (d *driveri) SQLBuilder() (sqlbuilder.FragmentBuilder, sqlbuilder.QueryBuilder) {
|
||||
return newFragmentBuilder(d.log), &sqlbuilder.BaseQueryBuilder{}
|
||||
// Renderer implements driver.SQLDriver.
|
||||
func (d *driveri) Renderer() *render.Renderer {
|
||||
r := render.NewDefaultRenderer()
|
||||
return r
|
||||
}
|
||||
|
||||
// RecordMeta implements driver.SQLDriver.
|
||||
@ -246,8 +251,8 @@ func (d *driveri) TableColumnTypes(ctx context.Context, db sqlz.DB, tblName stri
|
||||
const queryTpl = "SELECT %s FROM %s LIMIT 0"
|
||||
|
||||
dialect := d.Dialect()
|
||||
quote := string(dialect.Quote)
|
||||
tblNameQuoted := stringz.Surround(tblName, quote)
|
||||
quote := string(dialect.IdentQuote)
|
||||
tblNameQuoted := dialect.Enquote(tblName)
|
||||
|
||||
colsClause := "*"
|
||||
if len(colNames) > 0 {
|
||||
|
@ -5,26 +5,12 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/stringz"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/ast/sqlbuilder"
|
||||
"github.com/neilotoole/sq/libsq/core/kind"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/errz"
|
||||
"github.com/neilotoole/sq/libsq/core/sqlmodel"
|
||||
)
|
||||
|
||||
func newFragmentBuilder(log *slog.Logger) *sqlbuilder.BaseFragmentBuilder {
|
||||
r := &sqlbuilder.BaseFragmentBuilder{}
|
||||
r.Log = log
|
||||
r.Quote = "`"
|
||||
r.QuoteFn = stringz.BacktickQuote
|
||||
r.Ops = sqlbuilder.BaseOps()
|
||||
return r
|
||||
}
|
||||
|
||||
func dbTypeNameFromKind(knd kind.Kind) string {
|
||||
switch knd { //nolint:exhaustive // ignore kind.Unknown and kind.Null
|
||||
case kind.Text:
|
@ -8,6 +8,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/driver/dialect"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/lg/lga"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/lg/lgm"
|
||||
@ -19,7 +21,7 @@ import (
|
||||
"github.com/jackc/pgx/v4"
|
||||
// Import jackc/pgx, which is our postgres driver.
|
||||
_ "github.com/jackc/pgx/v4/stdlib"
|
||||
"github.com/neilotoole/sq/libsq/ast/sqlbuilder"
|
||||
"github.com/neilotoole/sq/libsq/ast/render"
|
||||
"github.com/neilotoole/sq/libsq/core/errz"
|
||||
"github.com/neilotoole/sq/libsq/core/kind"
|
||||
"github.com/neilotoole/sq/libsq/core/sqlmodel"
|
||||
@ -67,12 +69,14 @@ func (d *driveri) DriverMetadata() driver.Metadata {
|
||||
}
|
||||
|
||||
// Dialect implements driver.SQLDriver.
|
||||
func (d *driveri) Dialect() driver.Dialect {
|
||||
return driver.Dialect{
|
||||
func (d *driveri) Dialect() dialect.Dialect {
|
||||
return dialect.Dialect{
|
||||
Type: Type,
|
||||
Placeholders: placeholders,
|
||||
Quote: '"',
|
||||
IdentQuote: '"',
|
||||
Enquote: stringz.DoubleQuote,
|
||||
MaxBatchValues: 1000,
|
||||
Ops: dialect.DefaultOps(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,9 +103,10 @@ func placeholders(numCols, numRows int) string {
|
||||
return strings.Join(rows, driver.Comma)
|
||||
}
|
||||
|
||||
// SQLBuilder implements driver.SQLDriver.
|
||||
func (d *driveri) SQLBuilder() (sqlbuilder.FragmentBuilder, sqlbuilder.QueryBuilder) {
|
||||
return newFragmentBuilder(d.log), &sqlbuilder.BaseQueryBuilder{}
|
||||
// Renderer implements driver.SQLDriver.
|
||||
func (d *driveri) Renderer() *render.Renderer {
|
||||
r := render.NewDefaultRenderer()
|
||||
return r
|
||||
}
|
||||
|
||||
// Open implements driver.Driver.
|
||||
@ -340,7 +345,7 @@ func (d *driveri) TableColumnTypes(ctx context.Context, db sqlz.DB, tblName stri
|
||||
// (SELECT username FROM person LIMIT 1) AS username,
|
||||
// (SELECT email FROM person LIMIT 1) AS email
|
||||
// LIMIT 1;
|
||||
quote := string(d.Dialect().Quote)
|
||||
quote := string(d.Dialect().IdentQuote)
|
||||
tblNameQuoted := stringz.Surround(tblName, quote)
|
||||
|
||||
var query string
|
||||
|
@ -5,26 +5,12 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/stringz"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/ast/sqlbuilder"
|
||||
"github.com/neilotoole/sq/libsq/core/kind"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/errz"
|
||||
"github.com/neilotoole/sq/libsq/core/sqlmodel"
|
||||
)
|
||||
|
||||
func newFragmentBuilder(log *slog.Logger) *sqlbuilder.BaseFragmentBuilder {
|
||||
fb := &sqlbuilder.BaseFragmentBuilder{}
|
||||
fb.Log = log
|
||||
fb.Quote = `"`
|
||||
fb.QuoteFn = stringz.DoubleQuote
|
||||
fb.Ops = sqlbuilder.BaseOps()
|
||||
return fb
|
||||
}
|
||||
|
||||
func dbTypeNameFromKind(knd kind.Kind) string {
|
||||
switch knd { //nolint:exhaustive
|
||||
default:
|
@ -4,26 +4,12 @@ import (
|
||||
"bytes"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/stringz"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/ast/sqlbuilder"
|
||||
"github.com/neilotoole/sq/libsq/core/kind"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/errz"
|
||||
"github.com/neilotoole/sq/libsq/core/sqlmodel"
|
||||
)
|
||||
|
||||
func newFragmentBuilder(log *slog.Logger) *sqlbuilder.BaseFragmentBuilder {
|
||||
return &sqlbuilder.BaseFragmentBuilder{
|
||||
Log: log,
|
||||
Quote: `"`,
|
||||
QuoteFn: stringz.DoubleQuote,
|
||||
Ops: sqlbuilder.BaseOps(),
|
||||
}
|
||||
}
|
||||
|
||||
// createTblKindDefaults is a mapping of Kind to the value
|
||||
// to use for a column's DEFAULT clause in a CREATE TABLE statement.
|
||||
var createTblKindDefaults = map[kind.Kind]string{ //nolint:exhaustive // ignore kind.Null
|
@ -15,6 +15,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/driver/dialect"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/lg/lga"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/lg/lgm"
|
||||
@ -24,7 +26,7 @@ import (
|
||||
"golang.org/x/exp/slog"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3" // Import for side effect of loading the driver
|
||||
"github.com/neilotoole/sq/libsq/ast/sqlbuilder"
|
||||
"github.com/neilotoole/sq/libsq/ast/render"
|
||||
"github.com/neilotoole/sq/libsq/core/errz"
|
||||
"github.com/neilotoole/sq/libsq/core/kind"
|
||||
"github.com/neilotoole/sq/libsq/core/sqlmodel"
|
||||
@ -160,12 +162,14 @@ func (d *driveri) Ping(ctx context.Context, src *source.Source) error {
|
||||
}
|
||||
|
||||
// Dialect implements driver.SQLDriver.
|
||||
func (d *driveri) Dialect() driver.Dialect {
|
||||
return driver.Dialect{
|
||||
func (d *driveri) Dialect() dialect.Dialect {
|
||||
return dialect.Dialect{
|
||||
Type: Type,
|
||||
Placeholders: placeholders,
|
||||
Quote: '"',
|
||||
IdentQuote: '"',
|
||||
Enquote: stringz.DoubleQuote,
|
||||
MaxBatchValues: 500,
|
||||
Ops: dialect.DefaultOps(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -178,8 +182,9 @@ func placeholders(numCols, numRows int) string {
|
||||
}
|
||||
|
||||
// SQLBuilder implements driver.SQLDriver.
|
||||
func (d *driveri) SQLBuilder() (sqlbuilder.FragmentBuilder, sqlbuilder.QueryBuilder) {
|
||||
return newFragmentBuilder(d.log), &sqlbuilder.BaseQueryBuilder{}
|
||||
func (d *driveri) Renderer() *render.Renderer {
|
||||
r := render.NewDefaultRenderer()
|
||||
return r
|
||||
}
|
||||
|
||||
// CopyTable implements driver.SQLDriver.
|
||||
@ -682,7 +687,7 @@ func (d *driveri) TableColumnTypes(ctx context.Context, db sqlz.DB, tblName stri
|
||||
const queryTpl = "SELECT %s FROM %s LIMIT 1"
|
||||
|
||||
dialect := d.Dialect()
|
||||
quote := string(dialect.Quote)
|
||||
quote := string(dialect.IdentQuote)
|
||||
tblNameQuoted := stringz.Surround(tblName, quote)
|
||||
|
||||
colsClause := "*"
|
||||
|
@ -1,18 +1,11 @@
|
||||
package sqlserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/lg/lga"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/stringz"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/ast/sqlbuilder"
|
||||
"github.com/neilotoole/sq/libsq/ast/render"
|
||||
"github.com/neilotoole/sq/libsq/core/kind"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/ast"
|
||||
@ -20,22 +13,7 @@ import (
|
||||
"github.com/neilotoole/sq/libsq/core/sqlmodel"
|
||||
)
|
||||
|
||||
var _ sqlbuilder.FragmentBuilder = (*fragBuilder)(nil)
|
||||
|
||||
type fragBuilder struct {
|
||||
sqlbuilder.BaseFragmentBuilder
|
||||
}
|
||||
|
||||
func newFragmentBuilder(log *slog.Logger) *fragBuilder {
|
||||
r := &fragBuilder{}
|
||||
r.Log = log
|
||||
r.Quote = `"`
|
||||
r.QuoteFn = stringz.DoubleQuote
|
||||
r.Ops = sqlbuilder.BaseOps()
|
||||
return r
|
||||
}
|
||||
|
||||
func (fb *fragBuilder) Range(rr *ast.RowRangeNode) (string, error) {
|
||||
func renderRange(_ *render.Context, rr *ast.RowRangeNode) (string, error) {
|
||||
if rr == nil {
|
||||
return "", nil
|
||||
}
|
||||
@ -56,7 +34,7 @@ func (fb *fragBuilder) Range(rr *ast.RowRangeNode) (string, error) {
|
||||
offset = rr.Offset
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
var buf strings.Builder
|
||||
buf.WriteString(fmt.Sprintf("OFFSET %d ROWS", offset))
|
||||
|
||||
if rr.Limit > -1 {
|
||||
@ -64,17 +42,10 @@ func (fb *fragBuilder) Range(rr *ast.RowRangeNode) (string, error) {
|
||||
}
|
||||
|
||||
sql := buf.String()
|
||||
fb.Log.Debug("Returning SQL fragment", lga.SQL, sql)
|
||||
return sql, nil
|
||||
}
|
||||
|
||||
var _ sqlbuilder.QueryBuilder = (*queryBuilder)(nil)
|
||||
|
||||
type queryBuilder struct {
|
||||
sqlbuilder.BaseQueryBuilder
|
||||
}
|
||||
|
||||
func (qb *queryBuilder) Render() (string, error) {
|
||||
func preRender(_ *render.Context, f *render.Fragments) error {
|
||||
// SQL Server handles range (OFFSET, LIMIT) a little differently. If the query has a range,
|
||||
// then the ORDER BY clause is required. If ORDER BY is not specified, we use a trick (SELECT 0)
|
||||
// to satisfy SQL Server. For example:
|
||||
@ -83,13 +54,13 @@ func (qb *queryBuilder) Render() (string, error) {
|
||||
// ORDER BY (SELECT 0)
|
||||
// OFFSET 1 ROWS
|
||||
// FETCH NEXT 2 ROWS ONLY;
|
||||
if qb.Range != "" {
|
||||
if qb.OrderBy == "" {
|
||||
qb.OrderBy = "ORDER BY (SELECT 0)"
|
||||
if f.Range != "" {
|
||||
if f.OrderBy == "" {
|
||||
f.OrderBy = "ORDER BY (SELECT 0)"
|
||||
}
|
||||
}
|
||||
|
||||
return qb.BaseQueryBuilder.Render()
|
||||
return nil
|
||||
}
|
||||
|
||||
func dbTypeNameFromKind(knd kind.Kind) string {
|
@ -9,6 +9,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/driver/dialect"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/lg/lga"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/lg/lgm"
|
||||
@ -19,7 +21,7 @@ import (
|
||||
|
||||
mssql "github.com/microsoft/go-mssqldb"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/ast/sqlbuilder"
|
||||
"github.com/neilotoole/sq/libsq/ast/render"
|
||||
"github.com/neilotoole/sq/libsq/core/errz"
|
||||
"github.com/neilotoole/sq/libsq/core/kind"
|
||||
"github.com/neilotoole/sq/libsq/core/sqlmodel"
|
||||
@ -71,12 +73,14 @@ func (d *driveri) DriverMetadata() driver.Metadata {
|
||||
}
|
||||
|
||||
// Dialect implements driver.SQLDriver.
|
||||
func (d *driveri) Dialect() driver.Dialect {
|
||||
return driver.Dialect{
|
||||
func (d *driveri) Dialect() dialect.Dialect {
|
||||
return dialect.Dialect{
|
||||
Type: Type,
|
||||
Placeholders: placeholders,
|
||||
Quote: '"',
|
||||
IdentQuote: '"',
|
||||
Enquote: stringz.DoubleQuote,
|
||||
MaxBatchValues: 1000,
|
||||
Ops: dialect.DefaultOps(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,9 +107,15 @@ func placeholders(numCols, numRows int) string {
|
||||
return strings.Join(rows, driver.Comma)
|
||||
}
|
||||
|
||||
// SQLBuilder implements driver.SQLDriver.
|
||||
func (d *driveri) SQLBuilder() (sqlbuilder.FragmentBuilder, sqlbuilder.QueryBuilder) {
|
||||
return newFragmentBuilder(d.log), &queryBuilder{}
|
||||
// Renderer implements driver.SQLDriver.
|
||||
func (d *driveri) Renderer() *render.Renderer {
|
||||
r := render.NewDefaultRenderer()
|
||||
|
||||
// Custom functions for SQLServer specific stuff.
|
||||
r.Range = renderRange
|
||||
r.PreRender = preRender
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Open implements driver.Driver.
|
||||
@ -196,7 +206,7 @@ func (d *driveri) TableColumnTypes(ctx context.Context, db sqlz.DB, tblName stri
|
||||
const queryTpl = "SELECT %s FROM %s ORDER BY (SELECT 0) OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY"
|
||||
|
||||
dialect := d.Dialect()
|
||||
quote := string(dialect.Quote)
|
||||
quote := string(dialect.IdentQuote)
|
||||
tblNameQuoted := stringz.Surround(tblName, quote)
|
||||
|
||||
colsClause := "*"
|
||||
@ -403,7 +413,7 @@ func (d *driveri) getTableColsMeta(ctx context.Context, db sqlz.DB, tblName stri
|
||||
const queryTpl = "SELECT %s FROM %s ORDER BY (SELECT 0) OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY"
|
||||
|
||||
dialect := d.Dialect()
|
||||
quote := string(dialect.Quote)
|
||||
quote := string(dialect.IdentQuote)
|
||||
tblNameQuoted := stringz.Surround(tblName, quote)
|
||||
colNamesQuoted := stringz.SurroundSlice(colNames, quote)
|
||||
colsJoined := strings.Join(colNamesQuoted, driver.Comma)
|
||||
|
@ -150,6 +150,8 @@ ALIAS_RESERVED
|
||||
|
||||
ARG: '$' ID;
|
||||
|
||||
arg : ARG;
|
||||
|
||||
// handleTable is a handle.table pair.
|
||||
// - @my1.user
|
||||
handleTable: HANDLE NAME;
|
||||
@ -188,6 +190,7 @@ rowRange:
|
||||
expr:
|
||||
selector
|
||||
| literal
|
||||
| arg
|
||||
| unaryOperator expr
|
||||
| expr '||' expr
|
||||
| expr ( '*' | '/' | '%') expr
|
||||
@ -203,6 +206,7 @@ literal: NN | NUMBER | STRING | NULL;
|
||||
|
||||
unaryOperator: '-' | '+' | '~' | '!';
|
||||
|
||||
NULL: 'null';
|
||||
ID: [a-zA-Z_][a-zA-Z0-9_]*;
|
||||
WS: [ \t\r\n]+ -> skip;
|
||||
LPAR: '(';
|
||||
@ -212,7 +216,7 @@ RBRA: ']';
|
||||
COMMA: ',';
|
||||
PIPE: '|';
|
||||
COLON: ':';
|
||||
NULL: 'null' | 'NULL';
|
||||
|
||||
|
||||
// NN: Natural Number {0,1,2,3, ...}
|
||||
NN: INTF;
|
||||
|
37
libsq/ast/arg.go
Normal file
37
libsq/ast/arg.go
Normal file
@ -0,0 +1,37 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/ast/internal/slq"
|
||||
)
|
||||
|
||||
// ArgNode implements the SQL "DISTINCT" clause.
|
||||
type ArgNode struct {
|
||||
baseNode
|
||||
key string
|
||||
}
|
||||
|
||||
// String returns a log/debug-friendly representation.
|
||||
func (n *ArgNode) String() string {
|
||||
return nodeString(n)
|
||||
}
|
||||
|
||||
// Key returns the arg key. If the arg is "$name", the key is "name".
|
||||
func (n *ArgNode) Key() string {
|
||||
return n.key
|
||||
}
|
||||
|
||||
// VisitArg implements slq.SLQVisitor.
|
||||
func (v *parseTreeVisitor) VisitArg(ctx *slq.ArgContext) interface{} {
|
||||
node := &ArgNode{}
|
||||
node.ctx = ctx
|
||||
node.text = ctx.GetText()
|
||||
|
||||
if ctx.ARG() != nil {
|
||||
// The node text will be "$key". We need to trim the $ prefix.
|
||||
node.key = strings.TrimPrefix(ctx.ARG().GetText(), "$")
|
||||
}
|
||||
|
||||
return v.cur.AddChild(node)
|
||||
}
|
@ -31,7 +31,7 @@ func Parse(log *slog.Logger, input string) (*AST, error) { //nolint:staticcheck
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := verify(log, ast); err != nil {
|
||||
if err := verify(ast); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -68,7 +68,7 @@ func buildAST(log *slog.Logger, query slq.IQueryContext) (*AST, error) {
|
||||
}
|
||||
|
||||
for _, visitor := range visitors {
|
||||
w := NewWalker(log, tree.ast).AddVisitor(visitor.typ, visitor.fn)
|
||||
w := NewWalker(tree.ast).AddVisitor(visitor.typ, visitor.fn)
|
||||
if err := w.Walk(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -78,8 +78,8 @@ func buildAST(log *slog.Logger, query slq.IQueryContext) (*AST, error) {
|
||||
}
|
||||
|
||||
// verify performs additional checks on the state of the built AST.
|
||||
func verify(log *slog.Logger, ast *AST) error {
|
||||
selCount := NewInspector(log, ast).CountNodes(typeSelectorNode)
|
||||
func verify(ast *AST) error {
|
||||
selCount := NewInspector(ast).CountNodes(typeSelectorNode)
|
||||
if selCount != 0 {
|
||||
return errorf("AST should have zero nodes of type %T but found %d",
|
||||
(*SelectorNode)(nil), selCount)
|
||||
|
@ -3,35 +3,25 @@ package ast
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/ryboe/q"
|
||||
)
|
||||
|
||||
// Inspector provides functionality for AST interrogation.
|
||||
type Inspector struct {
|
||||
log *slog.Logger
|
||||
ast *AST
|
||||
}
|
||||
|
||||
// NewInspector returns an Inspector instance for ast.
|
||||
func NewInspector(log *slog.Logger, ast *AST) *Inspector {
|
||||
return &Inspector{log: log, ast: ast}
|
||||
func NewInspector(ast *AST) *Inspector {
|
||||
return &Inspector{ast: ast}
|
||||
}
|
||||
|
||||
// CountNodes counts the number of nodes having typ.
|
||||
func (in *Inspector) CountNodes(typ reflect.Type) int {
|
||||
count := 0
|
||||
w := NewWalker(in.log, in.ast)
|
||||
w.AddVisitor(typ, func(log *slog.Logger, w *Walker, node Node) error {
|
||||
w := NewWalker(in.ast)
|
||||
w.AddVisitor(typ, func(w *Walker, node Node) error {
|
||||
count++
|
||||
if typ == typeSelectorNode {
|
||||
// found it
|
||||
// FIXME: delete this
|
||||
q.Q("found it", node)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
@ -42,8 +32,8 @@ func (in *Inspector) CountNodes(typ reflect.Type) int {
|
||||
// FindNodes returns the nodes having typ.
|
||||
func (in *Inspector) FindNodes(typ reflect.Type) []Node {
|
||||
var nodes []Node
|
||||
w := NewWalker(in.log, in.ast)
|
||||
w.AddVisitor(typ, func(log *slog.Logger, w *Walker, node Node) error {
|
||||
w := NewWalker(in.ast)
|
||||
w.AddVisitor(typ, func(w *Walker, node Node) error {
|
||||
nodes = append(nodes, node)
|
||||
return nil
|
||||
})
|
||||
@ -59,14 +49,14 @@ func (in *Inspector) FindNodes(typ reflect.Type) []Node {
|
||||
func (in *Inspector) FindHandles() []string {
|
||||
var handles []string
|
||||
|
||||
if err := walkWith(in.log, in.ast, typeHandleNode, func(log *slog.Logger, walker *Walker, node Node) error {
|
||||
if err := walkWith(in.ast, typeHandleNode, func(walker *Walker, node Node) error {
|
||||
handles = append(handles, node.Text())
|
||||
return nil
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := walkWith(in.log, in.ast, typeTblSelectorNode, func(log *slog.Logger, walker *Walker, node Node) error {
|
||||
if err := walkWith(in.ast, typeTblSelectorNode, func(walker *Walker, node Node) error {
|
||||
n, _ := node.(*TblSelectorNode)
|
||||
if n.handle != "" {
|
||||
handles = append(handles, n.handle)
|
||||
|
@ -3,21 +3,17 @@ package ast
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/neilotoole/slogt"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestInspector_findSelectableSegments(t *testing.T) {
|
||||
log := slogt.New(t)
|
||||
|
||||
// `@mydb1 | .user | .uid, .username`
|
||||
ast, err := buildInitialAST(t, fixtSelect1)
|
||||
require.Nil(t, err)
|
||||
err = NewWalker(log, ast).AddVisitor(typeSelectorNode, narrowTblSel).Walk()
|
||||
err = NewWalker(ast).AddVisitor(typeSelectorNode, narrowTblSel).Walk()
|
||||
require.Nil(t, err)
|
||||
|
||||
insp := NewInspector(log, ast)
|
||||
insp := NewInspector(ast)
|
||||
|
||||
segs := ast.Segments()
|
||||
require.Equal(t, 3, len(segs))
|
||||
@ -31,9 +27,9 @@ func TestInspector_findSelectableSegments(t *testing.T) {
|
||||
// `@mydb1 | .user, .address | join(.user.uid == .address.uid) | .uid, .username, .country`
|
||||
ast, err = buildInitialAST(t, fixtJoinQuery1)
|
||||
require.Nil(t, err)
|
||||
err = NewWalker(log, ast).AddVisitor(typeSelectorNode, narrowTblSel).Walk()
|
||||
err = NewWalker(ast).AddVisitor(typeSelectorNode, narrowTblSel).Walk()
|
||||
require.Nil(t, err)
|
||||
insp = NewInspector(log, ast)
|
||||
insp = NewInspector(ast)
|
||||
|
||||
segs = ast.Segments()
|
||||
require.Equal(t, 4, len(segs))
|
||||
|
File diff suppressed because one or more lines are too long
@ -19,16 +19,16 @@ ORDER_DESC=18
|
||||
ORDER_BY=19
|
||||
ALIAS_RESERVED=20
|
||||
ARG=21
|
||||
ID=22
|
||||
WS=23
|
||||
LPAR=24
|
||||
RPAR=25
|
||||
LBRA=26
|
||||
RBRA=27
|
||||
COMMA=28
|
||||
PIPE=29
|
||||
COLON=30
|
||||
NULL=31
|
||||
NULL=22
|
||||
ID=23
|
||||
WS=24
|
||||
LPAR=25
|
||||
RPAR=26
|
||||
LBRA=27
|
||||
RBRA=28
|
||||
COMMA=29
|
||||
PIPE=30
|
||||
COLON=31
|
||||
NN=32
|
||||
NUMBER=33
|
||||
LT_EQ=34
|
||||
@ -59,13 +59,14 @@ LINECOMMENT=43
|
||||
'group_by'=16
|
||||
'+'=17
|
||||
'-'=18
|
||||
'('=24
|
||||
')'=25
|
||||
'['=26
|
||||
']'=27
|
||||
','=28
|
||||
'|'=29
|
||||
':'=30
|
||||
'null'=22
|
||||
'('=25
|
||||
')'=26
|
||||
'['=27
|
||||
']'=28
|
||||
','=29
|
||||
'|'=30
|
||||
':'=31
|
||||
'<='=34
|
||||
'<'=35
|
||||
'>='=36
|
||||
|
File diff suppressed because one or more lines are too long
@ -19,16 +19,16 @@ ORDER_DESC=18
|
||||
ORDER_BY=19
|
||||
ALIAS_RESERVED=20
|
||||
ARG=21
|
||||
ID=22
|
||||
WS=23
|
||||
LPAR=24
|
||||
RPAR=25
|
||||
LBRA=26
|
||||
RBRA=27
|
||||
COMMA=28
|
||||
PIPE=29
|
||||
COLON=30
|
||||
NULL=31
|
||||
NULL=22
|
||||
ID=23
|
||||
WS=24
|
||||
LPAR=25
|
||||
RPAR=26
|
||||
LBRA=27
|
||||
RBRA=28
|
||||
COMMA=29
|
||||
PIPE=30
|
||||
COLON=31
|
||||
NN=32
|
||||
NUMBER=33
|
||||
LT_EQ=34
|
||||
@ -59,13 +59,14 @@ LINECOMMENT=43
|
||||
'group_by'=16
|
||||
'+'=17
|
||||
'-'=18
|
||||
'('=24
|
||||
')'=25
|
||||
'['=26
|
||||
']'=27
|
||||
','=28
|
||||
'|'=29
|
||||
':'=30
|
||||
'null'=22
|
||||
'('=25
|
||||
')'=26
|
||||
'['=27
|
||||
']'=28
|
||||
','=29
|
||||
'|'=30
|
||||
':'=31
|
||||
'<='=34
|
||||
'<'=35
|
||||
'>='=36
|
||||
|
@ -134,6 +134,12 @@ func (s *BaseSLQListener) EnterAlias(ctx *AliasContext) {}
|
||||
// ExitAlias is called when production alias is exited.
|
||||
func (s *BaseSLQListener) ExitAlias(ctx *AliasContext) {}
|
||||
|
||||
// EnterArg is called when production arg is entered.
|
||||
func (s *BaseSLQListener) EnterArg(ctx *ArgContext) {}
|
||||
|
||||
// ExitArg is called when production arg is exited.
|
||||
func (s *BaseSLQListener) ExitArg(ctx *ArgContext) {}
|
||||
|
||||
// EnterHandleTable is called when production handleTable is entered.
|
||||
func (s *BaseSLQListener) EnterHandleTable(ctx *HandleTableContext) {}
|
||||
|
||||
|
@ -83,6 +83,10 @@ func (v *BaseSLQVisitor) VisitAlias(ctx *AliasContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BaseSLQVisitor) VisitArg(ctx *ArgContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BaseSLQVisitor) VisitHandleTable(ctx *HandleTableContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
@ -46,21 +46,21 @@ func slqlexerLexerInit() {
|
||||
staticData.literalNames = []string{
|
||||
"", "';'", "'*'", "'join'", "'unique'", "'count'", "'.['", "'||'", "'/'",
|
||||
"'%'", "'<<'", "'>>'", "'&'", "'&&'", "'~'", "'!'", "'group_by'", "'+'",
|
||||
"'-'", "", "", "", "", "", "'('", "')'", "'['", "']'", "','", "'|'",
|
||||
"':'", "", "", "", "'<='", "'<'", "'>='", "'>'", "'!='", "'=='",
|
||||
"'-'", "", "", "", "'null'", "", "", "'('", "')'", "'['", "']'", "','",
|
||||
"'|'", "':'", "", "", "'<='", "'<'", "'>='", "'>'", "'!='", "'=='",
|
||||
}
|
||||
staticData.symbolicNames = []string{
|
||||
"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "GROUP_BY",
|
||||
"ORDER_ASC", "ORDER_DESC", "ORDER_BY", "ALIAS_RESERVED", "ARG", "ID",
|
||||
"WS", "LPAR", "RPAR", "LBRA", "RBRA", "COMMA", "PIPE", "COLON", "NULL",
|
||||
"ORDER_ASC", "ORDER_DESC", "ORDER_BY", "ALIAS_RESERVED", "ARG", "NULL",
|
||||
"ID", "WS", "LPAR", "RPAR", "LBRA", "RBRA", "COMMA", "PIPE", "COLON",
|
||||
"NN", "NUMBER", "LT_EQ", "LT", "GT_EQ", "GT", "NEQ", "EQ", "NAME", "HANDLE",
|
||||
"STRING", "LINECOMMENT",
|
||||
}
|
||||
staticData.ruleNames = []string{
|
||||
"T__0", "T__1", "T__2", "T__3", "T__4", "T__5", "T__6", "T__7", "T__8",
|
||||
"T__9", "T__10", "T__11", "T__12", "T__13", "T__14", "GROUP_BY", "ORDER_ASC",
|
||||
"ORDER_DESC", "ORDER_BY", "ALIAS_RESERVED", "ARG", "ID", "WS", "LPAR",
|
||||
"RPAR", "LBRA", "RBRA", "COMMA", "PIPE", "COLON", "NULL", "NN", "NUMBER",
|
||||
"ORDER_DESC", "ORDER_BY", "ALIAS_RESERVED", "ARG", "NULL", "ID", "WS",
|
||||
"LPAR", "RPAR", "LBRA", "RBRA", "COMMA", "PIPE", "COLON", "NN", "NUMBER",
|
||||
"INTF", "EXP", "LT_EQ", "LT", "GT_EQ", "GT", "NEQ", "EQ", "NAME", "HANDLE",
|
||||
"STRING", "ESC", "UNICODE", "HEX", "DIGIT", "A", "B", "C", "D", "E",
|
||||
"F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S",
|
||||
@ -68,7 +68,7 @@ func slqlexerLexerInit() {
|
||||
}
|
||||
staticData.predictionContextCache = antlr.NewPredictionContextCache()
|
||||
staticData.serializedATN = []int32{
|
||||
4, 0, 43, 484, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2,
|
||||
4, 0, 43, 479, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2,
|
||||
4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2,
|
||||
10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15,
|
||||
7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7,
|
||||
@ -95,196 +95,194 @@ func slqlexerLexerInit() {
|
||||
1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1,
|
||||
19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19,
|
||||
1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 3, 19, 285, 8, 19, 1,
|
||||
20, 1, 20, 1, 20, 1, 21, 1, 21, 5, 21, 292, 8, 21, 10, 21, 12, 21, 295,
|
||||
9, 21, 1, 22, 4, 22, 298, 8, 22, 11, 22, 12, 22, 299, 1, 22, 1, 22, 1,
|
||||
23, 1, 23, 1, 24, 1, 24, 1, 25, 1, 25, 1, 26, 1, 26, 1, 27, 1, 27, 1, 28,
|
||||
1, 28, 1, 29, 1, 29, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1,
|
||||
30, 3, 30, 326, 8, 30, 1, 31, 1, 31, 1, 32, 1, 32, 3, 32, 332, 8, 32, 1,
|
||||
32, 1, 32, 1, 32, 4, 32, 337, 8, 32, 11, 32, 12, 32, 338, 1, 32, 3, 32,
|
||||
342, 8, 32, 1, 32, 3, 32, 345, 8, 32, 1, 32, 1, 32, 1, 32, 1, 32, 3, 32,
|
||||
351, 8, 32, 1, 32, 3, 32, 354, 8, 32, 1, 33, 1, 33, 1, 33, 5, 33, 359,
|
||||
8, 33, 10, 33, 12, 33, 362, 9, 33, 3, 33, 364, 8, 33, 1, 34, 1, 34, 3,
|
||||
34, 368, 8, 34, 1, 34, 1, 34, 1, 35, 1, 35, 1, 35, 1, 36, 1, 36, 1, 37,
|
||||
1, 37, 1, 37, 1, 38, 1, 38, 1, 39, 1, 39, 1, 39, 1, 40, 1, 40, 1, 40, 1,
|
||||
41, 1, 41, 1, 41, 1, 41, 3, 41, 392, 8, 41, 1, 42, 1, 42, 1, 42, 1, 43,
|
||||
1, 43, 1, 43, 5, 43, 400, 8, 43, 10, 43, 12, 43, 403, 9, 43, 1, 43, 1,
|
||||
43, 1, 44, 1, 44, 1, 44, 3, 44, 410, 8, 44, 1, 45, 1, 45, 1, 45, 1, 45,
|
||||
1, 45, 1, 45, 1, 46, 1, 46, 1, 47, 1, 47, 1, 48, 1, 48, 1, 49, 1, 49, 1,
|
||||
50, 1, 50, 1, 51, 1, 51, 1, 52, 1, 52, 1, 53, 1, 53, 1, 54, 1, 54, 1, 55,
|
||||
1, 55, 1, 56, 1, 56, 1, 57, 1, 57, 1, 58, 1, 58, 1, 59, 1, 59, 1, 60, 1,
|
||||
60, 1, 61, 1, 61, 1, 62, 1, 62, 1, 63, 1, 63, 1, 64, 1, 64, 1, 65, 1, 65,
|
||||
1, 66, 1, 66, 1, 67, 1, 67, 1, 68, 1, 68, 1, 69, 1, 69, 1, 70, 1, 70, 1,
|
||||
71, 1, 71, 1, 72, 1, 72, 1, 73, 1, 73, 1, 74, 1, 74, 5, 74, 476, 8, 74,
|
||||
10, 74, 12, 74, 479, 9, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 477, 0, 75,
|
||||
1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11,
|
||||
23, 12, 25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 35, 18, 37, 19, 39, 20,
|
||||
41, 21, 43, 22, 45, 23, 47, 24, 49, 25, 51, 26, 53, 27, 55, 28, 57, 29,
|
||||
59, 30, 61, 31, 63, 32, 65, 33, 67, 0, 69, 0, 71, 34, 73, 35, 75, 36, 77,
|
||||
37, 79, 38, 81, 39, 83, 40, 85, 41, 87, 42, 89, 0, 91, 0, 93, 0, 95, 0,
|
||||
97, 0, 99, 0, 101, 0, 103, 0, 105, 0, 107, 0, 109, 0, 111, 0, 113, 0, 115,
|
||||
0, 117, 0, 119, 0, 121, 0, 123, 0, 125, 0, 127, 0, 129, 0, 131, 0, 133,
|
||||
0, 135, 0, 137, 0, 139, 0, 141, 0, 143, 0, 145, 0, 147, 0, 149, 43, 1,
|
||||
0, 35, 3, 0, 65, 90, 95, 95, 97, 122, 4, 0, 48, 57, 65, 90, 95, 95, 97,
|
||||
122, 3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 48, 57, 1, 0, 49, 57, 2, 0, 69,
|
||||
69, 101, 101, 2, 0, 43, 43, 45, 45, 2, 0, 34, 34, 92, 92, 8, 0, 34, 34,
|
||||
47, 47, 92, 92, 98, 98, 102, 102, 110, 110, 114, 114, 116, 116, 3, 0, 48,
|
||||
57, 65, 70, 97, 102, 2, 0, 65, 65, 97, 97, 2, 0, 66, 66, 98, 98, 2, 0,
|
||||
67, 67, 99, 99, 2, 0, 68, 68, 100, 100, 2, 0, 70, 70, 102, 102, 2, 0, 71,
|
||||
71, 103, 103, 2, 0, 72, 72, 104, 104, 2, 0, 73, 73, 105, 105, 2, 0, 74,
|
||||
74, 106, 106, 2, 0, 75, 75, 107, 107, 2, 0, 76, 76, 108, 108, 2, 0, 77,
|
||||
77, 109, 109, 2, 0, 78, 78, 110, 110, 2, 0, 79, 79, 111, 111, 2, 0, 80,
|
||||
80, 112, 112, 2, 0, 81, 81, 113, 113, 2, 0, 82, 82, 114, 114, 2, 0, 83,
|
||||
83, 115, 115, 2, 0, 84, 84, 116, 116, 2, 0, 85, 85, 117, 117, 2, 0, 86,
|
||||
86, 118, 118, 2, 0, 87, 87, 119, 119, 2, 0, 88, 88, 120, 120, 2, 0, 89,
|
||||
89, 121, 121, 2, 0, 90, 90, 122, 122, 479, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0,
|
||||
0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0,
|
||||
0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1,
|
||||
0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 27,
|
||||
1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0, 0, 0, 0, 33, 1, 0, 0, 0, 0,
|
||||
35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39, 1, 0, 0, 0, 0, 41, 1, 0, 0, 0,
|
||||
0, 43, 1, 0, 0, 0, 0, 45, 1, 0, 0, 0, 0, 47, 1, 0, 0, 0, 0, 49, 1, 0, 0,
|
||||
0, 0, 51, 1, 0, 0, 0, 0, 53, 1, 0, 0, 0, 0, 55, 1, 0, 0, 0, 0, 57, 1, 0,
|
||||
0, 0, 0, 59, 1, 0, 0, 0, 0, 61, 1, 0, 0, 0, 0, 63, 1, 0, 0, 0, 0, 65, 1,
|
||||
0, 0, 0, 0, 71, 1, 0, 0, 0, 0, 73, 1, 0, 0, 0, 0, 75, 1, 0, 0, 0, 0, 77,
|
||||
1, 0, 0, 0, 0, 79, 1, 0, 0, 0, 0, 81, 1, 0, 0, 0, 0, 83, 1, 0, 0, 0, 0,
|
||||
85, 1, 0, 0, 0, 0, 87, 1, 0, 0, 0, 0, 149, 1, 0, 0, 0, 1, 151, 1, 0, 0,
|
||||
0, 3, 153, 1, 0, 0, 0, 5, 155, 1, 0, 0, 0, 7, 160, 1, 0, 0, 0, 9, 167,
|
||||
1, 0, 0, 0, 11, 173, 1, 0, 0, 0, 13, 176, 1, 0, 0, 0, 15, 179, 1, 0, 0,
|
||||
0, 17, 181, 1, 0, 0, 0, 19, 183, 1, 0, 0, 0, 21, 186, 1, 0, 0, 0, 23, 189,
|
||||
1, 0, 0, 0, 25, 191, 1, 0, 0, 0, 27, 194, 1, 0, 0, 0, 29, 196, 1, 0, 0,
|
||||
0, 31, 198, 1, 0, 0, 0, 33, 207, 1, 0, 0, 0, 35, 209, 1, 0, 0, 0, 37, 226,
|
||||
1, 0, 0, 0, 39, 284, 1, 0, 0, 0, 41, 286, 1, 0, 0, 0, 43, 289, 1, 0, 0,
|
||||
0, 45, 297, 1, 0, 0, 0, 47, 303, 1, 0, 0, 0, 49, 305, 1, 0, 0, 0, 51, 307,
|
||||
1, 0, 0, 0, 53, 309, 1, 0, 0, 0, 55, 311, 1, 0, 0, 0, 57, 313, 1, 0, 0,
|
||||
0, 59, 315, 1, 0, 0, 0, 61, 325, 1, 0, 0, 0, 63, 327, 1, 0, 0, 0, 65, 353,
|
||||
1, 0, 0, 0, 67, 363, 1, 0, 0, 0, 69, 365, 1, 0, 0, 0, 71, 371, 1, 0, 0,
|
||||
0, 73, 374, 1, 0, 0, 0, 75, 376, 1, 0, 0, 0, 77, 379, 1, 0, 0, 0, 79, 381,
|
||||
1, 0, 0, 0, 81, 384, 1, 0, 0, 0, 83, 387, 1, 0, 0, 0, 85, 393, 1, 0, 0,
|
||||
0, 87, 396, 1, 0, 0, 0, 89, 406, 1, 0, 0, 0, 91, 411, 1, 0, 0, 0, 93, 417,
|
||||
1, 0, 0, 0, 95, 419, 1, 0, 0, 0, 97, 421, 1, 0, 0, 0, 99, 423, 1, 0, 0,
|
||||
0, 101, 425, 1, 0, 0, 0, 103, 427, 1, 0, 0, 0, 105, 429, 1, 0, 0, 0, 107,
|
||||
431, 1, 0, 0, 0, 109, 433, 1, 0, 0, 0, 111, 435, 1, 0, 0, 0, 113, 437,
|
||||
1, 0, 0, 0, 115, 439, 1, 0, 0, 0, 117, 441, 1, 0, 0, 0, 119, 443, 1, 0,
|
||||
0, 0, 121, 445, 1, 0, 0, 0, 123, 447, 1, 0, 0, 0, 125, 449, 1, 0, 0, 0,
|
||||
127, 451, 1, 0, 0, 0, 129, 453, 1, 0, 0, 0, 131, 455, 1, 0, 0, 0, 133,
|
||||
457, 1, 0, 0, 0, 135, 459, 1, 0, 0, 0, 137, 461, 1, 0, 0, 0, 139, 463,
|
||||
1, 0, 0, 0, 141, 465, 1, 0, 0, 0, 143, 467, 1, 0, 0, 0, 145, 469, 1, 0,
|
||||
0, 0, 147, 471, 1, 0, 0, 0, 149, 473, 1, 0, 0, 0, 151, 152, 5, 59, 0, 0,
|
||||
152, 2, 1, 0, 0, 0, 153, 154, 5, 42, 0, 0, 154, 4, 1, 0, 0, 0, 155, 156,
|
||||
5, 106, 0, 0, 156, 157, 5, 111, 0, 0, 157, 158, 5, 105, 0, 0, 158, 159,
|
||||
5, 110, 0, 0, 159, 6, 1, 0, 0, 0, 160, 161, 5, 117, 0, 0, 161, 162, 5,
|
||||
110, 0, 0, 162, 163, 5, 105, 0, 0, 163, 164, 5, 113, 0, 0, 164, 165, 5,
|
||||
117, 0, 0, 165, 166, 5, 101, 0, 0, 166, 8, 1, 0, 0, 0, 167, 168, 5, 99,
|
||||
0, 0, 168, 169, 5, 111, 0, 0, 169, 170, 5, 117, 0, 0, 170, 171, 5, 110,
|
||||
0, 0, 171, 172, 5, 116, 0, 0, 172, 10, 1, 0, 0, 0, 173, 174, 5, 46, 0,
|
||||
0, 174, 175, 5, 91, 0, 0, 175, 12, 1, 0, 0, 0, 176, 177, 5, 124, 0, 0,
|
||||
177, 178, 5, 124, 0, 0, 178, 14, 1, 0, 0, 0, 179, 180, 5, 47, 0, 0, 180,
|
||||
16, 1, 0, 0, 0, 181, 182, 5, 37, 0, 0, 182, 18, 1, 0, 0, 0, 183, 184, 5,
|
||||
60, 0, 0, 184, 185, 5, 60, 0, 0, 185, 20, 1, 0, 0, 0, 186, 187, 5, 62,
|
||||
0, 0, 187, 188, 5, 62, 0, 0, 188, 22, 1, 0, 0, 0, 189, 190, 5, 38, 0, 0,
|
||||
190, 24, 1, 0, 0, 0, 191, 192, 5, 38, 0, 0, 192, 193, 5, 38, 0, 0, 193,
|
||||
26, 1, 0, 0, 0, 194, 195, 5, 126, 0, 0, 195, 28, 1, 0, 0, 0, 196, 197,
|
||||
5, 33, 0, 0, 197, 30, 1, 0, 0, 0, 198, 199, 5, 103, 0, 0, 199, 200, 5,
|
||||
114, 0, 0, 200, 201, 5, 111, 0, 0, 201, 202, 5, 117, 0, 0, 202, 203, 5,
|
||||
112, 0, 0, 203, 204, 5, 95, 0, 0, 204, 205, 5, 98, 0, 0, 205, 206, 5, 121,
|
||||
0, 0, 206, 32, 1, 0, 0, 0, 207, 208, 5, 43, 0, 0, 208, 34, 1, 0, 0, 0,
|
||||
209, 210, 5, 45, 0, 0, 210, 36, 1, 0, 0, 0, 211, 212, 5, 111, 0, 0, 212,
|
||||
213, 5, 114, 0, 0, 213, 214, 5, 100, 0, 0, 214, 215, 5, 101, 0, 0, 215,
|
||||
216, 5, 114, 0, 0, 216, 217, 5, 95, 0, 0, 217, 218, 5, 98, 0, 0, 218, 227,
|
||||
5, 121, 0, 0, 219, 220, 5, 115, 0, 0, 220, 221, 5, 111, 0, 0, 221, 222,
|
||||
5, 114, 0, 0, 222, 223, 5, 116, 0, 0, 223, 224, 5, 95, 0, 0, 224, 225,
|
||||
5, 98, 0, 0, 225, 227, 5, 121, 0, 0, 226, 211, 1, 0, 0, 0, 226, 219, 1,
|
||||
0, 0, 0, 227, 38, 1, 0, 0, 0, 228, 229, 5, 58, 0, 0, 229, 230, 5, 99, 0,
|
||||
0, 230, 231, 5, 111, 0, 0, 231, 232, 5, 117, 0, 0, 232, 233, 5, 110, 0,
|
||||
0, 233, 285, 5, 116, 0, 0, 234, 235, 5, 58, 0, 0, 235, 236, 5, 99, 0, 0,
|
||||
236, 237, 5, 111, 0, 0, 237, 238, 5, 117, 0, 0, 238, 239, 5, 110, 0, 0,
|
||||
239, 240, 5, 116, 0, 0, 240, 241, 5, 95, 0, 0, 241, 242, 5, 117, 0, 0,
|
||||
242, 243, 5, 110, 0, 0, 243, 244, 5, 105, 0, 0, 244, 245, 5, 113, 0, 0,
|
||||
245, 246, 5, 117, 0, 0, 246, 285, 5, 101, 0, 0, 247, 248, 5, 58, 0, 0,
|
||||
248, 249, 5, 97, 0, 0, 249, 250, 5, 118, 0, 0, 250, 285, 5, 103, 0, 0,
|
||||
251, 252, 5, 58, 0, 0, 252, 253, 5, 103, 0, 0, 253, 254, 5, 114, 0, 0,
|
||||
254, 255, 5, 111, 0, 0, 255, 256, 5, 117, 0, 0, 256, 257, 5, 112, 0, 0,
|
||||
257, 258, 5, 95, 0, 0, 258, 259, 5, 98, 0, 0, 259, 285, 5, 121, 0, 0, 260,
|
||||
261, 5, 58, 0, 0, 261, 262, 5, 109, 0, 0, 262, 263, 5, 97, 0, 0, 263, 285,
|
||||
5, 120, 0, 0, 264, 265, 5, 58, 0, 0, 265, 266, 5, 109, 0, 0, 266, 267,
|
||||
5, 105, 0, 0, 267, 285, 5, 110, 0, 0, 268, 269, 5, 58, 0, 0, 269, 270,
|
||||
5, 111, 0, 0, 270, 271, 5, 114, 0, 0, 271, 272, 5, 100, 0, 0, 272, 273,
|
||||
5, 101, 0, 0, 273, 274, 5, 114, 0, 0, 274, 275, 5, 95, 0, 0, 275, 276,
|
||||
5, 98, 0, 0, 276, 285, 5, 121, 0, 0, 277, 278, 5, 58, 0, 0, 278, 279, 5,
|
||||
117, 0, 0, 279, 280, 5, 110, 0, 0, 280, 281, 5, 105, 0, 0, 281, 282, 5,
|
||||
113, 0, 0, 282, 283, 5, 117, 0, 0, 283, 285, 5, 101, 0, 0, 284, 228, 1,
|
||||
0, 0, 0, 284, 234, 1, 0, 0, 0, 284, 247, 1, 0, 0, 0, 284, 251, 1, 0, 0,
|
||||
0, 284, 260, 1, 0, 0, 0, 284, 264, 1, 0, 0, 0, 284, 268, 1, 0, 0, 0, 284,
|
||||
277, 1, 0, 0, 0, 285, 40, 1, 0, 0, 0, 286, 287, 5, 36, 0, 0, 287, 288,
|
||||
3, 43, 21, 0, 288, 42, 1, 0, 0, 0, 289, 293, 7, 0, 0, 0, 290, 292, 7, 1,
|
||||
0, 0, 291, 290, 1, 0, 0, 0, 292, 295, 1, 0, 0, 0, 293, 291, 1, 0, 0, 0,
|
||||
293, 294, 1, 0, 0, 0, 294, 44, 1, 0, 0, 0, 295, 293, 1, 0, 0, 0, 296, 298,
|
||||
7, 2, 0, 0, 297, 296, 1, 0, 0, 0, 298, 299, 1, 0, 0, 0, 299, 297, 1, 0,
|
||||
0, 0, 299, 300, 1, 0, 0, 0, 300, 301, 1, 0, 0, 0, 301, 302, 6, 22, 0, 0,
|
||||
302, 46, 1, 0, 0, 0, 303, 304, 5, 40, 0, 0, 304, 48, 1, 0, 0, 0, 305, 306,
|
||||
5, 41, 0, 0, 306, 50, 1, 0, 0, 0, 307, 308, 5, 91, 0, 0, 308, 52, 1, 0,
|
||||
0, 0, 309, 310, 5, 93, 0, 0, 310, 54, 1, 0, 0, 0, 311, 312, 5, 44, 0, 0,
|
||||
312, 56, 1, 0, 0, 0, 313, 314, 5, 124, 0, 0, 314, 58, 1, 0, 0, 0, 315,
|
||||
316, 5, 58, 0, 0, 316, 60, 1, 0, 0, 0, 317, 318, 5, 110, 0, 0, 318, 319,
|
||||
5, 117, 0, 0, 319, 320, 5, 108, 0, 0, 320, 326, 5, 108, 0, 0, 321, 322,
|
||||
5, 78, 0, 0, 322, 323, 5, 85, 0, 0, 323, 324, 5, 76, 0, 0, 324, 326, 5,
|
||||
76, 0, 0, 325, 317, 1, 0, 0, 0, 325, 321, 1, 0, 0, 0, 326, 62, 1, 0, 0,
|
||||
0, 327, 328, 3, 67, 33, 0, 328, 64, 1, 0, 0, 0, 329, 354, 3, 63, 31, 0,
|
||||
330, 332, 5, 45, 0, 0, 331, 330, 1, 0, 0, 0, 331, 332, 1, 0, 0, 0, 332,
|
||||
333, 1, 0, 0, 0, 333, 334, 3, 67, 33, 0, 334, 336, 5, 46, 0, 0, 335, 337,
|
||||
7, 3, 0, 0, 336, 335, 1, 0, 0, 0, 337, 338, 1, 0, 0, 0, 338, 336, 1, 0,
|
||||
0, 0, 338, 339, 1, 0, 0, 0, 339, 341, 1, 0, 0, 0, 340, 342, 3, 69, 34,
|
||||
0, 341, 340, 1, 0, 0, 0, 341, 342, 1, 0, 0, 0, 342, 354, 1, 0, 0, 0, 343,
|
||||
345, 5, 45, 0, 0, 344, 343, 1, 0, 0, 0, 344, 345, 1, 0, 0, 0, 345, 346,
|
||||
1, 0, 0, 0, 346, 347, 3, 67, 33, 0, 347, 348, 3, 69, 34, 0, 348, 354, 1,
|
||||
0, 0, 0, 349, 351, 5, 45, 0, 0, 350, 349, 1, 0, 0, 0, 350, 351, 1, 0, 0,
|
||||
0, 351, 352, 1, 0, 0, 0, 352, 354, 3, 67, 33, 0, 353, 329, 1, 0, 0, 0,
|
||||
353, 331, 1, 0, 0, 0, 353, 344, 1, 0, 0, 0, 353, 350, 1, 0, 0, 0, 354,
|
||||
66, 1, 0, 0, 0, 355, 364, 5, 48, 0, 0, 356, 360, 7, 4, 0, 0, 357, 359,
|
||||
7, 3, 0, 0, 358, 357, 1, 0, 0, 0, 359, 362, 1, 0, 0, 0, 360, 358, 1, 0,
|
||||
0, 0, 360, 361, 1, 0, 0, 0, 361, 364, 1, 0, 0, 0, 362, 360, 1, 0, 0, 0,
|
||||
363, 355, 1, 0, 0, 0, 363, 356, 1, 0, 0, 0, 364, 68, 1, 0, 0, 0, 365, 367,
|
||||
7, 5, 0, 0, 366, 368, 7, 6, 0, 0, 367, 366, 1, 0, 0, 0, 367, 368, 1, 0,
|
||||
0, 0, 368, 369, 1, 0, 0, 0, 369, 370, 3, 67, 33, 0, 370, 70, 1, 0, 0, 0,
|
||||
371, 372, 5, 60, 0, 0, 372, 373, 5, 61, 0, 0, 373, 72, 1, 0, 0, 0, 374,
|
||||
375, 5, 60, 0, 0, 375, 74, 1, 0, 0, 0, 376, 377, 5, 62, 0, 0, 377, 378,
|
||||
5, 61, 0, 0, 378, 76, 1, 0, 0, 0, 379, 380, 5, 62, 0, 0, 380, 78, 1, 0,
|
||||
0, 0, 381, 382, 5, 33, 0, 0, 382, 383, 5, 61, 0, 0, 383, 80, 1, 0, 0, 0,
|
||||
384, 385, 5, 61, 0, 0, 385, 386, 5, 61, 0, 0, 386, 82, 1, 0, 0, 0, 387,
|
||||
391, 5, 46, 0, 0, 388, 392, 3, 41, 20, 0, 389, 392, 3, 43, 21, 0, 390,
|
||||
392, 3, 87, 43, 0, 391, 388, 1, 0, 0, 0, 391, 389, 1, 0, 0, 0, 391, 390,
|
||||
1, 0, 0, 0, 392, 84, 1, 0, 0, 0, 393, 394, 5, 64, 0, 0, 394, 395, 3, 43,
|
||||
21, 0, 395, 86, 1, 0, 0, 0, 396, 401, 5, 34, 0, 0, 397, 400, 3, 89, 44,
|
||||
0, 398, 400, 8, 7, 0, 0, 399, 397, 1, 0, 0, 0, 399, 398, 1, 0, 0, 0, 400,
|
||||
403, 1, 0, 0, 0, 401, 399, 1, 0, 0, 0, 401, 402, 1, 0, 0, 0, 402, 404,
|
||||
1, 0, 0, 0, 403, 401, 1, 0, 0, 0, 404, 405, 5, 34, 0, 0, 405, 88, 1, 0,
|
||||
0, 0, 406, 409, 5, 92, 0, 0, 407, 410, 7, 8, 0, 0, 408, 410, 3, 91, 45,
|
||||
0, 409, 407, 1, 0, 0, 0, 409, 408, 1, 0, 0, 0, 410, 90, 1, 0, 0, 0, 411,
|
||||
412, 5, 117, 0, 0, 412, 413, 3, 93, 46, 0, 413, 414, 3, 93, 46, 0, 414,
|
||||
415, 3, 93, 46, 0, 415, 416, 3, 93, 46, 0, 416, 92, 1, 0, 0, 0, 417, 418,
|
||||
7, 9, 0, 0, 418, 94, 1, 0, 0, 0, 419, 420, 7, 3, 0, 0, 420, 96, 1, 0, 0,
|
||||
0, 421, 422, 7, 10, 0, 0, 422, 98, 1, 0, 0, 0, 423, 424, 7, 11, 0, 0, 424,
|
||||
100, 1, 0, 0, 0, 425, 426, 7, 12, 0, 0, 426, 102, 1, 0, 0, 0, 427, 428,
|
||||
7, 13, 0, 0, 428, 104, 1, 0, 0, 0, 429, 430, 7, 5, 0, 0, 430, 106, 1, 0,
|
||||
0, 0, 431, 432, 7, 14, 0, 0, 432, 108, 1, 0, 0, 0, 433, 434, 7, 15, 0,
|
||||
0, 434, 110, 1, 0, 0, 0, 435, 436, 7, 16, 0, 0, 436, 112, 1, 0, 0, 0, 437,
|
||||
438, 7, 17, 0, 0, 438, 114, 1, 0, 0, 0, 439, 440, 7, 18, 0, 0, 440, 116,
|
||||
1, 0, 0, 0, 441, 442, 7, 19, 0, 0, 442, 118, 1, 0, 0, 0, 443, 444, 7, 20,
|
||||
0, 0, 444, 120, 1, 0, 0, 0, 445, 446, 7, 21, 0, 0, 446, 122, 1, 0, 0, 0,
|
||||
447, 448, 7, 22, 0, 0, 448, 124, 1, 0, 0, 0, 449, 450, 7, 23, 0, 0, 450,
|
||||
126, 1, 0, 0, 0, 451, 452, 7, 24, 0, 0, 452, 128, 1, 0, 0, 0, 453, 454,
|
||||
7, 25, 0, 0, 454, 130, 1, 0, 0, 0, 455, 456, 7, 26, 0, 0, 456, 132, 1,
|
||||
0, 0, 0, 457, 458, 7, 27, 0, 0, 458, 134, 1, 0, 0, 0, 459, 460, 7, 28,
|
||||
0, 0, 460, 136, 1, 0, 0, 0, 461, 462, 7, 29, 0, 0, 462, 138, 1, 0, 0, 0,
|
||||
463, 464, 7, 30, 0, 0, 464, 140, 1, 0, 0, 0, 465, 466, 7, 31, 0, 0, 466,
|
||||
142, 1, 0, 0, 0, 467, 468, 7, 32, 0, 0, 468, 144, 1, 0, 0, 0, 469, 470,
|
||||
7, 33, 0, 0, 470, 146, 1, 0, 0, 0, 471, 472, 7, 34, 0, 0, 472, 148, 1,
|
||||
0, 0, 0, 473, 477, 5, 35, 0, 0, 474, 476, 9, 0, 0, 0, 475, 474, 1, 0, 0,
|
||||
0, 476, 479, 1, 0, 0, 0, 477, 478, 1, 0, 0, 0, 477, 475, 1, 0, 0, 0, 478,
|
||||
480, 1, 0, 0, 0, 479, 477, 1, 0, 0, 0, 480, 481, 5, 10, 0, 0, 481, 482,
|
||||
1, 0, 0, 0, 482, 483, 6, 74, 0, 0, 483, 150, 1, 0, 0, 0, 20, 0, 226, 284,
|
||||
293, 299, 325, 331, 338, 341, 344, 350, 353, 360, 363, 367, 391, 399, 401,
|
||||
409, 477, 1, 6, 0, 0,
|
||||
20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 5, 22,
|
||||
297, 8, 22, 10, 22, 12, 22, 300, 9, 22, 1, 23, 4, 23, 303, 8, 23, 11, 23,
|
||||
12, 23, 304, 1, 23, 1, 23, 1, 24, 1, 24, 1, 25, 1, 25, 1, 26, 1, 26, 1,
|
||||
27, 1, 27, 1, 28, 1, 28, 1, 29, 1, 29, 1, 30, 1, 30, 1, 31, 1, 31, 1, 32,
|
||||
1, 32, 3, 32, 327, 8, 32, 1, 32, 1, 32, 1, 32, 4, 32, 332, 8, 32, 11, 32,
|
||||
12, 32, 333, 1, 32, 3, 32, 337, 8, 32, 1, 32, 3, 32, 340, 8, 32, 1, 32,
|
||||
1, 32, 1, 32, 1, 32, 3, 32, 346, 8, 32, 1, 32, 3, 32, 349, 8, 32, 1, 33,
|
||||
1, 33, 1, 33, 5, 33, 354, 8, 33, 10, 33, 12, 33, 357, 9, 33, 3, 33, 359,
|
||||
8, 33, 1, 34, 1, 34, 3, 34, 363, 8, 34, 1, 34, 1, 34, 1, 35, 1, 35, 1,
|
||||
35, 1, 36, 1, 36, 1, 37, 1, 37, 1, 37, 1, 38, 1, 38, 1, 39, 1, 39, 1, 39,
|
||||
1, 40, 1, 40, 1, 40, 1, 41, 1, 41, 1, 41, 1, 41, 3, 41, 387, 8, 41, 1,
|
||||
42, 1, 42, 1, 42, 1, 43, 1, 43, 1, 43, 5, 43, 395, 8, 43, 10, 43, 12, 43,
|
||||
398, 9, 43, 1, 43, 1, 43, 1, 44, 1, 44, 1, 44, 3, 44, 405, 8, 44, 1, 45,
|
||||
1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 46, 1, 46, 1, 47, 1, 47, 1, 48, 1,
|
||||
48, 1, 49, 1, 49, 1, 50, 1, 50, 1, 51, 1, 51, 1, 52, 1, 52, 1, 53, 1, 53,
|
||||
1, 54, 1, 54, 1, 55, 1, 55, 1, 56, 1, 56, 1, 57, 1, 57, 1, 58, 1, 58, 1,
|
||||
59, 1, 59, 1, 60, 1, 60, 1, 61, 1, 61, 1, 62, 1, 62, 1, 63, 1, 63, 1, 64,
|
||||
1, 64, 1, 65, 1, 65, 1, 66, 1, 66, 1, 67, 1, 67, 1, 68, 1, 68, 1, 69, 1,
|
||||
69, 1, 70, 1, 70, 1, 71, 1, 71, 1, 72, 1, 72, 1, 73, 1, 73, 1, 74, 1, 74,
|
||||
5, 74, 471, 8, 74, 10, 74, 12, 74, 474, 9, 74, 1, 74, 1, 74, 1, 74, 1,
|
||||
74, 1, 472, 0, 75, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17,
|
||||
9, 19, 10, 21, 11, 23, 12, 25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 35,
|
||||
18, 37, 19, 39, 20, 41, 21, 43, 22, 45, 23, 47, 24, 49, 25, 51, 26, 53,
|
||||
27, 55, 28, 57, 29, 59, 30, 61, 31, 63, 32, 65, 33, 67, 0, 69, 0, 71, 34,
|
||||
73, 35, 75, 36, 77, 37, 79, 38, 81, 39, 83, 40, 85, 41, 87, 42, 89, 0,
|
||||
91, 0, 93, 0, 95, 0, 97, 0, 99, 0, 101, 0, 103, 0, 105, 0, 107, 0, 109,
|
||||
0, 111, 0, 113, 0, 115, 0, 117, 0, 119, 0, 121, 0, 123, 0, 125, 0, 127,
|
||||
0, 129, 0, 131, 0, 133, 0, 135, 0, 137, 0, 139, 0, 141, 0, 143, 0, 145,
|
||||
0, 147, 0, 149, 43, 1, 0, 35, 3, 0, 65, 90, 95, 95, 97, 122, 4, 0, 48,
|
||||
57, 65, 90, 95, 95, 97, 122, 3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 48, 57,
|
||||
1, 0, 49, 57, 2, 0, 69, 69, 101, 101, 2, 0, 43, 43, 45, 45, 2, 0, 34, 34,
|
||||
92, 92, 8, 0, 34, 34, 47, 47, 92, 92, 98, 98, 102, 102, 110, 110, 114,
|
||||
114, 116, 116, 3, 0, 48, 57, 65, 70, 97, 102, 2, 0, 65, 65, 97, 97, 2,
|
||||
0, 66, 66, 98, 98, 2, 0, 67, 67, 99, 99, 2, 0, 68, 68, 100, 100, 2, 0,
|
||||
70, 70, 102, 102, 2, 0, 71, 71, 103, 103, 2, 0, 72, 72, 104, 104, 2, 0,
|
||||
73, 73, 105, 105, 2, 0, 74, 74, 106, 106, 2, 0, 75, 75, 107, 107, 2, 0,
|
||||
76, 76, 108, 108, 2, 0, 77, 77, 109, 109, 2, 0, 78, 78, 110, 110, 2, 0,
|
||||
79, 79, 111, 111, 2, 0, 80, 80, 112, 112, 2, 0, 81, 81, 113, 113, 2, 0,
|
||||
82, 82, 114, 114, 2, 0, 83, 83, 115, 115, 2, 0, 84, 84, 116, 116, 2, 0,
|
||||
85, 85, 117, 117, 2, 0, 86, 86, 118, 118, 2, 0, 87, 87, 119, 119, 2, 0,
|
||||
88, 88, 120, 120, 2, 0, 89, 89, 121, 121, 2, 0, 90, 90, 122, 122, 473,
|
||||
0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0,
|
||||
0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0,
|
||||
0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0,
|
||||
0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1,
|
||||
0, 0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39,
|
||||
1, 0, 0, 0, 0, 41, 1, 0, 0, 0, 0, 43, 1, 0, 0, 0, 0, 45, 1, 0, 0, 0, 0,
|
||||
47, 1, 0, 0, 0, 0, 49, 1, 0, 0, 0, 0, 51, 1, 0, 0, 0, 0, 53, 1, 0, 0, 0,
|
||||
0, 55, 1, 0, 0, 0, 0, 57, 1, 0, 0, 0, 0, 59, 1, 0, 0, 0, 0, 61, 1, 0, 0,
|
||||
0, 0, 63, 1, 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 71, 1, 0, 0, 0, 0, 73, 1, 0,
|
||||
0, 0, 0, 75, 1, 0, 0, 0, 0, 77, 1, 0, 0, 0, 0, 79, 1, 0, 0, 0, 0, 81, 1,
|
||||
0, 0, 0, 0, 83, 1, 0, 0, 0, 0, 85, 1, 0, 0, 0, 0, 87, 1, 0, 0, 0, 0, 149,
|
||||
1, 0, 0, 0, 1, 151, 1, 0, 0, 0, 3, 153, 1, 0, 0, 0, 5, 155, 1, 0, 0, 0,
|
||||
7, 160, 1, 0, 0, 0, 9, 167, 1, 0, 0, 0, 11, 173, 1, 0, 0, 0, 13, 176, 1,
|
||||
0, 0, 0, 15, 179, 1, 0, 0, 0, 17, 181, 1, 0, 0, 0, 19, 183, 1, 0, 0, 0,
|
||||
21, 186, 1, 0, 0, 0, 23, 189, 1, 0, 0, 0, 25, 191, 1, 0, 0, 0, 27, 194,
|
||||
1, 0, 0, 0, 29, 196, 1, 0, 0, 0, 31, 198, 1, 0, 0, 0, 33, 207, 1, 0, 0,
|
||||
0, 35, 209, 1, 0, 0, 0, 37, 226, 1, 0, 0, 0, 39, 284, 1, 0, 0, 0, 41, 286,
|
||||
1, 0, 0, 0, 43, 289, 1, 0, 0, 0, 45, 294, 1, 0, 0, 0, 47, 302, 1, 0, 0,
|
||||
0, 49, 308, 1, 0, 0, 0, 51, 310, 1, 0, 0, 0, 53, 312, 1, 0, 0, 0, 55, 314,
|
||||
1, 0, 0, 0, 57, 316, 1, 0, 0, 0, 59, 318, 1, 0, 0, 0, 61, 320, 1, 0, 0,
|
||||
0, 63, 322, 1, 0, 0, 0, 65, 348, 1, 0, 0, 0, 67, 358, 1, 0, 0, 0, 69, 360,
|
||||
1, 0, 0, 0, 71, 366, 1, 0, 0, 0, 73, 369, 1, 0, 0, 0, 75, 371, 1, 0, 0,
|
||||
0, 77, 374, 1, 0, 0, 0, 79, 376, 1, 0, 0, 0, 81, 379, 1, 0, 0, 0, 83, 382,
|
||||
1, 0, 0, 0, 85, 388, 1, 0, 0, 0, 87, 391, 1, 0, 0, 0, 89, 401, 1, 0, 0,
|
||||
0, 91, 406, 1, 0, 0, 0, 93, 412, 1, 0, 0, 0, 95, 414, 1, 0, 0, 0, 97, 416,
|
||||
1, 0, 0, 0, 99, 418, 1, 0, 0, 0, 101, 420, 1, 0, 0, 0, 103, 422, 1, 0,
|
||||
0, 0, 105, 424, 1, 0, 0, 0, 107, 426, 1, 0, 0, 0, 109, 428, 1, 0, 0, 0,
|
||||
111, 430, 1, 0, 0, 0, 113, 432, 1, 0, 0, 0, 115, 434, 1, 0, 0, 0, 117,
|
||||
436, 1, 0, 0, 0, 119, 438, 1, 0, 0, 0, 121, 440, 1, 0, 0, 0, 123, 442,
|
||||
1, 0, 0, 0, 125, 444, 1, 0, 0, 0, 127, 446, 1, 0, 0, 0, 129, 448, 1, 0,
|
||||
0, 0, 131, 450, 1, 0, 0, 0, 133, 452, 1, 0, 0, 0, 135, 454, 1, 0, 0, 0,
|
||||
137, 456, 1, 0, 0, 0, 139, 458, 1, 0, 0, 0, 141, 460, 1, 0, 0, 0, 143,
|
||||
462, 1, 0, 0, 0, 145, 464, 1, 0, 0, 0, 147, 466, 1, 0, 0, 0, 149, 468,
|
||||
1, 0, 0, 0, 151, 152, 5, 59, 0, 0, 152, 2, 1, 0, 0, 0, 153, 154, 5, 42,
|
||||
0, 0, 154, 4, 1, 0, 0, 0, 155, 156, 5, 106, 0, 0, 156, 157, 5, 111, 0,
|
||||
0, 157, 158, 5, 105, 0, 0, 158, 159, 5, 110, 0, 0, 159, 6, 1, 0, 0, 0,
|
||||
160, 161, 5, 117, 0, 0, 161, 162, 5, 110, 0, 0, 162, 163, 5, 105, 0, 0,
|
||||
163, 164, 5, 113, 0, 0, 164, 165, 5, 117, 0, 0, 165, 166, 5, 101, 0, 0,
|
||||
166, 8, 1, 0, 0, 0, 167, 168, 5, 99, 0, 0, 168, 169, 5, 111, 0, 0, 169,
|
||||
170, 5, 117, 0, 0, 170, 171, 5, 110, 0, 0, 171, 172, 5, 116, 0, 0, 172,
|
||||
10, 1, 0, 0, 0, 173, 174, 5, 46, 0, 0, 174, 175, 5, 91, 0, 0, 175, 12,
|
||||
1, 0, 0, 0, 176, 177, 5, 124, 0, 0, 177, 178, 5, 124, 0, 0, 178, 14, 1,
|
||||
0, 0, 0, 179, 180, 5, 47, 0, 0, 180, 16, 1, 0, 0, 0, 181, 182, 5, 37, 0,
|
||||
0, 182, 18, 1, 0, 0, 0, 183, 184, 5, 60, 0, 0, 184, 185, 5, 60, 0, 0, 185,
|
||||
20, 1, 0, 0, 0, 186, 187, 5, 62, 0, 0, 187, 188, 5, 62, 0, 0, 188, 22,
|
||||
1, 0, 0, 0, 189, 190, 5, 38, 0, 0, 190, 24, 1, 0, 0, 0, 191, 192, 5, 38,
|
||||
0, 0, 192, 193, 5, 38, 0, 0, 193, 26, 1, 0, 0, 0, 194, 195, 5, 126, 0,
|
||||
0, 195, 28, 1, 0, 0, 0, 196, 197, 5, 33, 0, 0, 197, 30, 1, 0, 0, 0, 198,
|
||||
199, 5, 103, 0, 0, 199, 200, 5, 114, 0, 0, 200, 201, 5, 111, 0, 0, 201,
|
||||
202, 5, 117, 0, 0, 202, 203, 5, 112, 0, 0, 203, 204, 5, 95, 0, 0, 204,
|
||||
205, 5, 98, 0, 0, 205, 206, 5, 121, 0, 0, 206, 32, 1, 0, 0, 0, 207, 208,
|
||||
5, 43, 0, 0, 208, 34, 1, 0, 0, 0, 209, 210, 5, 45, 0, 0, 210, 36, 1, 0,
|
||||
0, 0, 211, 212, 5, 111, 0, 0, 212, 213, 5, 114, 0, 0, 213, 214, 5, 100,
|
||||
0, 0, 214, 215, 5, 101, 0, 0, 215, 216, 5, 114, 0, 0, 216, 217, 5, 95,
|
||||
0, 0, 217, 218, 5, 98, 0, 0, 218, 227, 5, 121, 0, 0, 219, 220, 5, 115,
|
||||
0, 0, 220, 221, 5, 111, 0, 0, 221, 222, 5, 114, 0, 0, 222, 223, 5, 116,
|
||||
0, 0, 223, 224, 5, 95, 0, 0, 224, 225, 5, 98, 0, 0, 225, 227, 5, 121, 0,
|
||||
0, 226, 211, 1, 0, 0, 0, 226, 219, 1, 0, 0, 0, 227, 38, 1, 0, 0, 0, 228,
|
||||
229, 5, 58, 0, 0, 229, 230, 5, 99, 0, 0, 230, 231, 5, 111, 0, 0, 231, 232,
|
||||
5, 117, 0, 0, 232, 233, 5, 110, 0, 0, 233, 285, 5, 116, 0, 0, 234, 235,
|
||||
5, 58, 0, 0, 235, 236, 5, 99, 0, 0, 236, 237, 5, 111, 0, 0, 237, 238, 5,
|
||||
117, 0, 0, 238, 239, 5, 110, 0, 0, 239, 240, 5, 116, 0, 0, 240, 241, 5,
|
||||
95, 0, 0, 241, 242, 5, 117, 0, 0, 242, 243, 5, 110, 0, 0, 243, 244, 5,
|
||||
105, 0, 0, 244, 245, 5, 113, 0, 0, 245, 246, 5, 117, 0, 0, 246, 285, 5,
|
||||
101, 0, 0, 247, 248, 5, 58, 0, 0, 248, 249, 5, 97, 0, 0, 249, 250, 5, 118,
|
||||
0, 0, 250, 285, 5, 103, 0, 0, 251, 252, 5, 58, 0, 0, 252, 253, 5, 103,
|
||||
0, 0, 253, 254, 5, 114, 0, 0, 254, 255, 5, 111, 0, 0, 255, 256, 5, 117,
|
||||
0, 0, 256, 257, 5, 112, 0, 0, 257, 258, 5, 95, 0, 0, 258, 259, 5, 98, 0,
|
||||
0, 259, 285, 5, 121, 0, 0, 260, 261, 5, 58, 0, 0, 261, 262, 5, 109, 0,
|
||||
0, 262, 263, 5, 97, 0, 0, 263, 285, 5, 120, 0, 0, 264, 265, 5, 58, 0, 0,
|
||||
265, 266, 5, 109, 0, 0, 266, 267, 5, 105, 0, 0, 267, 285, 5, 110, 0, 0,
|
||||
268, 269, 5, 58, 0, 0, 269, 270, 5, 111, 0, 0, 270, 271, 5, 114, 0, 0,
|
||||
271, 272, 5, 100, 0, 0, 272, 273, 5, 101, 0, 0, 273, 274, 5, 114, 0, 0,
|
||||
274, 275, 5, 95, 0, 0, 275, 276, 5, 98, 0, 0, 276, 285, 5, 121, 0, 0, 277,
|
||||
278, 5, 58, 0, 0, 278, 279, 5, 117, 0, 0, 279, 280, 5, 110, 0, 0, 280,
|
||||
281, 5, 105, 0, 0, 281, 282, 5, 113, 0, 0, 282, 283, 5, 117, 0, 0, 283,
|
||||
285, 5, 101, 0, 0, 284, 228, 1, 0, 0, 0, 284, 234, 1, 0, 0, 0, 284, 247,
|
||||
1, 0, 0, 0, 284, 251, 1, 0, 0, 0, 284, 260, 1, 0, 0, 0, 284, 264, 1, 0,
|
||||
0, 0, 284, 268, 1, 0, 0, 0, 284, 277, 1, 0, 0, 0, 285, 40, 1, 0, 0, 0,
|
||||
286, 287, 5, 36, 0, 0, 287, 288, 3, 45, 22, 0, 288, 42, 1, 0, 0, 0, 289,
|
||||
290, 5, 110, 0, 0, 290, 291, 5, 117, 0, 0, 291, 292, 5, 108, 0, 0, 292,
|
||||
293, 5, 108, 0, 0, 293, 44, 1, 0, 0, 0, 294, 298, 7, 0, 0, 0, 295, 297,
|
||||
7, 1, 0, 0, 296, 295, 1, 0, 0, 0, 297, 300, 1, 0, 0, 0, 298, 296, 1, 0,
|
||||
0, 0, 298, 299, 1, 0, 0, 0, 299, 46, 1, 0, 0, 0, 300, 298, 1, 0, 0, 0,
|
||||
301, 303, 7, 2, 0, 0, 302, 301, 1, 0, 0, 0, 303, 304, 1, 0, 0, 0, 304,
|
||||
302, 1, 0, 0, 0, 304, 305, 1, 0, 0, 0, 305, 306, 1, 0, 0, 0, 306, 307,
|
||||
6, 23, 0, 0, 307, 48, 1, 0, 0, 0, 308, 309, 5, 40, 0, 0, 309, 50, 1, 0,
|
||||
0, 0, 310, 311, 5, 41, 0, 0, 311, 52, 1, 0, 0, 0, 312, 313, 5, 91, 0, 0,
|
||||
313, 54, 1, 0, 0, 0, 314, 315, 5, 93, 0, 0, 315, 56, 1, 0, 0, 0, 316, 317,
|
||||
5, 44, 0, 0, 317, 58, 1, 0, 0, 0, 318, 319, 5, 124, 0, 0, 319, 60, 1, 0,
|
||||
0, 0, 320, 321, 5, 58, 0, 0, 321, 62, 1, 0, 0, 0, 322, 323, 3, 67, 33,
|
||||
0, 323, 64, 1, 0, 0, 0, 324, 349, 3, 63, 31, 0, 325, 327, 5, 45, 0, 0,
|
||||
326, 325, 1, 0, 0, 0, 326, 327, 1, 0, 0, 0, 327, 328, 1, 0, 0, 0, 328,
|
||||
329, 3, 67, 33, 0, 329, 331, 5, 46, 0, 0, 330, 332, 7, 3, 0, 0, 331, 330,
|
||||
1, 0, 0, 0, 332, 333, 1, 0, 0, 0, 333, 331, 1, 0, 0, 0, 333, 334, 1, 0,
|
||||
0, 0, 334, 336, 1, 0, 0, 0, 335, 337, 3, 69, 34, 0, 336, 335, 1, 0, 0,
|
||||
0, 336, 337, 1, 0, 0, 0, 337, 349, 1, 0, 0, 0, 338, 340, 5, 45, 0, 0, 339,
|
||||
338, 1, 0, 0, 0, 339, 340, 1, 0, 0, 0, 340, 341, 1, 0, 0, 0, 341, 342,
|
||||
3, 67, 33, 0, 342, 343, 3, 69, 34, 0, 343, 349, 1, 0, 0, 0, 344, 346, 5,
|
||||
45, 0, 0, 345, 344, 1, 0, 0, 0, 345, 346, 1, 0, 0, 0, 346, 347, 1, 0, 0,
|
||||
0, 347, 349, 3, 67, 33, 0, 348, 324, 1, 0, 0, 0, 348, 326, 1, 0, 0, 0,
|
||||
348, 339, 1, 0, 0, 0, 348, 345, 1, 0, 0, 0, 349, 66, 1, 0, 0, 0, 350, 359,
|
||||
5, 48, 0, 0, 351, 355, 7, 4, 0, 0, 352, 354, 7, 3, 0, 0, 353, 352, 1, 0,
|
||||
0, 0, 354, 357, 1, 0, 0, 0, 355, 353, 1, 0, 0, 0, 355, 356, 1, 0, 0, 0,
|
||||
356, 359, 1, 0, 0, 0, 357, 355, 1, 0, 0, 0, 358, 350, 1, 0, 0, 0, 358,
|
||||
351, 1, 0, 0, 0, 359, 68, 1, 0, 0, 0, 360, 362, 7, 5, 0, 0, 361, 363, 7,
|
||||
6, 0, 0, 362, 361, 1, 0, 0, 0, 362, 363, 1, 0, 0, 0, 363, 364, 1, 0, 0,
|
||||
0, 364, 365, 3, 67, 33, 0, 365, 70, 1, 0, 0, 0, 366, 367, 5, 60, 0, 0,
|
||||
367, 368, 5, 61, 0, 0, 368, 72, 1, 0, 0, 0, 369, 370, 5, 60, 0, 0, 370,
|
||||
74, 1, 0, 0, 0, 371, 372, 5, 62, 0, 0, 372, 373, 5, 61, 0, 0, 373, 76,
|
||||
1, 0, 0, 0, 374, 375, 5, 62, 0, 0, 375, 78, 1, 0, 0, 0, 376, 377, 5, 33,
|
||||
0, 0, 377, 378, 5, 61, 0, 0, 378, 80, 1, 0, 0, 0, 379, 380, 5, 61, 0, 0,
|
||||
380, 381, 5, 61, 0, 0, 381, 82, 1, 0, 0, 0, 382, 386, 5, 46, 0, 0, 383,
|
||||
387, 3, 41, 20, 0, 384, 387, 3, 45, 22, 0, 385, 387, 3, 87, 43, 0, 386,
|
||||
383, 1, 0, 0, 0, 386, 384, 1, 0, 0, 0, 386, 385, 1, 0, 0, 0, 387, 84, 1,
|
||||
0, 0, 0, 388, 389, 5, 64, 0, 0, 389, 390, 3, 45, 22, 0, 390, 86, 1, 0,
|
||||
0, 0, 391, 396, 5, 34, 0, 0, 392, 395, 3, 89, 44, 0, 393, 395, 8, 7, 0,
|
||||
0, 394, 392, 1, 0, 0, 0, 394, 393, 1, 0, 0, 0, 395, 398, 1, 0, 0, 0, 396,
|
||||
394, 1, 0, 0, 0, 396, 397, 1, 0, 0, 0, 397, 399, 1, 0, 0, 0, 398, 396,
|
||||
1, 0, 0, 0, 399, 400, 5, 34, 0, 0, 400, 88, 1, 0, 0, 0, 401, 404, 5, 92,
|
||||
0, 0, 402, 405, 7, 8, 0, 0, 403, 405, 3, 91, 45, 0, 404, 402, 1, 0, 0,
|
||||
0, 404, 403, 1, 0, 0, 0, 405, 90, 1, 0, 0, 0, 406, 407, 5, 117, 0, 0, 407,
|
||||
408, 3, 93, 46, 0, 408, 409, 3, 93, 46, 0, 409, 410, 3, 93, 46, 0, 410,
|
||||
411, 3, 93, 46, 0, 411, 92, 1, 0, 0, 0, 412, 413, 7, 9, 0, 0, 413, 94,
|
||||
1, 0, 0, 0, 414, 415, 7, 3, 0, 0, 415, 96, 1, 0, 0, 0, 416, 417, 7, 10,
|
||||
0, 0, 417, 98, 1, 0, 0, 0, 418, 419, 7, 11, 0, 0, 419, 100, 1, 0, 0, 0,
|
||||
420, 421, 7, 12, 0, 0, 421, 102, 1, 0, 0, 0, 422, 423, 7, 13, 0, 0, 423,
|
||||
104, 1, 0, 0, 0, 424, 425, 7, 5, 0, 0, 425, 106, 1, 0, 0, 0, 426, 427,
|
||||
7, 14, 0, 0, 427, 108, 1, 0, 0, 0, 428, 429, 7, 15, 0, 0, 429, 110, 1,
|
||||
0, 0, 0, 430, 431, 7, 16, 0, 0, 431, 112, 1, 0, 0, 0, 432, 433, 7, 17,
|
||||
0, 0, 433, 114, 1, 0, 0, 0, 434, 435, 7, 18, 0, 0, 435, 116, 1, 0, 0, 0,
|
||||
436, 437, 7, 19, 0, 0, 437, 118, 1, 0, 0, 0, 438, 439, 7, 20, 0, 0, 439,
|
||||
120, 1, 0, 0, 0, 440, 441, 7, 21, 0, 0, 441, 122, 1, 0, 0, 0, 442, 443,
|
||||
7, 22, 0, 0, 443, 124, 1, 0, 0, 0, 444, 445, 7, 23, 0, 0, 445, 126, 1,
|
||||
0, 0, 0, 446, 447, 7, 24, 0, 0, 447, 128, 1, 0, 0, 0, 448, 449, 7, 25,
|
||||
0, 0, 449, 130, 1, 0, 0, 0, 450, 451, 7, 26, 0, 0, 451, 132, 1, 0, 0, 0,
|
||||
452, 453, 7, 27, 0, 0, 453, 134, 1, 0, 0, 0, 454, 455, 7, 28, 0, 0, 455,
|
||||
136, 1, 0, 0, 0, 456, 457, 7, 29, 0, 0, 457, 138, 1, 0, 0, 0, 458, 459,
|
||||
7, 30, 0, 0, 459, 140, 1, 0, 0, 0, 460, 461, 7, 31, 0, 0, 461, 142, 1,
|
||||
0, 0, 0, 462, 463, 7, 32, 0, 0, 463, 144, 1, 0, 0, 0, 464, 465, 7, 33,
|
||||
0, 0, 465, 146, 1, 0, 0, 0, 466, 467, 7, 34, 0, 0, 467, 148, 1, 0, 0, 0,
|
||||
468, 472, 5, 35, 0, 0, 469, 471, 9, 0, 0, 0, 470, 469, 1, 0, 0, 0, 471,
|
||||
474, 1, 0, 0, 0, 472, 473, 1, 0, 0, 0, 472, 470, 1, 0, 0, 0, 473, 475,
|
||||
1, 0, 0, 0, 474, 472, 1, 0, 0, 0, 475, 476, 5, 10, 0, 0, 476, 477, 1, 0,
|
||||
0, 0, 477, 478, 6, 74, 0, 0, 478, 150, 1, 0, 0, 0, 19, 0, 226, 284, 298,
|
||||
304, 326, 333, 336, 339, 345, 348, 355, 358, 362, 386, 394, 396, 404, 472,
|
||||
1, 6, 0, 0,
|
||||
}
|
||||
deserializer := antlr.NewATNDeserializer(nil)
|
||||
staticData.atn = deserializer.Deserialize(staticData.serializedATN)
|
||||
@ -346,16 +344,16 @@ const (
|
||||
SLQLexerORDER_BY = 19
|
||||
SLQLexerALIAS_RESERVED = 20
|
||||
SLQLexerARG = 21
|
||||
SLQLexerID = 22
|
||||
SLQLexerWS = 23
|
||||
SLQLexerLPAR = 24
|
||||
SLQLexerRPAR = 25
|
||||
SLQLexerLBRA = 26
|
||||
SLQLexerRBRA = 27
|
||||
SLQLexerCOMMA = 28
|
||||
SLQLexerPIPE = 29
|
||||
SLQLexerCOLON = 30
|
||||
SLQLexerNULL = 31
|
||||
SLQLexerNULL = 22
|
||||
SLQLexerID = 23
|
||||
SLQLexerWS = 24
|
||||
SLQLexerLPAR = 25
|
||||
SLQLexerRPAR = 26
|
||||
SLQLexerLBRA = 27
|
||||
SLQLexerRBRA = 28
|
||||
SLQLexerCOMMA = 29
|
||||
SLQLexerPIPE = 30
|
||||
SLQLexerCOLON = 31
|
||||
SLQLexerNN = 32
|
||||
SLQLexerNUMBER = 33
|
||||
SLQLexerLT_EQ = 34
|
||||
|
@ -64,6 +64,9 @@ type SLQListener interface {
|
||||
// EnterAlias is called when entering the alias production.
|
||||
EnterAlias(c *AliasContext)
|
||||
|
||||
// EnterArg is called when entering the arg production.
|
||||
EnterArg(c *ArgContext)
|
||||
|
||||
// EnterHandleTable is called when entering the handleTable production.
|
||||
EnterHandleTable(c *HandleTableContext)
|
||||
|
||||
@ -139,6 +142,9 @@ type SLQListener interface {
|
||||
// ExitAlias is called when exiting the alias production.
|
||||
ExitAlias(c *AliasContext)
|
||||
|
||||
// ExitArg is called when exiting the arg production.
|
||||
ExitArg(c *ArgContext)
|
||||
|
||||
// ExitHandleTable is called when exiting the handleTable production.
|
||||
ExitHandleTable(c *HandleTableContext)
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -64,6 +64,9 @@ type SLQVisitor interface {
|
||||
// Visit a parse tree produced by SLQParser#alias.
|
||||
VisitAlias(ctx *AliasContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by SLQParser#arg.
|
||||
VisitArg(ctx *ArgContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by SLQParser#handleTable.
|
||||
VisitHandleTable(ctx *HandleTableContext) interface{}
|
||||
|
||||
|
57
libsq/ast/literal.go
Normal file
57
libsq/ast/literal.go
Normal file
@ -0,0 +1,57 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"github.com/neilotoole/sq/libsq/ast/internal/slq"
|
||||
)
|
||||
|
||||
// LiteralType is an enum of literal types.
|
||||
type LiteralType string
|
||||
|
||||
const (
|
||||
LiteralNull LiteralType = "null"
|
||||
LiteralNaturalNumber LiteralType = "int"
|
||||
LiteralAnyNumber LiteralType = "float"
|
||||
LiteralString LiteralType = "string"
|
||||
)
|
||||
|
||||
// LiteralNode is a leaf node representing a literal such as a number or a string.
|
||||
type LiteralNode struct {
|
||||
baseNode
|
||||
typ LiteralType
|
||||
}
|
||||
|
||||
// String returns a log/debug-friendly representation.
|
||||
func (n *LiteralNode) String() string {
|
||||
return nodeString(n)
|
||||
}
|
||||
|
||||
// LiteralType returns the literal type (number, string, etc).
|
||||
func (n *LiteralNode) LiteralType() LiteralType {
|
||||
return n.typ
|
||||
}
|
||||
|
||||
// VisitLiteral implements slq.SLQVisitor.
|
||||
func (v *parseTreeVisitor) VisitLiteral(ctx *slq.LiteralContext) any {
|
||||
node := &LiteralNode{}
|
||||
node.ctx = ctx
|
||||
node.text = ctx.GetText()
|
||||
|
||||
switch {
|
||||
case ctx.NN() != nil:
|
||||
node.typ = LiteralNaturalNumber
|
||||
case ctx.NUMBER() != nil:
|
||||
node.typ = LiteralAnyNumber
|
||||
case ctx.NULL() != nil:
|
||||
node.typ = LiteralNull
|
||||
case ctx.STRING() != nil:
|
||||
node.typ = LiteralString
|
||||
default:
|
||||
// Shouldn't happen
|
||||
return errorf("unable to determine literal type for: %s", ctx.GetText())
|
||||
}
|
||||
|
||||
if err := node.SetParent(v.cur); err != nil {
|
||||
return err
|
||||
}
|
||||
return v.cur.AddChild(node)
|
||||
}
|
@ -34,7 +34,8 @@ type Node interface {
|
||||
// String returns a debug-friendly string representation.
|
||||
String() string
|
||||
|
||||
// Text returns the node's text representation.
|
||||
// Text returns the node's text value. This is convenience
|
||||
// method for Node.Context().GetText().
|
||||
Text() string
|
||||
}
|
||||
|
||||
@ -185,6 +186,74 @@ func nodesAreOnlyOfType(nodes []Node, types ...reflect.Type) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// NodeNextSibling returns the node's next sibling, or nil.
|
||||
func NodeNextSibling(node Node) Node {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
parent := node.Parent()
|
||||
if parent == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
i := nodeChildIndex(parent, node)
|
||||
if i < 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
children := parent.Children()
|
||||
if i >= len(children)-1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return children[i+1]
|
||||
}
|
||||
|
||||
// NodePrevSibling returns the node's previous sibling, or nil.
|
||||
func NodePrevSibling(node Node) Node {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
parent := node.Parent()
|
||||
if parent == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
i := nodeChildIndex(parent, node)
|
||||
if i < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
children := parent.Children()
|
||||
return children[i-1]
|
||||
}
|
||||
|
||||
// NodesHavingText returns any node whose node.Text()
|
||||
// method returns text.
|
||||
func NodesHavingText(tree Node, text string) []Node {
|
||||
if tree == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var nodes []Node
|
||||
|
||||
w := NewWalker(tree)
|
||||
w.AddVisitor(typeNode, func(w *Walker, node Node) error {
|
||||
nodeText := node.Text()
|
||||
if nodeText == text {
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err := w.Walk(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
// nodeChildIndex returns the index of child in parent's children, or -1.
|
||||
func nodeChildIndex(parent, child Node) int {
|
||||
for i, node := range parent.Children() {
|
||||
@ -270,16 +339,6 @@ func (n *OperatorNode) String() string {
|
||||
return nodeString(n)
|
||||
}
|
||||
|
||||
// LiteralNode is a leaf node representing a literal such as a number or a string.
|
||||
type LiteralNode struct {
|
||||
baseNode
|
||||
}
|
||||
|
||||
// String returns a log/debug-friendly representation.
|
||||
func (n *LiteralNode) String() string {
|
||||
return nodeString(n)
|
||||
}
|
||||
|
||||
// WhereNode represents a SQL WHERE clause, i.e. a filter on the SELECT.
|
||||
type WhereNode struct {
|
||||
baseNode
|
||||
@ -327,17 +386,21 @@ func isOperator(text string) bool {
|
||||
// Cached results from reflect.TypeOf for node types.
|
||||
var (
|
||||
typeAST = reflect.TypeOf((*AST)(nil))
|
||||
typeSegmentNode = reflect.TypeOf((*SegmentNode)(nil))
|
||||
typeHandleNode = reflect.TypeOf((*HandleNode)(nil))
|
||||
typeSelectorNode = reflect.TypeOf((*SelectorNode)(nil))
|
||||
typeTblSelectorNode = reflect.TypeOf((*TblSelectorNode)(nil))
|
||||
typeTblColSelectorNode = reflect.TypeOf((*TblColSelectorNode)(nil))
|
||||
typeColSelectorNode = reflect.TypeOf((*ColSelectorNode)(nil))
|
||||
typeJoinNode = reflect.TypeOf((*JoinNode)(nil))
|
||||
typeRowRangeNode = reflect.TypeOf((*RowRangeNode)(nil))
|
||||
typeOrderByNode = reflect.TypeOf((*OrderByNode)(nil))
|
||||
typeGroupByNode = reflect.TypeOf((*GroupByNode)(nil))
|
||||
typeExprNode = reflect.TypeOf((*ExprNode)(nil))
|
||||
typeFuncNode = reflect.TypeOf((*FuncNode)(nil))
|
||||
typeGroupByNode = reflect.TypeOf((*GroupByNode)(nil))
|
||||
typeHandleNode = reflect.TypeOf((*HandleNode)(nil))
|
||||
typeJoinNode = reflect.TypeOf((*JoinNode)(nil))
|
||||
typeNode = reflect.TypeOf((*Node)(nil)).Elem()
|
||||
_ = reflect.TypeOf((*OperatorNode)(nil))
|
||||
typeOrderByNode = reflect.TypeOf((*OrderByNode)(nil))
|
||||
typeRowRangeNode = reflect.TypeOf((*RowRangeNode)(nil))
|
||||
typeSegmentNode = reflect.TypeOf((*SegmentNode)(nil))
|
||||
_ = reflect.TypeOf((*Selector)(nil)).Elem()
|
||||
typeSelectorNode = reflect.TypeOf((*SelectorNode)(nil))
|
||||
_ = reflect.TypeOf((*Tabler)(nil)).Elem()
|
||||
typeTblColSelectorNode = reflect.TypeOf((*TblColSelectorNode)(nil))
|
||||
typeTblSelectorNode = reflect.TypeOf((*TblSelectorNode)(nil))
|
||||
typeUniqueNode = reflect.TypeOf((*UniqueNode)(nil))
|
||||
)
|
||||
|
@ -39,3 +39,22 @@ func TestAvg(t *testing.T) {
|
||||
ast := mustParse(t, input)
|
||||
require.NotNil(t, ast)
|
||||
}
|
||||
|
||||
func TestNodePrevNextSibling(t *testing.T) {
|
||||
const in = `@sakila | .actor | .actor_id == 2`
|
||||
|
||||
log := slogt.New(t)
|
||||
|
||||
a, err := Parse(log, in)
|
||||
require.NoError(t, err)
|
||||
|
||||
equalsNode := NodesHavingText(a, "==")[0]
|
||||
|
||||
gotPrev := NodePrevSibling(equalsNode)
|
||||
require.Equal(t, ".actor_id", gotPrev.Text())
|
||||
require.Nil(t, NodePrevSibling(gotPrev))
|
||||
|
||||
gotNext := NodeNextSibling(equalsNode)
|
||||
require.Equal(t, "2", gotNext.Text())
|
||||
require.Nil(t, NodeNextSibling(gotNext))
|
||||
}
|
||||
|
@ -200,6 +200,8 @@ func (v *parseTreeVisitor) Visit(ctx antlr.ParseTree) any {
|
||||
return v.VisitUniqueFunc(ctx)
|
||||
case *slq.CountFuncContext:
|
||||
return v.VisitCountFunc(ctx)
|
||||
case *slq.ArgContext:
|
||||
return v.VisitArg(ctx)
|
||||
}
|
||||
|
||||
// should never be reached
|
||||
@ -400,17 +402,6 @@ func (v *parseTreeVisitor) VisitStmtList(_ *slq.StmtListContext) any {
|
||||
return nil // not using StmtList just yet
|
||||
}
|
||||
|
||||
// VisitLiteral implements slq.SLQVisitor.
|
||||
func (v *parseTreeVisitor) VisitLiteral(ctx *slq.LiteralContext) any {
|
||||
node := &LiteralNode{}
|
||||
node.ctx = ctx
|
||||
node.text = ctx.GetText()
|
||||
if err := node.SetParent(v.cur); err != nil {
|
||||
return err
|
||||
}
|
||||
return v.cur.AddChild(node)
|
||||
}
|
||||
|
||||
// VisitUnaryOperator implements slq.SLQVisitor.
|
||||
func (v *parseTreeVisitor) VisitUnaryOperator(_ *slq.UnaryOperatorContext) any {
|
||||
return nil
|
||||
|
@ -115,7 +115,7 @@ func TestInspector_FindWhereClauses(t *testing.T) {
|
||||
nRoot, err := buildAST(log, ptree)
|
||||
require.Nil(t, err)
|
||||
|
||||
insp := NewInspector(log, nRoot)
|
||||
insp := NewInspector(nRoot)
|
||||
whereNodes, err := insp.FindWhereClauses()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, whereNodes, 1)
|
||||
|
@ -3,8 +3,6 @@ package ast
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/neilotoole/slogt"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@ -15,18 +13,16 @@ import (
|
||||
// [:15] same as above (0 thru 15)
|
||||
// [10:] select all rows from 10 onwards
|
||||
|
||||
func TestRowRange1(t *testing.T) {
|
||||
log := slogt.New(t)
|
||||
// TODO: Move this to libsq/query_range_test.go
|
||||
|
||||
func TestRowRange1(t *testing.T) {
|
||||
ast := mustParse(t, fixtRowRange1)
|
||||
assert.Equal(t, 0, NewInspector(log, ast).CountNodes(typeRowRangeNode))
|
||||
assert.Equal(t, 0, NewInspector(ast).CountNodes(typeRowRangeNode))
|
||||
}
|
||||
|
||||
func TestRowRange2(t *testing.T) {
|
||||
log := slogt.New(t)
|
||||
|
||||
ast := mustParse(t, fixtRowRange2)
|
||||
insp := NewInspector(log, ast)
|
||||
insp := NewInspector(ast)
|
||||
assert.Equal(t, 1, insp.CountNodes(typeRowRangeNode))
|
||||
nodes := insp.FindNodes(typeRowRangeNode)
|
||||
assert.Equal(t, 1, len(nodes))
|
||||
@ -36,38 +32,32 @@ func TestRowRange2(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRowRange3(t *testing.T) {
|
||||
log := slogt.New(t)
|
||||
|
||||
ast := mustParse(t, fixtRowRange3)
|
||||
insp := NewInspector(log, ast)
|
||||
insp := NewInspector(ast)
|
||||
rr, _ := insp.FindNodes(typeRowRangeNode)[0].(*RowRangeNode)
|
||||
assert.Equal(t, 1, rr.Offset)
|
||||
assert.Equal(t, 2, rr.Limit)
|
||||
}
|
||||
|
||||
func TestRowRange4(t *testing.T) {
|
||||
log := slogt.New(t)
|
||||
|
||||
ast := mustParse(t, fixtRowRange4)
|
||||
insp := NewInspector(log, ast)
|
||||
insp := NewInspector(ast)
|
||||
rr, _ := insp.FindNodes(typeRowRangeNode)[0].(*RowRangeNode)
|
||||
assert.Equal(t, 0, rr.Offset)
|
||||
assert.Equal(t, 3, rr.Limit)
|
||||
}
|
||||
|
||||
func TestRowRange5(t *testing.T) {
|
||||
log := slogt.New(t)
|
||||
ast := mustParse(t, fixtRowRange5)
|
||||
insp := NewInspector(log, ast)
|
||||
insp := NewInspector(ast)
|
||||
rr, _ := insp.FindNodes(typeRowRangeNode)[0].(*RowRangeNode)
|
||||
assert.Equal(t, 0, rr.Offset)
|
||||
assert.Equal(t, 3, rr.Limit)
|
||||
}
|
||||
|
||||
func TestRowRange6(t *testing.T) {
|
||||
log := slogt.New(t)
|
||||
ast := mustParse(t, fixtRowRange6)
|
||||
insp := NewInspector(log, ast)
|
||||
insp := NewInspector(ast)
|
||||
rr, _ := insp.FindNodes(typeRowRangeNode)[0].(*RowRangeNode)
|
||||
assert.Equal(t, 2, rr.Offset)
|
||||
assert.Equal(t, -1, rr.Limit)
|
||||
|
10
libsq/ast/render/distinct.go
Normal file
10
libsq/ast/render/distinct.go
Normal file
@ -0,0 +1,10 @@
|
||||
package render
|
||||
|
||||
import "github.com/neilotoole/sq/libsq/ast"
|
||||
|
||||
func doDistinct(_ *Context, n *ast.UniqueNode) (string, error) {
|
||||
if n == nil {
|
||||
return "", nil
|
||||
}
|
||||
return "DISTINCT", nil
|
||||
}
|
67
libsq/ast/render/expr.go
Normal file
67
libsq/ast/render/expr.go
Normal file
@ -0,0 +1,67 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/ast"
|
||||
"github.com/neilotoole/sq/libsq/core/errz"
|
||||
"github.com/neilotoole/sq/libsq/core/stringz"
|
||||
)
|
||||
|
||||
// Expr implements FragmentBuilder.
|
||||
func doExpr(rc *Context, expr *ast.ExprNode) (string, error) {
|
||||
if expr == nil {
|
||||
return "", nil
|
||||
}
|
||||
r := rc.Renderer
|
||||
|
||||
var sb strings.Builder
|
||||
for i, child := range expr.Children() {
|
||||
if i > 0 {
|
||||
sb.WriteRune(sp)
|
||||
}
|
||||
|
||||
switch child := child.(type) {
|
||||
case *ast.TblColSelectorNode, *ast.ColSelectorNode:
|
||||
val, err := renderSelectorNode(rc.Dialect, child)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sb.WriteString(val)
|
||||
case *ast.OperatorNode:
|
||||
val, err := r.Operator(rc, child)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sb.WriteString(val)
|
||||
case *ast.ArgNode:
|
||||
if rc.Args != nil {
|
||||
val, ok := rc.Args[child.Key()]
|
||||
if ok {
|
||||
sb.WriteString(stringz.SingleQuote(val))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// It's an error if the arg is not supplied.
|
||||
return "", errz.Errorf("no --arg value found for query variable %s", child.Text())
|
||||
case *ast.ExprNode:
|
||||
val, err := r.Expr(rc, child)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sb.WriteString(val)
|
||||
case *ast.LiteralNode:
|
||||
val, err := r.Literal(rc, child)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sb.WriteString(val)
|
||||
default:
|
||||
sb.WriteString(child.Text())
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String(), nil
|
||||
}
|
16
libsq/ast/render/fromtable.go
Normal file
16
libsq/ast/render/fromtable.go
Normal file
@ -0,0 +1,16 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"github.com/neilotoole/sq/libsq/ast"
|
||||
"github.com/neilotoole/sq/libsq/core/errz"
|
||||
)
|
||||
|
||||
func doFromTable(rc *Context, tblSel *ast.TblSelectorNode) (string, error) {
|
||||
tblName, _ := tblSel.SelValue()
|
||||
if tblName == "" {
|
||||
return "", errz.Errorf("selector has empty table name: {%s}", tblSel.Text())
|
||||
}
|
||||
|
||||
clause := "FROM " + rc.Dialect.Enquote(tblSel.TblName())
|
||||
return clause, nil
|
||||
}
|
77
libsq/ast/render/function.go
Normal file
77
libsq/ast/render/function.go
Normal file
@ -0,0 +1,77 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/stringz"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/ast"
|
||||
"github.com/neilotoole/sq/libsq/core/errz"
|
||||
)
|
||||
|
||||
func doFunction(rc *Context, fn *ast.FuncNode) (string, error) {
|
||||
sb := strings.Builder{}
|
||||
fnName := strings.ToLower(fn.FuncName())
|
||||
children := fn.Children()
|
||||
|
||||
if len(children) == 0 {
|
||||
sb.WriteString(fnName)
|
||||
sb.WriteRune('(')
|
||||
|
||||
if fnName == "count" {
|
||||
// Special handling for the count function, because COUNT()
|
||||
// isn't valid, but COUNT(*) is.
|
||||
sb.WriteRune('*')
|
||||
}
|
||||
|
||||
sb.WriteRune(')')
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// Special handling for "count_unique(.col)" function. We translate
|
||||
// it to "SELECT count(DISTINCT col)".
|
||||
if fnName == "count_unique" {
|
||||
sb.WriteString("count(DISTINCT ")
|
||||
} else {
|
||||
sb.WriteString(fnName)
|
||||
sb.WriteRune('(')
|
||||
}
|
||||
|
||||
for i, child := range children {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
|
||||
switch node := child.(type) {
|
||||
case *ast.ColSelectorNode, *ast.TblColSelectorNode, *ast.TblSelectorNode:
|
||||
s, err := renderSelectorNode(rc.Dialect, node)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sb.WriteString(s)
|
||||
case *ast.OperatorNode:
|
||||
sb.WriteString(node.Text())
|
||||
case *ast.LiteralNode:
|
||||
// TODO: This is all a bit of a mess. We probably need to
|
||||
// move to using bound parameters instead of inlining
|
||||
// literal values.
|
||||
val, wasQuoted, err := unquoteLiteral(node.Text())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if wasQuoted {
|
||||
// The literal had quotes, so it's a regular string.
|
||||
sb.WriteString(stringz.SingleQuote(val))
|
||||
} else {
|
||||
sb.WriteString(val)
|
||||
}
|
||||
default:
|
||||
return "", errz.Errorf("unknown AST child node %T: %s", node, node)
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteRune(')')
|
||||
sql := sb.String()
|
||||
return sql, nil
|
||||
}
|
45
libsq/ast/render/groupby.go
Normal file
45
libsq/ast/render/groupby.go
Normal file
@ -0,0 +1,45 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/ast"
|
||||
"github.com/neilotoole/sq/libsq/core/errz"
|
||||
)
|
||||
|
||||
func doGroupBy(rc *Context, gb *ast.GroupByNode) (string, error) {
|
||||
if gb == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var (
|
||||
term string
|
||||
err error
|
||||
sb strings.Builder
|
||||
)
|
||||
|
||||
sb.WriteString("GROUP BY ")
|
||||
for i, child := range gb.Children() {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
|
||||
switch child := child.(type) {
|
||||
case *ast.FuncNode:
|
||||
if term, err = rc.Renderer.Function(rc, child); err != nil {
|
||||
return "", err
|
||||
}
|
||||
case ast.Selector:
|
||||
if term, err = renderSelectorNode(rc.Dialect, child); err != nil {
|
||||
return "", err
|
||||
}
|
||||
default:
|
||||
// Should never happen
|
||||
return "", errz.Errorf("invalid child type: %T: %s", child, child)
|
||||
}
|
||||
|
||||
sb.WriteString(term)
|
||||
}
|
||||
|
||||
return sb.String(), nil
|
||||
}
|
1
libsq/ast/render/internal_test.go
Normal file
1
libsq/ast/render/internal_test.go
Normal file
@ -0,0 +1 @@
|
||||
package render
|
86
libsq/ast/render/join.go
Normal file
86
libsq/ast/render/join.go
Normal file
@ -0,0 +1,86 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/ast"
|
||||
"github.com/neilotoole/sq/libsq/core/errz"
|
||||
)
|
||||
|
||||
func doJoin(rc *Context, fnJoin *ast.JoinNode) (string, error) {
|
||||
enquote := rc.Dialect.Enquote
|
||||
|
||||
joinType := "INNER JOIN"
|
||||
onClause := ""
|
||||
|
||||
if len(fnJoin.Children()) == 0 {
|
||||
joinType = "NATURAL JOIN"
|
||||
} else {
|
||||
joinExpr, ok := fnJoin.Children()[0].(*ast.JoinConstraint)
|
||||
if !ok {
|
||||
return "", errz.Errorf("expected *FnJoinExpr but got %T", fnJoin.Children()[0])
|
||||
}
|
||||
|
||||
leftOperand := ""
|
||||
operator := ""
|
||||
rightOperand := ""
|
||||
|
||||
if len(joinExpr.Children()) == 1 {
|
||||
// It's a single col selector
|
||||
colSel, ok := joinExpr.Children()[0].(*ast.ColSelectorNode)
|
||||
if !ok {
|
||||
return "", errz.Errorf("expected *ColSelectorNode but got %T", joinExpr.Children()[0])
|
||||
}
|
||||
|
||||
colVal, err := colSel.SelValue()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
leftTblVal := fnJoin.LeftTbl().TblName()
|
||||
leftOperand = fmt.Sprintf(
|
||||
"%s.%s",
|
||||
enquote(leftTblVal),
|
||||
enquote(colVal),
|
||||
)
|
||||
|
||||
operator = "=="
|
||||
|
||||
rightTblVal := fnJoin.RightTbl().TblName()
|
||||
rightOperand = fmt.Sprintf(
|
||||
"%s.%s",
|
||||
enquote(rightTblVal),
|
||||
enquote(colVal),
|
||||
)
|
||||
} else {
|
||||
var err error
|
||||
leftOperand, err = renderSelectorNode(rc.Dialect, joinExpr.Children()[0])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
operator = joinExpr.Children()[1].Text()
|
||||
|
||||
rightOperand, err = renderSelectorNode(rc.Dialect, joinExpr.Children()[2])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if operator == "==" {
|
||||
operator = "="
|
||||
}
|
||||
|
||||
onClause = fmt.Sprintf("ON %s %s %s", leftOperand, operator, rightOperand)
|
||||
}
|
||||
|
||||
sql := fmt.Sprintf(
|
||||
"FROM %s %s %s",
|
||||
enquote(fnJoin.LeftTbl().TblName()),
|
||||
joinType,
|
||||
enquote(fnJoin.RightTbl().TblName()),
|
||||
)
|
||||
sql = sqlAppend(sql, onClause)
|
||||
|
||||
return sql, nil
|
||||
}
|
25
libsq/ast/render/literal.go
Normal file
25
libsq/ast/render/literal.go
Normal file
@ -0,0 +1,25 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"github.com/neilotoole/sq/libsq/ast"
|
||||
"github.com/neilotoole/sq/libsq/core/stringz"
|
||||
)
|
||||
|
||||
// Literal implement FragmentBuilder.
|
||||
func doLiteral(_ *Context, lit *ast.LiteralNode) (string, error) {
|
||||
switch lit.LiteralType() {
|
||||
case ast.LiteralNull:
|
||||
return "NULL", nil
|
||||
case ast.LiteralNaturalNumber, ast.LiteralAnyNumber:
|
||||
return lit.Text(), nil
|
||||
case ast.LiteralString:
|
||||
text, _, err := unquoteLiteral(lit.Text())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return stringz.SingleQuote(text), nil
|
||||
default:
|
||||
// Should never happen.
|
||||
panic("unknown literal type: " + string(lit.LiteralType()))
|
||||
}
|
||||
}
|
31
libsq/ast/render/operator.go
Normal file
31
libsq/ast/render/operator.go
Normal file
@ -0,0 +1,31 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"github.com/neilotoole/sq/libsq/ast"
|
||||
"github.com/neilotoole/sq/libsq/core/errz"
|
||||
)
|
||||
|
||||
func doOperator(rc *Context, op *ast.OperatorNode) (string, error) {
|
||||
if op == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
val, ok := rc.Dialect.Ops[op.Text()]
|
||||
if !ok {
|
||||
return "", errz.Errorf("invalid operator: %s", op.Text())
|
||||
}
|
||||
|
||||
rhs := ast.NodeNextSibling(op)
|
||||
if lit, ok := rhs.(*ast.LiteralNode); ok && lit.Text() == "null" {
|
||||
switch op.Text() {
|
||||
case "==":
|
||||
val = "IS"
|
||||
case "!=":
|
||||
val = "IS NOT"
|
||||
default:
|
||||
return "", errz.Errorf("invalid operator for null")
|
||||
}
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
40
libsq/ast/render/orderby.go
Normal file
40
libsq/ast/render/orderby.go
Normal file
@ -0,0 +1,40 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"github.com/neilotoole/sq/libsq/ast"
|
||||
"github.com/neilotoole/sq/libsq/core/errz"
|
||||
)
|
||||
|
||||
func doOrderBy(rc *Context, ob *ast.OrderByNode) (string, error) {
|
||||
if ob == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
terms := ob.Terms()
|
||||
if len(terms) == 0 {
|
||||
return "", errz.Errorf("%T has no ordering terms: %s", ob, ob)
|
||||
}
|
||||
|
||||
clause := "ORDER BY "
|
||||
for i := 0; i < len(terms); i++ {
|
||||
if i > 0 {
|
||||
clause += ", "
|
||||
}
|
||||
|
||||
sel, err := renderSelectorNode(rc.Dialect, terms[i].Selector())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
clause += sel
|
||||
switch terms[i].Direction() { //nolint:exhaustive
|
||||
case ast.OrderByDirectionAsc:
|
||||
clause += " ASC"
|
||||
case ast.OrderByDirectionDesc:
|
||||
clause += " DESC"
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return clause, nil
|
||||
}
|
37
libsq/ast/render/range.go
Normal file
37
libsq/ast/render/range.go
Normal file
@ -0,0 +1,37 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/ast"
|
||||
)
|
||||
|
||||
func doRange(_ *Context, rr *ast.RowRangeNode) (string, error) {
|
||||
if rr == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if rr.Limit < 0 && rr.Offset < 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
limit := ""
|
||||
offset := ""
|
||||
if rr.Limit > -1 {
|
||||
limit = fmt.Sprintf(" LIMIT %d", rr.Limit)
|
||||
}
|
||||
if rr.Offset > -1 {
|
||||
offset = fmt.Sprintf(" OFFSET %d", rr.Offset)
|
||||
|
||||
if rr.Limit == -1 {
|
||||
// MySQL requires a LIMIT if OFFSET is used. Therefore
|
||||
// we make the LIMIT a very large number
|
||||
limit = fmt.Sprintf(" LIMIT %d", math.MaxInt64)
|
||||
}
|
||||
}
|
||||
|
||||
sql := limit + offset
|
||||
|
||||
return sql, nil
|
||||
}
|
210
libsq/ast/render/render.go
Normal file
210
libsq/ast/render/render.go
Normal file
@ -0,0 +1,210 @@
|
||||
// Package render provides the mechanism for rendering ast into SQL.
|
||||
package render
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/driver/dialect"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/errz"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/ast"
|
||||
)
|
||||
|
||||
// Context contains context for rendering a query.
|
||||
type Context struct {
|
||||
// Renderer holds the rendering functions.
|
||||
Renderer *Renderer
|
||||
|
||||
// Dialect is the driver dialect.
|
||||
Dialect dialect.Dialect
|
||||
|
||||
// The args map contains predefined variables that are
|
||||
// substituted into the query. It may be empty or nil.
|
||||
Args map[string]string
|
||||
}
|
||||
|
||||
// Renderer is a set of functions for rendering ast elements into SQL.
|
||||
// Use NewDefaultRenderer to get a new instance. Each function can be
|
||||
// swapped with a custom implementation for a SQL dialect.
|
||||
type Renderer struct {
|
||||
// FromTable renders a FROM table fragment.
|
||||
FromTable func(rc *Context, tblSel *ast.TblSelectorNode) (string, error)
|
||||
|
||||
// SelectCols renders a column names/expression fragment.
|
||||
// It shouldn't render the actual SELECT keyword. Example return value:
|
||||
//
|
||||
// "first_name" AS "given_name", "last name" AS "family_name"
|
||||
SelectCols func(rc *Context, cols []ast.ResultColumn) (string, error)
|
||||
|
||||
// Range renders a row range fragment.
|
||||
Range func(rc *Context, rr *ast.RowRangeNode) (string, error)
|
||||
|
||||
// OrderBy renders the ORDER BY fragment.
|
||||
OrderBy func(rc *Context, ob *ast.OrderByNode) (string, error)
|
||||
|
||||
// GroupBy renders the GROUP BY fragment.
|
||||
GroupBy func(rc *Context, gb *ast.GroupByNode) (string, error)
|
||||
|
||||
// Join renders a join fragment.
|
||||
Join func(rc *Context, fnJoin *ast.JoinNode) (string, error)
|
||||
|
||||
// Function renders a function fragment.
|
||||
Function func(rc *Context, fn *ast.FuncNode) (string, error)
|
||||
|
||||
// Literal renders a literal fragment.
|
||||
Literal func(rc *Context, lit *ast.LiteralNode) (string, error)
|
||||
|
||||
// Where renders a WHERE fragment.
|
||||
Where func(rc *Context, where *ast.WhereNode) (string, error)
|
||||
|
||||
// Expr renders an expression fragment.
|
||||
Expr func(rc *Context, expr *ast.ExprNode) (string, error)
|
||||
|
||||
// Operator renders an operator fragment.
|
||||
Operator func(rc *Context, op *ast.OperatorNode) (string, error)
|
||||
|
||||
// Distinct renders the DISTINCT fragment. Returns an
|
||||
// empty string if n is nil.
|
||||
Distinct func(rc *Context, n *ast.UniqueNode) (string, error)
|
||||
|
||||
// PreRender is a hook that is called before Render. It is a final
|
||||
// opportunity to customize f before rendering. It is nil by default.
|
||||
PreRender func(rc *Context, f *Fragments) error
|
||||
|
||||
// Render renders f into a SQL query.
|
||||
Render func(rc *Context, f *Fragments) (string, error)
|
||||
}
|
||||
|
||||
// NewDefaultRenderer returns a Renderer that works for most SQL dialects.
|
||||
// Driver implementations can override specific rendering functions
|
||||
// as needed.
|
||||
func NewDefaultRenderer() *Renderer {
|
||||
return &Renderer{
|
||||
FromTable: doFromTable,
|
||||
SelectCols: doSelectCols,
|
||||
Range: doRange,
|
||||
OrderBy: doOrderBy,
|
||||
GroupBy: doGroupBy,
|
||||
Join: doJoin,
|
||||
Function: doFunction,
|
||||
Literal: doLiteral,
|
||||
Where: doWhere,
|
||||
Expr: doExpr,
|
||||
Operator: doOperator,
|
||||
Distinct: doDistinct,
|
||||
Render: doRender,
|
||||
}
|
||||
}
|
||||
|
||||
// Fragments holds the fragments of a SQL query.
|
||||
// It is passed to Renderer.PreRender and Renderer.Render.
|
||||
type Fragments struct {
|
||||
Distinct string
|
||||
Columns string
|
||||
From string
|
||||
Where string
|
||||
GroupBy string
|
||||
OrderBy string
|
||||
Range string
|
||||
}
|
||||
|
||||
// Render implements QueryBuilder.
|
||||
func doRender(_ *Context, f *Fragments) (string, error) {
|
||||
sb := strings.Builder{}
|
||||
|
||||
sb.WriteString("SELECT")
|
||||
|
||||
if f.Distinct != "" {
|
||||
sb.WriteRune(sp)
|
||||
sb.WriteString(f.Distinct)
|
||||
}
|
||||
|
||||
sb.WriteRune(sp)
|
||||
sb.WriteString(f.Columns)
|
||||
sb.WriteRune(sp)
|
||||
sb.WriteString(f.From)
|
||||
|
||||
if f.Where != "" {
|
||||
sb.WriteRune(sp)
|
||||
sb.WriteString(f.Where)
|
||||
}
|
||||
|
||||
if f.OrderBy != "" {
|
||||
sb.WriteRune(sp)
|
||||
sb.WriteString(f.OrderBy)
|
||||
}
|
||||
|
||||
if f.GroupBy != "" {
|
||||
sb.WriteRune(sp)
|
||||
sb.WriteString(f.GroupBy)
|
||||
}
|
||||
|
||||
if f.Range != "" {
|
||||
sb.WriteRune(sp)
|
||||
sb.WriteString(f.Range)
|
||||
}
|
||||
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
const (
|
||||
singleQuote = '\''
|
||||
sp = ' '
|
||||
)
|
||||
|
||||
// renderSelectorNode renders a selector such as ".actor.first_name"
|
||||
// or ".last_name".
|
||||
func renderSelectorNode(d dialect.Dialect, node ast.Node) (string, error) {
|
||||
// FIXME: switch to using enquote
|
||||
switch node := node.(type) {
|
||||
case *ast.ColSelectorNode:
|
||||
return d.Enquote(node.ColName()), nil
|
||||
case *ast.TblColSelectorNode:
|
||||
return d.Enquote(node.TblName()) + "." + d.Enquote(node.ColName()), nil
|
||||
case *ast.TblSelectorNode:
|
||||
return d.Enquote(node.TblName()), nil
|
||||
default:
|
||||
return "", errz.Errorf(
|
||||
"expected selector node type, but got %T: %s",
|
||||
node,
|
||||
node.Text(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// sqlAppend is a convenience function for building the SQL string.
|
||||
// The main purpose is to ensure that there's always a consistent amount
|
||||
// of whitespace. Thus, if existing has a space suffix and add has a
|
||||
// space prefix, the returned string will only have one space. If add
|
||||
// is the empty string or just whitespace, this function simply
|
||||
// returns existing.
|
||||
func sqlAppend(existing, add string) string {
|
||||
add = strings.TrimSpace(add)
|
||||
if add == "" {
|
||||
return existing
|
||||
}
|
||||
|
||||
existing = strings.TrimSpace(existing)
|
||||
return existing + " " + add
|
||||
}
|
||||
|
||||
// unquoteLiteral returns true if s is a double-quoted string, and also returns
|
||||
// the value with the quotes stripped. An error is returned if the string
|
||||
// is malformed.
|
||||
func unquoteLiteral(s string) (val string, ok bool, err error) {
|
||||
hasPrefix := strings.HasPrefix(s, `"`)
|
||||
hasSuffix := strings.HasSuffix(s, `"`)
|
||||
|
||||
if hasPrefix && hasSuffix {
|
||||
val = strings.TrimPrefix(s, `"`)
|
||||
val = strings.TrimSuffix(val, `"`)
|
||||
return val, true, nil
|
||||
}
|
||||
|
||||
if hasPrefix != hasSuffix {
|
||||
return "", false, errz.Errorf("malformed literal: %s", s)
|
||||
}
|
||||
|
||||
return s, false, nil
|
||||
}
|
50
libsq/ast/render/selectcols.go
Normal file
50
libsq/ast/render/selectcols.go
Normal file
@ -0,0 +1,50 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/ast"
|
||||
)
|
||||
|
||||
func doSelectCols(rc *Context, cols []ast.ResultColumn) (string, error) {
|
||||
var err error
|
||||
|
||||
if len(cols) == 0 {
|
||||
return "*", nil
|
||||
}
|
||||
|
||||
vals := make([]string, len(cols))
|
||||
for i, col := range cols {
|
||||
// aliasFrag holds the "AS alias" fragment (if applicable).
|
||||
// For example: "@sakila | .actor | .first_name:given_name" becomes
|
||||
// "SELECT first_name AS given_name FROM actor".
|
||||
var aliasFrag string
|
||||
if col.Alias() != "" {
|
||||
aliasFrag = " AS " + rc.Dialect.Enquote(col.Alias())
|
||||
}
|
||||
|
||||
switch col := col.(type) {
|
||||
case *ast.ColSelectorNode:
|
||||
if vals[i], err = renderSelectorNode(rc.Dialect, col); err != nil {
|
||||
return "", err
|
||||
}
|
||||
case *ast.TblColSelectorNode:
|
||||
if vals[i], err = renderSelectorNode(rc.Dialect, col); err != nil {
|
||||
return "", err
|
||||
}
|
||||
case *ast.FuncNode:
|
||||
if vals[i], err = rc.Renderer.Function(rc, col); err != nil {
|
||||
return "", err
|
||||
}
|
||||
default:
|
||||
// FIXME: We should be exhaustively checking the cases.
|
||||
// Here, it's probably an ExprNode?
|
||||
vals[i] = col.Text() // for now, we just return the raw text
|
||||
}
|
||||
|
||||
vals[i] += aliasFrag
|
||||
}
|
||||
|
||||
text := strings.Join(vals, ", ")
|
||||
return text, nil
|
||||
}
|
16
libsq/ast/render/where.go
Normal file
16
libsq/ast/render/where.go
Normal file
@ -0,0 +1,16 @@
|
||||
package render
|
||||
|
||||
import "github.com/neilotoole/sq/libsq/ast"
|
||||
|
||||
func doWhere(rc *Context, where *ast.WhereNode) (string, error) {
|
||||
if where == nil {
|
||||
return "", nil
|
||||
}
|
||||
sql, err := rc.Renderer.Expr(rc, where.Expr())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sql = "WHERE " + sql
|
||||
return sql, nil
|
||||
}
|
@ -41,7 +41,7 @@ func TestColumnAlias(t *testing.T) {
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
insp := NewInspector(log, ast)
|
||||
insp := NewInspector(ast)
|
||||
nodes := insp.FindNodes(typeColSelectorNode)
|
||||
require.Equal(t, 1, len(nodes))
|
||||
colSel, ok := nodes[0].(*ColSelectorNode)
|
||||
|
@ -1,637 +0,0 @@
|
||||
package sqlbuilder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/ast"
|
||||
"github.com/neilotoole/sq/libsq/core/errz"
|
||||
)
|
||||
|
||||
const (
|
||||
singleQuote = '\''
|
||||
sp = ' '
|
||||
)
|
||||
|
||||
// baseOps is a map of SLQ operator (e.g. "==" or "!=") to its default SQL rendering.
|
||||
var baseOps = map[string]string{
|
||||
`==`: `=`,
|
||||
}
|
||||
|
||||
// BaseOps returns a default map of SLQ operator (e.g. "==" or "!=") to its default SQL rendering.
|
||||
// The returned map is a copy and can be safely modified by the caller.
|
||||
func BaseOps() map[string]string {
|
||||
ops := make(map[string]string, len(baseOps))
|
||||
for k, v := range baseOps {
|
||||
ops[k] = v
|
||||
}
|
||||
return ops
|
||||
}
|
||||
|
||||
var _ FragmentBuilder = (*BaseFragmentBuilder)(nil)
|
||||
|
||||
// BaseFragmentBuilder is a default implementation of sqlbuilder.FragmentBuilder.
|
||||
type BaseFragmentBuilder struct {
|
||||
Log *slog.Logger
|
||||
// Quote is the driver-specific quote rune, e.g. " or `
|
||||
Quote string
|
||||
|
||||
// QuoteFn quotes an identifier.
|
||||
QuoteFn func(string) string
|
||||
Ops map[string]string
|
||||
}
|
||||
|
||||
// Distinct implements FragmentBuilder.
|
||||
func (fb *BaseFragmentBuilder) Distinct(n *ast.UniqueNode) (string, error) {
|
||||
if n == nil {
|
||||
return "", nil
|
||||
}
|
||||
return "DISTINCT", nil
|
||||
}
|
||||
|
||||
// GroupBy implements FragmentBuilder.
|
||||
func (fb *BaseFragmentBuilder) GroupBy(gb *ast.GroupByNode) (string, error) {
|
||||
if gb == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var (
|
||||
term string
|
||||
err error
|
||||
sb strings.Builder
|
||||
)
|
||||
|
||||
sb.WriteString("GROUP BY ")
|
||||
for i, child := range gb.Children() {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
|
||||
switch child := child.(type) {
|
||||
case *ast.FuncNode:
|
||||
if term, err = fb.Function(child); err != nil {
|
||||
return "", err
|
||||
}
|
||||
case ast.Selector:
|
||||
if term, err = renderSelectorNode(fb.Quote, child); err != nil {
|
||||
return "", err
|
||||
}
|
||||
default:
|
||||
// Should never happen
|
||||
return "", errz.Errorf("invalid child type: %T: %s", child, child)
|
||||
}
|
||||
|
||||
sb.WriteString(term)
|
||||
}
|
||||
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// OrderBy implements FragmentBuilder.
|
||||
func (fb *BaseFragmentBuilder) OrderBy(ob *ast.OrderByNode) (string, error) {
|
||||
if ob == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
terms := ob.Terms()
|
||||
if len(terms) == 0 {
|
||||
return "", errz.Errorf("%T has no ordering terms: %s", ob, ob)
|
||||
}
|
||||
|
||||
clause := "ORDER BY "
|
||||
for i := 0; i < len(terms); i++ {
|
||||
if i > 0 {
|
||||
clause += ", "
|
||||
}
|
||||
|
||||
sel, err := renderSelectorNode(fb.Quote, terms[i].Selector())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
clause += sel
|
||||
switch terms[i].Direction() { //nolint:exhaustive
|
||||
case ast.OrderByDirectionAsc:
|
||||
clause += " ASC"
|
||||
case ast.OrderByDirectionDesc:
|
||||
clause += " DESC"
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return clause, nil
|
||||
}
|
||||
|
||||
// Operator implements FragmentBuilder.
|
||||
func (fb *BaseFragmentBuilder) Operator(op *ast.OperatorNode) (string, error) {
|
||||
if op == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if val, ok := fb.Ops[op.Text()]; ok {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
return op.Text(), nil
|
||||
}
|
||||
|
||||
// Where implements FragmentBuilder.
|
||||
func (fb *BaseFragmentBuilder) Where(where *ast.WhereNode) (string, error) {
|
||||
if where == nil {
|
||||
return "", nil
|
||||
}
|
||||
sql, err := fb.Expr(where.Expr())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sql = "WHERE " + sql
|
||||
return sql, nil
|
||||
}
|
||||
|
||||
// Expr implements FragmentBuilder.
|
||||
func (fb *BaseFragmentBuilder) Expr(expr *ast.ExprNode) (string, error) {
|
||||
if expr == nil {
|
||||
return "", nil
|
||||
}
|
||||
var sb strings.Builder
|
||||
|
||||
for _, child := range expr.Children() {
|
||||
switch child := child.(type) {
|
||||
case *ast.TblColSelectorNode, *ast.ColSelectorNode:
|
||||
val, err := renderSelectorNode(fb.Quote, child)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sb.WriteString(val)
|
||||
case *ast.OperatorNode:
|
||||
val, err := fb.Operator(child)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sb.WriteRune(sp)
|
||||
sb.WriteString(val)
|
||||
case *ast.ExprNode:
|
||||
val, err := fb.Expr(child)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sb.WriteRune(sp)
|
||||
sb.WriteString(val)
|
||||
default:
|
||||
sb.WriteRune(sp)
|
||||
sb.WriteString(child.Text())
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// Function implements FragmentBuilder.
|
||||
func (fb *BaseFragmentBuilder) Function(fn *ast.FuncNode) (string, error) {
|
||||
sb := strings.Builder{}
|
||||
fnName := strings.ToLower(fn.FuncName())
|
||||
children := fn.Children()
|
||||
|
||||
if len(children) == 0 {
|
||||
sb.WriteString(fnName)
|
||||
sb.WriteRune('(')
|
||||
|
||||
if fnName == "count" {
|
||||
// Special handling for the count function, because COUNT()
|
||||
// isn't valid, but COUNT(*) is.
|
||||
sb.WriteRune('*')
|
||||
}
|
||||
|
||||
sb.WriteRune(')')
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// Special handling for "count_unique(.col)" function. We translate
|
||||
// it to "SELECT count(DISTINCT col)".
|
||||
if fnName == "count_unique" {
|
||||
sb.WriteString("count(DISTINCT ")
|
||||
} else {
|
||||
sb.WriteString(fnName)
|
||||
sb.WriteRune('(')
|
||||
}
|
||||
|
||||
for i, child := range children {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
|
||||
switch node := child.(type) {
|
||||
case *ast.ColSelectorNode, *ast.TblColSelectorNode, *ast.TblSelectorNode:
|
||||
s, err := renderSelectorNode(fb.Quote, node)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sb.WriteString(s)
|
||||
case *ast.OperatorNode:
|
||||
sb.WriteString(node.Text())
|
||||
case *ast.LiteralNode:
|
||||
// TODO: This is all a bit of a mess. We probably need to
|
||||
// move to using bound parameters instead of inlining
|
||||
// literal values.
|
||||
val, wasQuoted, err := unquoteLiteral(node.Text())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if wasQuoted {
|
||||
// The literal had quotes, so it's a regular string.
|
||||
// FIXME: replace with stringz.SingleQuote
|
||||
sb.WriteRune(singleQuote)
|
||||
sb.WriteString(escapeLiteral(val))
|
||||
sb.WriteRune(singleQuote)
|
||||
} else {
|
||||
sb.WriteString(val)
|
||||
}
|
||||
default:
|
||||
return "", errz.Errorf("unknown AST child node %T: %s", node, node)
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteRune(')')
|
||||
sql := sb.String()
|
||||
return sql, nil
|
||||
}
|
||||
|
||||
// FromTable implements FragmentBuilder.
|
||||
func (fb *BaseFragmentBuilder) FromTable(tblSel *ast.TblSelectorNode) (string, error) {
|
||||
tblName, _ := tblSel.SelValue()
|
||||
if tblName == "" {
|
||||
return "", errz.Errorf("selector has empty table name: {%s}", tblSel.Text())
|
||||
}
|
||||
|
||||
clause := fmt.Sprintf("FROM %v%s%v", fb.Quote, tblSel.TblName(), fb.Quote)
|
||||
return clause, nil
|
||||
}
|
||||
|
||||
// Join implements FragmentBuilder.
|
||||
func (fb *BaseFragmentBuilder) Join(fnJoin *ast.JoinNode) (string, error) {
|
||||
joinType := "INNER JOIN"
|
||||
onClause := ""
|
||||
|
||||
if len(fnJoin.Children()) == 0 {
|
||||
joinType = "NATURAL JOIN"
|
||||
} else {
|
||||
joinExpr, ok := fnJoin.Children()[0].(*ast.JoinConstraint)
|
||||
if !ok {
|
||||
return "", errz.Errorf("expected *FnJoinExpr but got %T", fnJoin.Children()[0])
|
||||
}
|
||||
|
||||
leftOperand := ""
|
||||
operator := ""
|
||||
rightOperand := ""
|
||||
|
||||
if len(joinExpr.Children()) == 1 {
|
||||
// It's a single col selector
|
||||
colSel, ok := joinExpr.Children()[0].(*ast.ColSelectorNode)
|
||||
if !ok {
|
||||
return "", errz.Errorf("expected *ColSelectorNode but got %T", joinExpr.Children()[0])
|
||||
}
|
||||
|
||||
colVal, err := colSel.SelValue()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
leftTblVal := fnJoin.LeftTbl().TblName()
|
||||
leftOperand = fmt.Sprintf(
|
||||
"%s%s%s.%s%s%s",
|
||||
fb.Quote,
|
||||
leftTblVal,
|
||||
fb.Quote,
|
||||
fb.Quote,
|
||||
colVal,
|
||||
fb.Quote,
|
||||
)
|
||||
|
||||
operator = "=="
|
||||
|
||||
rightTblVal := fnJoin.RightTbl().TblName()
|
||||
rightOperand = fmt.Sprintf(
|
||||
"%s%s%s.%s%s%s",
|
||||
fb.Quote,
|
||||
rightTblVal,
|
||||
fb.Quote,
|
||||
fb.Quote,
|
||||
colVal,
|
||||
fb.Quote,
|
||||
)
|
||||
} else {
|
||||
var err error
|
||||
leftOperand, err = renderSelectorNode(fb.Quote, joinExpr.Children()[0])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
operator = joinExpr.Children()[1].Text()
|
||||
|
||||
rightOperand, err = renderSelectorNode(fb.Quote, joinExpr.Children()[2])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if operator == "==" {
|
||||
operator = "="
|
||||
}
|
||||
|
||||
onClause = fmt.Sprintf("ON %s %s %s", leftOperand, operator, rightOperand)
|
||||
}
|
||||
|
||||
sql := fmt.Sprintf(
|
||||
"FROM %s%s%s %s %s%s%s",
|
||||
fb.Quote,
|
||||
fnJoin.LeftTbl().TblName(),
|
||||
fb.Quote,
|
||||
joinType,
|
||||
fb.Quote,
|
||||
fnJoin.RightTbl().TblName(),
|
||||
fb.Quote,
|
||||
)
|
||||
sql = sqlAppend(sql, onClause)
|
||||
|
||||
return sql, nil
|
||||
}
|
||||
|
||||
// renderSelectorNode renders a selector such as ".actor.first_name"
|
||||
// or ".last_name".
|
||||
func renderSelectorNode(quote string, node ast.Node) (string, error) {
|
||||
switch node := node.(type) {
|
||||
case *ast.ColSelectorNode:
|
||||
return fmt.Sprintf(
|
||||
"%s%s%s",
|
||||
quote,
|
||||
node.ColName(),
|
||||
quote,
|
||||
), nil
|
||||
case *ast.TblColSelectorNode:
|
||||
return fmt.Sprintf(
|
||||
"%s%s%s.%s%s%s",
|
||||
quote,
|
||||
node.TblName(),
|
||||
quote,
|
||||
quote,
|
||||
node.ColName(),
|
||||
quote,
|
||||
), nil
|
||||
case *ast.TblSelectorNode:
|
||||
return fmt.Sprintf(
|
||||
"%s%s%s",
|
||||
quote,
|
||||
node.TblName(),
|
||||
quote,
|
||||
), nil
|
||||
|
||||
default:
|
||||
return "", errz.Errorf(
|
||||
"expected selector node type, but got %T: %s",
|
||||
node,
|
||||
node.Text(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// sqlAppend is a convenience function for building the SQL string.
|
||||
// The main purpose is to ensure that there's always a consistent amount
|
||||
// of whitespace. Thus, if existing has a space suffix and add has a
|
||||
// space prefix, the returned string will only have one space. If add
|
||||
// is the empty string or just whitespace, this function simply
|
||||
// returns existing.
|
||||
func sqlAppend(existing, add string) string {
|
||||
add = strings.TrimSpace(add)
|
||||
if add == "" {
|
||||
return existing
|
||||
}
|
||||
|
||||
existing = strings.TrimSpace(existing)
|
||||
return existing + " " + add
|
||||
}
|
||||
|
||||
// quoteTableOrColSelector returns a quote table, col, or table/col
|
||||
// selector for use in a SQL statement. For example:
|
||||
//
|
||||
// .table --> "table"
|
||||
// .col --> "col"
|
||||
// .table.col --> "table"."col"
|
||||
//
|
||||
// Thus, the selector must have exactly one or two periods.
|
||||
func quoteTableOrColSelector(quote, selector string) (string, error) {
|
||||
if len(selector) < 2 || selector[0] != '.' {
|
||||
return "", errz.Errorf("invalid selector: %s", selector)
|
||||
}
|
||||
|
||||
parts := strings.Split(selector[1:], ".")
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
return quote + parts[0] + quote, nil
|
||||
case 2:
|
||||
return quote + parts[0] + quote + "." + quote + parts[1] + quote, nil
|
||||
default:
|
||||
return "", errz.Errorf("invalid selector: %s", selector)
|
||||
}
|
||||
}
|
||||
|
||||
// Range implements FragmentBuilder.
|
||||
func (fb *BaseFragmentBuilder) Range(rr *ast.RowRangeNode) (string, error) {
|
||||
if rr == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if rr.Limit < 0 && rr.Offset < 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
limit := ""
|
||||
offset := ""
|
||||
if rr.Limit > -1 {
|
||||
limit = fmt.Sprintf(" LIMIT %d", rr.Limit)
|
||||
}
|
||||
if rr.Offset > -1 {
|
||||
offset = fmt.Sprintf(" OFFSET %d", rr.Offset)
|
||||
|
||||
if rr.Limit == -1 {
|
||||
// MySQL requires a LIMIT if OFFSET is used. Therefore
|
||||
// we make the LIMIT a very large number
|
||||
limit = fmt.Sprintf(" LIMIT %d", math.MaxInt64)
|
||||
}
|
||||
}
|
||||
|
||||
sql := limit + offset
|
||||
|
||||
return sql, nil
|
||||
}
|
||||
|
||||
// SelectCols implements FragmentBuilder.
|
||||
func (fb *BaseFragmentBuilder) SelectCols(cols []ast.ResultColumn) (string, error) {
|
||||
if len(cols) == 0 {
|
||||
return "*", nil
|
||||
}
|
||||
|
||||
vals := make([]string, len(cols))
|
||||
for i, col := range cols {
|
||||
// aliasFrag holds the "AS alias" fragment (if applicable).
|
||||
// For example: "@sakila | .actor | .first_name:given_name" becomes
|
||||
// "SELECT first_name AS given_name FROM actor".
|
||||
var aliasFrag string
|
||||
if col.Alias() != "" {
|
||||
aliasFrag = " AS " + fb.QuoteFn(col.Alias())
|
||||
}
|
||||
|
||||
switch col := col.(type) {
|
||||
case *ast.ColSelectorNode:
|
||||
vals[i] = fmt.Sprintf("%s%s%s", fb.Quote, col.ColName(), fb.Quote)
|
||||
case *ast.TblColSelectorNode:
|
||||
vals[i] = fmt.Sprintf("%s%s%s.%s%s%s", fb.Quote, col.TblName(), fb.Quote, fb.Quote, col.ColName(), fb.Quote)
|
||||
case *ast.FuncNode:
|
||||
// it's a function
|
||||
var err error
|
||||
if vals[i], err = fb.Function(col); err != nil {
|
||||
return "", err
|
||||
}
|
||||
default:
|
||||
// FIXME: We should be exhaustively checking the cases.
|
||||
// Here, it's probably an ExprNode?
|
||||
vals[i] = col.Text() // for now, we just return the raw text
|
||||
}
|
||||
|
||||
vals[i] += aliasFrag
|
||||
}
|
||||
|
||||
text := strings.Join(vals, ", ")
|
||||
return text, nil
|
||||
}
|
||||
|
||||
var _ QueryBuilder = (*BaseQueryBuilder)(nil)
|
||||
|
||||
// BaseQueryBuilder is a default implementation
|
||||
// of sqlbuilder.QueryBuilder.
|
||||
type BaseQueryBuilder struct {
|
||||
Distinct string
|
||||
Columns string
|
||||
From string
|
||||
Where string
|
||||
GroupBy string
|
||||
OrderBy string
|
||||
Range string
|
||||
}
|
||||
|
||||
// SetDistinct implements QueryBuilder.
|
||||
func (qb *BaseQueryBuilder) SetDistinct(d string) {
|
||||
qb.Distinct = d
|
||||
}
|
||||
|
||||
// SetGroupBy implements QueryBuilder.
|
||||
func (qb *BaseQueryBuilder) SetGroupBy(gb string) {
|
||||
qb.GroupBy = gb
|
||||
}
|
||||
|
||||
// SetOrderBy implements QueryBuilder.
|
||||
func (qb *BaseQueryBuilder) SetOrderBy(ob string) {
|
||||
qb.OrderBy = ob
|
||||
}
|
||||
|
||||
// SetColumns implements QueryBuilder.
|
||||
func (qb *BaseQueryBuilder) SetColumns(cols string) {
|
||||
qb.Columns = cols
|
||||
}
|
||||
|
||||
// SetFrom implements QueryBuilder.
|
||||
func (qb *BaseQueryBuilder) SetFrom(from string) {
|
||||
qb.From = from
|
||||
}
|
||||
|
||||
// SetWhere implements QueryBuilder.
|
||||
func (qb *BaseQueryBuilder) SetWhere(where string) {
|
||||
qb.Where = where
|
||||
}
|
||||
|
||||
// SetRange implements QueryBuilder.
|
||||
func (qb *BaseQueryBuilder) SetRange(rng string) {
|
||||
qb.Range = rng
|
||||
}
|
||||
|
||||
// Render implements QueryBuilder.
|
||||
func (qb *BaseQueryBuilder) Render() (string, error) {
|
||||
sb := strings.Builder{}
|
||||
|
||||
sb.WriteString("SELECT")
|
||||
|
||||
if qb.Distinct != "" {
|
||||
sb.WriteRune(sp)
|
||||
sb.WriteString(qb.Distinct)
|
||||
}
|
||||
|
||||
sb.WriteRune(sp)
|
||||
sb.WriteString(qb.Columns)
|
||||
sb.WriteRune(sp)
|
||||
sb.WriteString(qb.From)
|
||||
|
||||
if qb.Where != "" {
|
||||
sb.WriteRune(sp)
|
||||
sb.WriteString(qb.Where)
|
||||
}
|
||||
|
||||
if qb.OrderBy != "" {
|
||||
sb.WriteRune(sp)
|
||||
sb.WriteString(qb.OrderBy)
|
||||
}
|
||||
|
||||
if qb.GroupBy != "" {
|
||||
sb.WriteRune(sp)
|
||||
sb.WriteString(qb.GroupBy)
|
||||
}
|
||||
|
||||
if qb.Range != "" {
|
||||
sb.WriteRune(sp)
|
||||
sb.WriteString(qb.Range)
|
||||
}
|
||||
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// escapeLiteral escapes the single quotes in s.
|
||||
//
|
||||
// jessie's girl --> jessie''s girl
|
||||
func escapeLiteral(s string) string {
|
||||
sb := strings.Builder{}
|
||||
for _, r := range s {
|
||||
if r == singleQuote {
|
||||
_, _ = sb.WriteRune(singleQuote)
|
||||
}
|
||||
|
||||
_, _ = sb.WriteRune(r)
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// unquoteLiteral returns true if s is a double-quoted string, and also returns
|
||||
// the value with the quotes stripped. An error is returned if the string
|
||||
// is malformed.
|
||||
//
|
||||
// REVISIT: why not use strconv.Unquote or such?
|
||||
func unquoteLiteral(s string) (val string, ok bool, err error) {
|
||||
hasPrefix := strings.HasPrefix(s, `"`)
|
||||
hasSuffix := strings.HasSuffix(s, `"`)
|
||||
|
||||
if hasPrefix && hasSuffix {
|
||||
val = strings.TrimPrefix(s, `"`)
|
||||
val = strings.TrimSuffix(val, `"`)
|
||||
return val, true, nil
|
||||
}
|
||||
|
||||
if hasPrefix != hasSuffix {
|
||||
return "", false, errz.Errorf("malformed literal: %s", s)
|
||||
}
|
||||
|
||||
return s, false, nil
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
package sqlbuilder
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/neilotoole/sq/testh/tutil"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestQuoteTableOrColSelector(t *testing.T) {
|
||||
testCases := []struct {
|
||||
in string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{in: "", wantErr: true},
|
||||
{in: " ", wantErr: true},
|
||||
{in: "not_start_with_period", wantErr: true},
|
||||
{in: ".table", want: `"table"`},
|
||||
{in: ".table.col", want: `"table"."col"`},
|
||||
{in: ".table.col.other", wantErr: true},
|
||||
}
|
||||
|
||||
const quote = `"`
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.in, func(t *testing.T) {
|
||||
got, gotErr := quoteTableOrColSelector(quote, tc.in)
|
||||
if tc.wantErr {
|
||||
require.Error(t, gotErr)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, gotErr)
|
||||
require.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEscapeLiteralString(t *testing.T) {
|
||||
testCases := []struct {
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{in: ``, want: ``},
|
||||
{in: ` `, want: ` `},
|
||||
{in: `hello`, want: `hello`},
|
||||
{in: `"hello"`, want: `"hello"`},
|
||||
{in: `there's`, want: `there''s`},
|
||||
{in: `double''`, want: `double''''`},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
tc := tc
|
||||
|
||||
t.Run(tutil.Name(i, tc.in), func(t *testing.T) {
|
||||
got := escapeLiteral(tc.in)
|
||||
require.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
// Package sqlbuilder contains functionality for building SQL from
|
||||
// the ast.
|
||||
package sqlbuilder
|
||||
|
||||
import (
|
||||
"github.com/neilotoole/sq/libsq/ast"
|
||||
)
|
||||
|
||||
// FragmentBuilder renders driver-specific SQL fragments.
|
||||
type FragmentBuilder interface {
|
||||
// FromTable renders a FROM table fragment.
|
||||
FromTable(tblSel *ast.TblSelectorNode) (string, error)
|
||||
|
||||
// SelectCols renders a column names/expression fragment.
|
||||
// It shouldn't render the actual SELECT keyword. Example:
|
||||
//
|
||||
// "first_name", "last name" AS given_name
|
||||
SelectCols(cols []ast.ResultColumn) (string, error)
|
||||
|
||||
// Range renders a row range fragment.
|
||||
Range(rr *ast.RowRangeNode) (string, error)
|
||||
|
||||
// OrderBy renders the ORDER BY fragment.
|
||||
OrderBy(ob *ast.OrderByNode) (string, error)
|
||||
|
||||
// GroupBy renders the GROUP BY fragment.
|
||||
GroupBy(gb *ast.GroupByNode) (string, error)
|
||||
|
||||
// Join renders a join fragment.
|
||||
Join(fnJoin *ast.JoinNode) (string, error)
|
||||
|
||||
// Function renders a function fragment.
|
||||
Function(fn *ast.FuncNode) (string, error)
|
||||
|
||||
// Where renders a WHERE fragment.
|
||||
Where(where *ast.WhereNode) (string, error)
|
||||
|
||||
// Expr renders an expression fragment.
|
||||
Expr(expr *ast.ExprNode) (string, error)
|
||||
|
||||
// Operator renders an operator fragment.
|
||||
Operator(op *ast.OperatorNode) (string, error)
|
||||
|
||||
// Distinct renders the DISTINCT fragment. Returns an
|
||||
// empty string if n is nil.
|
||||
Distinct(n *ast.UniqueNode) (string, error)
|
||||
}
|
||||
|
||||
// QueryBuilder provides an abstraction for building a SQL query.
|
||||
type QueryBuilder interface {
|
||||
// SetColumns sets the columns to select.
|
||||
SetColumns(cols string)
|
||||
|
||||
// SetFrom sets the FROM clause.
|
||||
SetFrom(from string)
|
||||
|
||||
// SetWhere sets the WHERE clause.
|
||||
SetWhere(where string)
|
||||
|
||||
// SetRange sets the LIMIT ... OFFSET clause.
|
||||
SetRange(rng string)
|
||||
|
||||
// SetOrderBy sets the ORDER BY clause.
|
||||
SetOrderBy(ob string)
|
||||
|
||||
// SetGroupBy sets the GROUP BY clause.
|
||||
SetGroupBy(gb string)
|
||||
|
||||
// SetDistinct sets the DISTINCT clause.
|
||||
SetDistinct(d string)
|
||||
|
||||
// Render renders the SQL query.
|
||||
Render() (string, error)
|
||||
}
|
@ -2,19 +2,13 @@ package ast
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/lg/lga"
|
||||
"github.com/neilotoole/sq/libsq/core/stringz"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
// nodeVisitorFn is a visitor function that the walker invokes for each node it visits.
|
||||
type nodeVisitorFn func(*slog.Logger, *Walker, Node) error
|
||||
type nodeVisitorFn func(*Walker, Node) error
|
||||
|
||||
// Walker traverses a node tree (the AST, or a subset thereof).
|
||||
type Walker struct {
|
||||
log *slog.Logger
|
||||
root Node
|
||||
visitors map[reflect.Type][]nodeVisitorFn
|
||||
// state is a generic field to hold any data that a visitor function
|
||||
@ -23,14 +17,14 @@ type Walker struct {
|
||||
}
|
||||
|
||||
// NewWalker returns a new Walker instance.
|
||||
func NewWalker(log *slog.Logger, node Node) *Walker {
|
||||
w := &Walker{log: log, root: node}
|
||||
func NewWalker(node Node) *Walker {
|
||||
w := &Walker{root: node}
|
||||
w.visitors = map[reflect.Type][]nodeVisitorFn{}
|
||||
return w
|
||||
}
|
||||
|
||||
// AddVisitor adds a visitor function for the specified node type (and returns
|
||||
// the receiver Walker, to enabled chaining).
|
||||
// AddVisitor adds a visitor function for any node that is assignable
|
||||
// to typ.
|
||||
func (w *Walker) AddVisitor(typ reflect.Type, visitor nodeVisitorFn) *Walker {
|
||||
funcs := w.visitors[typ]
|
||||
if funcs == nil {
|
||||
@ -48,17 +42,19 @@ func (w *Walker) Walk() error {
|
||||
}
|
||||
|
||||
func (w *Walker) visit(node Node) error {
|
||||
typ := reflect.TypeOf(node)
|
||||
visitFns, ok := w.visitors[typ]
|
||||
|
||||
if ok {
|
||||
for _, visitFn := range visitFns {
|
||||
err := visitFn(w.log, w, node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var visitFns []nodeVisitorFn
|
||||
nodeType := reflect.TypeOf(node)
|
||||
for fnType, fns := range w.visitors {
|
||||
if nodeType.AssignableTo(fnType) {
|
||||
visitFns = append(visitFns, fns...)
|
||||
}
|
||||
}
|
||||
|
||||
for _, visitFn := range visitFns {
|
||||
err := visitFn(w, node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return w.visitChildren(node)
|
||||
@ -76,12 +72,12 @@ func (w *Walker) visitChildren(node Node) error {
|
||||
}
|
||||
|
||||
// walkWith is a convenience function for using Walker.
|
||||
func walkWith(log *slog.Logger, ast *AST, typ reflect.Type, fn nodeVisitorFn) error {
|
||||
return NewWalker(log, ast).AddVisitor(typ, fn).Walk()
|
||||
func walkWith(ast *AST, typ reflect.Type, fn nodeVisitorFn) error {
|
||||
return NewWalker(ast).AddVisitor(typ, fn).Walk()
|
||||
}
|
||||
|
||||
// narrowTblSel takes a generic selector, and if appropriate, converts it to a TblSel.
|
||||
func narrowTblSel(_ *slog.Logger, _ *Walker, node Node) error {
|
||||
func narrowTblSel(_ *Walker, node Node) error {
|
||||
// node is guaranteed to be typeSelectorNode
|
||||
sel, ok := node.(*SelectorNode)
|
||||
if !ok {
|
||||
@ -125,7 +121,7 @@ func narrowTblSel(_ *slog.Logger, _ *Walker, node Node) error {
|
||||
|
||||
// narrowTblColSel takes a generic selector, and if appropriate, replaces it
|
||||
// with a TblColSelectorNode.
|
||||
func narrowTblColSel(log *slog.Logger, w *Walker, node Node) error {
|
||||
func narrowTblColSel(w *Walker, node Node) error {
|
||||
// node is guaranteed to be type SelectorNode
|
||||
sel, ok := node.(*SelectorNode)
|
||||
if !ok {
|
||||
@ -148,13 +144,13 @@ func narrowTblColSel(log *slog.Logger, w *Walker, node Node) error {
|
||||
// if the parent is a segment, this is a "top-level" selector.
|
||||
// Only top-level selectors after the final selectable seg are
|
||||
// convert to TblColSelectorNode.
|
||||
selectableSeg, err := NewInspector(log, w.root.(*AST)).FindFinalTablerSegment()
|
||||
selectableSeg, err := NewInspector(w.root.(*AST)).FindFinalTablerSegment()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if parent.SegIndex() <= selectableSeg.SegIndex() {
|
||||
log.Debug("skipping this selector because it's not after the final selectable segment")
|
||||
// Skipping this selector because it's not after the final selectable segment
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -177,7 +173,7 @@ func narrowTblColSel(log *slog.Logger, w *Walker, node Node) error {
|
||||
}
|
||||
|
||||
// narrowColSel takes a generic selector, and if appropriate, converts it to a ColSel.
|
||||
func narrowColSel(log *slog.Logger, w *Walker, node Node) error {
|
||||
func narrowColSel(w *Walker, node Node) error {
|
||||
// node is guaranteed to be type SelectorNode
|
||||
sel, ok := node.(*SelectorNode)
|
||||
if !ok {
|
||||
@ -197,13 +193,13 @@ func narrowColSel(log *slog.Logger, w *Walker, node Node) error {
|
||||
// if the parent is a segment, this is a "top-level" selector.
|
||||
// Only top-level selectors after the final selectable seg are
|
||||
// convert to colSels.
|
||||
selectableSeg, err := NewInspector(log, w.root.(*AST)).FindFinalTablerSegment()
|
||||
selectableSeg, err := NewInspector(w.root.(*AST)).FindFinalTablerSegment()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if parent.SegIndex() <= selectableSeg.SegIndex() {
|
||||
log.Debug("Skipping this selector because it's not after the final selectable segment")
|
||||
// Skipping this selector because it's not after the final selectable segment
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -214,21 +210,23 @@ func narrowColSel(log *slog.Logger, w *Walker, node Node) error {
|
||||
return nodeReplace(sel, colSel)
|
||||
|
||||
default:
|
||||
log.Warn("Skipping this selector, as parent is not of a relevant type", lga.Type, stringz.Type(parent))
|
||||
// Skipping this selector, as parent is not of a relevant type
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// findWhereClause locates any expressions that represent the WHERE clause of the SQL SELECT stmt, and
|
||||
// inserts a SetWhere node into the AST for that expression.
|
||||
// findWhereClause locates any expressions that represent the WHERE clause
|
||||
// of the SQL SELECT stmt, and inserts a WhereNode
|
||||
// into the AST for that expression.
|
||||
//
|
||||
// In practice, a WHERE clause is an *ExprNode that is the only child of a segment. For example:
|
||||
// In practice, a WHERE clause is an *ExprNode that
|
||||
// is the only child of a segment. For example:
|
||||
//
|
||||
// @my1 | .tbluser | .uid > 4 | .uid, .email
|
||||
// @sakila | .actor | .actor_id > 4 | .first_name, .last_name
|
||||
//
|
||||
// In this case, ".uid > 4" is the WHERE clause.
|
||||
func findWhereClause(_ *slog.Logger, _ *Walker, node Node) error {
|
||||
// In this case, ".actor_id > 4" is the WHERE clause.
|
||||
func findWhereClause(_ *Walker, node Node) error {
|
||||
// node is guaranteed to be *ExprNode
|
||||
expr, ok := node.(*ExprNode)
|
||||
if !ok {
|
||||
@ -263,7 +261,7 @@ func findWhereClause(_ *slog.Logger, _ *Walker, node Node) error {
|
||||
}
|
||||
|
||||
// determineJoinTables attempts to determine the tables that a JOIN refers to.
|
||||
func determineJoinTables(_ *slog.Logger, _ *Walker, node Node) error {
|
||||
func determineJoinTables(_ *Walker, node Node) error {
|
||||
// node is guaranteed to be FnJoin
|
||||
fnJoin, ok := node.(*JoinNode)
|
||||
if !ok {
|
||||
@ -298,7 +296,7 @@ func determineJoinTables(_ *slog.Logger, _ *Walker, node Node) error {
|
||||
}
|
||||
|
||||
// visitCheckRowRange validates the RowRangeNode element.
|
||||
func visitCheckRowRange(_ *slog.Logger, w *Walker, node Node) error {
|
||||
func visitCheckRowRange(w *Walker, node Node) error {
|
||||
// node is guaranteed to be FnJoin
|
||||
rr, ok := node.(*RowRangeNode)
|
||||
if !ok {
|
||||
|
@ -4,8 +4,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/neilotoole/slogt"
|
||||
"golang.org/x/exp/slog"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@ -20,10 +18,10 @@ func TestWalker(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, ast)
|
||||
|
||||
walker := NewWalker(log, ast)
|
||||
walker := NewWalker(ast)
|
||||
count := 0
|
||||
|
||||
visitor := func(log *slog.Logger, w *Walker, node Node) error {
|
||||
visitor := func(w *Walker, node Node) error {
|
||||
count++
|
||||
return w.visitChildren(node)
|
||||
}
|
||||
@ -34,14 +32,14 @@ func TestWalker(t *testing.T) {
|
||||
assert.Equal(t, 1, count)
|
||||
|
||||
// test multiple visitors on the same node type
|
||||
walker = NewWalker(log, ast)
|
||||
walker = NewWalker(ast)
|
||||
countA := 0
|
||||
visitorA := func(log *slog.Logger, w *Walker, node Node) error {
|
||||
visitorA := func(w *Walker, node Node) error {
|
||||
countA++
|
||||
return w.visitChildren(node)
|
||||
}
|
||||
countB := 0
|
||||
visitorB := func(log *slog.Logger, w *Walker, node Node) error {
|
||||
visitorB := func(w *Walker, node Node) error {
|
||||
countB++
|
||||
return w.visitChildren(node)
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -595,3 +596,16 @@ func SingleQuote(s string) string {
|
||||
func Type(v any) string {
|
||||
return fmt.Sprintf("%T", v)
|
||||
}
|
||||
|
||||
var identRegex = regexp.MustCompile(`\A[a-zA-Z][a-zA-Z0-9_]*$`)
|
||||
|
||||
// ValidIdent returns an error if s is not a valid identifier.
|
||||
// And identifier must start with a letter, and may contain letters,
|
||||
// numbers, and underscore.
|
||||
func ValidIdent(s string) error {
|
||||
if identRegex.Match([]byte(s)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errz.Errorf("invalid identifier: %s", s)
|
||||
}
|
||||
|
@ -409,3 +409,32 @@ func TestSingleQuote(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidIdent(t *testing.T) {
|
||||
testCases := []struct {
|
||||
in string
|
||||
wantErr bool
|
||||
}{
|
||||
{in: "", wantErr: true},
|
||||
{in: "hello world", wantErr: true},
|
||||
{in: "hello", wantErr: false},
|
||||
{in: "1", wantErr: true},
|
||||
{in: "$hello", wantErr: true},
|
||||
{in: "_hello", wantErr: true},
|
||||
{in: "hello_", wantErr: false},
|
||||
{in: "Hello_", wantErr: false},
|
||||
{in: "Hello_1", wantErr: false},
|
||||
{in: "Hello_!!", wantErr: true},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tutil.Name(tc.in), func(t *testing.T) {
|
||||
gotErr := stringz.ValidIdent(tc.in)
|
||||
if tc.wantErr {
|
||||
require.Error(t, gotErr)
|
||||
} else {
|
||||
require.NoError(t, gotErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
61
libsq/driver/dialect/dialect.go
Normal file
61
libsq/driver/dialect/dialect.go
Normal file
@ -0,0 +1,61 @@
|
||||
// Package dialect contains functionality for SQL dialects.
|
||||
package dialect
|
||||
|
||||
import "github.com/neilotoole/sq/libsq/source"
|
||||
|
||||
// Dialect holds driver-specific SQL dialect values and functions.
|
||||
type Dialect struct {
|
||||
// Type is the dialect's driver source type.
|
||||
Type source.Type `json:"type"`
|
||||
|
||||
// Placeholders returns a string a SQL placeholders string.
|
||||
// For example "(?, ?, ?)" or "($1, $2, $3), ($4, $5, $6)".
|
||||
Placeholders func(numCols, numRows int) string
|
||||
|
||||
// IdentQuote is the identifier quote rune. Most often this is
|
||||
// double-quote, e.g. SELECT * FROM "my_table", but can be other
|
||||
// values such as backtick, e.g. SELECT * FROM `my_table`.
|
||||
//
|
||||
// Arguably, this field should be deprecated. There's probably
|
||||
// no reason not to always use Enquote.
|
||||
IdentQuote rune `json:"quote"`
|
||||
|
||||
// Enquote is a function that quotes and escapes an
|
||||
// identifier (such as a table or column name).
|
||||
Enquote func(string) string
|
||||
|
||||
// IntBool is true if BOOLEAN is handled as an INT by the DB driver.
|
||||
IntBool bool `json:"int_bool"`
|
||||
|
||||
// MaxBatchValues is the maximum number of values in a batch insert.
|
||||
MaxBatchValues int
|
||||
|
||||
// Ops is a map of SLQ operator (e.g. "==" or "!=") to
|
||||
// its default SQL rendering.
|
||||
Ops map[string]string
|
||||
}
|
||||
|
||||
// String returns a log/debug-friendly representation.
|
||||
func (d Dialect) String() string {
|
||||
return d.Type.String()
|
||||
}
|
||||
|
||||
// defaultOps is a map of SLQ operator (e.g. "==" or "!=") to
|
||||
// its default SQL rendering.
|
||||
var defaultOps = map[string]string{
|
||||
`==`: `=`,
|
||||
`&&`: `AND`,
|
||||
`||`: `OR`,
|
||||
`!=`: `!=`,
|
||||
}
|
||||
|
||||
// DefaultOps returns a default map of SLQ operator (e.g. "==" or "!=") to
|
||||
// its SQL rendering. The returned map is a copy and can be safely
|
||||
// modified by the caller.
|
||||
func DefaultOps() map[string]string {
|
||||
ops := make(map[string]string, len(defaultOps))
|
||||
for k, v := range defaultOps {
|
||||
ops[k] = v
|
||||
}
|
||||
return ops
|
||||
}
|
@ -6,20 +6,20 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/driver/dialect"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/lg/lga"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/lg"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/kind"
|
||||
"github.com/neilotoole/sq/libsq/core/stringz"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/cleanup"
|
||||
"github.com/neilotoole/sq/libsq/core/kind"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/errz"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/ast/sqlbuilder"
|
||||
"github.com/neilotoole/sq/libsq/ast/render"
|
||||
"github.com/neilotoole/sq/libsq/core/sqlmodel"
|
||||
"github.com/neilotoole/sq/libsq/core/sqlz"
|
||||
"github.com/neilotoole/sq/libsq/source"
|
||||
@ -82,10 +82,10 @@ type SQLDriver interface {
|
||||
Driver
|
||||
|
||||
// Dialect returns the SQL dialect.
|
||||
Dialect() Dialect
|
||||
Dialect() dialect.Dialect
|
||||
|
||||
// SQLBuilder returns the SQL builder for this driver.
|
||||
SQLBuilder() (sqlbuilder.FragmentBuilder, sqlbuilder.QueryBuilder)
|
||||
// Renderer returns the SQL renderer for this driver.
|
||||
Renderer() *render.Renderer
|
||||
|
||||
// CurrentSchema returns the current schema name.
|
||||
CurrentSchema(ctx context.Context, db sqlz.DB) (string, error)
|
||||
@ -218,34 +218,6 @@ type Metadata struct {
|
||||
Monotable bool `json:"monotable"`
|
||||
}
|
||||
|
||||
// Dialect holds driver-specific SQL dialect values.
|
||||
type Dialect struct {
|
||||
// Type is the dialect's driver source type.
|
||||
Type source.Type `json:"type"`
|
||||
|
||||
// Placeholders returns a string a SQL placeholders string.
|
||||
// For example "(?, ?, ?)" or "($1, $2, $3), ($4, $5, $6)".
|
||||
Placeholders func(numCols, numRows int) string
|
||||
|
||||
// Quote is the quote rune, typically the double quote rune.
|
||||
Quote rune `json:"quote"`
|
||||
|
||||
// IntBool is true if BOOLEAN is handled as an INT by the DB driver.
|
||||
IntBool bool `json:"int_bool"`
|
||||
|
||||
// MaxBatchValues is the maximum number of values in a batch insert.
|
||||
MaxBatchValues int
|
||||
}
|
||||
|
||||
// Enquote returns s surrounded by d.Quote.
|
||||
func (d Dialect) Enquote(s string) string {
|
||||
return stringz.Surround(s, string(d.Quote))
|
||||
}
|
||||
|
||||
func (d Dialect) String() string {
|
||||
return d.Type.String()
|
||||
}
|
||||
|
||||
// Databases provides a mechanism for getting Database instances.
|
||||
// Note that at this time instances returned by Open are cached
|
||||
// and then closed by Close. This may be a bad approach.
|
||||
|
@ -400,7 +400,7 @@ func TestRegistry_DriversMetadata_SQL(t *testing.T) {
|
||||
|
||||
dialect := sqlDrvr.Dialect()
|
||||
require.Equal(t, typ, dialect.Type)
|
||||
require.NotEmpty(t, dialect.Quote)
|
||||
require.NotEmpty(t, dialect.IdentQuote)
|
||||
require.NotNil(t, dialect.Placeholders)
|
||||
})
|
||||
}
|
||||
|
@ -354,7 +354,7 @@ func PrepareInsertStmt(ctx context.Context, drvr SQLDriver, db sqlz.Preparer, de
|
||||
}
|
||||
|
||||
dialect := drvr.Dialect()
|
||||
quote := string(dialect.Quote)
|
||||
quote := string(dialect.IdentQuote)
|
||||
tblNameQuoted, colNamesQuoted := stringz.Surround(destTbl, quote), stringz.SurroundSlice(destCols, quote)
|
||||
colsJoined := strings.Join(colNamesQuoted, Comma)
|
||||
placeholders := dialect.Placeholders(len(colNamesQuoted), numRows)
|
||||
|
136
libsq/engine.go
136
libsq/engine.go
@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/ast/render"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/lg"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/core/lg/lga"
|
||||
@ -22,15 +24,21 @@ import (
|
||||
type engine struct {
|
||||
log *slog.Logger
|
||||
|
||||
// qc is the context in which the query is executed.
|
||||
qc *QueryContext
|
||||
|
||||
// rc is the Context for rendering SQL.
|
||||
// This field is set during engine.prepare. It can't be set before
|
||||
// then because the target DB to use is calculated during engine.prepare,
|
||||
// based on the input query and other context.
|
||||
rc *render.Context
|
||||
|
||||
// tasks contains tasks that must be completed before targetSQL
|
||||
// is executed against targetDB. Typically tasks is used to
|
||||
// set up the joindb before it is queried.
|
||||
tasks []tasker
|
||||
|
||||
// targetSQL is the ultimate SQL query to be executed against
|
||||
// targetDB.
|
||||
// targetSQL is the ultimate SQL query to be executed against targetDB.
|
||||
targetSQL string
|
||||
|
||||
// targetDB is the destination for the ultimate SQL query to
|
||||
@ -63,87 +71,6 @@ func newEngine(ctx context.Context, qc *QueryContext, query string) (*engine, er
|
||||
return ng, nil
|
||||
}
|
||||
|
||||
// prepare prepares the engine to execute queryModel.
|
||||
// When this method returns, targetDB and targetSQL will be set,
|
||||
// as will any tasks (which may be empty). The tasks must be executed
|
||||
// against targetDB before targetSQL is executed (the engine.execute
|
||||
// method does this work).
|
||||
func (ng *engine) prepare(ctx context.Context, qm *queryModel) error {
|
||||
var (
|
||||
s string
|
||||
err error
|
||||
)
|
||||
|
||||
switch node := qm.Table.(type) {
|
||||
case *ast.TblSelectorNode:
|
||||
s, ng.targetDB, err = ng.buildTableFromClause(ctx, node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case *ast.JoinNode:
|
||||
s, ng.targetDB, err = ng.buildJoinFromClause(ctx, node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errz.Errorf("unknown selectable %T(%s)", node, node)
|
||||
}
|
||||
|
||||
fb, qb := ng.targetDB.SQLDriver().SQLBuilder()
|
||||
qb.SetFrom(s)
|
||||
|
||||
if s, err = fb.SelectCols(qm.Cols); err != nil {
|
||||
return err
|
||||
}
|
||||
qb.SetColumns(s)
|
||||
|
||||
if qm.Distinct != nil {
|
||||
if s, err = fb.Distinct(qm.Distinct); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
qb.SetDistinct(s)
|
||||
}
|
||||
|
||||
if qm.Range != nil {
|
||||
if s, err = fb.Range(qm.Range); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
qb.SetRange(s)
|
||||
}
|
||||
|
||||
if qm.Where != nil {
|
||||
if s, err = fb.Where(qm.Where); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
qb.SetWhere(s)
|
||||
}
|
||||
|
||||
if qm.OrderBy != nil {
|
||||
if s, err = fb.OrderBy(qm.OrderBy); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
qb.SetOrderBy(s)
|
||||
}
|
||||
|
||||
if qm.GroupBy != nil {
|
||||
if s, err = fb.GroupBy(qm.GroupBy); err != nil {
|
||||
return err
|
||||
}
|
||||
qb.SetGroupBy(s)
|
||||
}
|
||||
|
||||
ng.targetSQL, err = qb.Render()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// execute executes the plan that was built by engine.prepare.
|
||||
func (ng *engine) execute(ctx context.Context, recw RecordWriter) error {
|
||||
ng.log.Debug(
|
||||
@ -186,6 +113,9 @@ func (ng *engine) executeTasks(ctx context.Context) error {
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
// buildTableFromClause builds the "FROM table" fragment.
|
||||
//
|
||||
// When this function returns, ng.rc will be set.
|
||||
func (ng *engine) buildTableFromClause(ctx context.Context, tblSel *ast.TblSelectorNode) (fromClause string,
|
||||
fromConn driver.Database, err error,
|
||||
) {
|
||||
@ -199,8 +129,14 @@ func (ng *engine) buildTableFromClause(ctx context.Context, tblSel *ast.TblSelec
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
fragBuilder, _ := fromConn.SQLDriver().SQLBuilder()
|
||||
fromClause, err = fragBuilder.FromTable(tblSel)
|
||||
rndr := fromConn.SQLDriver().Renderer()
|
||||
ng.rc = &render.Context{
|
||||
Renderer: rndr,
|
||||
Args: ng.qc.Args,
|
||||
Dialect: fromConn.SQLDriver().Dialect(),
|
||||
}
|
||||
|
||||
fromClause, err = rndr.FromTable(ng.rc, tblSel)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
@ -208,6 +144,9 @@ func (ng *engine) buildTableFromClause(ctx context.Context, tblSel *ast.TblSelec
|
||||
return fromClause, fromConn, nil
|
||||
}
|
||||
|
||||
// buildJoinFromClause builds the "JOIN" clause.
|
||||
//
|
||||
// When this function returns, ng.rc will be set.
|
||||
func (ng *engine) buildJoinFromClause(ctx context.Context, fnJoin *ast.JoinNode) (fromClause string,
|
||||
fromConn driver.Database, err error,
|
||||
) {
|
||||
@ -226,6 +165,9 @@ func (ng *engine) buildJoinFromClause(ctx context.Context, fnJoin *ast.JoinNode)
|
||||
return ng.singleSourceJoin(ctx, fnJoin)
|
||||
}
|
||||
|
||||
// singleSourceJoin sets up a join against a single source.
|
||||
//
|
||||
// On return, ng.rc will be set.
|
||||
func (ng *engine) singleSourceJoin(ctx context.Context, fnJoin *ast.JoinNode) (fromClause string,
|
||||
fromDB driver.Database, err error,
|
||||
) {
|
||||
@ -239,8 +181,14 @@ func (ng *engine) singleSourceJoin(ctx context.Context, fnJoin *ast.JoinNode) (f
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
fragBuilder, _ := fromDB.SQLDriver().SQLBuilder()
|
||||
fromClause, err = fragBuilder.Join(fnJoin)
|
||||
rndr := fromDB.SQLDriver().Renderer()
|
||||
ng.rc = &render.Context{
|
||||
Renderer: rndr,
|
||||
Args: ng.qc.Args,
|
||||
Dialect: fromDB.SQLDriver().Dialect(),
|
||||
}
|
||||
|
||||
fromClause, err = rndr.Join(ng.rc, fnJoin)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
@ -250,6 +198,8 @@ func (ng *engine) singleSourceJoin(ctx context.Context, fnJoin *ast.JoinNode) (f
|
||||
|
||||
// crossSourceJoin returns a FROM clause that forms part of
|
||||
// the SQL SELECT statement against fromDB.
|
||||
//
|
||||
// On return, ng.rc will be set.
|
||||
func (ng *engine) crossSourceJoin(ctx context.Context, fnJoin *ast.JoinNode) (fromClause string, fromDB driver.Database,
|
||||
err error,
|
||||
) {
|
||||
@ -275,6 +225,13 @@ func (ng *engine) crossSourceJoin(ctx context.Context, fnJoin *ast.JoinNode) (fr
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
rndr := joinDB.SQLDriver().Renderer()
|
||||
ng.rc = &render.Context{
|
||||
Renderer: rndr,
|
||||
Args: ng.qc.Args,
|
||||
Dialect: joinDB.SQLDriver().Dialect(),
|
||||
}
|
||||
|
||||
leftDB, err := ng.qc.DBOpener.Open(ctx, leftSrc)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
@ -300,8 +257,7 @@ func (ng *engine) crossSourceJoin(ctx context.Context, fnJoin *ast.JoinNode) (fr
|
||||
ng.tasks = append(ng.tasks, leftCopyTask)
|
||||
ng.tasks = append(ng.tasks, rightCopyTask)
|
||||
|
||||
joinDBFragBuilder, _ := joinDB.SQLDriver().SQLBuilder()
|
||||
fromClause, err = joinDBFragBuilder.Join(fnJoin)
|
||||
fromClause, err = rndr.Join(ng.rc, fnJoin)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
@ -389,7 +345,7 @@ func buildQueryModel(log *slog.Logger, a *ast.AST) (*queryModel, error) {
|
||||
return nil, errz.Errorf("query model error: query does not have enough segments")
|
||||
}
|
||||
|
||||
insp := ast.NewInspector(log, a)
|
||||
insp := ast.NewInspector(a)
|
||||
tablerSeg, err := insp.FindFinalTablerSegment()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
82
libsq/prepare.go
Normal file
82
libsq/prepare.go
Normal file
@ -0,0 +1,82 @@
|
||||
package libsq
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/ast/render"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/ast"
|
||||
"github.com/neilotoole/sq/libsq/core/errz"
|
||||
)
|
||||
|
||||
// prepare prepares the engine to execute queryModel.
|
||||
// When this method returns, targetDB and targetSQL will be set,
|
||||
// as will any tasks (which may be empty). The tasks must be executed
|
||||
// against targetDB before targetSQL is executed (the engine.execute
|
||||
// method does this work).
|
||||
func (ng *engine) prepare(ctx context.Context, qm *queryModel) error {
|
||||
var (
|
||||
err error
|
||||
frags = &render.Fragments{}
|
||||
)
|
||||
|
||||
// After this switch, ng.rc will be set.
|
||||
switch node := qm.Table.(type) {
|
||||
case *ast.TblSelectorNode:
|
||||
if frags.From, ng.targetDB, err = ng.buildTableFromClause(ctx, node); err != nil {
|
||||
return err
|
||||
}
|
||||
case *ast.JoinNode:
|
||||
if frags.From, ng.targetDB, err = ng.buildJoinFromClause(ctx, node); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
// Should never happen
|
||||
return errz.Errorf("unknown ast.Tabler %T: %s", node, node)
|
||||
}
|
||||
|
||||
rndr := ng.targetDB.SQLDriver().Renderer()
|
||||
|
||||
if frags.Columns, err = rndr.SelectCols(ng.rc, qm.Cols); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if qm.Distinct != nil {
|
||||
if frags.Distinct, err = rndr.Distinct(ng.rc, qm.Distinct); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if qm.Range != nil {
|
||||
if frags.Range, err = rndr.Range(ng.rc, qm.Range); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if qm.Where != nil {
|
||||
if frags.Where, err = rndr.Where(ng.rc, qm.Where); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if qm.OrderBy != nil {
|
||||
if frags.OrderBy, err = rndr.OrderBy(ng.rc, qm.OrderBy); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if qm.GroupBy != nil {
|
||||
if frags.GroupBy, err = rndr.GroupBy(ng.rc, qm.GroupBy); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if rndr.PreRender != nil {
|
||||
if err = rndr.PreRender(ng.rc, frags); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ng.targetSQL, err = rndr.Render(ng.rc, frags)
|
||||
return err
|
||||
}
|
@ -8,20 +8,35 @@ import (
|
||||
"github.com/neilotoole/sq/libsq/source"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/neilotoole/sq/testh/sakila"
|
||||
)
|
||||
|
||||
//nolint:exhaustive
|
||||
//nolint:exhaustive,lll
|
||||
func TestQuery_args(t *testing.T) {
|
||||
testCases := []queryTestCase{
|
||||
{
|
||||
name: "cols",
|
||||
in: `@sakila | .actor | .$a`,
|
||||
args: map[string]string{"a": "first_name"},
|
||||
wantSQL: `SELECT "first_name", "last_name" FROM "actor"`,
|
||||
override: map[source.Type]string{mysql.Type: "SELECT `first_name`, `last_name` FROM `actor`"},
|
||||
wantRecs: sakila.TblActorCount,
|
||||
skip: true,
|
||||
name: "arg_value_string",
|
||||
in: `@sakila | .actor | .first_name == $first`,
|
||||
args: map[string]string{"first": "TOM"},
|
||||
wantSQL: `SELECT * FROM "actor" WHERE "first_name" = 'TOM'`,
|
||||
override: map[source.Type]string{mysql.Type: "SELECT * FROM `actor` WHERE `first_name` = 'TOM'"},
|
||||
wantRecs: 2,
|
||||
},
|
||||
{
|
||||
name: "arg_value_string_2",
|
||||
in: `@sakila | .actor | .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.Type]string{mysql.Type: "SELECT * FROM `actor` WHERE `first_name` = 'TOM' AND `last_name` = 'MIRANDA'"},
|
||||
wantRecs: 1,
|
||||
},
|
||||
{
|
||||
name: "arg_value_int",
|
||||
in: `@sakila | .actor | .actor_id == int($id)`,
|
||||
args: map[string]string{"id": "1"},
|
||||
wantSQL: `SELECT * FROM "actor" WHERE "actor_id" = 1`,
|
||||
override: map[source.Type]string{mysql.Type: "SELECT * FROM `actor` WHERE `actor_id` = 1"},
|
||||
skip: true, // Skip until we implement casting, e.g. .actor_id == int($id)
|
||||
wantRecs: 1,
|
||||
},
|
||||
}
|
||||
|
||||
|
65
libsq/query_expr_test.go
Normal file
65
libsq/query_expr_test.go
Normal file
@ -0,0 +1,65 @@
|
||||
package libsq_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/neilotoole/sq/drivers/mysql"
|
||||
|
||||
"github.com/neilotoole/sq/libsq/source"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
//nolint:exhaustive,lll
|
||||
func TestQuery_expr(t *testing.T) {
|
||||
testCases := []queryTestCase{
|
||||
{
|
||||
name: "literal/string",
|
||||
in: `@sakila | .actor | .first_name == "TOM"`,
|
||||
wantSQL: `SELECT * FROM "actor" WHERE "first_name" = 'TOM'`,
|
||||
override: map[source.Type]string{mysql.Type: "SELECT * FROM `actor` WHERE `first_name` = 'TOM'"},
|
||||
wantRecs: 2,
|
||||
},
|
||||
{
|
||||
name: "literal/two-strings",
|
||||
in: `@sakila | .actor | .first_name == "TOM" && .last_name == "MIRANDA"`,
|
||||
wantSQL: `SELECT * FROM "actor" WHERE "first_name" = 'TOM' AND "last_name" = 'MIRANDA'`,
|
||||
override: map[source.Type]string{mysql.Type: "SELECT * FROM `actor` WHERE `first_name` = 'TOM' AND `last_name` = 'MIRANDA'"},
|
||||
wantRecs: 1,
|
||||
},
|
||||
{
|
||||
name: "literal/integer",
|
||||
in: `@sakila | .actor | .actor_id == 1`,
|
||||
wantSQL: `SELECT * FROM "actor" WHERE "actor_id" = 1`,
|
||||
override: map[source.Type]string{mysql.Type: "SELECT * FROM `actor` WHERE `actor_id` = 1"},
|
||||
wantRecs: 1,
|
||||
},
|
||||
{
|
||||
name: "is_null",
|
||||
in: `@sakila | .address | .postal_code == null`,
|
||||
wantSQL: `SELECT * FROM "address" WHERE "postal_code" IS NULL`,
|
||||
override: map[source.Type]string{mysql.Type: "SELECT * FROM `address` WHERE `postal_code` IS NULL"},
|
||||
wantRecs: 4,
|
||||
// skipExec because mysql sakila db doesn't have the same null values.
|
||||
// This is a bug in the dataset.
|
||||
skipExec: true,
|
||||
},
|
||||
{
|
||||
name: "is_not_null",
|
||||
in: `@sakila | .address | .postal_code != null`,
|
||||
wantSQL: `SELECT * FROM "address" WHERE "postal_code" IS NOT NULL`,
|
||||
override: map[source.Type]string{mysql.Type: "SELECT * FROM `address` WHERE `postal_code` IS NOT NULL"},
|
||||
wantRecs: 599,
|
||||
// skipExec because mysql sakila db doesn't have the same null values.
|
||||
// This is a bug in the dataset.
|
||||
skipExec: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
execQueryTestCase(t, tc)
|
||||
})
|
||||
}
|
||||
}
|
@ -65,11 +65,14 @@ func execQueryTestCase(t *testing.T, tc queryTestCase) {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
t.Helper()
|
||||
srcs := testh.New(t).NewSourceSet(sakila.SQLLatest()...)
|
||||
for _, src := range srcs.Items() {
|
||||
src := src
|
||||
|
||||
t.Run(string(src.Type), func(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
if len(tc.onlyFor) > 0 {
|
||||
if !slices.Contains(tc.onlyFor, src.Type) {
|
||||
t.Skip()
|
||||
@ -110,7 +113,7 @@ func execQueryTestCase(t *testing.T, tc queryTestCase) {
|
||||
return
|
||||
}
|
||||
|
||||
sink, err := th.QuerySLQ(in)
|
||||
sink, err := th.QuerySLQ(in, tc.args)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.wantRecs, len(sink.Recs))
|
||||
})
|
||||
|
@ -466,13 +466,14 @@ func (h *Helper) QuerySQL(src *source.Source, query string, args ...any) (*Recor
|
||||
return sink, nil
|
||||
}
|
||||
|
||||
// QuerySLQ executes the SLQ query.
|
||||
func (h *Helper) QuerySLQ(query string) (*RecordSink, error) {
|
||||
// QuerySLQ executes the SLQ query. Args are predefined variables for
|
||||
// substitution.
|
||||
func (h *Helper) QuerySLQ(query string, args map[string]string) (*RecordSink, error) {
|
||||
// We need to ensure that each of the handles in the query is loaded.
|
||||
a, err := ast.Parse(h.Log, query)
|
||||
require.NoError(h.T, err)
|
||||
|
||||
for _, handle := range ast.NewInspector(h.Log, a).FindHandles() {
|
||||
for _, handle := range ast.NewInspector(a).FindHandles() {
|
||||
// This triggers handle loading
|
||||
_ = h.Source(handle)
|
||||
}
|
||||
@ -481,6 +482,7 @@ func (h *Helper) QuerySLQ(query string) (*RecordSink, error) {
|
||||
Sources: h.srcs,
|
||||
DBOpener: h.databases,
|
||||
JoinDBOpener: h.databases,
|
||||
Args: args,
|
||||
}
|
||||
|
||||
sink := &RecordSink{}
|
||||
|
Loading…
Reference in New Issue
Block a user