12 Commits

Author SHA1 Message Date
Pijus Kamandulis
3584f9b5ce Enable ARM builds for Windows and Linux 2024-11-16 20:09:24 +02:00
Pijus Kamandulis
c7d01b4593 Fix cosmos explorer incorrect redirect 2024-11-14 18:42:17 +02:00
erikzeneco
2834f3f641 check isUpsert header in POST document request (#5)
* check isUpsert header in POST document request

* Verify response code on "CreateItem that already exists" test

---------

Co-authored-by: Pijus Kamandulis <pikami@users.noreply.github.com>
2024-11-01 21:11:59 +02:00
Pijus Kamandulis
a6b5d32ff7 Merge pull request #4 from pikami/erikzeneco/serve_request_paths_with_trailing_slashes
Erikzeneco/serve request paths with trailing slashes
2024-10-29 18:06:56 +02:00
Pijus Kamandulis
0e98e3481a Strip trailing slash using middleware 2024-10-28 20:20:52 +02:00
Erik Zentveld
827046f634 re-add removed blank lines 2024-10-28 16:18:49 +01:00
Erik Zentveld
475d586dc5 Merge branch 'master' into serve_request_paths_with_trailing_slashes 2024-10-28 14:37:01 +01:00
Erik Zentveld
9abef691d6 serve request paths with trailing slashes, as sent by python client 2024-10-28 13:29:26 +01:00
Pijus Kamandulis
62dcbc1f2b Merge pull request #1 from erikzeneco/master
Update README.md
2024-10-16 18:34:14 +03:00
erikzeneco
2f42651fb7 Update README.md
Use envPrefix for parameter passed in as environment variable with docker.
2024-10-16 16:24:53 +02:00
Pijus Kamandulis
20af73ee9c Partial JOIN implementation 2024-07-17 21:56:17 +03:00
Pijus Kamandulis
3bdff9b643 Implement Mathematical Functions 2024-06-19 00:44:46 +03:00
23 changed files with 6013 additions and 1099 deletions

View File

@@ -9,11 +9,6 @@ builds:
- arm64 - arm64
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
ignore:
- goos: linux
goarch: arm64
- goos: windows
goarch: arm64
release: release:
prerelease: auto prerelease: auto

View File

@@ -1,6 +1,6 @@
# Cosmium # Cosmium
Cosmium is a lightweight Cosmos DB emulator designed to facilitate local development and testing. While it aims to provide developers with a solution for running a local database during development, it's important to note that it's not 100% compatible with Cosmos DB. However, it serves as a convenient tool for E2E or integration tests during the CI/CD pipeline. Read more about compatibility [here](./docs/compatibility.md). Cosmium is a lightweight Cosmos DB emulator designed to facilitate local development and testing. While it aims to provide developers with a solution for running a local database during development, it's important to note that it's not 100% compatible with Cosmos DB. However, it serves as a convenient tool for E2E or integration tests during the CI/CD pipeline. Read more about compatibility [here](./docs/COMPATIBILITY.md).
One of Cosmium's notable features is its ability to save and load state to a single JSON file. This feature makes it easy to load different test cases or share state with other developers, enhancing collaboration and efficiency in development workflows. One of Cosmium's notable features is its ability to save and load state to a single JSON file. This feature makes it easy to load different test cases or share state with other developers, enhancing collaboration and efficiency in development workflows.
@@ -56,7 +56,7 @@ If you wan to run the application using docker, configure it using environment v
```sh ```sh
docker run --rm \ docker run --rm \
-e Persist=/save.json \ -e COSMIUM_PERSIST=/save.json \
-v ./save.json:/save.json \ -v ./save.json:/save.json \
-p 8081:8081 \ -p 8081:8081 \
ghcr.io/pikami/cosmium ghcr.io/pikami/cosmium

View File

@@ -8,8 +8,9 @@ import (
) )
const ( const (
DefaultAccountKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==" DefaultAccountKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
EnvPrefix = "COSMIUM_" EnvPrefix = "COSMIUM_"
ExplorerBaseUrlLocation = "/_explorer"
) )
var Config = ServerConfig{} var Config = ServerConfig{}
@@ -45,6 +46,7 @@ func ParseFlags() {
Config.DatabaseDomain = Config.Host Config.DatabaseDomain = Config.Host
Config.DatabaseEndpoint = fmt.Sprintf("https://%s:%d/", Config.Host, Config.Port) Config.DatabaseEndpoint = fmt.Sprintf("https://%s:%d/", Config.Host, Config.Port)
Config.AccountKey = *accountKey Config.AccountKey = *accountKey
Config.ExplorerBaseUrlLocation = ExplorerBaseUrlLocation
} }
func setFlagsFromEnvironment() (err error) { func setFlagsFromEnvironment() (err error) {

View File

@@ -6,14 +6,15 @@ type ServerConfig struct {
DatabaseEndpoint string DatabaseEndpoint string
AccountKey string AccountKey string
ExplorerPath string ExplorerPath string
Port int Port int
Host string Host string
TLS_CertificatePath string TLS_CertificatePath string
TLS_CertificateKey string TLS_CertificateKey string
InitialDataFilePath string InitialDataFilePath string
PersistDataFilePath string PersistDataFilePath string
DisableAuth bool DisableAuth bool
DisableTls bool DisableTls bool
Debug bool Debug bool
ExplorerBaseUrlLocation string
} }

View File

@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"strconv"
jsonpatch "github.com/evanphx/json-patch/v5" jsonpatch "github.com/evanphx/json-patch/v5"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -223,6 +224,11 @@ func DocumentsPost(c *gin.Context) {
return return
} }
isUpsert, _ := strconv.ParseBool(c.GetHeader("x-ms-documentdb-is-upsert"))
if isUpsert {
repositories.DeleteDocument(databaseId, collectionId, requestBody["id"].(string))
}
createdDocument, status := repositories.CreateDocument(databaseId, collectionId, requestBody) createdDocument, status := repositories.CreateDocument(databaseId, collectionId, requestBody)
if status == repositorymodels.Conflict { if status == repositorymodels.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"}) c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"})

View File

@@ -8,7 +8,7 @@ import (
) )
func RegisterExplorerHandlers(router *gin.Engine) { func RegisterExplorerHandlers(router *gin.Engine) {
explorer := router.Group("/_explorer") explorer := router.Group(config.Config.ExplorerBaseUrlLocation)
{ {
explorer.Use(func(ctx *gin.Context) { explorer.Use(func(ctx *gin.Context) {
if ctx.Param("filepath") == "/config.json" { if ctx.Param("filepath") == "/config.json" {

View File

@@ -14,7 +14,7 @@ func Authentication() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
requestUrl := c.Request.URL.String() requestUrl := c.Request.URL.String()
if config.Config.DisableAuth || if config.Config.DisableAuth ||
strings.HasPrefix(requestUrl, "/_explorer") || strings.HasPrefix(requestUrl, config.Config.ExplorerBaseUrlLocation) ||
strings.HasPrefix(requestUrl, "/cosmium") { strings.HasPrefix(requestUrl, "/cosmium") {
return return
} }

View File

@@ -0,0 +1,21 @@
package middleware
import (
"strings"
"github.com/gin-gonic/gin"
"github.com/pikami/cosmium/api/config"
)
func StripTrailingSlashes(r *gin.Engine) gin.HandlerFunc {
return func(c *gin.Context) {
path := c.Request.URL.Path
if len(path) > 1 && path[len(path)-1] == '/' && !strings.Contains(path, config.Config.ExplorerBaseUrlLocation) {
c.Request.URL.Path = path[:len(path)-1]
r.HandleContext(c)
c.Abort()
return
}
c.Next()
}
}

View File

@@ -13,12 +13,15 @@ import (
) )
func CreateRouter() *gin.Engine { func CreateRouter() *gin.Engine {
router := gin.Default() router := gin.Default(func(e *gin.Engine) {
e.RedirectTrailingSlash = false
})
if config.Config.Debug { if config.Config.Debug {
router.Use(middleware.RequestLogger()) router.Use(middleware.RequestLogger())
} }
router.Use(middleware.StripTrailingSlashes(router))
router.Use(middleware.Authentication()) router.Use(middleware.Authentication())
router.GET("/dbs/:databaseId/colls/:collId/pkranges", handlers.GetPartitionKeyRanges) router.GET("/dbs/:databaseId/colls/:collId/pkranges", handlers.GetPartitionKeyRanges)

View File

@@ -10,6 +10,7 @@ import (
func runTestServer() *httptest.Server { func runTestServer() *httptest.Server {
config.Config.AccountKey = config.DefaultAccountKey config.Config.AccountKey = config.DefaultAccountKey
config.Config.ExplorerPath = "/tmp/nothing" config.Config.ExplorerPath = "/tmp/nothing"
config.Config.ExplorerBaseUrlLocation = config.ExplorerBaseUrlLocation
return httptest.NewServer(api.CreateRouter()) return httptest.NewServer(api.CreateRouter())
} }

View File

@@ -220,4 +220,92 @@ func Test_Documents_Patch(t *testing.T) {
panic(err) panic(err)
} }
}) })
t.Run("CreateItem", func(t *testing.T) {
context := context.TODO()
item := map[string]interface{}{
"Id": "6789011",
"pk": "456",
"newField": "newValue2",
}
bytes, err := json.Marshal(item)
assert.Nil(t, err)
r, err2 := collectionClient.CreateItem(
context,
azcosmos.PartitionKey{},
bytes,
&azcosmos.ItemOptions{
EnableContentResponseOnWrite: false,
},
)
assert.NotNil(t, r)
assert.Nil(t, err2)
})
t.Run("CreateItem that already exists", func(t *testing.T) {
context := context.TODO()
item := map[string]interface{}{"id": "12345", "pk": "123", "isCool": false, "arr": []int{1, 2, 3}}
bytes, err := json.Marshal(item)
assert.Nil(t, err)
r, err := collectionClient.CreateItem(
context,
azcosmos.PartitionKey{},
bytes,
&azcosmos.ItemOptions{
EnableContentResponseOnWrite: false,
},
)
assert.NotNil(t, r)
assert.NotNil(t, err)
var respErr *azcore.ResponseError
if errors.As(err, &respErr) {
assert.Equal(t, http.StatusConflict, respErr.StatusCode)
} else {
panic(err)
}
})
t.Run("UpsertItem new", func(t *testing.T) {
context := context.TODO()
item := map[string]interface{}{"id": "123456", "pk": "1234", "isCool": false, "arr": []int{1, 2, 3}}
bytes, err := json.Marshal(item)
assert.Nil(t, err)
r, err2 := collectionClient.UpsertItem(
context,
azcosmos.PartitionKey{},
bytes,
&azcosmos.ItemOptions{
EnableContentResponseOnWrite: false,
},
)
assert.NotNil(t, r)
assert.Nil(t, err2)
})
t.Run("UpsertItem that already exists", func(t *testing.T) {
context := context.TODO()
item := map[string]interface{}{"id": "12345", "pk": "123", "isCool": false, "arr": []int{1, 2, 3, 4}}
bytes, err := json.Marshal(item)
assert.Nil(t, err)
r, err2 := collectionClient.UpsertItem(
context,
azcosmos.PartitionKey{},
bytes,
&azcosmos.ItemOptions{
EnableContentResponseOnWrite: false,
},
)
assert.NotNil(t, r)
assert.Nil(t, err2)
})
} }

View File

@@ -0,0 +1,41 @@
package tests_test
import (
"fmt"
"net/http"
"net/url"
"testing"
"time"
"github.com/pikami/cosmium/api/config"
"github.com/pikami/cosmium/internal/authentication"
"github.com/stretchr/testify/assert"
)
// Request document with trailing slash like python cosmosdb client does.
func Test_Documents_Read_Trailing_Slash(t *testing.T) {
ts, _ := documents_InitializeDb(t)
defer ts.Close()
t.Run("Read doc with client that appends slash to path", func(t *testing.T) {
resourceIdTemplate := "dbs/%s/colls/%s/docs/%s"
path := fmt.Sprintf(resourceIdTemplate, testDatabaseName, testCollectionName, "12345")
testUrl := ts.URL + "/" + path + "/"
date := time.Now().Format(time.RFC1123)
signature := authentication.GenerateSignature("GET", "docs", path, date, config.Config.AccountKey)
httpClient := &http.Client{}
req, _ := http.NewRequest("GET", testUrl, nil)
req.Header.Add("x-ms-date", date)
req.Header.Add("authorization", "sig="+url.QueryEscape(signature))
res, err := httpClient.Do(req)
assert.Nil(t, err)
if res != nil {
defer res.Body.Close()
assert.Equal(t, http.StatusOK, res.StatusCode, "Expected HTTP status 200 OK")
} else {
t.FailNow()
}
})
}

View File

@@ -15,8 +15,9 @@ Cosmium strives to support the core features of Cosmos DB, including:
## Compatibility Matrix ## Compatibility Matrix
### Features ### Features
| Feature | Implemented | | Feature | Implemented |
|-------------------------------|-------------| | ----------------------------- | ----------- |
| Subqueries | No | | Subqueries | No |
| Joins | No | | Joins | No |
| Computed properties | No | | Computed properties | No |
@@ -29,8 +30,9 @@ Cosmium strives to support the core features of Cosmos DB, including:
| User-defined functions (UDFs) | No | | User-defined functions (UDFs) | No |
### Clauses ### Clauses
| Clause | Implemented | | Clause | Implemented |
|--------------|-------------| | ------------ | ----------- |
| SELECT | Yes | | SELECT | Yes |
| FROM | Yes | | FROM | Yes |
| WHERE | Yes | | WHERE | Yes |
@@ -39,8 +41,9 @@ Cosmium strives to support the core features of Cosmos DB, including:
| OFFSET LIMIT | Yes | | OFFSET LIMIT | Yes |
### Keywords ### Keywords
| Keyword | Implemented | | Keyword | Implemented |
|----------|-------------| | -------- | ----------- |
| BETWEEN | No | | BETWEEN | No |
| DISTINCT | Yes | | DISTINCT | Yes |
| LIKE | No | | LIKE | No |
@@ -48,8 +51,9 @@ Cosmium strives to support the core features of Cosmos DB, including:
| TOP | Yes | | TOP | Yes |
### Aggregate Functions ### Aggregate Functions
| Function | Implemented | | Function | Implemented |
|----------|-------------| | -------- | ----------- |
| AVG | Yes | | AVG | Yes |
| COUNT | Yes | | COUNT | Yes |
| MAX | Yes | | MAX | Yes |
@@ -57,8 +61,9 @@ Cosmium strives to support the core features of Cosmos DB, including:
| SUM | Yes | | SUM | Yes |
### Array Functions ### Array Functions
| Function | Implemented | | Function | Implemented |
|----------------|-------------| | -------------- | ----------- |
| ARRAY_CONCAT | Yes | | ARRAY_CONCAT | Yes |
| ARRAY_CONTAINS | No | | ARRAY_CONTAINS | No |
| ARRAY_LENGTH | Yes | | ARRAY_LENGTH | Yes |
@@ -69,13 +74,15 @@ Cosmium strives to support the core features of Cosmos DB, including:
| SetUnion | Yes | | SetUnion | Yes |
### Conditional Functions ### Conditional Functions
| Function | Implemented | | Function | Implemented |
|----------|-------------| | -------- | ----------- |
| IIF | No | | IIF | No |
### Date and time Functions ### Date and time Functions
| Function | Implemented | | Function | Implemented |
|---------------------------|-------------| | ------------------------- | ----------- |
| DateTimeAdd | No | | DateTimeAdd | No |
| DateTimeBin | No | | DateTimeBin | No |
| DateTimeDiff | No | | DateTimeDiff | No |
@@ -93,53 +100,56 @@ Cosmium strives to support the core features of Cosmos DB, including:
| TimestampToDateTime | No | | TimestampToDateTime | No |
### Item Functions ### Item Functions
| Function | Implemented | | Function | Implemented |
|------------|-------------| | ---------- | ----------- |
| DocumentId | No | | DocumentId | No |
### Mathematical Functions ### Mathematical Functions
| Function | Implemented | | Function | Implemented |
|------------------|-------------| | ---------------- | ----------- |
| ABS | No | | ABS | Yes |
| ACOS | No | | ACOS | Yes |
| ASIN | No | | ASIN | Yes |
| ATAN | No | | ATAN | Yes |
| ATN2 | No | | ATN2 | Yes |
| CEILING | No | | CEILING | Yes |
| COS | No | | COS | Yes |
| COT | No | | COT | Yes |
| DEGREES | No | | DEGREES | Yes |
| EXP | No | | EXP | Yes |
| FLOOR | No | | FLOOR | Yes |
| IntAdd | No | | IntAdd | Yes |
| IntBitAnd | No | | IntBitAnd | Yes |
| IntBitLeftShift | No | | IntBitLeftShift | Yes |
| IntBitNot | No | | IntBitNot | Yes |
| IntBitOr | No | | IntBitOr | Yes |
| IntBitRightShift | No | | IntBitRightShift | Yes |
| IntBitXor | No | | IntBitXor | Yes |
| IntDiv | No | | IntDiv | Yes |
| IntMod | No | | IntMod | Yes |
| IntMul | No | | IntMul | Yes |
| IntSub | No | | IntSub | Yes |
| LOG | No | | LOG | Yes |
| LOG10 | No | | LOG10 | Yes |
| NumberBin | No | | NumberBin | Yes |
| PI | No | | PI | Yes |
| POWER | No | | POWER | Yes |
| RADIANS | No | | RADIANS | Yes |
| RAND | No | | RAND | Yes |
| ROUND | No | | ROUND | Yes |
| SIGN | No | | SIGN | Yes |
| SIN | No | | SIN | Yes |
| SQRT | No | | SQRT | Yes |
| SQUARE | No | | SQUARE | Yes |
| TAN | No | | TAN | Yes |
| TRUNC | No | | TRUNC | Yes |
### Spatial Functions ### Spatial Functions
| Function | Implemented | | Function | Implemented |
|--------------------|-------------| | ------------------ | ----------- |
| ST_AREA | No | | ST_AREA | No |
| ST_DISTANCE | No | | ST_DISTANCE | No |
| ST_WITHIN | No | | ST_WITHIN | No |
@@ -148,8 +158,9 @@ Cosmium strives to support the core features of Cosmos DB, including:
| ST_ISVALIDDETAILED | No | | ST_ISVALIDDETAILED | No |
### String Functions ### String Functions
| Function | Implemented | | Function | Implemented |
|-----------------|-------------| | --------------- | ----------- |
| CONCAT | Yes | | CONCAT | Yes |
| CONTAINS | Yes | | CONTAINS | Yes |
| ENDSWITH | Yes | | ENDSWITH | Yes |
@@ -177,8 +188,9 @@ Cosmium strives to support the core features of Cosmos DB, including:
| UPPER | Yes | | UPPER | Yes |
### Type checking Functions ### Type checking Functions
| Function | Implemented | | Function | Implemented |
|------------------|-------------| | ---------------- | ----------- |
| IS_ARRAY | Yes | | IS_ARRAY | Yes |
| IS_BOOL | Yes | | IS_BOOL | Yes |
| IS_DEFINED | Yes | | IS_DEFINED | Yes |

View File

@@ -3,6 +3,7 @@ package parsers
type SelectStmt struct { type SelectStmt struct {
SelectItems []SelectItem SelectItems []SelectItem
Table Table Table Table
JoinItems []JoinItem
Filters interface{} Filters interface{}
Distinct bool Distinct bool
Count int Count int
@@ -16,6 +17,11 @@ type Table struct {
Value string Value string
} }
type JoinItem struct {
Table Table
SelectItem SelectItem
}
type SelectItemType int type SelectItemType int
const ( const (
@@ -120,6 +126,43 @@ const (
FunctionCallSetIntersect FunctionCallType = "SetIntersect" FunctionCallSetIntersect FunctionCallType = "SetIntersect"
FunctionCallSetUnion FunctionCallType = "SetUnion" FunctionCallSetUnion FunctionCallType = "SetUnion"
FunctionCallMathAbs FunctionCallType = "MathAbs"
FunctionCallMathAcos FunctionCallType = "MathAcos"
FunctionCallMathAsin FunctionCallType = "MathAsin"
FunctionCallMathAtan FunctionCallType = "MathAtan"
FunctionCallMathAtn2 FunctionCallType = "MathAtn2"
FunctionCallMathCeiling FunctionCallType = "MathCeiling"
FunctionCallMathCos FunctionCallType = "MathCos"
FunctionCallMathCot FunctionCallType = "MathCot"
FunctionCallMathDegrees FunctionCallType = "MathDegrees"
FunctionCallMathExp FunctionCallType = "MathExp"
FunctionCallMathFloor FunctionCallType = "MathFloor"
FunctionCallMathIntAdd FunctionCallType = "MathIntAdd"
FunctionCallMathIntBitAnd FunctionCallType = "MathIntBitAnd"
FunctionCallMathIntBitLeftShift FunctionCallType = "MathIntBitLeftShift"
FunctionCallMathIntBitNot FunctionCallType = "MathIntBitNot"
FunctionCallMathIntBitOr FunctionCallType = "MathIntBitOr"
FunctionCallMathIntBitRightShift FunctionCallType = "MathIntBitRightShift"
FunctionCallMathIntBitXor FunctionCallType = "MathIntBitXor"
FunctionCallMathIntDiv FunctionCallType = "MathIntDiv"
FunctionCallMathIntMod FunctionCallType = "MathIntMod"
FunctionCallMathIntMul FunctionCallType = "MathIntMul"
FunctionCallMathIntSub FunctionCallType = "MathIntSub"
FunctionCallMathLog FunctionCallType = "MathLog"
FunctionCallMathLog10 FunctionCallType = "MathLog10"
FunctionCallMathNumberBin FunctionCallType = "MathNumberBin"
FunctionCallMathPi FunctionCallType = "MathPi"
FunctionCallMathPower FunctionCallType = "MathPower"
FunctionCallMathRadians FunctionCallType = "MathRadians"
FunctionCallMathRand FunctionCallType = "MathRand"
FunctionCallMathRound FunctionCallType = "MathRound"
FunctionCallMathSign FunctionCallType = "MathSign"
FunctionCallMathSin FunctionCallType = "MathSin"
FunctionCallMathSqrt FunctionCallType = "MathSqrt"
FunctionCallMathSquare FunctionCallType = "MathSquare"
FunctionCallMathTan FunctionCallType = "MathTan"
FunctionCallMathTrunc FunctionCallType = "MathTrunc"
FunctionCallAggregateAvg FunctionCallType = "AggregateAvg" FunctionCallAggregateAvg FunctionCallType = "AggregateAvg"
FunctionCallAggregateCount FunctionCallType = "AggregateCount" FunctionCallAggregateCount FunctionCallType = "AggregateCount"
FunctionCallAggregateMax FunctionCallType = "AggregateMax" FunctionCallAggregateMax FunctionCallType = "AggregateMax"

View File

@@ -0,0 +1,57 @@
package nosql_test
import (
"testing"
"github.com/pikami/cosmium/parsers"
)
func Test_Parse_Join(t *testing.T) {
t.Run("Should parse simple JOIN", func(t *testing.T) {
testQueryParse(
t,
`SELECT c.id, c["pk"] FROM c JOIN cc IN c["tags"]`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{Path: []string{"c", "id"}},
{Path: []string{"c", "pk"}},
},
Table: parsers.Table{Value: "c"},
JoinItems: []parsers.JoinItem{
{
Table: parsers.Table{
Value: "cc",
},
SelectItem: parsers.SelectItem{
Path: []string{"c", "tags"},
},
},
},
},
)
})
t.Run("Should parse JOIN VALUE", func(t *testing.T) {
testQueryParse(
t,
`SELECT VALUE cc FROM c JOIN cc IN c["tags"]`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{Path: []string{"cc"}, IsTopLevel: true},
},
Table: parsers.Table{Value: "c"},
JoinItems: []parsers.JoinItem{
{
Table: parsers.Table{
Value: "cc",
},
SelectItem: parsers.SelectItem{
Path: []string{"c", "tags"},
},
},
},
},
)
})
}

View File

@@ -0,0 +1,650 @@
package nosql_test
import (
"testing"
"github.com/pikami/cosmium/parsers"
)
func Test_Execute_MathFunctions(t *testing.T) {
t.Run("Should parse function ABS(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT ABS(c.value) FROM c`,
parsers.FunctionCallMathAbs,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function ACOS(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT ACOS(c.value) FROM c`,
parsers.FunctionCallMathAcos,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function ASIN(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT ASIN(c.value) FROM c`,
parsers.FunctionCallMathAsin,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function ATAN(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT ATAN(c.value) FROM c`,
parsers.FunctionCallMathAtan,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function CEILING(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT CEILING(c.value) FROM c`,
parsers.FunctionCallMathCeiling,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function COS(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT COS(c.value) FROM c`,
parsers.FunctionCallMathCos,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function COT(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT COT(c.value) FROM c`,
parsers.FunctionCallMathCot,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function DEGREES(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT DEGREES(c.value) FROM c`,
parsers.FunctionCallMathDegrees,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function EXP(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT EXP(c.value) FROM c`,
parsers.FunctionCallMathExp,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function FLOOR(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT FLOOR(c.value) FROM c`,
parsers.FunctionCallMathFloor,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function IntBitNot(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT IntBitNot(c.value) FROM c`,
parsers.FunctionCallMathIntBitNot,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function LOG10(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT LOG10(c.value) FROM c`,
parsers.FunctionCallMathLog10,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function RADIANS(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT RADIANS(c.value) FROM c`,
parsers.FunctionCallMathRadians,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function ROUND(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT ROUND(c.value) FROM c`,
parsers.FunctionCallMathRound,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function SIGN(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT SIGN(c.value) FROM c`,
parsers.FunctionCallMathSign,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function SIN(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT SIN(c.value) FROM c`,
parsers.FunctionCallMathSin,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function SQRT(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT SQRT(c.value) FROM c`,
parsers.FunctionCallMathSqrt,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function SQUARE(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT SQUARE(c.value) FROM c`,
parsers.FunctionCallMathSquare,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function TAN(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT TAN(c.value) FROM c`,
parsers.FunctionCallMathTan,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function TRUNC(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT TRUNC(c.value) FROM c`,
parsers.FunctionCallMathTrunc,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function ATN2(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT ATN2(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathAtn2,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function IntAdd(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT IntAdd(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathIntAdd,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function IntBitAnd(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT IntBitAnd(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathIntBitAnd,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function IntBitLeftShift(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT IntBitLeftShift(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathIntBitLeftShift,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function IntBitOr(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT IntBitOr(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathIntBitOr,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function IntBitRightShift(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT IntBitRightShift(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathIntBitRightShift,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function IntBitXor(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT IntBitXor(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathIntBitXor,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function IntDiv(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT IntDiv(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathIntDiv,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function IntMod(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT IntMod(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathIntMod,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function IntMul(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT IntMul(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathIntMul,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function IntSub(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT IntSub(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathIntSub,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function POWER(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT POWER(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathPower,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function LOG(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT LOG(c.value) FROM c`,
parsers.FunctionCallMathLog,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function LOG(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT LOG(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathLog,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function NumberBin(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT NumberBin(c.value) FROM c`,
parsers.FunctionCallMathNumberBin,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function NumberBin(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT NumberBin(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathNumberBin,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function PI()", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT PI() FROM c`,
parsers.FunctionCallMathPi,
[]interface{}{},
"c",
)
})
t.Run("Should parse function RAND()", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT RAND() FROM c`,
parsers.FunctionCallMathRand,
[]interface{}{},
"c",
)
})
}
func testMathFunctionParse(
t *testing.T,
query string,
expectedFunctionType parsers.FunctionCallType,
expectedArguments []interface{},
expectedTable string,
) {
testQueryParse(
t,
query,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: expectedFunctionType,
Arguments: expectedArguments,
},
},
},
Table: parsers.Table{Value: expectedTable},
},
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ package nosql
import "github.com/pikami/cosmium/parsers" import "github.com/pikami/cosmium/parsers"
func makeSelectStmt( func makeSelectStmt(
columns, table, columns, table, joinItems,
whereClause interface{}, distinctClause interface{}, whereClause interface{}, distinctClause interface{},
count interface{}, groupByClause interface{}, orderList interface{}, count interface{}, groupByClause interface{}, orderList interface{},
offsetClause interface{}, offsetClause interface{},
@@ -14,6 +14,13 @@ func makeSelectStmt(
Table: table.(parsers.Table), Table: table.(parsers.Table),
} }
if joinItemsArray, ok := joinItems.([]interface{}); ok && len(joinItemsArray) > 0 {
selectStmt.JoinItems = make([]parsers.JoinItem, len(joinItemsArray))
for i, joinItem := range joinItemsArray {
selectStmt.JoinItems[i] = joinItem.(parsers.JoinItem)
}
}
switch v := whereClause.(type) { switch v := whereClause.(type) {
case parsers.ComparisonExpression, parsers.LogicalExpression, parsers.Constant, parsers.SelectItem: case parsers.ComparisonExpression, parsers.LogicalExpression, parsers.Constant, parsers.SelectItem:
selectStmt.Filters = v selectStmt.Filters = v
@@ -48,6 +55,13 @@ func makeSelectStmt(
return selectStmt, nil return selectStmt, nil
} }
func makeJoin(table interface{}, column interface{}) (parsers.JoinItem, error) {
return parsers.JoinItem{
Table: table.(parsers.Table),
SelectItem: column.(parsers.SelectItem),
}, nil
}
func makeSelectItem(name interface{}, path interface{}, selectItemType parsers.SelectItemType) (parsers.SelectItem, error) { func makeSelectItem(name interface{}, path interface{}, selectItemType parsers.SelectItemType) (parsers.SelectItem, error) {
ps := path.([]interface{}) ps := path.([]interface{})
@@ -161,13 +175,15 @@ Input <- selectStmt:SelectStmt {
SelectStmt <- Select ws SelectStmt <- Select ws
distinctClause:DistinctClause? ws distinctClause:DistinctClause? ws
topClause:TopClause? ws columns:Selection ws topClause:TopClause? ws
columns:Selection ws
From ws table:TableName ws From ws table:TableName ws
joinClauses:JoinClause* ws
whereClause:(ws Where ws condition:Condition { return condition, nil })? whereClause:(ws Where ws condition:Condition { return condition, nil })?
groupByClause:(ws GroupBy ws columns:ColumnList { return columns, nil })? groupByClause:(ws GroupBy ws columns:ColumnList { return columns, nil })?
orderByClause:OrderByClause? orderByClause:OrderByClause?
offsetClause:OffsetClause? { offsetClause:OffsetClause? {
return makeSelectStmt(columns, table, whereClause, return makeSelectStmt(columns, table, joinClauses, whereClause,
distinctClause, topClause, groupByClause, orderByClause, offsetClause) distinctClause, topClause, groupByClause, orderByClause, offsetClause)
} }
@@ -177,6 +193,10 @@ TopClause <- Top ws count:Integer {
return count, nil return count, nil
} }
JoinClause <- Join ws table:TableName ws "IN"i ws column:SelectItem {
return makeJoin(table, column)
}
OffsetClause <- "OFFSET"i ws offset:IntegerLiteral ws "LIMIT"i ws limit:IntegerLiteral { OffsetClause <- "OFFSET"i ws offset:IntegerLiteral ws "LIMIT"i ws limit:IntegerLiteral {
return []interface{}{offset.(parsers.Constant).Value, limit.(parsers.Constant).Value}, nil return []interface{}{offset.(parsers.Constant).Value, limit.(parsers.Constant).Value}, nil
} }
@@ -300,6 +320,8 @@ As <- "AS"i
From <- "FROM"i From <- "FROM"i
Join <- "JOIN"i
Where <- "WHERE"i Where <- "WHERE"i
And <- "AND"i And <- "AND"i
@@ -343,6 +365,7 @@ FunctionCall <- StringFunctions
/ ArrayFunctions / ArrayFunctions
/ InFunction / InFunction
/ AggregateFunctions / AggregateFunctions
/ MathFunctions
StringFunctions <- StringEqualsExpression StringFunctions <- StringEqualsExpression
/ ToStringExpression / ToStringExpression
@@ -384,6 +407,43 @@ ArrayFunctions <- ArrayConcatExpression
/ SetIntersectExpression / SetIntersectExpression
/ SetUnionExpression / SetUnionExpression
MathFunctions <- MathAbsExpression
/ MathAcosExpression
/ MathAsinExpression
/ MathAtanExpression
/ MathCeilingExpression
/ MathCosExpression
/ MathCotExpression
/ MathDegreesExpression
/ MathExpExpression
/ MathFloorExpression
/ MathIntBitNotExpression
/ MathLog10Expression
/ MathRadiansExpression
/ MathRoundExpression
/ MathSignExpression
/ MathSinExpression
/ MathSqrtExpression
/ MathSquareExpression
/ MathTanExpression
/ MathTruncExpression
/ MathAtn2Expression
/ MathIntAddExpression
/ MathIntBitAndExpression
/ MathIntBitLeftShiftExpression
/ MathIntBitOrExpression
/ MathIntBitRightShiftExpression
/ MathIntBitXorExpression
/ MathIntDivExpression
/ MathIntModExpression
/ MathIntMulExpression
/ MathIntSubExpression
/ MathPowerExpression
/ MathLogExpression
/ MathNumberBinExpression
/ MathPiExpression
/ MathRandExpression
UpperExpression <- "UPPER"i ws "(" ex:SelectItem ")" { UpperExpression <- "UPPER"i ws "(" ex:SelectItem ")" {
return createFunctionCall(parsers.FunctionCallUpper, []interface{}{ex}) return createFunctionCall(parsers.FunctionCallUpper, []interface{}{ex})
} }
@@ -527,6 +587,49 @@ SetUnionExpression <- "SetUnion"i ws "(" ws set1:SelectItem ws "," ws set2:Selec
return createFunctionCall(parsers.FunctionCallSetUnion, []interface{}{set1, set2}) return createFunctionCall(parsers.FunctionCallSetUnion, []interface{}{set1, set2})
} }
MathAbsExpression <- "ABS"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathAbs, []interface{}{ex}) }
MathAcosExpression <- "ACOS"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathAcos, []interface{}{ex}) }
MathAsinExpression <- "ASIN"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathAsin, []interface{}{ex}) }
MathAtanExpression <- "ATAN"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathAtan, []interface{}{ex}) }
MathCeilingExpression <- "CEILING"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathCeiling, []interface{}{ex}) }
MathCosExpression <- "COS"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathCos, []interface{}{ex}) }
MathCotExpression <- "COT"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathCot, []interface{}{ex}) }
MathDegreesExpression <- "DEGREES"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathDegrees, []interface{}{ex}) }
MathExpExpression <- "EXP"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathExp, []interface{}{ex}) }
MathFloorExpression <- "FLOOR"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathFloor, []interface{}{ex}) }
MathIntBitNotExpression <- "IntBitNot"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathIntBitNot, []interface{}{ex}) }
MathLog10Expression <- "LOG10"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathLog10, []interface{}{ex}) }
MathRadiansExpression <- "RADIANS"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathRadians, []interface{}{ex}) }
MathRoundExpression <- "ROUND"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathRound, []interface{}{ex}) }
MathSignExpression <- "SIGN"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathSign, []interface{}{ex}) }
MathSinExpression <- "SIN"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathSin, []interface{}{ex}) }
MathSqrtExpression <- "SQRT"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathSqrt, []interface{}{ex}) }
MathSquareExpression <- "SQUARE"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathSquare, []interface{}{ex}) }
MathTanExpression <- "TAN"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathTan, []interface{}{ex}) }
MathTruncExpression <- "TRUNC"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathTrunc, []interface{}{ex}) }
MathAtn2Expression <- "ATN2"i ws "(" ws set1:SelectItem ws "," ws set2:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathAtn2, []interface{}{set1, set2}) }
MathIntAddExpression <- "IntAdd"i ws "(" ws set1:SelectItem ws "," ws set2:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathIntAdd, []interface{}{set1, set2}) }
MathIntBitAndExpression <- "IntBitAnd"i ws "(" ws set1:SelectItem ws "," ws set2:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathIntBitAnd, []interface{}{set1, set2}) }
MathIntBitLeftShiftExpression <- "IntBitLeftShift"i ws "(" ws set1:SelectItem ws "," ws set2:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathIntBitLeftShift, []interface{}{set1, set2}) }
MathIntBitOrExpression <- "IntBitOr"i ws "(" ws set1:SelectItem ws "," ws set2:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathIntBitOr, []interface{}{set1, set2}) }
MathIntBitRightShiftExpression <- "IntBitRightShift"i ws "(" ws set1:SelectItem ws "," ws set2:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathIntBitRightShift, []interface{}{set1, set2}) }
MathIntBitXorExpression <- "IntBitXor"i ws "(" ws set1:SelectItem ws "," ws set2:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathIntBitXor, []interface{}{set1, set2}) }
MathIntDivExpression <- "IntDiv"i ws "(" ws set1:SelectItem ws "," ws set2:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathIntDiv, []interface{}{set1, set2}) }
MathIntModExpression <- "IntMod"i ws "(" ws set1:SelectItem ws "," ws set2:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathIntMod, []interface{}{set1, set2}) }
MathIntMulExpression <- "IntMul"i ws "(" ws set1:SelectItem ws "," ws set2:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathIntMul, []interface{}{set1, set2}) }
MathIntSubExpression <- "IntSub"i ws "(" ws set1:SelectItem ws "," ws set2:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathIntSub, []interface{}{set1, set2}) }
MathPowerExpression <- "POWER"i ws "(" ws set1:SelectItem ws "," ws set2:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathPower, []interface{}{set1, set2}) }
MathLogExpression <- "LOG"i ws "(" ws ex1:SelectItem others:(ws "," ws ex:SelectItem { return ex, nil })* ws ")" {
return createFunctionCall(parsers.FunctionCallMathLog, append([]interface{}{ex1}, others.([]interface{})...))
}
MathNumberBinExpression <- "NumberBin"i ws "(" ws ex1:SelectItem others:(ws "," ws ex:SelectItem { return ex, nil })* ws ")" {
return createFunctionCall(parsers.FunctionCallMathNumberBin, append([]interface{}{ex1}, others.([]interface{})...))
}
MathPiExpression <- "PI"i ws "(" ws ")" { return createFunctionCall(parsers.FunctionCallMathPi, []interface{}{}) }
MathRandExpression <- "RAND"i ws "(" ws ")" { return createFunctionCall(parsers.FunctionCallMathRand, []interface{}{}) }
InFunction <- ex1:SelectProperty ws "IN"i ws "(" ws ex2:SelectItem others:(ws "," ws ex:SelectItem { return ex, nil })* ws ")" { InFunction <- ex1:SelectProperty ws "IN"i ws "(" ws ex2:SelectItem others:(ws "," ws ex:SelectItem { return ex, nil })* ws ")" {
return createFunctionCall(parsers.FunctionCallIn, append([]interface{}{ex1, ex2}, others.([]interface{})...)) return createFunctionCall(parsers.FunctionCallIn, append([]interface{}{ex1, ex2}, others.([]interface{})...))
} }

View File

@@ -11,7 +11,7 @@ func (c memoryExecutorContext) aggregate_Avg(arguments []interface{}, row RowTyp
sum := 0.0 sum := 0.0
count := 0 count := 0
if array, isArray := row.([]RowType); isArray { if array, isArray := row.([]RowWithJoins); isArray {
for _, item := range array { for _, item := range array {
value := c.getFieldValue(selectExpression, item) value := c.getFieldValue(selectExpression, item)
if numericValue, ok := value.(float64); ok { if numericValue, ok := value.(float64); ok {
@@ -35,7 +35,7 @@ func (c memoryExecutorContext) aggregate_Count(arguments []interface{}, row RowT
selectExpression := arguments[0].(parsers.SelectItem) selectExpression := arguments[0].(parsers.SelectItem)
count := 0 count := 0
if array, isArray := row.([]RowType); isArray { if array, isArray := row.([]RowWithJoins); isArray {
for _, item := range array { for _, item := range array {
value := c.getFieldValue(selectExpression, item) value := c.getFieldValue(selectExpression, item)
if value != nil { if value != nil {
@@ -52,7 +52,7 @@ func (c memoryExecutorContext) aggregate_Max(arguments []interface{}, row RowTyp
max := 0.0 max := 0.0
count := 0 count := 0
if array, isArray := row.([]RowType); isArray { if array, isArray := row.([]RowWithJoins); isArray {
for _, item := range array { for _, item := range array {
value := c.getFieldValue(selectExpression, item) value := c.getFieldValue(selectExpression, item)
if numericValue, ok := value.(float64); ok { if numericValue, ok := value.(float64); ok {
@@ -81,7 +81,7 @@ func (c memoryExecutorContext) aggregate_Min(arguments []interface{}, row RowTyp
min := math.MaxFloat64 min := math.MaxFloat64
count := 0 count := 0
if array, isArray := row.([]RowType); isArray { if array, isArray := row.([]RowWithJoins); isArray {
for _, item := range array { for _, item := range array {
value := c.getFieldValue(selectExpression, item) value := c.getFieldValue(selectExpression, item)
if numericValue, ok := value.(float64); ok { if numericValue, ok := value.(float64); ok {
@@ -110,7 +110,7 @@ func (c memoryExecutorContext) aggregate_Sum(arguments []interface{}, row RowTyp
sum := 0.0 sum := 0.0
count := 0 count := 0
if array, isArray := row.([]RowType); isArray { if array, isArray := row.([]RowWithJoins); isArray {
for _, item := range array { for _, item := range array {
value := c.getFieldValue(selectExpression, item) value := c.getFieldValue(selectExpression, item)
if numericValue, ok := value.(float64); ok { if numericValue, ok := value.(float64); ok {

View File

@@ -0,0 +1,86 @@
package memoryexecutor_test
import (
"testing"
"github.com/pikami/cosmium/parsers"
memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor"
)
func Test_Execute_Joins(t *testing.T) {
mockData := []memoryexecutor.RowType{
map[string]interface{}{
"id": 1,
"tags": []map[string]interface{}{
{"name": "a"},
{"name": "b"},
},
},
map[string]interface{}{
"id": 2,
"tags": []map[string]interface{}{
{"name": "b"},
{"name": "c"},
},
},
}
t.Run("Should execute JOIN on 'tags'", func(t *testing.T) {
testQueryExecute(
t,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{Path: []string{"c", "id"}},
{Path: []string{"cc", "name"}},
},
Table: parsers.Table{Value: "c"},
JoinItems: []parsers.JoinItem{
{
Table: parsers.Table{
Value: "cc",
},
SelectItem: parsers.SelectItem{
Path: []string{"c", "tags"},
},
},
},
},
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"id": 1, "name": "a"},
map[string]interface{}{"id": 1, "name": "b"},
map[string]interface{}{"id": 2, "name": "b"},
map[string]interface{}{"id": 2, "name": "c"},
},
)
})
t.Run("Should execute JOIN VALUE on 'tags'", func(t *testing.T) {
testQueryExecute(
t,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{Path: []string{"cc"}, IsTopLevel: true},
},
Table: parsers.Table{Value: "c"},
JoinItems: []parsers.JoinItem{
{
Table: parsers.Table{
Value: "cc",
},
SelectItem: parsers.SelectItem{
Path: []string{"c", "tags"},
},
},
},
},
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"name": "a"},
map[string]interface{}{"name": "b"},
map[string]interface{}{"name": "b"},
map[string]interface{}{"name": "c"},
},
)
})
}

View File

@@ -0,0 +1,615 @@
package memoryexecutor
import (
"math"
"math/rand"
"github.com/pikami/cosmium/internal/logger"
"github.com/pikami/cosmium/parsers"
)
func (c memoryExecutorContext) math_Abs(arguments []interface{}, row RowType) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row)
switch val := ex.(type) {
case float64:
return math.Abs(val)
case int:
if val < 0 {
return -val
}
return val
default:
logger.Debug("math_Abs - got parameters of wrong type")
return 0
}
}
func (c memoryExecutorContext) math_Acos(arguments []interface{}, row RowType) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.Debug("math_Acos - got parameters of wrong type")
return nil
}
if val < -1 || val > 1 {
logger.Debug("math_Acos - value out of domain for acos")
return nil
}
return math.Acos(val) * 180 / math.Pi
}
func (c memoryExecutorContext) math_Asin(arguments []interface{}, row RowType) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.Debug("math_Asin - got parameters of wrong type")
return nil
}
if val < -1 || val > 1 {
logger.Debug("math_Asin - value out of domain for acos")
return nil
}
return math.Asin(val) * 180 / math.Pi
}
func (c memoryExecutorContext) math_Atan(arguments []interface{}, row RowType) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.Debug("math_Atan - got parameters of wrong type")
return nil
}
return math.Atan(val) * 180 / math.Pi
}
func (c memoryExecutorContext) math_Ceiling(arguments []interface{}, row RowType) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row)
switch val := ex.(type) {
case float64:
return math.Ceil(val)
case int:
return val
default:
logger.Debug("math_Ceiling - got parameters of wrong type")
return 0
}
}
func (c memoryExecutorContext) math_Cos(arguments []interface{}, row RowType) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.Debug("math_Cos - got parameters of wrong type")
return nil
}
return math.Cos(val)
}
func (c memoryExecutorContext) math_Cot(arguments []interface{}, row RowType) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.Debug("math_Cot - got parameters of wrong type")
return nil
}
if val == 0 {
logger.Debug("math_Cot - cotangent undefined for zero")
return nil
}
return 1 / math.Tan(val)
}
func (c memoryExecutorContext) math_Degrees(arguments []interface{}, row RowType) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.Debug("math_Degrees - got parameters of wrong type")
return nil
}
return val * (180 / math.Pi)
}
func (c memoryExecutorContext) math_Exp(arguments []interface{}, row RowType) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.Debug("math_Exp - got parameters of wrong type")
return nil
}
return math.Exp(val)
}
func (c memoryExecutorContext) math_Floor(arguments []interface{}, row RowType) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row)
switch val := ex.(type) {
case float64:
return math.Floor(val)
case int:
return val
default:
logger.Debug("math_Floor - got parameters of wrong type")
return 0
}
}
func (c memoryExecutorContext) math_IntBitNot(arguments []interface{}, row RowType) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row)
switch val := ex.(type) {
case int:
return ^val
default:
logger.Debug("math_IntBitNot - got parameters of wrong type")
return nil
}
}
func (c memoryExecutorContext) math_Log10(arguments []interface{}, row RowType) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.Debug("math_Log10 - got parameters of wrong type")
return nil
}
if val <= 0 {
logger.Debug("math_Log10 - value must be greater than 0")
return nil
}
return math.Log10(val)
}
func (c memoryExecutorContext) math_Radians(arguments []interface{}, row RowType) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.Debug("math_Radians - got parameters of wrong type")
return nil
}
return val * (math.Pi / 180.0)
}
func (c memoryExecutorContext) math_Round(arguments []interface{}, row RowType) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row)
switch val := ex.(type) {
case float64:
return math.Round(val)
case int:
return val
default:
logger.Debug("math_Round - got parameters of wrong type")
return nil
}
}
func (c memoryExecutorContext) math_Sign(arguments []interface{}, row RowType) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row)
switch val := ex.(type) {
case float64:
if val > 0 {
return 1
} else if val < 0 {
return -1
} else {
return 0
}
case int:
if val > 0 {
return 1
} else if val < 0 {
return -1
} else {
return 0
}
default:
logger.Debug("math_Sign - got parameters of wrong type")
return nil
}
}
func (c memoryExecutorContext) math_Sin(arguments []interface{}, row RowType) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.Debug("math_Sin - got parameters of wrong type")
return nil
}
return math.Sin(val)
}
func (c memoryExecutorContext) math_Sqrt(arguments []interface{}, row RowType) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.Debug("math_Sqrt - got parameters of wrong type")
return nil
}
return math.Sqrt(val)
}
func (c memoryExecutorContext) math_Square(arguments []interface{}, row RowType) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.Debug("math_Square - got parameters of wrong type")
return nil
}
return math.Pow(val, 2)
}
func (c memoryExecutorContext) math_Tan(arguments []interface{}, row RowType) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.Debug("math_Tan - got parameters of wrong type")
return nil
}
return math.Tan(val)
}
func (c memoryExecutorContext) math_Trunc(arguments []interface{}, row RowType) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row)
switch val := ex.(type) {
case float64:
return math.Trunc(val)
case int:
return float64(val)
default:
logger.Debug("math_Trunc - got parameters of wrong type")
return nil
}
}
func (c memoryExecutorContext) math_Atn2(arguments []interface{}, row RowType) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
exItem2 := arguments[1].(parsers.SelectItem)
ex1 := c.getFieldValue(exItem1, row)
ex2 := c.getFieldValue(exItem2, row)
y, yIsNumber := numToFloat64(ex1)
x, xIsNumber := numToFloat64(ex2)
if !yIsNumber || !xIsNumber {
logger.Debug("math_Atn2 - got parameters of wrong type")
return nil
}
return math.Atan2(y, x)
}
func (c memoryExecutorContext) math_IntAdd(arguments []interface{}, row RowType) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
exItem2 := arguments[1].(parsers.SelectItem)
ex1 := c.getFieldValue(exItem1, row)
ex2 := c.getFieldValue(exItem2, row)
ex1Number, ex1IsNumber := numToInt(ex1)
ex2Number, ex2IsNumber := numToInt(ex2)
if !ex1IsNumber || !ex2IsNumber {
logger.Debug("math_IntAdd - got parameters of wrong type")
return nil
}
return ex1Number + ex2Number
}
func (c memoryExecutorContext) math_IntBitAnd(arguments []interface{}, row RowType) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
exItem2 := arguments[1].(parsers.SelectItem)
ex1 := c.getFieldValue(exItem1, row)
ex2 := c.getFieldValue(exItem2, row)
ex1Int, ex1IsInt := numToInt(ex1)
ex2Int, ex2IsInt := numToInt(ex2)
if !ex1IsInt || !ex2IsInt {
logger.Debug("math_IntBitAnd - got parameters of wrong type")
return nil
}
return ex1Int & ex2Int
}
func (c memoryExecutorContext) math_IntBitLeftShift(arguments []interface{}, row RowType) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
exItem2 := arguments[1].(parsers.SelectItem)
ex1 := c.getFieldValue(exItem1, row)
ex2 := c.getFieldValue(exItem2, row)
num1, num1IsInt := numToInt(ex1)
num2, num2IsInt := numToInt(ex2)
if !num1IsInt || !num2IsInt {
logger.Debug("math_IntBitLeftShift - got parameters of wrong type")
return nil
}
return num1 << uint(num2)
}
func (c memoryExecutorContext) math_IntBitOr(arguments []interface{}, row RowType) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
exItem2 := arguments[1].(parsers.SelectItem)
ex1 := c.getFieldValue(exItem1, row)
ex2 := c.getFieldValue(exItem2, row)
num1, num1IsInt := ex1.(int)
num2, num2IsInt := ex2.(int)
if !num1IsInt || !num2IsInt {
logger.Debug("math_IntBitOr - got parameters of wrong type")
return nil
}
return num1 | num2
}
func (c memoryExecutorContext) math_IntBitRightShift(arguments []interface{}, row RowType) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
exItem2 := arguments[1].(parsers.SelectItem)
ex1 := c.getFieldValue(exItem1, row)
ex2 := c.getFieldValue(exItem2, row)
num1, num1IsInt := numToInt(ex1)
num2, num2IsInt := numToInt(ex2)
if !num1IsInt || !num2IsInt {
logger.Debug("math_IntBitRightShift - got parameters of wrong type")
return nil
}
return num1 >> uint(num2)
}
func (c memoryExecutorContext) math_IntBitXor(arguments []interface{}, row RowType) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
exItem2 := arguments[1].(parsers.SelectItem)
ex1 := c.getFieldValue(exItem1, row)
ex2 := c.getFieldValue(exItem2, row)
num1, num1IsInt := ex1.(int)
num2, num2IsInt := ex2.(int)
if !num1IsInt || !num2IsInt {
logger.Debug("math_IntBitXor - got parameters of wrong type")
return nil
}
return num1 ^ num2
}
func (c memoryExecutorContext) math_IntDiv(arguments []interface{}, row RowType) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
exItem2 := arguments[1].(parsers.SelectItem)
ex1 := c.getFieldValue(exItem1, row)
ex2 := c.getFieldValue(exItem2, row)
num1, num1IsInt := ex1.(int)
num2, num2IsInt := ex2.(int)
if !num1IsInt || !num2IsInt || num2 == 0 {
logger.Debug("math_IntDiv - got parameters of wrong type or divide by zero")
return nil
}
return num1 / num2
}
func (c memoryExecutorContext) math_IntMul(arguments []interface{}, row RowType) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
exItem2 := arguments[1].(parsers.SelectItem)
ex1 := c.getFieldValue(exItem1, row)
ex2 := c.getFieldValue(exItem2, row)
num1, num1IsInt := ex1.(int)
num2, num2IsInt := ex2.(int)
if !num1IsInt || !num2IsInt {
logger.Debug("math_IntMul - got parameters of wrong type")
return nil
}
return num1 * num2
}
func (c memoryExecutorContext) math_IntSub(arguments []interface{}, row RowType) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
exItem2 := arguments[1].(parsers.SelectItem)
ex1 := c.getFieldValue(exItem1, row)
ex2 := c.getFieldValue(exItem2, row)
num1, num1IsInt := ex1.(int)
num2, num2IsInt := ex2.(int)
if !num1IsInt || !num2IsInt {
logger.Debug("math_IntSub - got parameters of wrong type")
return nil
}
return num1 - num2
}
func (c memoryExecutorContext) math_IntMod(arguments []interface{}, row RowType) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
exItem2 := arguments[1].(parsers.SelectItem)
ex1 := c.getFieldValue(exItem1, row)
ex2 := c.getFieldValue(exItem2, row)
num1, num1IsInt := ex1.(int)
num2, num2IsInt := ex2.(int)
if !num1IsInt || !num2IsInt || num2 == 0 {
logger.Debug("math_IntMod - got parameters of wrong type or divide by zero")
return nil
}
return num1 % num2
}
func (c memoryExecutorContext) math_Power(arguments []interface{}, row RowType) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
exItem2 := arguments[1].(parsers.SelectItem)
ex1 := c.getFieldValue(exItem1, row)
ex2 := c.getFieldValue(exItem2, row)
base, baseIsNumber := numToFloat64(ex1)
exponent, exponentIsNumber := numToFloat64(ex2)
if !baseIsNumber || !exponentIsNumber {
logger.Debug("math_Power - got parameters of wrong type")
return nil
}
return math.Pow(base, exponent)
}
func (c memoryExecutorContext) math_Log(arguments []interface{}, row RowType) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem1, row)
var base float64 = math.E
if len(arguments) > 1 {
exItem2 := arguments[1].(parsers.SelectItem)
baseValueObject := c.getFieldValue(exItem2, row)
baseValue, baseValueIsNumber := numToFloat64(baseValueObject)
if !baseValueIsNumber {
logger.Debug("math_Log - base parameter must be a numeric value")
return nil
}
if baseValue > 0 && baseValue != 1 {
base = baseValue
} else {
logger.Debug("math_Log - base must be greater than 0 and not equal to 1")
return nil
}
}
num, numIsNumber := numToFloat64(ex)
if !numIsNumber || num <= 0 {
logger.Debug("math_Log - parameter must be a positive numeric value")
return nil
}
return math.Log(num) / math.Log(base)
}
func (c memoryExecutorContext) math_NumberBin(arguments []interface{}, row RowType) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem1, row)
binSize := 1.0
if len(arguments) > 1 {
exItem2 := arguments[1].(parsers.SelectItem)
binSizeValueObject := c.getFieldValue(exItem2, row)
binSizeValue, binSizeValueIsNumber := numToFloat64(binSizeValueObject)
if !binSizeValueIsNumber {
logger.Debug("math_NumberBin - base parameter must be a numeric value")
return nil
}
if binSizeValue != 0 {
binSize = binSizeValue
} else {
logger.Debug("math_NumberBin - base must not be equal to 0")
return nil
}
}
num, numIsNumber := numToFloat64(ex)
if !numIsNumber {
logger.Debug("math_NumberBin - parameter must be a numeric value")
return nil
}
return math.Floor(num/binSize) * binSize
}
func (c memoryExecutorContext) math_Pi() interface{} {
return math.Pi
}
func (c memoryExecutorContext) math_Rand() interface{} {
return rand.Float64()
}
func numToInt(ex interface{}) (int, bool) {
switch val := ex.(type) {
case float64:
return int(val), true
case int:
return val, true
default:
return 0, false
}
}
func numToFloat64(num interface{}) (float64, bool) {
switch val := num.(type) {
case float64:
return val, true
case int:
return float64(val), true
default:
return 0, false
}
}

View File

@@ -0,0 +1,269 @@
package memoryexecutor_test
import (
"math"
"testing"
"github.com/pikami/cosmium/parsers"
memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor"
)
func Test_Execute_MathFunctions(t *testing.T) {
mockData := []memoryexecutor.RowType{
map[string]interface{}{"id": 1, "value": 0.0},
map[string]interface{}{"id": 2, "value": 1.0},
map[string]interface{}{"id": 3, "value": -1.0},
map[string]interface{}{"id": 4, "value": 0.5},
map[string]interface{}{"id": 5, "value": -0.5},
map[string]interface{}{"id": 6, "value": 0.707},
map[string]interface{}{"id": 7, "value": -0.707},
map[string]interface{}{"id": 8, "value": 0.866},
map[string]interface{}{"id": 9, "value": -0.866},
}
mockDataInts := []memoryexecutor.RowType{
map[string]interface{}{"id": 1, "value": -1},
map[string]interface{}{"id": 2, "value": 0},
map[string]interface{}{"id": 3, "value": 1},
map[string]interface{}{"id": 4, "value": 5},
}
t.Run("Should execute function ABS(value)", func(t *testing.T) {
testMathFunctionExecute(
t,
parsers.FunctionCallMathAbs,
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"value": 0.0, "result": 0.0},
map[string]interface{}{"value": 1.0, "result": 1.0},
map[string]interface{}{"value": -1.0, "result": 1.0},
map[string]interface{}{"value": 0.5, "result": 0.5},
map[string]interface{}{"value": -0.5, "result": 0.5},
map[string]interface{}{"value": 0.707, "result": 0.707},
map[string]interface{}{"value": -0.707, "result": 0.707},
map[string]interface{}{"value": 0.866, "result": 0.866},
map[string]interface{}{"value": -0.866, "result": 0.866},
},
)
})
t.Run("Should execute function ACOS(cosine)", func(t *testing.T) {
testMathFunctionExecute(
t,
parsers.FunctionCallMathAcos,
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"value": 0.0, "result": math.Acos(0.0) * 180 / math.Pi},
map[string]interface{}{"value": 1.0, "result": math.Acos(1.0) * 180 / math.Pi},
map[string]interface{}{"value": -1.0, "result": math.Acos(-1.0) * 180 / math.Pi},
map[string]interface{}{"value": 0.5, "result": math.Acos(0.5) * 180 / math.Pi},
map[string]interface{}{"value": -0.5, "result": math.Acos(-0.5) * 180 / math.Pi},
map[string]interface{}{"value": 0.707, "result": math.Acos(0.707) * 180 / math.Pi},
map[string]interface{}{"value": -0.707, "result": math.Acos(-0.707) * 180 / math.Pi},
map[string]interface{}{"value": 0.866, "result": math.Acos(0.866) * 180 / math.Pi},
map[string]interface{}{"value": -0.866, "result": math.Acos(-0.866) * 180 / math.Pi},
},
)
})
t.Run("Should execute function ASIN(value)", func(t *testing.T) {
testMathFunctionExecute(
t,
parsers.FunctionCallMathAsin,
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"value": 0.0, "result": math.Asin(0.0) * 180 / math.Pi},
map[string]interface{}{"value": 1.0, "result": math.Asin(1.0) * 180 / math.Pi},
map[string]interface{}{"value": -1.0, "result": math.Asin(-1.0) * 180 / math.Pi},
map[string]interface{}{"value": 0.5, "result": math.Asin(0.5) * 180 / math.Pi},
map[string]interface{}{"value": -0.5, "result": math.Asin(-0.5) * 180 / math.Pi},
map[string]interface{}{"value": 0.707, "result": math.Asin(0.707) * 180 / math.Pi},
map[string]interface{}{"value": -0.707, "result": math.Asin(-0.707) * 180 / math.Pi},
map[string]interface{}{"value": 0.866, "result": math.Asin(0.866) * 180 / math.Pi},
map[string]interface{}{"value": -0.866, "result": math.Asin(-0.866) * 180 / math.Pi},
},
)
})
t.Run("Should execute function ATAN(tangent)", func(t *testing.T) {
testMathFunctionExecute(
t,
parsers.FunctionCallMathAtan,
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"value": 0.0, "result": math.Atan(0.0) * 180 / math.Pi},
map[string]interface{}{"value": 1.0, "result": math.Atan(1.0) * 180 / math.Pi},
map[string]interface{}{"value": -1.0, "result": math.Atan(-1.0) * 180 / math.Pi},
map[string]interface{}{"value": 0.5, "result": math.Atan(0.5) * 180 / math.Pi},
map[string]interface{}{"value": -0.5, "result": math.Atan(-0.5) * 180 / math.Pi},
map[string]interface{}{"value": 0.707, "result": math.Atan(0.707) * 180 / math.Pi},
map[string]interface{}{"value": -0.707, "result": math.Atan(-0.707) * 180 / math.Pi},
map[string]interface{}{"value": 0.866, "result": math.Atan(0.866) * 180 / math.Pi},
map[string]interface{}{"value": -0.866, "result": math.Atan(-0.866) * 180 / math.Pi},
},
)
})
t.Run("Should execute function COS(value)", func(t *testing.T) {
testMathFunctionExecute(
t,
parsers.FunctionCallMathCos,
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"value": 0.0, "result": math.Cos(0.0)},
map[string]interface{}{"value": 1.0, "result": math.Cos(1.0)},
map[string]interface{}{"value": -1.0, "result": math.Cos(-1.0)},
map[string]interface{}{"value": 0.5, "result": math.Cos(0.5)},
map[string]interface{}{"value": -0.5, "result": math.Cos(-0.5)},
map[string]interface{}{"value": 0.707, "result": math.Cos(0.707)},
map[string]interface{}{"value": -0.707, "result": math.Cos(-0.707)},
map[string]interface{}{"value": 0.866, "result": math.Cos(0.866)},
map[string]interface{}{"value": -0.866, "result": math.Cos(-0.866)},
},
)
})
t.Run("Should execute function COT(value)", func(t *testing.T) {
testMathFunctionExecute(
t,
parsers.FunctionCallMathCot,
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"value": 0.0, "result": nil},
map[string]interface{}{"value": 1.0, "result": 1 / math.Tan(1.0)},
map[string]interface{}{"value": -1.0, "result": 1 / math.Tan(-1.0)},
map[string]interface{}{"value": 0.5, "result": 1 / math.Tan(0.5)},
map[string]interface{}{"value": -0.5, "result": 1 / math.Tan(-0.5)},
map[string]interface{}{"value": 0.707, "result": 1 / math.Tan(0.707)},
map[string]interface{}{"value": -0.707, "result": 1 / math.Tan(-0.707)},
map[string]interface{}{"value": 0.866, "result": 1 / math.Tan(0.866)},
map[string]interface{}{"value": -0.866, "result": 1 / math.Tan(-0.866)},
},
)
})
t.Run("Should execute function Degrees(value)", func(t *testing.T) {
testMathFunctionExecute(
t,
parsers.FunctionCallMathDegrees,
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"value": 0.0, "result": 0.0 * (180 / math.Pi)},
map[string]interface{}{"value": 1.0, "result": 1.0 * (180 / math.Pi)},
map[string]interface{}{"value": -1.0, "result": -1.0 * (180 / math.Pi)},
map[string]interface{}{"value": 0.5, "result": 0.5 * (180 / math.Pi)},
map[string]interface{}{"value": -0.5, "result": -0.5 * (180 / math.Pi)},
map[string]interface{}{"value": 0.707, "result": 0.707 * (180 / math.Pi)},
map[string]interface{}{"value": -0.707, "result": -0.707 * (180 / math.Pi)},
map[string]interface{}{"value": 0.866, "result": 0.866 * (180 / math.Pi)},
map[string]interface{}{"value": -0.866, "result": -0.866 * (180 / math.Pi)},
},
)
})
t.Run("Should execute function EXP(value)", func(t *testing.T) {
testMathFunctionExecute(
t,
parsers.FunctionCallMathExp,
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"value": 0.0, "result": math.Exp(0.0)},
map[string]interface{}{"value": 1.0, "result": math.Exp(1.0)},
map[string]interface{}{"value": -1.0, "result": math.Exp(-1.0)},
map[string]interface{}{"value": 0.5, "result": math.Exp(0.5)},
map[string]interface{}{"value": -0.5, "result": math.Exp(-0.5)},
map[string]interface{}{"value": 0.707, "result": math.Exp(0.707)},
map[string]interface{}{"value": -0.707, "result": math.Exp(-0.707)},
map[string]interface{}{"value": 0.866, "result": math.Exp(0.866)},
map[string]interface{}{"value": -0.866, "result": math.Exp(-0.866)},
},
)
})
t.Run("Should execute function FLOOR(value)", func(t *testing.T) {
testMathFunctionExecute(
t,
parsers.FunctionCallMathFloor,
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"value": 0.0, "result": math.Floor(0.0)},
map[string]interface{}{"value": 1.0, "result": math.Floor(1.0)},
map[string]interface{}{"value": -1.0, "result": math.Floor(-1.0)},
map[string]interface{}{"value": 0.5, "result": math.Floor(0.5)},
map[string]interface{}{"value": -0.5, "result": math.Floor(-0.5)},
map[string]interface{}{"value": 0.707, "result": math.Floor(0.707)},
map[string]interface{}{"value": -0.707, "result": math.Floor(-0.707)},
map[string]interface{}{"value": 0.866, "result": math.Floor(0.866)},
map[string]interface{}{"value": -0.866, "result": math.Floor(-0.866)},
},
)
})
t.Run("Should execute function IntBitNot(value)", func(t *testing.T) {
testMathFunctionExecute(
t,
parsers.FunctionCallMathIntBitNot,
mockDataInts,
[]memoryexecutor.RowType{
map[string]interface{}{"value": -1, "result": ^-1},
map[string]interface{}{"value": 0, "result": ^0},
map[string]interface{}{"value": 1, "result": ^1},
map[string]interface{}{"value": 5, "result": ^5},
},
)
})
t.Run("Should execute function LOG10(value)", func(t *testing.T) {
testMathFunctionExecute(
t,
parsers.FunctionCallMathLog10,
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"value": 0.0, "result": nil},
map[string]interface{}{"value": 1.0, "result": math.Log10(1.0)},
map[string]interface{}{"value": -1.0, "result": nil},
map[string]interface{}{"value": 0.5, "result": math.Log10(0.5)},
map[string]interface{}{"value": -0.5, "result": nil},
map[string]interface{}{"value": 0.707, "result": math.Log10(0.707)},
map[string]interface{}{"value": -0.707, "result": nil},
map[string]interface{}{"value": 0.866, "result": math.Log10(0.866)},
map[string]interface{}{"value": -0.866, "result": nil},
},
)
})
}
func testMathFunctionExecute(
t *testing.T,
functionCallType parsers.FunctionCallType,
data []memoryexecutor.RowType,
expectedData []memoryexecutor.RowType,
) {
testQueryExecute(
t,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
{
Alias: "result",
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: functionCallType,
Arguments: []interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
},
},
},
Table: parsers.Table{Value: "c"},
},
data,
expectedData,
)
}

View File

@@ -13,6 +13,7 @@ import (
) )
type RowType interface{} type RowType interface{}
type RowWithJoins map[string]RowType
type ExpressionType interface{} type ExpressionType interface{}
type memoryExecutorContext struct { type memoryExecutorContext struct {
@@ -24,24 +25,52 @@ func Execute(query parsers.SelectStmt, data []RowType) []RowType {
parameters: query.Parameters, parameters: query.Parameters,
} }
result := make([]RowType, 0) joinedRows := make([]RowWithJoins, 0)
// Apply Filter
for _, row := range data { for _, row := range data {
if ctx.evaluateFilters(query.Filters, row) { // Perform joins
result = append(result, row) dataTables := map[string][]RowType{}
for _, join := range query.JoinItems {
joinedData := ctx.getFieldValue(join.SelectItem, row)
if joinedDataArray, isArray := joinedData.([]map[string]interface{}); isArray {
var rows []RowType
for _, m := range joinedDataArray {
rows = append(rows, RowType(m))
}
dataTables[join.Table.Value] = rows
}
} }
// Generate flat rows
flatRows := []RowWithJoins{
{query.Table.Value: row},
}
for joinedTableName, joinedTable := range dataTables {
flatRows = zipRows(flatRows, joinedTableName, joinedTable)
}
// Apply filters
filteredRows := []RowWithJoins{}
for _, rowWithJoins := range flatRows {
if ctx.evaluateFilters(query.Filters, rowWithJoins) {
filteredRows = append(filteredRows, rowWithJoins)
}
}
joinedRows = append(joinedRows, filteredRows...)
} }
// Apply order // Apply order
if query.OrderExpressions != nil && len(query.OrderExpressions) > 0 { if query.OrderExpressions != nil && len(query.OrderExpressions) > 0 {
ctx.orderBy(query.OrderExpressions, result) ctx.orderBy(query.OrderExpressions, joinedRows)
} }
result := make([]RowType, 0)
// Apply group // Apply group
isGroupSelect := query.GroupBy != nil && len(query.GroupBy) > 0 isGroupSelect := query.GroupBy != nil && len(query.GroupBy) > 0
if isGroupSelect { if isGroupSelect {
result = ctx.groupBy(query, result) result = ctx.groupBy(query, joinedRows)
} }
// Apply select // Apply select
@@ -50,9 +79,9 @@ func Execute(query parsers.SelectStmt, data []RowType) []RowType {
if hasAggregateFunctions(query.SelectItems) { if hasAggregateFunctions(query.SelectItems) {
// When can have aggregate functions without GROUP BY clause, // When can have aggregate functions without GROUP BY clause,
// we should aggregate all rows in that case // we should aggregate all rows in that case
selectedData = append(selectedData, ctx.selectRow(query.SelectItems, result)) selectedData = append(selectedData, ctx.selectRow(query.SelectItems, joinedRows))
} else { } else {
for _, row := range result { for _, row := range joinedRows {
selectedData = append(selectedData, ctx.selectRow(query.SelectItems, row)) selectedData = append(selectedData, ctx.selectRow(query.SelectItems, row))
} }
} }
@@ -79,7 +108,7 @@ func Execute(query parsers.SelectStmt, data []RowType) []RowType {
return result return result
} }
func (c memoryExecutorContext) selectRow(selectItems []parsers.SelectItem, row RowType) interface{} { func (c memoryExecutorContext) selectRow(selectItems []parsers.SelectItem, row interface{}) interface{} {
// When the first value is top level, select it instead // When the first value is top level, select it instead
if len(selectItems) > 0 && selectItems[0].IsTopLevel { if len(selectItems) > 0 && selectItems[0].IsTopLevel {
return c.getFieldValue(selectItems[0], row) return c.getFieldValue(selectItems[0], row)
@@ -103,7 +132,7 @@ func (c memoryExecutorContext) selectRow(selectItems []parsers.SelectItem, row R
return newRow return newRow
} }
func (c memoryExecutorContext) evaluateFilters(expr ExpressionType, row RowType) bool { func (c memoryExecutorContext) evaluateFilters(expr ExpressionType, row RowWithJoins) bool {
if expr == nil { if expr == nil {
return true return true
} }
@@ -164,7 +193,7 @@ func (c memoryExecutorContext) evaluateFilters(expr ExpressionType, row RowType)
return false return false
} }
func (c memoryExecutorContext) getFieldValue(field parsers.SelectItem, row RowType) interface{} { func (c memoryExecutorContext) getFieldValue(field parsers.SelectItem, row interface{}) interface{} {
if field.Type == parsers.SelectItemTypeArray { if field.Type == parsers.SelectItemTypeArray {
arrayValue := make([]interface{}, 0) arrayValue := make([]interface{}, 0)
for _, selectItem := range field.SelectItems { for _, selectItem := range field.SelectItems {
@@ -200,7 +229,8 @@ func (c memoryExecutorContext) getFieldValue(field parsers.SelectItem, row RowTy
} }
rowValue := row rowValue := row
if array, isArray := row.([]RowType); isArray { // Used for aggregates
if array, isArray := row.([]RowWithJoins); isArray {
rowValue = array[0] rowValue = array[0]
} }
@@ -284,6 +314,79 @@ func (c memoryExecutorContext) getFieldValue(field parsers.SelectItem, row RowTy
case parsers.FunctionCallSetUnion: case parsers.FunctionCallSetUnion:
return c.set_Union(typedValue.Arguments, rowValue) return c.set_Union(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathAbs:
return c.math_Abs(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathAcos:
return c.math_Acos(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathAsin:
return c.math_Asin(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathAtan:
return c.math_Atan(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathCeiling:
return c.math_Ceiling(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathCos:
return c.math_Cos(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathCot:
return c.math_Cot(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathDegrees:
return c.math_Degrees(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathExp:
return c.math_Exp(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathFloor:
return c.math_Floor(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathIntBitNot:
return c.math_IntBitNot(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathLog10:
return c.math_Log10(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathRadians:
return c.math_Radians(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathRound:
return c.math_Round(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathSign:
return c.math_Sign(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathSin:
return c.math_Sin(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathSqrt:
return c.math_Sqrt(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathSquare:
return c.math_Square(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathTan:
return c.math_Tan(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathTrunc:
return c.math_Trunc(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathAtn2:
return c.math_Atn2(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathIntAdd:
return c.math_IntAdd(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathIntBitAnd:
return c.math_IntBitAnd(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathIntBitLeftShift:
return c.math_IntBitLeftShift(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathIntBitOr:
return c.math_IntBitOr(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathIntBitRightShift:
return c.math_IntBitRightShift(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathIntBitXor:
return c.math_IntBitXor(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathIntDiv:
return c.math_IntDiv(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathIntMod:
return c.math_IntMod(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathIntMul:
return c.math_IntMul(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathIntSub:
return c.math_IntSub(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathPower:
return c.math_Power(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathLog:
return c.math_Log(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathNumberBin:
return c.math_NumberBin(typedValue.Arguments, rowValue)
case parsers.FunctionCallMathPi:
return c.math_Pi()
case parsers.FunctionCallMathRand:
return c.math_Rand()
case parsers.FunctionCallAggregateAvg: case parsers.FunctionCallAggregateAvg:
return c.aggregate_Avg(typedValue.Arguments, row) return c.aggregate_Avg(typedValue.Arguments, row)
case parsers.FunctionCallAggregateCount: case parsers.FunctionCallAggregateCount:
@@ -301,6 +404,9 @@ func (c memoryExecutorContext) getFieldValue(field parsers.SelectItem, row RowTy
} }
value := rowValue value := rowValue
if joinedRow, isRowWithJoins := value.(RowWithJoins); isRowWithJoins {
value = joinedRow[field.Path[0]]
}
if len(field.Path) > 1 { if len(field.Path) > 1 {
for _, pathSegment := range field.Path[1:] { for _, pathSegment := range field.Path[1:] {
@@ -308,6 +414,8 @@ func (c memoryExecutorContext) getFieldValue(field parsers.SelectItem, row RowTy
switch nestedValue := value.(type) { switch nestedValue := value.(type) {
case map[string]interface{}: case map[string]interface{}:
value = nestedValue[pathSegment] value = nestedValue[pathSegment]
case RowWithJoins:
value = nestedValue[pathSegment]
case []int, []string, []interface{}: case []int, []string, []interface{}:
slice := reflect.ValueOf(nestedValue) slice := reflect.ValueOf(nestedValue)
if arrayIndex, err := strconv.Atoi(pathSegment); err == nil && slice.Len() > arrayIndex { if arrayIndex, err := strconv.Atoi(pathSegment); err == nil && slice.Len() > arrayIndex {
@@ -325,7 +433,7 @@ func (c memoryExecutorContext) getFieldValue(field parsers.SelectItem, row RowTy
func (c memoryExecutorContext) getExpressionParameterValue( func (c memoryExecutorContext) getExpressionParameterValue(
parameter interface{}, parameter interface{},
row RowType, row RowWithJoins,
) interface{} { ) interface{} {
switch typedParameter := parameter.(type) { switch typedParameter := parameter.(type) {
case parsers.SelectItem: case parsers.SelectItem:
@@ -337,7 +445,7 @@ func (c memoryExecutorContext) getExpressionParameterValue(
return nil return nil
} }
func (c memoryExecutorContext) orderBy(orderBy []parsers.OrderExpression, data []RowType) { func (c memoryExecutorContext) orderBy(orderBy []parsers.OrderExpression, data []RowWithJoins) {
less := func(i, j int) bool { less := func(i, j int) bool {
for _, order := range orderBy { for _, order := range orderBy {
val1 := c.getFieldValue(order.SelectItem, data[i]) val1 := c.getFieldValue(order.SelectItem, data[i])
@@ -357,8 +465,8 @@ func (c memoryExecutorContext) orderBy(orderBy []parsers.OrderExpression, data [
sort.SliceStable(data, less) sort.SliceStable(data, less)
} }
func (c memoryExecutorContext) groupBy(selectStmt parsers.SelectStmt, data []RowType) []RowType { func (c memoryExecutorContext) groupBy(selectStmt parsers.SelectStmt, data []RowWithJoins) []RowType {
groupedRows := make(map[string][]RowType) groupedRows := make(map[string][]RowWithJoins)
groupedKeys := make([]string, 0) groupedKeys := make([]string, 0)
// Group rows by group by columns // Group rows by group by columns
@@ -381,7 +489,7 @@ func (c memoryExecutorContext) groupBy(selectStmt parsers.SelectStmt, data []Row
return aggregatedRows return aggregatedRows
} }
func (c memoryExecutorContext) generateGroupKey(groupByFields []parsers.SelectItem, row RowType) string { func (c memoryExecutorContext) generateGroupKey(groupByFields []parsers.SelectItem, row RowWithJoins) string {
var keyBuilder strings.Builder var keyBuilder strings.Builder
for _, column := range groupByFields { for _, column := range groupByFields {
fieldValue := c.getFieldValue(column, row) fieldValue := c.getFieldValue(column, row)
@@ -392,7 +500,7 @@ func (c memoryExecutorContext) generateGroupKey(groupByFields []parsers.SelectIt
return keyBuilder.String() return keyBuilder.String()
} }
func (c memoryExecutorContext) aggregateGroup(selectStmt parsers.SelectStmt, groupRows []RowType) RowType { func (c memoryExecutorContext) aggregateGroup(selectStmt parsers.SelectStmt, groupRows []RowWithJoins) RowType {
aggregatedRow := c.selectRow(selectStmt.SelectItems, groupRows) aggregatedRow := c.selectRow(selectStmt.SelectItems, groupRows)
return aggregatedRow return aggregatedRow
@@ -480,3 +588,27 @@ func hasAggregateFunctions(selectItems []parsers.SelectItem) bool {
return false return false
} }
func zipRows(current []RowWithJoins, joinedTableName string, rowsToZip []RowType) []RowWithJoins {
resultMap := make([]RowWithJoins, 0)
for _, currentRow := range current {
for _, rowToZip := range rowsToZip {
newRow := copyMap(currentRow)
newRow[joinedTableName] = rowToZip
resultMap = append(resultMap, newRow)
}
}
return resultMap
}
func copyMap(originalMap map[string]RowType) map[string]RowType {
targetMap := make(map[string]RowType)
for k, v := range originalMap {
targetMap[k] = v
}
return targetMap
}