mirror of
https://github.com/neilotoole/sq.git
synced 2024-11-28 03:53:07 +03:00
Table alias implemented (#278)
This commit is contained in:
parent
072cb4f515
commit
1edc02c378
12
CHANGELOG.md
12
CHANGELOG.md
@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
Breaking changes are annotated with ☢️.
|
Breaking changes are annotated with ☢️.
|
||||||
|
|
||||||
|
## Upcoming
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- [#277]: A table selector can now have an alias. This in and of itself is not
|
||||||
|
particularly useful, but it's a building block for [multiple joins](https://github.com/neilotoole/sq/issues/12).
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ sq `@sakila | .actor:a | .a.first_name`
|
||||||
|
```
|
||||||
|
|
||||||
## [v0.39.1] - 2023-06-22
|
## [v0.39.1] - 2023-06-22
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
@ -641,6 +652,7 @@ make working with lots of sources much easier.
|
|||||||
[#258]: https://github.com/neilotoole/sq/issues/258
|
[#258]: https://github.com/neilotoole/sq/issues/258
|
||||||
[#261]: https://github.com/neilotoole/sq/issues/261
|
[#261]: https://github.com/neilotoole/sq/issues/261
|
||||||
[#263]: https://github.com/neilotoole/sq/issues/263
|
[#263]: https://github.com/neilotoole/sq/issues/263
|
||||||
|
[#277]: https://github.com/neilotoole/sq/issues/277
|
||||||
|
|
||||||
[v0.15.2]: https://github.com/neilotoole/sq/releases/tag/v0.15.2
|
[v0.15.2]: https://github.com/neilotoole/sq/releases/tag/v0.15.2
|
||||||
[v0.15.3]: https://github.com/neilotoole/sq/compare/v0.15.2...v0.15.3
|
[v0.15.3]: https://github.com/neilotoole/sq/compare/v0.15.2...v0.15.3
|
||||||
|
@ -27,6 +27,10 @@ type ExprElementNode struct {
|
|||||||
exprNode *ExprNode
|
exprNode *ExprNode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resultColumn implements ast.ResultColumn.
|
||||||
|
func (ex *ExprElementNode) resultColumn() {
|
||||||
|
}
|
||||||
|
|
||||||
// String returns a log/debug-friendly representation.
|
// String returns a log/debug-friendly representation.
|
||||||
func (ex *ExprElementNode) String() string {
|
func (ex *ExprElementNode) String() string {
|
||||||
str := nodeString(ex)
|
str := nodeString(ex)
|
||||||
|
@ -19,6 +19,12 @@ type FuncNode struct {
|
|||||||
proprietary bool
|
proprietary bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resultColumn implements ast.ResultColumn.
|
||||||
|
//
|
||||||
|
// REVISIT: should ast.FuncNode implement ast.ResultColumn?
|
||||||
|
func (fn *FuncNode) resultColumn() {
|
||||||
|
}
|
||||||
|
|
||||||
// FuncName returns the function name.
|
// FuncName returns the function name.
|
||||||
func (fn *FuncNode) FuncName() string {
|
func (fn *FuncNode) FuncName() string {
|
||||||
return fn.fnName
|
return fn.fnName
|
||||||
|
@ -70,6 +70,8 @@ type ResultColumn interface {
|
|||||||
|
|
||||||
// Text returns the raw text of the node, e.g. ".actor" or "1*2".
|
// Text returns the raw text of the node, e.g. ".actor" or "1*2".
|
||||||
Text() string
|
Text() string
|
||||||
|
|
||||||
|
resultColumn()
|
||||||
}
|
}
|
||||||
|
|
||||||
// baseNode is a base implementation of Node.
|
// baseNode is a base implementation of Node.
|
||||||
|
@ -5,12 +5,21 @@ import (
|
|||||||
"github.com/neilotoole/sq/libsq/core/errz"
|
"github.com/neilotoole/sq/libsq/core/errz"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// doFromTable renders a table selector to SQL.
|
||||||
|
//
|
||||||
|
// .actor --> FROM "actor"
|
||||||
|
// .actor:a --> FROM "actor" "a"
|
||||||
func doFromTable(rc *Context, tblSel *ast.TblSelectorNode) (string, error) {
|
func doFromTable(rc *Context, tblSel *ast.TblSelectorNode) (string, error) {
|
||||||
tblName, _ := tblSel.SelValue()
|
tblName := tblSel.TblName()
|
||||||
if tblName == "" {
|
if tblName == "" {
|
||||||
return "", errz.Errorf("selector has empty table name: {%s}", tblSel.Text())
|
return "", errz.Errorf("selector has empty table name: {%s}", tblSel.Text())
|
||||||
}
|
}
|
||||||
|
|
||||||
clause := "FROM " + rc.Dialect.Enquote(tblSel.TblName())
|
clause := "FROM " + rc.Dialect.Enquote(tblName)
|
||||||
|
alias := tblSel.Alias()
|
||||||
|
if alias != "" {
|
||||||
|
clause += " " + rc.Dialect.Enquote(alias)
|
||||||
|
}
|
||||||
|
|
||||||
return clause, nil
|
return clause, nil
|
||||||
}
|
}
|
||||||
|
@ -146,34 +146,39 @@ func newTblSelector(selNode *SelectorNode) (*TblSelectorNode, error) { //nolint:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TblName returns the table name. This is the raw value without punctuation.
|
// TblName returns the table name. This is the raw value without punctuation.
|
||||||
func (s *TblSelectorNode) TblName() string {
|
func (n *TblSelectorNode) TblName() string {
|
||||||
return s.tblName
|
return n.tblName
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alias returns the node's alias, or empty string.
|
||||||
|
func (n *TblSelectorNode) Alias() string {
|
||||||
|
return n.alias
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle returns the handle, which may be empty.
|
// Handle returns the handle, which may be empty.
|
||||||
func (s *TblSelectorNode) Handle() string {
|
func (n *TblSelectorNode) Handle() string {
|
||||||
return s.handle
|
return n.handle
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tabler implements the Tabler marker interface.
|
// Tabler implements the Tabler marker interface.
|
||||||
func (s *TblSelectorNode) tabler() {
|
func (n *TblSelectorNode) tabler() {
|
||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
// SelValue returns the table name.
|
// SelValue returns the table name.
|
||||||
// TODO: Can we get rid of this method SelValue?
|
// TODO: Can we get rid of this method SelValue?
|
||||||
func (s *TblSelectorNode) SelValue() (string, error) {
|
func (n *TblSelectorNode) SelValue() (string, error) {
|
||||||
return s.TblName(), nil
|
return n.TblName(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns a log/debug-friendly representation.
|
// String returns a log/debug-friendly representation.
|
||||||
func (s *TblSelectorNode) String() string {
|
func (n *TblSelectorNode) String() string {
|
||||||
text := nodeString(s)
|
text := nodeString(n)
|
||||||
selVal, err := s.SelValue()
|
selVal, err := n.SelValue()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
selVal = "error: " + err.Error()
|
selVal = "error: " + err.Error()
|
||||||
}
|
}
|
||||||
text += fmt.Sprintf(" | table: {%s} | datasource: {%s}", selVal, s.Handle())
|
text += fmt.Sprintf(" | table: {%s} | datasource: {%s}", selVal, n.Handle())
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,6 +195,10 @@ type TblColSelectorNode struct {
|
|||||||
colName string
|
colName string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resultColumn implements ast.ResultColumn.
|
||||||
|
func (n *TblColSelectorNode) resultColumn() {
|
||||||
|
}
|
||||||
|
|
||||||
// IsColumn implements ResultColumn.
|
// IsColumn implements ResultColumn.
|
||||||
func (n *TblColSelectorNode) IsColumn() bool {
|
func (n *TblColSelectorNode) IsColumn() bool {
|
||||||
return true
|
return true
|
||||||
@ -253,6 +262,10 @@ type ColSelectorNode struct {
|
|||||||
colName string
|
colName string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resultColumn implements ast.ResultColumn.
|
||||||
|
func (n *ColSelectorNode) resultColumn() {
|
||||||
|
}
|
||||||
|
|
||||||
// newColSelectorNode returns a ColSelectorNode constructed from ctx.
|
// newColSelectorNode returns a ColSelectorNode constructed from ctx.
|
||||||
func newColSelectorNode(selNode *SelectorNode) (*ColSelectorNode, error) { //nolint:unparam
|
func newColSelectorNode(selNode *SelectorNode) (*ColSelectorNode, error) { //nolint:unparam
|
||||||
n := &ColSelectorNode{SelectorNode: selNode}
|
n := &ColSelectorNode{SelectorNode: selNode}
|
||||||
|
@ -176,10 +176,10 @@ func (ng *engine) prepareNoTabler(ctx context.Context, qm *queryModel) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildTableFromClause builds the "FROM table" fragment.
|
// prepareFromTable builds the "FROM table" fragment.
|
||||||
//
|
//
|
||||||
// When this function returns, ng.rc will be set.
|
// When this function returns, ng.rc will be set.
|
||||||
func (ng *engine) buildTableFromClause(ctx context.Context, tblSel *ast.TblSelectorNode) (fromClause string,
|
func (ng *engine) prepareFromTable(ctx context.Context, tblSel *ast.TblSelectorNode) (fromClause string,
|
||||||
fromConn driver.Database, err error,
|
fromConn driver.Database, err error,
|
||||||
) {
|
) {
|
||||||
handle := tblSel.Handle()
|
handle := tblSel.Handle()
|
||||||
@ -215,10 +215,10 @@ func (ng *engine) buildTableFromClause(ctx context.Context, tblSel *ast.TblSelec
|
|||||||
return fromClause, fromConn, nil
|
return fromClause, fromConn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildJoinFromClause builds the "JOIN" clause.
|
// prepareFromJoin builds the "JOIN" clause.
|
||||||
//
|
//
|
||||||
// When this function returns, ng.rc will be set.
|
// When this function returns, ng.rc will be set.
|
||||||
func (ng *engine) buildJoinFromClause(ctx context.Context, fnJoin *ast.JoinNode) (fromClause string,
|
func (ng *engine) prepareFromJoin(ctx context.Context, fnJoin *ast.JoinNode) (fromClause string,
|
||||||
fromConn driver.Database, err error,
|
fromConn driver.Database, err error,
|
||||||
) {
|
) {
|
||||||
if fnJoin.LeftTbl() == nil || fnJoin.LeftTbl().TblName() == "" {
|
if fnJoin.LeftTbl() == nil || fnJoin.LeftTbl().TblName() == "" {
|
||||||
@ -230,16 +230,16 @@ func (ng *engine) buildJoinFromClause(ctx context.Context, fnJoin *ast.JoinNode)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if fnJoin.LeftTbl().Handle() != fnJoin.RightTbl().Handle() {
|
if fnJoin.LeftTbl().Handle() != fnJoin.RightTbl().Handle() {
|
||||||
return ng.crossSourceJoin(ctx, fnJoin)
|
return ng.joinCrossSource(ctx, fnJoin)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ng.singleSourceJoin(ctx, fnJoin)
|
return ng.joinSingleSource(ctx, fnJoin)
|
||||||
}
|
}
|
||||||
|
|
||||||
// singleSourceJoin sets up a join against a single source.
|
// joinSingleSource sets up a join against a single source.
|
||||||
//
|
//
|
||||||
// On return, ng.rc will be set.
|
// On return, ng.rc will be set.
|
||||||
func (ng *engine) singleSourceJoin(ctx context.Context, fnJoin *ast.JoinNode) (fromClause string,
|
func (ng *engine) joinSingleSource(ctx context.Context, fnJoin *ast.JoinNode) (fromClause string,
|
||||||
fromDB driver.Database, err error,
|
fromDB driver.Database, err error,
|
||||||
) {
|
) {
|
||||||
src, err := ng.qc.Collection.Get(fnJoin.LeftTbl().Handle())
|
src, err := ng.qc.Collection.Get(fnJoin.LeftTbl().Handle())
|
||||||
@ -267,11 +267,11 @@ func (ng *engine) singleSourceJoin(ctx context.Context, fnJoin *ast.JoinNode) (f
|
|||||||
return fromClause, fromDB, nil
|
return fromClause, fromDB, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// crossSourceJoin returns a FROM clause that forms part of
|
// joinCrossSource returns a FROM clause that forms part of
|
||||||
// the SQL SELECT statement against fromDB.
|
// the SQL SELECT statement against fromDB.
|
||||||
//
|
//
|
||||||
// On return, ng.rc will be set.
|
// On return, ng.rc will be set.
|
||||||
func (ng *engine) crossSourceJoin(ctx context.Context, fnJoin *ast.JoinNode) (fromClause string, fromDB driver.Database,
|
func (ng *engine) joinCrossSource(ctx context.Context, fnJoin *ast.JoinNode) (fromClause string, fromDB driver.Database,
|
||||||
err error,
|
err error,
|
||||||
) {
|
) {
|
||||||
leftTblName, rightTblName := fnJoin.LeftTbl().TblName(), fnJoin.RightTbl().TblName()
|
leftTblName, rightTblName := fnJoin.LeftTbl().TblName(), fnJoin.RightTbl().TblName()
|
||||||
|
@ -26,13 +26,12 @@ func (ng *engine) prepare(ctx context.Context, qm *queryModel) error {
|
|||||||
if err = ng.prepareNoTabler(ctx, qm); err != nil {
|
if err = ng.prepareNoTabler(ctx, qm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
case *ast.TblSelectorNode:
|
case *ast.TblSelectorNode:
|
||||||
if frags.From, ng.targetDB, err = ng.buildTableFromClause(ctx, node); err != nil {
|
if frags.From, ng.targetDB, err = ng.prepareFromTable(ctx, node); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case *ast.JoinNode:
|
case *ast.JoinNode:
|
||||||
if frags.From, ng.targetDB, err = ng.buildJoinFromClause(ctx, node); err != nil {
|
if frags.From, ng.targetDB, err = ng.prepareFromJoin(ctx, node); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@ -3,6 +3,8 @@ package libsq_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/neilotoole/sq/testh/tutil"
|
||||||
|
|
||||||
"github.com/neilotoole/sq/testh/sakila"
|
"github.com/neilotoole/sq/testh/sakila"
|
||||||
|
|
||||||
"github.com/neilotoole/sq/drivers/mysql"
|
"github.com/neilotoole/sq/drivers/mysql"
|
||||||
@ -10,6 +12,59 @@ import (
|
|||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//nolint:exhaustive
|
||||||
|
func TestQuery_table_alias(t *testing.T) {
|
||||||
|
testCases := []queryTestCase{
|
||||||
|
{
|
||||||
|
name: "table-alias",
|
||||||
|
in: `@sakila | .actor:a | .a.first_name`,
|
||||||
|
wantSQL: `SELECT "a"."first_name" FROM "actor" "a"`,
|
||||||
|
override: driverMap{mysql.Type: "SELECT `a`.`first_name` FROM `actor` `a`"},
|
||||||
|
wantRecCount: sakila.TblActorCount,
|
||||||
|
sinkFns: []SinkTestFunc{
|
||||||
|
assertSinkColName(0, "first_name"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "table-whitespace-alias",
|
||||||
|
in: `@sakila | .actor:"oy vey" | ."oy vey".first_name`,
|
||||||
|
wantSQL: `SELECT "oy vey"."first_name" FROM "actor" "oy vey"`,
|
||||||
|
override: driverMap{mysql.Type: "SELECT `oy vey`.`first_name` FROM `actor` `oy vey`"},
|
||||||
|
wantRecCount: sakila.TblActorCount,
|
||||||
|
sinkFns: []SinkTestFunc{
|
||||||
|
assertSinkColName(0, "first_name"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "table-whitespace-alias-with-col-alias",
|
||||||
|
in: `@sakila | .actor:"oy vey" | ."oy vey".first_name:given_name`,
|
||||||
|
wantSQL: `SELECT "oy vey"."first_name" AS "given_name" FROM "actor" "oy vey"`,
|
||||||
|
override: driverMap{mysql.Type: "SELECT `oy vey`.`first_name` AS `given_name` FROM `actor` `oy vey`"},
|
||||||
|
wantRecCount: sakila.TblActorCount,
|
||||||
|
sinkFns: []SinkTestFunc{
|
||||||
|
assertSinkColName(0, "given_name"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "table-whitespace-alias-with-col-whitespace-alias",
|
||||||
|
in: `@sakila | .actor:"oy vey" | ."oy vey".first_name:"oy vey"`,
|
||||||
|
wantSQL: `SELECT "oy vey"."first_name" AS "oy vey" FROM "actor" "oy vey"`,
|
||||||
|
override: driverMap{mysql.Type: "SELECT `oy vey`.`first_name` AS `oy vey` FROM `actor` `oy vey`"},
|
||||||
|
wantRecCount: sakila.TblActorCount,
|
||||||
|
sinkFns: []SinkTestFunc{
|
||||||
|
assertSinkColName(0, "oy vey"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range testCases {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tutil.Name(i, tc.name), func(t *testing.T) {
|
||||||
|
execQueryTestCase(t, tc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//nolint:exhaustive,lll
|
//nolint:exhaustive,lll
|
||||||
func TestQuery_join(t *testing.T) {
|
func TestQuery_join(t *testing.T) {
|
||||||
testCases := []queryTestCase{
|
testCases := []queryTestCase{
|
||||||
|
@ -81,7 +81,6 @@ func execQueryTestCase(t *testing.T, tc queryTestCase) {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
coll := testh.New(t).NewCollection(sakila.SQLLatest()...)
|
coll := testh.New(t).NewCollection(sakila.SQLLatest()...)
|
||||||
// coll := testh.New(t).NewCollection(sakila.Pg)
|
|
||||||
|
|
||||||
for _, src := range coll.Sources() {
|
for _, src := range coll.Sources() {
|
||||||
src := src
|
src := src
|
||||||
@ -153,7 +152,7 @@ func assertSinkColValue(colIndex int, val any) SinkTestFunc {
|
|||||||
|
|
||||||
// assertSinkColValue returns a SinkTestFunc that asserts that
|
// assertSinkColValue returns a SinkTestFunc that asserts that
|
||||||
// the name of column colIndex matches name.
|
// the name of column colIndex matches name.
|
||||||
func assertSinkColName(colIndex int, name string) SinkTestFunc {
|
func assertSinkColName(colIndex int, name string) SinkTestFunc { //nolint:unparam
|
||||||
return func(t testing.TB, sink *testh.RecordSink) {
|
return func(t testing.TB, sink *testh.RecordSink) {
|
||||||
assert.Equal(t, name, sink.RecMeta[colIndex].Name(), "column %d", colIndex)
|
assert.Equal(t, name, sink.RecMeta[colIndex].Name(), "column %d", colIndex)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user