Feature/173 args (#183)

- Implement --arg feature
- Refactor sqlbuilder package (now called "render").
- Bug fixes, especially around expressions.
This commit is contained in:
Neil O'Toole 2023-04-07 02:00:49 -06:00 committed by GitHub
parent 801dd69218
commit 9a1c6a7d09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 2337 additions and 1727 deletions

View File

@ -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.

View File

@ -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
}

View File

@ -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 (

View File

@ -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)
})
}
}

View File

@ -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 {

View File

@ -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:

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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 := "*"

View File

@ -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 {

View File

@ -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)

View File

@ -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
View 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)
}

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) {}

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -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
View 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)
}

View File

@ -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))
)

View File

@ -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))
}

View File

@ -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

View File

@ -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)

View File

@ -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)

View 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
View 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
}

View 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
}

View 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
}

View 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
}

View File

@ -0,0 +1 @@
package render

86
libsq/ast/render/join.go Normal file
View 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
}

View 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()))
}
}

View 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
}

View 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
View 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
View 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
}

View 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
View 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
}

View File

@ -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)

View File

@ -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
}

View File

@ -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)
})
}
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}
})
}
}

View 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
}

View File

@ -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.

View File

@ -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)
})
}

View File

@ -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)

View File

@ -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
View 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
}

View File

@ -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
View 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)
})
}
}

View File

@ -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))
})

View File

@ -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{}