rql

repository·master·Indexed 18 days ago

https://github.com/a8m/rql

A resource query language for REST that provides a lightweight API for adding dynamic querying capabilities to Go web applications using SQL-based databases. It translates MongoDB-style JSON query syntax into SQL expressions and arguments, acting as a bridge between HTTP handlers and database engines. It supports integration with ORMs such as GORM, Entgo.io, XORM, and Go-PG/PG, and allows for field-level control via struct tags for filtering and sorting.

Tokens
3.7K
Snippets
10
Records
16
Agent score
62%

What's inside rql

  1. Use the RQL JSON Query API

    master

    The RQL JSON API allows frontend developers to construct complex queries using a subset of MongoDB syntax. The top-level JSON object accepts the following optional fields:

    • offset: Integer $\ge 0$. Defaults to 0.
    • limit: Integer $> 0$ and $\le$ LimitMaxValue. Defaults to DefaultLimit.
    • sort: An array of strings ([]string). Use + for ascending (default) and - for descending. Example: ["name", "-age"].
    • select: An array of strings ([]string) representing columns to include in the result.
    • filter: An object defining the WHERE clause conditions.
  2. Construct filters with RQL

    master

    Filters are translated into SQL WHERE clauses. RQL uses logical AND for object properties and logical OR for array elements (via the $or key).

    Equality and Predicates

    • Simple Equality: {"field": value} $\rightarrow$ field = ?
    • Predicate Objects: {"field": {"$predicate": value}} $\rightarrow$ field <operator> ?. Multiple predicates in one object are joined by AND.

    Supported Predicates

    • $eq, $neq: All types.
    • $gt, $lt, $gte, $lte: Numbers, strings, and timestamps.
    • $like: Strings only.

    Logical OR

    Use the $or key with an array of condition objects to create disjunctions.

    Example Input:

    {
      "$or": [
        { "city": "TLV" },
        { "zip": { "$gte": 49800, "$lte": 57080 } }
      ]
    }

    Resulting SQL Logic: city = ? OR (zip >= ? AND zip <= ?)

    {
      "age": {
        "$gt": 20,
        "$lt": 30
      }
    }
  3. Integrate rql.Param with Database ORMs

    master

    The rql.Param object is designed to be passed directly into popular Go ORMs and database drivers. Use params.FilterExp as the query expression and params.FilterArgs... as the arguments.

    GORM

    err = db.Where(p.FilterExp, p.FilterArgs).Offset(p.Offset).Limit(p.Limit).Order(p.Sort).Find(&users).Error

    Entgo.io

    users, err = client.User.Query().Where(func(s *sql.Selector) {
        s.Where(sql.ExprP(p.FilterExp, p.FilterArgs...))
    }).Limit(p.Limit).Offset(p.Offset).All(ctx)

    XORM

    err = engine.Where(p.FilterExp, p.FilterArgs...).Limit(p.Limit, p.Offset).OrderBy(p.Sort).Find(&users)

    Go-PG/PG

    err = db.Model(&users).Where(p.FilterExp, p.FilterArgs).Offset(p.Offset).Limit(p.Limit).Order(p.Sort).Select()
  4. Integrate RQL into your Go models

    master

    To enable dynamic querying for a resource, add rql struct tags to your model definition. Use the filter tag to allow a field to be used in WHERE clauses and the sort tag to allow it to be used in ORDER BY clauses.

    You can also specify a custom time layout for time.Time fields using the layout option within the tag.

    type User struct {
    	ID          uint      `gorm:"primary_key" rql:"filter,sort"` 
    	Admin       bool      `rql:"filter"` 
    	Name        string    `rql:"filter"` 
    	AddressName string    `rql:"filter"` 
    	CreatedAt   time.Time `rql:"filter,sort"` 
    	// Custom time layout
    	T1          time.Time `rql:"filter,layout=UnixDate"` 
    	T2          time.Time `rql:"filter,layout=2006-01-02 15:04"` 
    }
  5. Initialize an RQL Parser

    master

    To use RQL, you must first create a parser using rql.MustNewParser. You provide an rql.Config which defines the model being queried, the field separator (e.g., . for nested fields), and the maximum allowed value for the limit parameter to prevent resource exhaustion.

    Fields in your Go structs must be decorated with rql tags to indicate which fields are allowed for filter and sort operations.

    var QueryParser = rql.MustNewParser(rql.Config{
    	Model:    	User{},
    	FieldSep: 	".",
    	LimitMaxValue: 	25,
    })
    
    type User struct {
    	ID          uint      `gorm:"primary_key" rql:"filter,sort"`
    	Admin       bool      `rql:"filter"`
    	Name        string    `rql:"filter"`
    	Address     string    `rql:"filter"`
    	CreatedAt   time.Time `rql:"filter,sort"`
    }
  6. Parse a query from an HTTP request

    master

    RQL can parse query data from either a URL query string or the request body. If using a URL query string, the data is typically expected to be a Base64 encoded JSON blob. The Parse method returns an rql.Params object containing the parsed FilterExp, FilterArgs, Sort, Limit, and Offset which can be passed directly to a database driver like GORM.

    func getDBQuery(r *http.Request) (*rql.Params, error) {
    	var (
    		b   []byte
    		err error
    	)
    	if v := r.URL.Query().Get("query"); v != "" {
    		b, err = base64.StdEncoding.DecodeString(v)
    	} else {
    		b, err = ioutil.ReadAll(io.LimitReader(r.Body, 1<<12))
    	}
    	if err != nil {
    		return nil, err
    	}
    	return QueryParser.Parse(b)
    }
  7. Understand the Query and Params structures

    master

    RQL uses two primary data structures to represent the lifecycle of a query:

    1. Query: The raw, decoded representation of the user's JSON input. It uses high-level types like []string for Select and map[string]interface{} for Filter.
    2. Params: The processed, database-ready output. It flattens the Filter map into a single FilterExp string and a slice of FilterArgs for safe parameter binding.
    FieldQuery TypeParams TypeDescription
    LimitintintMax rows to return
    OffsetintintPagination offset
    Select[]stringstringList of fields vs comma-separated string
    Sort[]stringstringList of sort expressions vs formatted string
    Filtermap[string]interface{}FilterExp & FilterArgsNested JSON object vs SQL expression + args
  8. Initialize a Parser with NewParser or MustNewParser

    master

    To parse RQL queries, you must first create a Parser instance using a Config object. The Config defines the model (struct) being queried, limits, and field mapping rules.

    • NewParser(c Config): Returns a *Parser and an error. Use this for standard error handling.
    • MustNewParser(c Config): Returns a *Parser but panics if the configuration is invalid. This is useful for initializing global parser instances during application startup.
    // Example initialization
    config := rql.Config{
        Model: User{}, // Your data model
        LimitMaxValue: 100,
    }
    
    parser, err := rql.NewParser(config)
    if err != nil {
        log.Fatal(err)
    }
  9. Parse RQL JSON queries into rql.Param

    master

    Use QueryParser.Parse([]byte(jsonString)) to convert a JSON query string into an rql.Param object. The resulting object contains the parsed SQL-compatible expression (FilterExp), the arguments for that expression (FilterArgs), and pagination details (Limit, Offset, Sort).

    Supported JSON Query Structure

    • limit: Integer for pagination.
    • offset: Integer for pagination.
    • filter: An object defining filters. Supports:
      • Direct equality: {"admin": false}
      • Comparison operators: $gt, $lt, etc.
      • Logical operators: $or (array of objects).
    • sort: An array of strings using + for ASC or - for DESC (e.g., ["+name"] or ["-created_at"]).
    params, err := QueryParser.Parse([]byte(`{
      "limit": 25,
      "filter": {
        "admin": false,
        "created_at": {
          "$gt": "2018-01-01T16:00:00.000Z",
          "$lt": "2018-04-01T16:00:00.000Z"
        },
        "$or": [
          { "address": "TLV" },
          { "address": "NYC" }
        ]
      },
      "sort": ["-created_at"]
    }`)) 
    // params.FilterExp: "admin = ? AND created_at > ? AND created_at < ? AND (address = ? OR address = ?)"
    // params.FilterArgs: [true, 2018-01-01..., 2018-04-01..., "TLV", "NYC"]
  10. Configure an RQL Parser

    master

    To use RQL, you must create a parser using rql.New(rql.Config) or rql.MustNew(rql.Config).

    • rql.New returns an error if the configuration is invalid.
    • rql.MustNew panics if the configuration is invalid (useful for global variable initialization).

    Key configuration options in rql.Config:

    • Model: The struct representing the resource being queried.
    • ColumnFn: A function to map struct field names to database column names (e.g., gorm.ToDBName).
    • Log: A logger function used during the build stage.
    • DefaultLimit: The fallback limit if the user doesn't provide one.
    • LimitMaxValue: The maximum allowed value for the limit parameter to prevent resource exhaustion.
    var Parser = rql.MustNew(rql.Config{
    	Model:         User{},
    	ColumnFn:      gorm.ToDBName,
    	Log:           logrus.Printf,
    	DefaultLimit:  100,
    	LimitMaxValue: 200,
    })
  11. Configure the RQL parser with Config

    master

    The Config struct defines how the RQL parser interprets your data models and translates queries into SQL. When initializing a parser, you must provide a Model (a struct instance) that represents your resource.

    Key configuration options include:

    • Model: (Required) A struct instance used to define the schema via struct tags.
    • TagName: The struct tag name used for RQL metadata (defaults to rql).
    • OpPrefix: The prefix required for operators (defaults to $, e.g., $gt).
    • FieldSep: The character used to represent nested struct fields in queries (defaults to _).
    • ColumnFn: A function to map struct field names to database column names (defaults to a standard Column function).
    • DefaultLimit: The limit applied if no limit is provided (defaults to 25).
    • LimitMaxValue: The maximum allowed limit value (defaults to 100).
    • DefaultSort: The default sorting expression if none is provided.
    • Log: A logging function for debug information (defaults to log.Printf).
    type User struct {
    	Age  int    `rql:"filter,sort"` 
    	Name string `rql:"filter"` 
    }
    
    // Create a parser using the User model
    var QueryParser = rql.MustNewParser(
    	Config{
    		Model: User{},
    		FieldSep: ".", // Use dots for nested fields instead of underscores
    	}) 
  12. Handle RQL Parsing Errors

    master

    When parsing fails due to invalid JSON or schema violations (e.g., filtering on a non-filterable field), the parser returns a *ParseError.

    ParseError is a custom error type that wraps the underlying cause. You can check for it to distinguish between transport errors and query validation errors.