ostafen / clover

A lightweight document-oriented NoSQL database written in pure Golang.
MIT License
633 stars 54 forks source link

Hope to support semantic query building function #140

Closed duolabmeng6 closed 7 months ago

duolabmeng6 commented 8 months ago
    dataAll, err := a.FindAll(
        a.BuildQuery("names", "name=xiaobai or age=20"),
    )

    dataAll, err := a.FindAll(
        a.BuildQuery("names", "name=xiaobai and age=20"),
    )

I have a simple implementation here

func isPureNumber(s string) bool {
    _, err := strconv.Atoi(s)
    return err == nil
}

    func (a *easyDB) BuildQuery(collectionName string, querySyntax string) *query.Query {
        // Query syntax = != > >= < <= like
        // Analyze query syntax and build a query object
        // Example: name=xiaobai or age>=18
        operatorList := []string{"!=", ">=", "<=", "like", "<", ">", "="}
        q := query.NewQuery(collectionName)
        var fieldName string
        var operator string
        var value interface{}
        var _q query.Criteria
        one := false

        if strings.Contains(querySyntax, "or") {
            orConditions := strings.Split(querySyntax, "or")
            for _, conditionStatement := range orConditions {
                operator = ""
                for _, op := range operatorList {
                    if strings.Contains(conditionStatement, op) {
                        conditionStatement = strings.TrimSpace(conditionStatement)
                        fieldName = strings.Split(conditionStatement, op)[0]
                        value = strings.Split(conditionStatement, op)[1]
                        operator = op
                        break
                    }
                }
                if operator == "" {
                    break
                }
                if !one {
                    switch operator {
                    case "=":
                        if isPureNumber(value.(string)) {
                            v, _ := strconv.Atoi(value.(string))
                            _q = query.Field(fieldName).Eq(v)
                        } else {
                            _q = query.Field(fieldName).Eq(value)
                        }
                    case "!=":
                        if isPureNumber(value.(string)) {
                            v, _ := strconv.Atoi(value.(string))
                            _q = query.Field(fieldName).Neq(v)
                        } else {
                            _q = query.Field(fieldName).Neq(value)
                        }
                    case ">":
                        v, _ := strconv.Atoi(value.(string))
                        _q = query.Field(fieldName).Gt(v)
                    case ">=":
                        v, _ := strconv.Atoi(value.(string))
                        _q = query.Field(fieldName).GtEq(v)
                    case "<":
                        v, _ := strconv.Atoi(value.(string))
                        _q = query.Field(fieldName).Lt(v)
                    case "<=":
                        v, _ := strconv.Atoi(value.(string))
                        _q = query.Field(fieldName).LtEq(v)
                    case "like":
                        _q = query.Field(fieldName).Like(value.(string))
                    }
                } else {
                    switch operator {
                    case "=":
                        if isPureNumber(value.(string)) {
                            v, _ := strconv.Atoi(value.(string))
                            _q = _q.Or(query.Field(fieldName).Eq(v))
                        } else {
                            _q = _q.Or(query.Field(fieldName).Eq(value))
                        }
                    case "!=":
                        if isPureNumber(value.(string)) {
                            v, _ := strconv.Atoi(value.(string))
                            _q = _q.Or(query.Field(fieldName).Neq(v))
                        } else {
                            _q = _q.Or(query.Field(fieldName).Neq(value))
                        }
                    case ">":
                        v, _ := strconv.Atoi(value.(string))
                        _q = _q.Or(query.Field(fieldName).Gt(v))
                    case ">=":
                        v, _ := strconv.Atoi(value.(string))
                        _q = _q.Or(query.Field(fieldName).GtEq(v))
                    case "<":
                        v, _ := strconv.Atoi(value.(string))
                        _q = _q.Or(query.Field(fieldName).Lt(v))
                    case "<=":
                        v, _ := strconv.Atoi(value.(string))
                        _q = _q.Or(query.Field(fieldName).LtEq(v))
                    case "like":
                        _q = _q.Or(query.Field(fieldName).Like(value.(string)))
                    }
                }
                println(conditionStatement, fieldName, operator, value)
                one = true
            }
        }

        if strings.Contains(querySyntax, "and") {
            andConditions := strings.Split(querySyntax, "and")
            for _, conditionStatement := range andConditions {
                operator = ""
                for _, op := range operatorList {
                    if strings.Contains(conditionStatement, op) {
                        conditionStatement = strings.TrimSpace(conditionStatement)
                        fieldName = strings.Split(conditionStatement, op)[0]
                        value = strings.Split(conditionStatement, op)[1]
                        operator = op
                        break
                    }
                }
                if operator == "" {
                    break
                }
                if !one {
                    switch operator {
                    case "=":
                        if isPureNumber(value.(string)) {
                            v, _ := strconv.Atoi(value.(string))
                            _q = query.Field(fieldName).Eq(v)
                        } else {
                            _q = query.Field(fieldName).Eq(value)
                        }
                    case "!=":
                        if isPureNumber(value.(string)) {
                            v, _ := strconv.Atoi(value.(string))
                            _q = query.Field(fieldName).Neq(v)
                        } else {
                            _q = query.Field(fieldName).Neq(value)
                        }
                    case ">":
                        v, _ := strconv.Atoi(value.(string))
                        _q = query.Field(fieldName).Gt(v)
                    case ">=":
                        v, _ := strconv.Atoi(value.(string))
                        _q = query.Field(fieldName).GtEq(v)
                    case "<":
                        v, _ := strconv.Atoi(value.(string))
                        _q = query.Field(fieldName).Lt(v)
                    case "<=":
                        v, _ := strconv.Atoi(value.(string))
                        _q = query.Field(fieldName).LtEq(v)
                    case "like":
                        _q = query.Field(fieldName).Like(value.(string))
                    }
                } else {
                    switch operator {
                    case "=":
                        if isPureNumber(value.(string)) {
                            v, _ := strconv.Atoi(value.(string))
                            _q = _q.And(query.Field(fieldName).Eq(v))
                        } else {
                            _q = _q.And(query.Field(fieldName).Eq(value))
                        }
                    case "!=":
                        if isPureNumber(value.(string)) {
                            v, _ := strconv.Atoi(value.(string))
                            _q = _q.And(query.Field(fieldName).Neq(v))
                        } else {
                            _q = _q.And(query.Field(fieldName).Neq(value))
                        }
                    case ">":
                        v, _ := strconv.Atoi(value.(string))
                        _q = _q.And(query.Field(fieldName).Gt(v))
                    case ">=":
                        v, _ := strconv.Atoi(value.(string))
                        _q = _q.And(query.Field(fieldName).GtEq(v))
                    case "<":
                        v, _ := strconv.Atoi(value.(string))
                        _q = _q.And(query.Field(fieldName).Lt(v))
                    case "<=":
                        v, _ := strconv.Atoi(value.(string))
                        _q = _q.And(query.Field(fieldName).LtEq(v))
                    case "like":
                        _q = _q.And(query.Field(fieldName).Like(value.(string)))
                    }
                }
                println(conditionStatement, fieldName, operator, value)
                one = true
            }
        }

        q = q.Where(_q)

        return q
    }
ostafen commented 8 months ago

Hey, @duolabmeng6, what would be the advantage of such a functionality?

duolabmeng6 commented 8 months ago

Hey, @duolabmeng6, what would be the advantage of such a functionality?

The current writing method is too long, I hope it's simpler

Where similar to SQL statements

ostafen commented 8 months ago

I see the value of having a query language, but I don't think it should be part of clover, as it is meant to stay very simple. Also, I would say there are a lot of potential query languages that could be supported, and for such a reason, I would delegate to the user of the library the responsibility of building such an utility on top of the lib

duolabmeng6 commented 8 months ago
func TestBUG(t *testing.T) {
    store, _ := badgerstore.OpenWithOptions(badger.DefaultOptions("").WithInMemory(true))
    db, _ := c.OpenWithStore(store)
    text := `[
  {
    "name": "liming",
    "age": 18
  },
  {
    "name": "xiaobai",
    "age": 20
  },
  {
    "name": "xiaohong",
    "age": 22
  }
]`
    os.WriteFile("text.json", []byte(text), 0666)

    db.ImportCollection("names", "text.json")

    dataAll, err := db.FindAll(
        query.NewQuery("names").
            Where(
                query.Field("name").Eq("xiaobai").Or(query.Field("name").GtEq("liming")),
            ))
    if err != nil {
        println("SEARCH_FAILED")
    }
    for _, doc := range dataAll {
        fmt.Printf("%+v\n", doc)
    }
    //&{fields:map[_id:26c9acd6-f9b1-46fe-81ce-2da6f96f886c age:20 name:xiaobai]}
    //&{fields:map[_id:83dc8aaa-94ad-4333-9945-98ec6198b83d age:18 name:liming]}
    //&{fields:map[_id:ff5f6b48-0687-49ee-a4fe-ae392ac488af age:22 name:xiaohong]}
}

Your current API is not simple and has logic bugs

duolabmeng6 commented 8 months ago

func TestBUG(t *testing.T) {
    store, _ := badgerstore.OpenWithOptions(badger.DefaultOptions("").WithInMemory(true))
    db, _ := c.OpenWithStore(store)
    text := `[
  {
    "name": "liming",
    "age": 18
  },
  {
    "name": "xiaobai",
    "age": 20
  },
  {
    "name": "xiaohong",
    "age": 22
  }
]`
    os.WriteFile("text.json", []byte(text), 0666)

    db.ImportCollection("names", "text.json")

    dataAll, err := db.FindAll(
        query.NewQuery("names").
            Where(
                query.Field("name").Eq("xiaobai").And(query.Field("age").Gt("18")),
            ))
    if err != nil {
        println("SEARCH_FAILED")
    }
    for _, doc := range dataAll {
        fmt.Printf("%+v\n", doc)
    }

}

Your current API is difficult to use

query.Field("name").Eq("xiaobai").And(query.Field("age").Gt("18")), could not find it query.Field("name").Eq("xiaobai").And(query.Field("age").Gt(18)), have found

duolabmeng6 commented 8 months ago

Your design concept is very different from tinydb’s query statement A simple query should look like this

>>> User = Query()
>>> # Search for a field value
>>> db.search(User.name == 'John')
[{'name': 'John', 'age': 22}, {'name': 'John', 'age': 37}]

>>> # Combine two queries with logical and
>>> db.search((User.name == 'John') & (User.age <= 30))
[{'name': 'John', 'age': 22}]

>>> # Combine two queries with logical or
>>> db.search((User.name == 'John') | (User.name == 'Bob'))
[{'name': 'John', 'age': 22}, {'name': 'John', 'age': 37}, {'name': 'Bob', 'age': 42}]

>>> # Apply transformation to field with `map`
>>> db.search((User.age.map(lambda x: x + x) == 44))
>>> [{'name': 'John', 'age': 22}]

>>> # More possible comparisons:  !=  <  >  <=  >=
>>> # More possible checks: where(...).matches(regex), where(...).test(your_test_func)
ostafen commented 8 months ago

If there are bugs, please open separate issue, as they are not related to supporting the query language you suggest. Also, regarding TinyDB API, it's clear that Golang doesn't allow to reach same level of simplicity, but if you have some suggestions on how to improve the API your are welcome. Other than that I'm not going to support a query language from text at the moment

duolabmeng6 commented 8 months ago

I understand that Golang cannot achieve the syntax effects of Python, but it can refer to the query building API of Laravel. It is very simple and easy to write.


$users = DB::table('users')
                    ->where('votes', '>', 100)
                    ->orWhere('name', 'John')
                    ->get();

$users = DB::table('users')->where([
    ['status', '=', '1'],
    ['subscribed', '<>', '1'],
])->get();

https://laravel.com/docs/10.x/queries#where-clauses

ostafen commented 8 months ago

Supporting such API would require major effort and time that I currently don't have. Will consider to do it in future if more and more users see an advantage in that