database: Support SAKE filter expression

This commit is contained in:
Palapeli
2026-04-01 05:15:13 -04:00
parent 94c718f6d2
commit 5fed8960dc
12 changed files with 250 additions and 4 deletions

View File

@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"wwfc/filter"
"github.com/jackc/pgconn"
"github.com/jackc/pgerrcode"
@@ -74,7 +75,7 @@ func parseSakeFieldsFromJson(fieldsJson []byte) (map[string]SakeField, error) {
return fields, nil
}
func GetSakeRecords(pool *pgxpool.Pool, ctx context.Context, gameId int, ownerIds []int32, tableId string, recordIds []int32, fields []string, filter string) ([]SakeRecord, error) {
func GetSakeRecords(pool *pgxpool.Pool, ctx context.Context, gameId int, ownerIds []int32, tableId string, recordIds []int32, fields []string, filterExpr string) ([]SakeRecord, error) {
if fields == nil {
fields = []string{}
}
@@ -85,7 +86,28 @@ func GetSakeRecords(pool *pgxpool.Pool, ctx context.Context, gameId int, ownerId
recordIds = []int32{}
}
rows, err := pool.Query(ctx, getSakeRecordsQuery, gameId, tableId, ownerIds, recordIds)
query := getSakeRecordsQuery
if filterExpr != "" {
tree, err := filter.Parse(filterExpr)
if err != nil {
return nil, err
}
var filterQuery string
err = pool.AcquireFunc(ctx, func(conn *pgxpool.Conn) error {
filterQuery, err = createSqlFilter(conn.Conn().PgConn(), tree)
return err
})
if err != nil {
return nil, err
}
// This filter has been entirely rewritten by our filter code,
// based on the expression supplied by the user. This should be safe!!!
query += " AND (" + filterQuery + ")"
}
rows, err := pool.Query(ctx, query, gameId, tableId, ownerIds, recordIds)
if err != nil {
return nil, err
}

189
database/sake_filter.go Normal file
View File

@@ -0,0 +1,189 @@
package database
import (
"errors"
"strconv"
"strings"
"wwfc/filter"
"github.com/jackc/pgconn"
)
type expression struct {
ast *filter.TreeNode
query string
conn *pgconn.PgConn
}
func createSqlFilter(conn *pgconn.PgConn, basenode *filter.TreeNode) (value string, err error) {
defer func() {
if str := recover(); str != nil {
value = ""
err = errors.New(str.(string))
}
}()
this := &expression{basenode, "", conn}
this.filterAppendRoot(basenode)
return "(" + this.query + ")", nil
}
func (this *expression) filterAppendRoot(basenode *filter.TreeNode) {
for _, node := range basenode.Items() {
switch node.Value.Category() {
case filter.CatFunction:
this.filterSwitchFunction(node)
return
case filter.CatValue:
this.filterAppendNode(node)
return
case filter.CatOther:
this.filterSwitchOther(node)
return
}
}
panic("eval failed")
}
func (this *expression) filterSwitchOther(node *filter.TreeNode) {
switch v1 := node.Value.(type) {
case *filter.GroupToken:
if v1.GroupType == "()" {
this.filterAppendRoot(node)
return
}
}
panic("invalid node " + node.String())
}
func (this *expression) filterSwitchFunction(node *filter.TreeNode) {
val1 := node.Value.(*filter.OperatorToken)
switch strings.ToLower(val1.Operator) {
case "=", "!=":
this.filterAppendOperator(strings.ToLower(val1.Operator), node.Items())
case ">", "<", ">=", "<=", "+", "-", "&", "|", "^", "<<", ">>":
this.filterAppendMathOperator(strings.ToLower(val1.Operator), node.Items())
case "and":
this.filterAppendAnd(node.Items())
case "or":
this.filterAppendOr(node.Items())
default:
panic("function not supported: " + val1.Operator)
}
}
func (this *expression) filterAppendNode(node *filter.TreeNode) {
switch v := node.Value.(type) {
case *filter.NumberToken:
this.query += "'" + strconv.FormatInt(int64(int32(v.Value)), 10) + "'"
case *filter.IdentityToken:
this.filterAppendQueryValue(v)
case *filter.OperatorToken:
this.filterSwitchFunction(node)
case *filter.GroupToken:
if v.GroupType == "()" {
this.query += "("
this.filterAppendRoot(node)
this.query += ")"
return
}
panic("unexpected grouping type '" + v.GroupType + "': " + node.String())
case *filter.TextToken:
this.query += "(" + this.filterPushArg(v.Text) + ")::varchar"
default:
panic("unexpected value: " + node.String())
}
}
func (this *expression) filterAppendAnd(args []*filter.TreeNode) {
cnt := len(args)
if cnt < 2 {
panic("operator missing arguments")
}
this.query += "( "
this.filterAppendNode(args[0])
this.query += " AND "
this.filterAppendNode(args[1])
this.query += " )"
}
func (this *expression) filterAppendOr(args []*filter.TreeNode) {
cnt := len(args)
if cnt < 2 {
panic("operator missing arguments")
}
this.query += "( "
this.filterAppendNode(args[0])
this.query += " OR "
this.filterAppendNode(args[1])
this.query += " )"
}
func (this *expression) filterAppendOperator(operator string, args []*filter.TreeNode) {
cnt := len(args)
if cnt != 2 {
panic("operator requires exactly 2 arguments")
}
this.query += "( "
this.filterAppendNode(args[0])
this.query += " " + operator + " "
this.filterAppendNode(args[1])
this.query += " )"
}
func (this *expression) filterAppendMathOperator(operator string, args []*filter.TreeNode) {
cnt := len(args)
if cnt != 2 {
panic("operator requires exactly 2 arguments")
}
this.query += "( ("
this.filterAppendNode(args[0])
this.query += ")::int " + operator + " ("
this.filterAppendNode(args[1])
this.query += ")::int )"
}
// Get a value from the record
func (this *expression) filterAppendQueryValue(token *filter.IdentityToken) {
if token.Name == "ownerid" {
this.query += "(owner_id)"
return
}
if token.Name == "recordid" {
this.query += "(record_id)"
return
}
if token.Name == "gameid" {
this.query += "(game_id)"
return
}
if token.Name == "tableid" {
this.query += "(table_id)"
return
}
this.query += "(fields->" + this.filterPushArg(token.Name) + "->>'value')"
}
func (this *expression) filterPushArg(arg string) string {
// This is scary!!!
if this.conn == nil {
return `'` + strings.Replace(arg, "'", "''", -1) + `'`
}
str, err := this.conn.EscapeString(arg)
if err != nil {
panic(err)
}
return `'` + str + `'`
}

View File

@@ -0,0 +1,35 @@
package database
import (
"fmt"
"testing"
"wwfc/filter"
)
func testGenerateFilter(t *testing.T, expression string) (string, error) {
tree, err := filter.Parse(expression)
if err != nil {
t.Error(err)
return "", err
}
fmt.Printf("tree: %s\n", tree.String())
query, err := createSqlFilter(nil, tree)
if err != nil {
t.Error(err)
return "", err
}
fmt.Printf("query: %s\n", query)
return query, err
}
func TestSakeFilter(t *testing.T) {
testGenerateFilter(t, "ownerid = 1")
testGenerateFilter(t, "course = 12 and gameid = 1687 and time < 195")
// Random complex filter I made up
testGenerateFilter(t, "gameid = 1687 and (test = 'aaa' or (DROP = 100000) and ((((UPDATE != 4))))) or (1 = 2 + 7 & SELECT - 9)")
}

View File

@@ -1,8 +1,8 @@
package serverbrowser
import (
"wwfc/filter"
"wwfc/logging"
"wwfc/serverbrowser/filter"
"github.com/logrusorgru/aurora/v3"
)

View File

@@ -3,7 +3,7 @@ package serverbrowser
import (
"fmt"
"testing"
"wwfc/serverbrowser/filter"
"wwfc/filter"
)
func parseFilter(t *testing.T, expression string) error {