mirror of
https://github.com/neilotoole/sq.git
synced 2024-11-24 11:54:37 +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 ☢️.
|
||||
|
||||
## 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
|
||||
|
||||
### Fixed
|
||||
@ -641,6 +652,7 @@ make working with lots of sources much easier.
|
||||
[#258]: https://github.com/neilotoole/sq/issues/258
|
||||
[#261]: https://github.com/neilotoole/sq/issues/261
|
||||
[#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.3]: https://github.com/neilotoole/sq/compare/v0.15.2...v0.15.3
|
||||
|
@ -27,6 +27,10 @@ type ExprElementNode struct {
|
||||
exprNode *ExprNode
|
||||
}
|
||||
|
||||
// resultColumn implements ast.ResultColumn.
|
||||
func (ex *ExprElementNode) resultColumn() {
|
||||
}
|
||||
|
||||
// String returns a log/debug-friendly representation.
|
||||
func (ex *ExprElementNode) String() string {
|
||||
str := nodeString(ex)
|
||||
|
@ -19,6 +19,12 @@ type FuncNode struct {
|
||||
proprietary bool
|
||||
}
|
||||
|
||||
// resultColumn implements ast.ResultColumn.
|
||||
//
|
||||
// REVISIT: should ast.FuncNode implement ast.ResultColumn?
|
||||
func (fn *FuncNode) resultColumn() {
|
||||
}
|
||||
|
||||
// FuncName returns the function name.
|
||||
func (fn *FuncNode) FuncName() string {
|
||||
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() string
|
||||
|
||||
resultColumn()
|
||||
}
|
||||
|
||||
// baseNode is a base implementation of Node.
|
||||
|
@ -5,12 +5,21 @@ import (
|
||||
"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) {
|
||||
tblName, _ := tblSel.SelValue()
|
||||
tblName := tblSel.TblName()
|
||||
if tblName == "" {
|
||||
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
|
||||
}
|
||||
|
@ -146,34 +146,39 @@ func newTblSelector(selNode *SelectorNode) (*TblSelectorNode, error) { //nolint:
|
||||
}
|
||||
|
||||
// TblName returns the table name. This is the raw value without punctuation.
|
||||
func (s *TblSelectorNode) TblName() string {
|
||||
return s.tblName
|
||||
func (n *TblSelectorNode) TblName() string {
|
||||
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.
|
||||
func (s *TblSelectorNode) Handle() string {
|
||||
return s.handle
|
||||
func (n *TblSelectorNode) Handle() string {
|
||||
return n.handle
|
||||
}
|
||||
|
||||
// Tabler implements the Tabler marker interface.
|
||||
func (s *TblSelectorNode) tabler() {
|
||||
func (n *TblSelectorNode) tabler() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
// SelValue returns the table name.
|
||||
// TODO: Can we get rid of this method SelValue?
|
||||
func (s *TblSelectorNode) SelValue() (string, error) {
|
||||
return s.TblName(), nil
|
||||
func (n *TblSelectorNode) SelValue() (string, error) {
|
||||
return n.TblName(), nil
|
||||
}
|
||||
|
||||
// String returns a log/debug-friendly representation.
|
||||
func (s *TblSelectorNode) String() string {
|
||||
text := nodeString(s)
|
||||
selVal, err := s.SelValue()
|
||||
func (n *TblSelectorNode) String() string {
|
||||
text := nodeString(n)
|
||||
selVal, err := n.SelValue()
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
@ -190,6 +195,10 @@ type TblColSelectorNode struct {
|
||||
colName string
|
||||
}
|
||||
|
||||
// resultColumn implements ast.ResultColumn.
|
||||
func (n *TblColSelectorNode) resultColumn() {
|
||||
}
|
||||
|
||||
// IsColumn implements ResultColumn.
|
||||
func (n *TblColSelectorNode) IsColumn() bool {
|
||||
return true
|
||||
@ -253,6 +262,10 @@ type ColSelectorNode struct {
|
||||
colName string
|
||||
}
|
||||
|
||||
// resultColumn implements ast.ResultColumn.
|
||||
func (n *ColSelectorNode) resultColumn() {
|
||||
}
|
||||
|
||||
// newColSelectorNode returns a ColSelectorNode constructed from ctx.
|
||||
func newColSelectorNode(selNode *SelectorNode) (*ColSelectorNode, error) { //nolint:unparam
|
||||
n := &ColSelectorNode{SelectorNode: selNode}
|
||||
|
@ -176,10 +176,10 @@ func (ng *engine) prepareNoTabler(ctx context.Context, qm *queryModel) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildTableFromClause builds the "FROM table" fragment.
|
||||
// prepareFromTable 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,
|
||||
func (ng *engine) prepareFromTable(ctx context.Context, tblSel *ast.TblSelectorNode) (fromClause string,
|
||||
fromConn driver.Database, err error,
|
||||
) {
|
||||
handle := tblSel.Handle()
|
||||
@ -215,10 +215,10 @@ func (ng *engine) buildTableFromClause(ctx context.Context, tblSel *ast.TblSelec
|
||||
return fromClause, fromConn, nil
|
||||
}
|
||||
|
||||
// buildJoinFromClause builds the "JOIN" clause.
|
||||
// prepareFromJoin 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,
|
||||
func (ng *engine) prepareFromJoin(ctx context.Context, fnJoin *ast.JoinNode) (fromClause string,
|
||||
fromConn driver.Database, err error,
|
||||
) {
|
||||
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() {
|
||||
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.
|
||||
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,
|
||||
) {
|
||||
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
|
||||
}
|
||||
|
||||
// crossSourceJoin returns a FROM clause that forms part of
|
||||
// joinCrossSource 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,
|
||||
func (ng *engine) joinCrossSource(ctx context.Context, fnJoin *ast.JoinNode) (fromClause string, fromDB driver.Database,
|
||||
err error,
|
||||
) {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
default:
|
||||
|
@ -3,6 +3,8 @@ package libsq_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/neilotoole/sq/testh/tutil"
|
||||
|
||||
"github.com/neilotoole/sq/testh/sakila"
|
||||
|
||||
"github.com/neilotoole/sq/drivers/mysql"
|
||||
@ -10,6 +12,59 @@ import (
|
||||
_ "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
|
||||
func TestQuery_join(t *testing.T) {
|
||||
testCases := []queryTestCase{
|
||||
|
@ -81,7 +81,6 @@ func execQueryTestCase(t *testing.T, tc queryTestCase) {
|
||||
t.Helper()
|
||||
|
||||
coll := testh.New(t).NewCollection(sakila.SQLLatest()...)
|
||||
// coll := testh.New(t).NewCollection(sakila.Pg)
|
||||
|
||||
for _, src := range coll.Sources() {
|
||||
src := src
|
||||
@ -153,7 +152,7 @@ func assertSinkColValue(colIndex int, val any) SinkTestFunc {
|
||||
|
||||
// assertSinkColValue returns a SinkTestFunc that asserts that
|
||||
// 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) {
|
||||
assert.Equal(t, name, sink.RecMeta[colIndex].Name(), "column %d", colIndex)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user