Skip to content

Commit

Permalink
orderby() (#159)
Browse files Browse the repository at this point in the history
* wip: orderby impl

* Tests passing (note: ast.checkASTIntegrity is disabled)

* ExprNode now rendered via renderSelectorNode

* linting

* CHAGELOG for v0.27.0
  • Loading branch information
neilotoole authored Mar 26, 2023
1 parent d48adee commit 9746f4c
Show file tree
Hide file tree
Showing 34 changed files with 1,640 additions and 793 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [v0.27.0] - 2023-03-25

### Added

- [#158]: Use `orderby()` to order results. See [query guide](https://sq.io/docs/query/#ordering).


## [v0.26.0] - 2023-03-22

Expand Down Expand Up @@ -188,6 +194,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [#89]: Bug with SQL generated for joins.


[v0.27.0]: https://github.com/neilotoole/sq/compare/v0.26.0...v0.27.0
[v0.26.0]: https://github.com/neilotoole/sq/compare/v0.25.1...v0.26.0
[v0.25.1]: https://github.com/neilotoole/sq/compare/v0.25.0...v0.25.1
[v0.25.0]: https://github.com/neilotoole/sq/compare/v0.24.4...v0.25.0
Expand All @@ -210,6 +217,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[v0.15.3]: https://github.com/neilotoole/sq/compare/v0.15.2...v0.15.3
[v0.15.2]: https://github.com/neilotoole/sq/releases/tag/v0.15.2

[#158]: https://github.com/neilotoole/sq/issues/158
[#155]: https://github.com/neilotoole/sq/issues/155
[#153]: https://github.com/neilotoole/sq/issues/153
[#151]: https://github.com/neilotoole/sq/issues/151
Expand Down
2 changes: 1 addition & 1 deletion cli/cmd_slq_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
"github.com/neilotoole/sq/testh/sakila"
)

// TestCmdSLQ_Insert tests "sq slq QUERY --insert=_newdest.tbl".
// TestCmdSLQ_Insert tests "sq QUERY --insert=@src.tbl".
func TestCmdSLQ_Insert_Create(t *testing.T) {
th := testh.New(t)
originSrc, destSrc := th.Source(sakila.SL3), th.Source(sakila.SL3)
Expand Down
2 changes: 1 addition & 1 deletion drivers/mysql/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ func getAllTblMetas(ctx context.Context, log lg.Log, db sqlz.DB) ([]*source.Tabl
const query = `SELECT t.TABLE_SCHEMA, t.TABLE_NAME, t.TABLE_TYPE, t.TABLE_COMMENT,
(DATA_LENGTH + INDEX_LENGTH) AS table_size,
c.COLUMN_NAME, c.ORDINAL_POSITION, c.COLUMN_KEY, c.DATA_TYPE, c.COLUMN_TYPE,
c.IS_NULLABLE, c.COLßUMN_DEFAULT, c.COLUMN_COMMENT, c.EXTRA
c.IS_NULLABLE, c.COLUMN_DEFAULT, c.COLUMN_COMMENT, c.EXTRA
FROM information_schema.TABLES t
LEFT JOIN information_schema.COLUMNS c
ON c.TABLE_CATALOG = t.TABLE_CATALOG
Expand Down
57 changes: 54 additions & 3 deletions drivers/slq2sql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,18 @@ func TestSLQ2SQLNew(t *testing.T) {
want: `SELECT COUNT(*) AS "quantity" FROM "actor"`,
override: map[source.Type]string{mysql.Type: "SELECT COUNT(*) AS `quantity` FROM `actor`"},
},
{
name: "filter/equal",
in: `@sakila | .actor | .actor_id == 1`,
want: `SELECT * FROM "actor" WHERE "actor_id" = 1`,
override: map[source.Type]string{mysql.Type: "SELECT * FROM `actor` WHERE `actor_id` = 1"},
},
{
name: "join/single-selector",
in: `@sakila | .actor, .film_actor | join(.actor_id)`,
want: `SELECT * FROM "actor" INNER JOIN "film_actor" ON "actor"."actor_id" = "film_actor"."actor_id"`,
override: map[source.Type]string{mysql.Type: "SELECT * FROM `actor` INNER JOIN `film_actor` ON `actor`.`actor_id` = `film_actor`.`actor_id`"},
},
{
name: "join/fq-table-cols-equal",
in: `@sakila | .actor, .film_actor | join(.film_actor.actor_id == .actor.actor_id)`,
Expand All @@ -109,9 +121,51 @@ func TestSLQ2SQLNew(t *testing.T) {
want: `SELECT * FROM "actor" INNER JOIN "film actor" ON "film actor"."actor_id" = "actor"."actor_id"`,
override: map[source.Type]string{mysql.Type: "SELECT * FROM `actor` INNER JOIN `film actor` ON `film actor`.`actor_id` = `actor`.`actor_id`"},
},
{
name: "orderby/single-element",
in: `@sakila | .actor | orderby(.first_name)`,
want: `SELECT * FROM "actor" ORDER BY "first_name"`,
override: map[source.Type]string{mysql.Type: "SELECT * FROM `actor` ORDER BY `first_name`"},
},
{
name: "orderby/single-element-table-selector",
in: `@sakila | .actor | orderby(.actor.first_name)`,
want: `SELECT * FROM "actor" ORDER BY "actor"."first_name"`,
override: map[source.Type]string{mysql.Type: "SELECT * FROM `actor` ORDER BY `actor`.`first_name`"},
},
{
name: "orderby/single-element-asc",
in: `@sakila | .actor | orderby(.first_name+)`,
want: `SELECT * FROM "actor" ORDER BY "first_name" ASC`,
override: map[source.Type]string{mysql.Type: "SELECT * FROM `actor` ORDER BY `first_name` ASC"},
},
{
name: "orderby/single-element-desc",
in: `@sakila | .actor | orderby(.first_name-)`,
want: `SELECT * FROM "actor" ORDER BY "first_name" DESC`,
override: map[source.Type]string{mysql.Type: "SELECT * FROM `actor` ORDER BY `first_name` DESC"},
},
{
name: "orderby/multiple-elements",
in: `@sakila | .actor | orderby(.first_name+, .last_name-)`,
want: `SELECT * FROM "actor" ORDER BY "first_name" ASC, "last_name" DESC`,
override: map[source.Type]string{mysql.Type: "SELECT * FROM `actor` ORDER BY `first_name` ASC, `last_name` DESC"},
},
{
name: "orderby/synonym-sort-by",
in: `@sakila | .actor | sort_by(.first_name)`,
want: `SELECT * FROM "actor" ORDER BY "first_name"`,
override: map[source.Type]string{mysql.Type: "SELECT * FROM `actor` ORDER BY `first_name`"},
},
{
name: "orderby/error-no-selector",
in: `@sakila | .actor | orderby()`,
wantErr: true,
},
}

srcs := testh.New(t).NewSourceSet(sakila.SQLLatest()...)
// srcs := testh.New(t).NewSourceSet(sakila.SL3)
for _, tc := range testCases {
tc := tc

Expand All @@ -133,9 +187,6 @@ func TestSLQ2SQLNew(t *testing.T) {
th := testh.New(t)
dbases := th.Databases()

// drvr := th.DriverFor(src).(driver.SQLDriver)
// drvr.AlterTableAddColumn()

gotSQL, gotErr := libsq.SLQ2SQL(th.Context, th.Log, dbases, dbases, srcs, in)
if tc.wantErr {
require.Error(t, gotErr)
Expand Down
10 changes: 7 additions & 3 deletions drivers/sqlserver/sqlbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (
"github.com/neilotoole/sq/libsq/core/sqlmodel"
)

var _ sqlbuilder.FragmentBuilder = (*fragBuilder)(nil)

type fragBuilder struct {
sqlbuilder.BaseFragmentBuilder
}
Expand All @@ -29,13 +31,13 @@ func newFragmentBuilder(log lg.Log) *fragBuilder {
return r
}

func (fb *fragBuilder) Range(rr *ast.RowRange) (string, error) {
func (fb *fragBuilder) Range(rr *ast.RowRangeNode) (string, error) {
if rr == nil {
return "", nil
}

/*
SELECT * FROM tbluser
SELECT * FROM actor
ORDER BY (SELECT 0)
OFFSET 1 ROWS
FETCH NEXT 2 ROWS ONLY;
Expand All @@ -62,6 +64,8 @@ func (fb *fragBuilder) Range(rr *ast.RowRange) (string, error) {
return sql, nil
}

var _ sqlbuilder.QueryBuilder = (*queryBuilder)(nil)

type queryBuilder struct {
sqlbuilder.BaseQueryBuilder
}
Expand All @@ -71,7 +75,7 @@ func (qb *queryBuilder) SQL() (string, error) {
// 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:
//
// SELECT * FROM tbluser
// SELECT * FROM actor
// ORDER BY (SELECT 0)
// OFFSET 1 ROWS
// FETCH NEXT 2 ROWS ONLY;
Expand Down
27 changes: 27 additions & 0 deletions grammar/SLQ.g4
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ element:
| selectorElement
| join
| group
| orderBy
| rowRange
| fnElement
| expr;
Expand All @@ -33,6 +34,31 @@ joinConstraint:

group: ('group' | 'GROUP' | 'g') '(' selector (',' selector)* ')';

/*
orderby
The "orderby" construct implements the SQL "ORDER BY" clause.
.actor | orderby(.first_name, .last_name)
.actor | orderby(.first_name+)
.actor | orderby(.actor.first_name-)
The optional plus/minus tokens specify ASC or DESC order.
For jq compatability, the "sort_by" synonym is provided.
See: https://stedolan.github.io/jq/manual/v1.6/#sort,sort_by(path_expression)
We do not implement a "sort" synonym for the jq "sort" function, because SQL
results are inherently sorted. Although perhaps it should be implemented
as a no-op.
*/

ORDER_ASC: '+';
ORDER_DESC: '-';
ORDER_BY: 'orderby' | 'sort_by';
orderByTerm: selector (ORDER_ASC | ORDER_DESC)?;
orderBy: ORDER_BY '(' orderByTerm (',' orderByTerm)* ')';

// selector specfies a table name, a column name, or table.column.
// - .first_name
// - ."first name"
Expand Down Expand Up @@ -142,6 +168,7 @@ GT: '>';
NEQ: '!=';
EQ: '==';


NAME: '.' (ID | STRING);

// SEL can be .THING or .THING.OTHERTHING.
Expand Down
25 changes: 24 additions & 1 deletion libsq/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,23 @@ import (

// Parse parses the SLQ input string and builds the AST.
func Parse(log lg.Log, input string) (*AST, error) { //nolint:staticcheck
// REVISIT: We need a better solution for disabling parser logging.
log = lg.Discard() //nolint:staticcheck // Disable parser logging.
ptree, err := parseSLQ(log, input)
if err != nil {
return nil, err
}

return buildAST(log, ptree)
ast, err := buildAST(log, ptree)
if err != nil {
return nil, err
}

if err := verify(log, ast); err != nil {
return nil, err
}

return ast, nil
}

// buildAST constructs sq's AST from a parse tree.
Expand Down Expand Up @@ -65,6 +75,19 @@ func buildAST(log lg.Log, query slq.IQueryContext) (*AST, error) {
return tree.ast, nil
}

// verify performs additional checks on the state of the built AST.
func verify(log lg.Log, ast *AST) error {
selCount := NewInspector(log, ast).CountNodes(typeSelectorNode)
if selCount != 0 {
return errorf("AST should have zero nodes of type %T but found %d",
(*SelectorNode)(nil), selCount)
}

// TODO: Lots more checks could go here

return nil
}

var _ Node = (*AST)(nil)

// AST is the Abstract Syntax Tree. It is the root node of a SQL query/stmt.
Expand Down
24 changes: 12 additions & 12 deletions libsq/ast/func.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
package ast

var (
_ Node = (*Func)(nil)
_ ResultColumn = (*Func)(nil)
_ Node = (*FuncNode)(nil)
_ ResultColumn = (*FuncNode)(nil)
)

// Func models a function. For example, "COUNT()".
type Func struct {
// FuncNode models a function. For example, "COUNT()".
type FuncNode struct {
baseNode
fnName string
alias string
}

// FuncName returns the function name.
func (fn *Func) FuncName() string {
func (fn *FuncNode) FuncName() string {
return fn.fnName
}

// String returns a log/debug-friendly representation.
func (fn *Func) String() string {
func (fn *FuncNode) String() string {
str := nodeString(fn)
if fn.alias != "" {
str += ":" + fn.alias
Expand All @@ -27,28 +27,28 @@ func (fn *Func) String() string {
}

// Text implements ResultColumn.
func (fn *Func) Text() string {
func (fn *FuncNode) Text() string {
return fn.ctx.GetText()
}

// Alias implements ResultColumn.
func (fn *Func) Alias() string {
func (fn *FuncNode) Alias() string {
return fn.alias
}

// SetChildren implements Node.
func (fn *Func) SetChildren(children []Node) error {
func (fn *FuncNode) SetChildren(children []Node) error {
fn.setChildren(children)
return nil
}

// IsColumn implements ResultColumn.
func (fn *Func) IsColumn() bool {
func (fn *FuncNode) IsColumn() bool {
return false
}

func (fn *Func) AddChild(child Node) error {
// TODO: add check for valid Func child types
func (fn *FuncNode) AddChild(child Node) error {
// TODO: add check for valid FuncNode child types
fn.addChild(child)
return child.SetParent(fn)
}
Loading

0 comments on commit 9746f4c

Please sign in to comment.