opti: bql query impl in reflect
This commit is contained in:
parent
5c05aaa102
commit
09e75df763
170
script/bql.go
170
script/bql.go
|
|
@ -1,6 +1,7 @@
|
||||||
package script
|
package script
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
@ -13,35 +14,147 @@ type QueryParams struct {
|
||||||
AccountType string `bql:"account ~"`
|
AccountType string `bql:"account ~"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func BQLQuery(ledgerConfig *Config, queryParams QueryParams, queryResult interface{}) error {
|
func BQLQueryOne(ledgerConfig *Config, queryParams *QueryParams, queryResultPtr interface{}) error {
|
||||||
bql := "SELECT '\\', id, '\\', date, '\\', payee, '\\', narration, '\\', account, '\\', position, '\\', tags, '\\' WHERE"
|
assertQueryResultIsPointer(queryResultPtr)
|
||||||
queryParamsType := reflect.TypeOf(queryParams)
|
output, err := bqlRawQuery(ledgerConfig, queryParams, queryResultPtr)
|
||||||
queryParamsValue := reflect.ValueOf(queryParams)
|
|
||||||
for i := 0; i < queryParamsValue.NumField(); i++ {
|
|
||||||
typeField := queryParamsType.Field(i)
|
|
||||||
valueField := queryParamsValue.Field(i)
|
|
||||||
switch valueField.Kind() {
|
|
||||||
case reflect.String:
|
|
||||||
val := valueField.String()
|
|
||||||
if val != "" {
|
|
||||||
bql = fmt.Sprintf("%s %s '%s' AND", bql, typeField.Tag.Get("bql"), val)
|
|
||||||
}
|
|
||||||
case reflect.Int:
|
|
||||||
val := valueField.Int()
|
|
||||||
if val != 0 {
|
|
||||||
bql = fmt.Sprintf("%s %s %d AND", bql, typeField.Tag.Get("bql"), val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bql = strings.TrimRight(bql, " AND")
|
|
||||||
|
|
||||||
output, err := queryByBQL(ledgerConfig, bql)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
err = parseResult(output, queryResultPtr, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println(output)
|
func BQLQueryList(ledgerConfig *Config, queryParams *QueryParams, queryResultPtr interface{}) error {
|
||||||
//panic("Unsupported result type")
|
assertQueryResultIsPointer(queryResultPtr)
|
||||||
|
output, err := bqlRawQuery(ledgerConfig, queryParams, queryResultPtr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = parseResult(output, queryResultPtr, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func bqlRawQuery(ledgerConfig *Config, queryParamsPtr *QueryParams, queryResultPtr interface{}) (string, error) {
|
||||||
|
bql := "SELECT"
|
||||||
|
queryResultPtrType := reflect.TypeOf(queryResultPtr)
|
||||||
|
queryResultType := queryResultPtrType.Elem()
|
||||||
|
|
||||||
|
if queryResultType.Kind() == reflect.Slice {
|
||||||
|
queryResultType = queryResultType.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < queryResultType.NumField(); i++ {
|
||||||
|
typeField := queryResultType.Field(i)
|
||||||
|
// 字段的 tag 不带 bql 的不进行拼接
|
||||||
|
b := typeField.Tag.Get("bql")
|
||||||
|
if b != "" {
|
||||||
|
if strings.Contains(b, "distinct") {
|
||||||
|
b = strings.ReplaceAll(b, "distinct", "")
|
||||||
|
bql = fmt.Sprintf("%s distinct '\\', %s, ", bql, b)
|
||||||
|
} else {
|
||||||
|
bql = fmt.Sprintf("%s '\\', %s, ", bql, typeField.Tag.Get("bql"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 查询条件不为空时,拼接查询条件
|
||||||
|
if queryParamsPtr != nil {
|
||||||
|
bql += " '\\' WHERE"
|
||||||
|
queryParamsType := reflect.TypeOf(queryParamsPtr).Elem()
|
||||||
|
queryParamsValue := reflect.ValueOf(queryParamsPtr).Elem()
|
||||||
|
for i := 0; i < queryParamsType.NumField(); i++ {
|
||||||
|
typeField := queryParamsType.Field(i)
|
||||||
|
valueField := queryParamsValue.Field(i)
|
||||||
|
switch valueField.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
val := valueField.String()
|
||||||
|
if val != "" {
|
||||||
|
bql = fmt.Sprintf("%s %s '%s' AND", bql, typeField.Tag.Get("bql"), val)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case reflect.Int:
|
||||||
|
val := valueField.Int()
|
||||||
|
if val != 0 {
|
||||||
|
bql = fmt.Sprintf("%s %s %d AND", bql, typeField.Tag.Get("bql"), val)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bql = strings.TrimRight(bql, " AND")
|
||||||
|
} else {
|
||||||
|
bql += " '\\'"
|
||||||
|
}
|
||||||
|
return queryByBQL(ledgerConfig, bql)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseResult(output string, queryResultPtr interface{}, selectOne bool) error {
|
||||||
|
queryResultPtrType := reflect.TypeOf(queryResultPtr)
|
||||||
|
queryResultType := queryResultPtrType.Elem()
|
||||||
|
|
||||||
|
if queryResultType.Kind() == reflect.Slice {
|
||||||
|
queryResultType = queryResultType.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(output, "\n")[2:]
|
||||||
|
if selectOne && len(lines) >= 3 {
|
||||||
|
lines = lines[2:3]
|
||||||
|
}
|
||||||
|
|
||||||
|
l := make([]map[string]interface{}, 0)
|
||||||
|
for _, line := range lines {
|
||||||
|
if line != "" {
|
||||||
|
values := strings.Split(line, "\\")
|
||||||
|
// 去除 '\' 分割带来的空字符串
|
||||||
|
values = values[1 : len(values)-1]
|
||||||
|
temp := make(map[string]interface{})
|
||||||
|
for i, val := range values {
|
||||||
|
field := queryResultType.Field(i)
|
||||||
|
jsonName := field.Tag.Get("json")
|
||||||
|
if jsonName == "" {
|
||||||
|
jsonName = field.Name
|
||||||
|
}
|
||||||
|
switch field.Type.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
temp[jsonName] = strings.Trim(val, " ")
|
||||||
|
break
|
||||||
|
case reflect.Array, reflect.Slice:
|
||||||
|
// 去除空格
|
||||||
|
strArray := strings.Split(val, ",")
|
||||||
|
notBlanks := make([]string, 0)
|
||||||
|
for _, s := range strArray {
|
||||||
|
if strings.Trim(s, " ") != "" {
|
||||||
|
notBlanks = append(notBlanks, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
temp[jsonName] = notBlanks
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
panic("Unsupported field type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
l = append(l, temp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsonBytes []byte
|
||||||
|
var err error
|
||||||
|
if selectOne {
|
||||||
|
jsonBytes, err = json.Marshal(l[0])
|
||||||
|
} else {
|
||||||
|
jsonBytes, err = json.Marshal(l)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(jsonBytes, queryResultPtr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,3 +168,10 @@ func queryByBQL(ledgerConfig *Config, bql string) (string, error) {
|
||||||
}
|
}
|
||||||
return string(output), nil
|
return string(output), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func assertQueryResultIsPointer(queryResult interface{}) {
|
||||||
|
k := reflect.TypeOf(queryResult).Kind()
|
||||||
|
if k != reflect.Ptr {
|
||||||
|
panic("QueryResult type must be pointer, it's " + k.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -118,3 +118,13 @@ func WriteLedgerConfigMap(newLedgerConfigMap ConfigMap) error {
|
||||||
LogSystemInfo("Success write ledger_config file (" + path + ")")
|
LogSystemInfo("Success write ledger_config file (" + path + ")")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetCommoditySymbol(commodity string) string {
|
||||||
|
switch commodity {
|
||||||
|
case "CNY":
|
||||||
|
return "¥"
|
||||||
|
case "USD":
|
||||||
|
return "$"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,29 +3,24 @@ package service
|
||||||
import (
|
import (
|
||||||
"github.com/beancount-gs/script"
|
"github.com/beancount-gs/script"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func MonthsList(c *gin.Context) {
|
type YearMonth struct {
|
||||||
months := make([]string, 0)
|
Year string `bql:"distinct year(date)" json:"year"`
|
||||||
|
Month string `bql:"month(date)" json:"month"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func MonthsList(c *gin.Context) {
|
||||||
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
||||||
beanFilePath := ledgerConfig.DataPath + "/index.bean"
|
yearMonthList := make([]YearMonth, 0)
|
||||||
bql := "SELECT distinct year(date), month(date)"
|
err := script.BQLQueryList(ledgerConfig, nil, &yearMonthList)
|
||||||
cmd := exec.Command("bean-query", beanFilePath, bql)
|
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
InternalError(c, "Failed to exec bql")
|
InternalError(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
execResult := string(output)
|
months := make([]string, 0)
|
||||||
months = make([]string, 0)
|
for _, yearMonth := range yearMonthList {
|
||||||
for _, line := range strings.Split(execResult, "\n")[2:] {
|
months = append(months, yearMonth.Year+"-"+yearMonth.Month)
|
||||||
if line != "" {
|
|
||||||
yearMonth := strings.Fields(line)
|
|
||||||
months = append(months, yearMonth[0]+"-"+yearMonth[1])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
OK(c, months)
|
OK(c, months)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,20 @@ import (
|
||||||
"github.com/beancount-gs/script"
|
"github.com/beancount-gs/script"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Transactions struct {
|
type Transactions struct {
|
||||||
Id string `bql:"id"`
|
Id string `bql:"id" json:"id"`
|
||||||
Date string `bql:"date"`
|
Date string `bql:"date" json:"date"`
|
||||||
payee string
|
Payee string `bql:"payee" json:"payee"`
|
||||||
narration string
|
Narration string `bql:"narration" json:"desc"`
|
||||||
account string
|
Account string `bql:"account" json:"account"`
|
||||||
position string
|
Tags []string `bql:"tags" json:"tags"`
|
||||||
tags string
|
Position string `bql:"position" json:"position"`
|
||||||
|
Amount string `json:"amount"`
|
||||||
|
Commodity string `json:"commodity"`
|
||||||
|
CommoditySymbol string `json:"commoditySymbol"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func getQueryModel(c *gin.Context) script.QueryParams {
|
func getQueryModel(c *gin.Context) script.QueryParams {
|
||||||
|
|
@ -40,10 +44,20 @@ func QueryTransactions(c *gin.Context) {
|
||||||
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
ledgerConfig := script.GetLedgerConfigFromContext(c)
|
||||||
queryParams := getQueryModel(c)
|
queryParams := getQueryModel(c)
|
||||||
transactions := make([]Transactions, 0)
|
transactions := make([]Transactions, 0)
|
||||||
err := script.BQLQuery(ledgerConfig, queryParams, transactions)
|
err := script.BQLQueryList(ledgerConfig, &queryParams, &transactions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
InternalError(c, err.Error())
|
InternalError(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 格式化金额
|
||||||
|
for i := 0; i < len(transactions); i++ {
|
||||||
|
pos := strings.Split(transactions[i].Position, " ")
|
||||||
|
if len(pos) == 2 {
|
||||||
|
transactions[i].Amount = pos[0]
|
||||||
|
transactions[i].Commodity = pos[1]
|
||||||
|
transactions[i].CommoditySymbol = script.GetCommoditySymbol(pos[1])
|
||||||
|
}
|
||||||
|
transactions[i].Position = ""
|
||||||
|
}
|
||||||
OK(c, transactions)
|
OK(c, transactions)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue