Skip to content

Commit

Permalink
Merge pull request #148 from doug-martin/window_function_support
Browse files Browse the repository at this point in the history
Window function support
  • Loading branch information
doug-martin authored Aug 25, 2019
2 parents b8f9834 + 939c031 commit ce5475c
Show file tree
Hide file tree
Showing 30 changed files with 1,570 additions and 34 deletions.
15 changes: 13 additions & 2 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,31 @@ on:
pull_request: # Specify a second event with pattern matching
jobs:
test:
name: Test go ${{ matrix.go_version }}
name: Test go - ${{ matrix.go_version }} mysql - ${{ matrix.db_versions.mysql_version}} postgres - ${{ matrix.db_versions.postgres_version}}
runs-on: ubuntu-latest
strategy:
matrix:
go_version: ["1.10", "1.11", "latest"]
db_versions:
- mysql_version: 5
postgres_version: 9.6
- mysql_version: 5
postgres_version: "10.10"
- mysql_version: 8
postgres_version: 11.5
steps:
- name: checkout
uses: actions/checkout@v1
- name: Test
env:
GO_VERSION: ${{ matrix.go_version }}
MYSQL_VERSION: ${{ matrix.db_versions.mysql_version }}
POSTGRES_VERSION: ${{ matrix.db_versions.postgres_version }}
run: docker-compose run goqu-coverage
- name: Upload coverage to Codecov
run: bash <(curl -s https://codecov.io/bash) -n $GO_VERSION -e GO_VERSION,GITHUB_WORKFLOW,GITHUB_ACTION
run: bash <(curl -s https://codecov.io/bash) -n $GO_VERSION -e GO_VERSION,MYSQL_VERSION,POSTGRES_VERSION,GITHUB_WORKFLOW,GITHUB_ACTION
env:
CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}}
GO_VERSION: ${{ matrix.go_version }}
MYSQL_VERSION: ${{ matrix.db_versions.mysql_version }}
POSTGRES_VERSION: ${{ matrix.db_versions.postgres_version }}
4 changes: 4 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 8.5.0

* [ADDED] Window Function support [#128](https://github.com/doug-martin/goqu/issues/128) - [@Xuyuanp](https://github.com/Xuyuanp)

## 8.4.1

* [FIXED] Returning func be able to handle nil [#140](https://github.com/doug-martin/goqu/issues/140)
Expand Down
28 changes: 12 additions & 16 deletions database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import (
"sync"
"testing"

"github.com/stretchr/testify/assert"

"github.com/DATA-DOG/go-sqlmock"
"github.com/doug-martin/goqu/v8/internal/errors"
"github.com/stretchr/testify/suite"
Expand Down Expand Up @@ -317,9 +315,8 @@ func (ds *databaseSuite) TestWithTx() {
}

func (ds *databaseSuite) TestDataRace() {
t := ds.T()
mDb, mock, err := sqlmock.New()
assert.NoError(t, err)
ds.NoError(err)
db := newDatabase("mock", mDb)

const concurrency = 10
Expand All @@ -340,10 +337,10 @@ func (ds *databaseSuite) TestDataRace() {
sql := db.From("items").Limit(1)
var item testActionItem
found, err := sql.ScanStruct(&item)
assert.NoError(t, err)
assert.True(t, found)
assert.Equal(t, item.Address, "111 Test Addr")
assert.Equal(t, item.Name, "Test1")
ds.NoError(err)
ds.True(found)
ds.Equal(item.Address, "111 Test Addr")
ds.Equal(item.Name, "Test1")
}()
}

Expand Down Expand Up @@ -661,13 +658,12 @@ func (tds *txdatabaseSuite) TestWrap() {
}

func (tds *txdatabaseSuite) TestDataRace() {
t := tds.T()
mDb, mock, err := sqlmock.New()
assert.NoError(t, err)
tds.NoError(err)
mock.ExpectBegin()
db := newDatabase("mock", mDb)
tx, err := db.Begin()
assert.NoError(t, err)
tds.NoError(err)

const concurrency = 10

Expand All @@ -687,16 +683,16 @@ func (tds *txdatabaseSuite) TestDataRace() {
sql := tx.From("items").Limit(1)
var item testActionItem
found, err := sql.ScanStruct(&item)
assert.NoError(t, err)
assert.True(t, found)
assert.Equal(t, item.Address, "111 Test Addr")
assert.Equal(t, item.Name, "Test1")
tds.NoError(err)
tds.True(found)
tds.Equal(item.Address, "111 Test Addr")
tds.Equal(item.Name, "Test1")
}()
}

wg.Wait()
mock.ExpectCommit()
assert.NoError(t, tx.Commit())
tds.NoError(tx.Commit())
}

func TestTxDatabaseSuite(t *testing.T) {
Expand Down
8 changes: 8 additions & 0 deletions dialect/mysql/mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func DialectOptions() *goqu.SQLDialectOptions {
opts.SupportsWithCTE = false
opts.SupportsWithCTERecursive = false
opts.SupportsDistinctOn = false
opts.SupportsWindowFunction = false

opts.UseFromClauseForMultipleUpdateTables = false

Expand Down Expand Up @@ -65,6 +66,13 @@ func DialectOptions() *goqu.SQLDialectOptions {
return opts
}

func DialectOptionsV8() *goqu.SQLDialectOptions {
opts := DialectOptions()
opts.SupportsWindowFunction = true
return opts
}

func init() {
goqu.RegisterDialect("mysql", DialectOptions())
goqu.RegisterDialect("mysql8", DialectOptionsV8())
}
40 changes: 40 additions & 0 deletions dialect/mysql/mysql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"database/sql"
"fmt"
"os"
"strconv"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -393,6 +395,44 @@ func (mt *mysqlTest) TestInsert_OnConflict() {
mt.EqualError(err, "goqu: dialect does not support upsert with where clause [dialect=mysql]")
}

func (mt *mysqlTest) TestWindowFunction() {
var version string
ok, err := mt.db.Select(goqu.Func("version")).ScanVal(&version)
mt.NoError(err)
mt.True(ok)

fields := strings.Split(version, ".")
mt.True(len(fields) > 0)
major, err := strconv.Atoi(fields[0])
mt.NoError(err)
if major < 8 {
fmt.Printf("SKIPPING MYSQL WINDOW FUNCTION TEST BECAUSE VERSION IS < 8 [mysql_version:=%d]\n", major)
return
}

ds := mt.db.From("entry").
Select("int", goqu.ROW_NUMBER().OverName(goqu.I("w")).As("id")).
Window(goqu.W("w").OrderBy(goqu.I("int").Desc()))

var entries []entry
mt.NoError(ds.WithDialect("mysql8").ScanStructs(&entries))

mt.Equal([]entry{
{Int: 9, ID: 1},
{Int: 8, ID: 2},
{Int: 7, ID: 3},
{Int: 6, ID: 4},
{Int: 5, ID: 5},
{Int: 4, ID: 6},
{Int: 3, ID: 7},
{Int: 2, ID: 8},
{Int: 1, ID: 9},
{Int: 0, ID: 10},
}, entries)

mt.Error(ds.WithDialect("mysql").ScanStructs(&entries), "goqu: adapter does not support window function clause")
}

func TestMysqlSuite(t *testing.T) {
suite.Run(t, new(mysqlTest))
}
22 changes: 22 additions & 0 deletions dialect/postgres/postgres_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,28 @@ func (pt *postgresTest) TestInsert_OnConflict() {
pt.Equal("upsert", entry9.String)
}

func (pt *postgresTest) TestWindowFunction() {
ds := pt.db.From("entry").
Select("int", goqu.ROW_NUMBER().OverName(goqu.I("w")).As("id")).
Window(goqu.W("w").OrderBy(goqu.I("int").Desc()))

var entries []entry
pt.NoError(ds.ScanStructs(&entries))

pt.Equal([]entry{
{Int: 9, ID: 1},
{Int: 8, ID: 2},
{Int: 7, ID: 3},
{Int: 6, ID: 4},
{Int: 5, ID: 5},
{Int: 4, ID: 6},
{Int: 3, ID: 7},
{Int: 2, ID: 8},
{Int: 1, ID: 9},
{Int: 0, ID: 10},
}, entries)
}

func TestPostgresSuite(t *testing.T) {
suite.Run(t, new(postgresTest))
}
1 change: 1 addition & 0 deletions dialect/sqlite3/sqlite3.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func DialectOptions() *goqu.SQLDialectOptions {
opts.SupportsMultipleUpdateTables = false
opts.WrapCompoundsInParens = false
opts.SupportsDistinctOn = false
opts.SupportsWindowFunction = false

opts.PlaceHolderRune = '?'
opts.IncludePlaceholderNum = false
Expand Down
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ version: "2"

services:
postgres:
image: postgres:9.6
image: "postgres:${POSTGRES_VERSION}"
environment:
- "POSTGRES_USER=postgres"
- "POSTGRES_DB=goqupostgres"
expose:
- "5432"

mysql:
image: mysql:5
image: "mysql:${MYSQL_VERSION}"
environment:
- "MYSQL_DATABASE=goqumysql"
- "MYSQL_ALLOW_EMPTY_PASSWORD=yes"
Expand Down
40 changes: 39 additions & 1 deletion docs/selecting.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* [`Offset`](#offset)
* [`GroupBy`](#group_by)
* [`Having`](#having)
* [`Window`](#window)
* Executing Queries
* [`ScanStructs`](#scan-structs) - Scans rows into a slice of structs
* [`ScanStruct`](#scan-struct) - Scans a row into a slice a struct, returns false if a row wasnt found
Expand Down Expand Up @@ -610,6 +611,42 @@ Output:
SELECT * FROM "test" GROUP BY "age" HAVING (SUM("income") > 1000)
```


<a name="window"></a>
**[`Window Function`](https://godoc.org/github.com/doug-martin/goqu/#SelectDataset.Window)**

**NOTE** currently only the `postgres`, `mysql8` (NOT `mysql`) and the default dialect support `Window Function`

To use windowing in select you can use the `Over` method on an `SQLFunction`

```go
sql, _, _ := goqu.From("test").Select(
goqu.ROW_NUMBER().Over(goqu.W().PartitionBy("a").OrderBy(goqu.I("b").Asc())),
)
fmt.Println(sql)
```

Output:

```
SELECT ROW_NUMBER() OVER (PARTITION BY "a" ORDER BY "b") FROM "test"
```

`goqu` also supports the `WINDOW` clause.

```go
sql, _, _ := goqu.From("test").
Select(goqu.ROW_NUMBER().OverName(goqu.I("w"))).
Window(goqu.W("w").PartitionBy("a").OrderBy(goqu.I("b").Asc()))
fmt.Println(sql)
```

Output:

```
SELECT ROW_NUMBER() OVER "w" FROM "test" WINDOW "w" AS (PARTITION BY "a" ORDER BY "b")
```

## Executing Queries

To execute your query use [`goqu.Database#From`](https://godoc.org/github.com/doug-martin/goqu/#Database.From) to create your dataset
Expand Down Expand Up @@ -748,4 +785,5 @@ if err := db.From("user").Pluck(&ids, "id"); err != nil{
return
}
fmt.Printf("\nIds := %+v", ids)
```
```

2 changes: 1 addition & 1 deletion exp/col.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type columnList struct {
}

func NewColumnListExpression(vals ...interface{}) ColumnListExpression {
var cols []Expression
cols := []Expression{}
for _, val := range vals {
switch t := val.(type) {
case nil: // do nothing
Expand Down
41 changes: 41 additions & 0 deletions exp/exp.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,11 @@ type (
End() interface{}
}

Windowable interface {
Over(WindowExpression) SQLWindowFunctionExpression
OverName(IdentifierExpression) SQLWindowFunctionExpression
}

// Expression for representing a SQLFunction(e.g. COUNT, SUM, MIN, MAX...)
SQLFunctionExpression interface {
Expression
Expand All @@ -350,6 +355,7 @@ type (
Isable
Inable
Likeable
Windowable
// The function name
Name() string
// Arguments to be passed to the function
Expand All @@ -360,6 +366,41 @@ type (
Col() IdentifierExpression
Val() interface{}
}

SQLWindowFunctionExpression interface {
Expression
Aliaseable
Rangeable
Comparable
Isable
Inable
Likeable
Func() SQLFunctionExpression

Window() WindowExpression
WindowName() IdentifierExpression

HasWindow() bool
HasWindowName() bool
}

WindowExpression interface {
Expression

Name() IdentifierExpression
HasName() bool

Parent() IdentifierExpression
HasParent() bool
PartitionCols() ColumnListExpression
HasPartitionBy() bool
OrderCols() ColumnListExpression
HasOrder() bool

Inherit(parent string) WindowExpression
PartitionBy(cols ...interface{}) WindowExpression
OrderBy(cols ...interface{}) WindowExpression
}
)

const (
Expand Down
8 changes: 8 additions & 0 deletions exp/func.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,11 @@ func (sfe sqlFunctionExpression) IsTrue() BooleanExpression { retu
func (sfe sqlFunctionExpression) IsNotTrue() BooleanExpression { return isNot(sfe, true) }
func (sfe sqlFunctionExpression) IsFalse() BooleanExpression { return is(sfe, false) }
func (sfe sqlFunctionExpression) IsNotFalse() BooleanExpression { return isNot(sfe, false) }

func (sfe sqlFunctionExpression) Over(we WindowExpression) SQLWindowFunctionExpression {
return NewSQLWindowFunctionExpression(sfe, nil, we)
}

func (sfe sqlFunctionExpression) OverName(windowName IdentifierExpression) SQLWindowFunctionExpression {
return NewSQLWindowFunctionExpression(sfe, windowName, nil)
}
Loading

0 comments on commit ce5475c

Please sign in to comment.