Resource Query Language for REST

RQL
GoDoc LICENSE Build Status

RQL is a resource query language for REST. It provides a simple and light-weight API for adding dynamic querying capabilities to web-applications that use SQL-based database. It functions as the connector between the HTTP handler and the DB engine, and manages all validations and translations for user inputs.

rql diagram

Motivation

In the last several years I have found myself working on different web applications in Go, some of them were small and some of them were big with a lot of entities and relations. In all cases I never found a simple and standard API to query my resources.

What do I mean by query? Let's say our application has a table of orders, and we want our users to be able to search and filter by dynamic parameters. For example: select all orders from today with price greater than 100.
In order to achieve that I used to pass these parameters in the query string like this: created_at_gt=X&price_gt=100.
But sometimes it became complicated when I needed to apply a disjunction between two conditions. For example, when I wanted to select all orders that canceled or created last week and still didn't ship. And in SQL syntax:

SELECT * FROM ORDER WHERE canceled = 1 OR (created_at < X AND created_at > Y AND shipped = 0)

I was familiar with the MongoDB syntax and I felt that it was simple and robust enough to achieve my goals, and decided to use it as the query language for this project. I wanted it to be project agnostic in the sense of not relying on anything that related to some specific application or resource. Therefore, in order to embed rql in a new project, a user just needs to import the package and add the desired tags to his struct definition. Follow the Getting Started section to learn more.

Getting Started

rql uses a subset of MongoDB query syntax. If you are familiar with the MongoDB syntax, it will be easy for you to start. Although, it's pretty simple and easy to learn.
In order to embed rql you simply need to add the tags you want (filter or sort) to your struct definition, and rql will manage all validations for you and return an informative error for the end user if the query doesn't follow the schema. Here's a short example of how to start using rql quickly, or you can go to API for more expanded documentation.

// An example of an HTTP handler that uses gorm, and accepts user query in either the body
// or the URL query string.
package main

var (
	db *gorm.DB
	// QueryParam is the name of the query string key.
	QueryParam = "query"
	// MustNewParser panics if the configuration is invalid.
	QueryParser = rql.MustNewParser(rql.Config{
		Model:    User{},
		FieldSep: ".",
	})
)

// User is the model in gorm's terminology.
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"`
}


// GetUsers is an http.Handler that accepts a db query in either the body or the query string.
func GetUsers(w http.ResponseWriter, r *http.Request) {
	var users []User
	p, err := getDBQuery(r)
	if err != nil {
		io.WriteString(w, err.Error())
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	err = db.Where(p.FilterExp, p.FilterArgs).
		Offset(p.Offset).
		Limit(p.Limit).
		Order(p.Sort).
		Find(&users).Error
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	if err := json.NewEncoder(w).Encode(users); err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/json")
}

// getDBQuery extract the query blob from either the body or the query string
// and execute the parser.
func getDBQuery(r *http.Request) (*rql.Params, error) {
	var (
		b   []byte
		err error
	)
	if v := r.URL.Query().Get(QueryParam); 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)
}

Go to examples/simple to see the full working example.

API

In order to start using rql, you need to configure your parser. Let's go over a basic example of how to do this. For more details and updated documentation, please checkout the godoc.
There are two options to build a parser, rql.New(rql.Config), and rql.MustNew(rql.Config). The only difference between the two is that rql.New returns an error if the configuration is invalid, and rql.MustNew panics.

// we use rql.MustPanic because we don't want to deal with error handling in top level declarations.
var Parser = rql.MustNew(rql.Config{
	// User if the resource we want to query.
	Model: User{},
	// Since we work with gorm, we want to use its column-function, and not rql default.
	// although, they are pretty the same.
	ColumnFn: gorm.ToDBName,
	// Use your own custom logger. This logger is used only in the building stage.
	Log: logrus.Printf,
	// Default limit returned by the `Parse` function if no limit provided by the user.
	DefaultLimit: 100,
	// Accept only requests that pass limit value that is greater than or equal to 200.
	LimitMaxValue: 200,
})

rql uses reflection in the build process to detect the type of each field, and create a set of validation rules for each one. If one of the validation rules fails or rql encounters an unknown field, it returns an informative error to the user. Don't worry about the usage of reflection, it happens only once when you build the parser. Let's go over the validation rules:

  1. int (8,16,32,64), sql.NullInt6 - Round number
  2. uint (8,16,32,64), uintptr - Round number and greater than or equal to 0
  3. float (32,64), sql.NullFloat64: - Number
  4. bool, sql.NullBool - Boolean
  5. string, sql.NullString - String
  6. time.Time, and other types that convertible to time.Time - The default layout is time.RFC3339 format (JS format), and parsable to time.Time. It's possible to override the time.Time layout format with custom one. You can either use one of the standard layouts in the time package, or use a custom one. For example:
    type User struct {
     	T1 time.Time `rql:"filter"`                         // time.RFC3339
     	T2 time.Time `rql:"filter,layout=UnixDate"`         // time.UnixDate
     	T3 time.Time `rql:"filter,layout=2006-01-02 15:04"` // 2006-01-02 15:04 (custom)
    }

Note that all rules are applied to pointers as well. It means, if you have a field Name *string in your struct, we still use the string validation rule for it.

User API

We consider developers as the users of this API (usually FE developers). Let's go over the JSON API we export for resources.
The top-level query accepts JSON with 4 fields: offset, limit, filter and sort. All of them are optional.

offset and limit

These two fields are useful for paging and they are equivalent to OFFSET and LIMIT in a standard SQL syntax.

  • offset must be greater than or equal to 0 and its default value is 0
  • limit must be greater than and less than or equal to the configured LimitMaxValue. The default value for LimitMaxValue is 100

sort

Sort accepts a slice of strings ([]string) that is translated to the SQL ORDER BY clause. The given slice must contain only columns that are sortable (have tag rql:"sort"). The default order for column is ascending order in SQL, but you can control it with an optional prefix: + or -. + means ascending order, and - means descending order. Let's see a short example:

For input - ["address.name", "-address.zip.code", "+age"]
Result is - address_name, address_zip_code DESC, age ASC

select

Select accepts a slice of strings ([]string) that is joined with comma (",") to the SQL SELECT clause.

For input - ["name", "age"]
Result is - "name, age"

filter

Filter is the one who is translated to the SQL WHERE clause. This object that contains filterable fields or the disjunction ($or) operator. Each field in the object represents a condition in the WHERE clause. It contains a specific value that matched the type of the field or an object of predicates. Let's go over them:

  • Field follows the format: field: <value>, means the predicate that will be used is =. For example:
    For input:
    {
      "admin": true
    }
    
    Result is: admin = ?
    
    You can see that RQL uses placeholders in the generated WHERE statement. Follow the examples section to see how to use it properly.
  • If the field follows the format: field: { <predicate>: <value>, ...}, For example:
    For input:
    {
      "age": {
        "$gt": 20,
        "$lt": 30
      }
    }
    
    Result is: age > ? AND age < ?
    
    It means that the logical AND operator used between the two predicates. Scroll below to see the full list of the supported predicates.
  • $or is a field that represents the logical OR operator, and can be in any level of the query. Its type need to be an array of condition objects and the result of it is the disjunction between them. For example:
    For input:
    {
      "$or": [
        { "city": "TLV" },
        { "zip": { "$gte": 49800, "$lte": 57080 } }
      ]
    }
    
    Result is: city = ? OR (zip >= ? AND zip <= ?)
    

To simplify that, the rule is AND for objects and OR for arrays. Let's go over the list of supported predicates and then we'll show a few examples.

Predicates
  • $eq and $neq - can be used on all types
  • $gt, $lt, $gte and $lte - can be used on numbers, strings, and timestamp
  • $like - can be used only on type string

If a user tries to apply an unsupported predicate on a field it will get an informative error. For example:

For input:
{
  "age": {
    "$like": "%0"
  }
}

Result is: can not apply op "$like" on field "age"

Examples

Assume this is the parser for all examples.

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"`
}

Simple Example

params, err := QueryParser.Parse([]byte(`{
  "limit": 25,
  "offset": 0,
  "filter": {
    "admin": false
  }
  "sort": ["+name"]
}`))
must(err, "parse should pass")
fmt.Println(params.Limit)	// 25
fmt.Println(params.Offset)	// 0
fmt.Println(params.Sort)	// "name ASC"
fmt.Println(params.FilterExp)	// "name = ?"
fmt.Println(params.FilterArgs)	// [true]

In this case you've a valid generated rql.Param object and you can pass its to your favorite package connector.

var users []User

// gorm
err := db.Where(p.FilterExp, p.FilterArgs).
	Offset(p.Offset).
	Limit(p.Limit).
	Order(p.Sort).
	Find(&users).Error
must(err, "failed to query gorm")

// xorm
err := engine.Where(p.FilterExp, p.FilterArgs...).
	Limit(p.Limit, p.Offset).
	OrderBy(p.Sort).
	Find(&users)
must(err, "failed to query xorm")

// go-pg/pg
err := db.Model(&users).
	Where(p.FilterExp, p.FilterArgs).
	Offset(p.Offest).
	Limit(p.Limit).
	Order(p.Sort).
	Select()
must(err, "failed to query pg/orm")

// Have more example? feel free to add.

Medium Example

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"]
}`))
must(err, "parse should pass")
fmt.Println(params.Limit)	// 25
fmt.Println(params.Offset)	// 0
fmt.Println(params.Sort)	// "created_at DESC"
fmt.Println(params.FilterExp)	// "admin = ? AND created_at > ? AND created_at < ? AND (address = ? OR address = ?)"
fmt.Println(params.FilterArgs)	// [true, Time(2018-01-01T16:00:00.000Z), Time(2018-04-01T16:00:00.000Z), "TLV", "NYC"]

Future Plans and Contributions

If you want to help with the development of this package, here is a list of options things I want to add

  • JS library for query building
  • Option to ignore validation with specific tag
  • Add $not and $nor operators
  • Automatically (or by config) filter and sort gorm.Model fields
  • benchcmp for PRs
  • Support MongoDB. Output need to be a bison object. here's a usage example
  • Right now rql assume all fields are flatted in the db, even for nested fields. For example, if you have a struct like this:
    type User struct {
        Address struct {
            Name string `rql:"filter"`
        }
    }
    rql assumes that the address_name field exists in the table. Sometimes it's not the case and address exists in a different table. Therefore, I want to add the table= option for fields, and support nested queries.
  • Code generation version - low priority

Performance and Reliability

The performance of RQL looks pretty good, but there is always a room for improvement. Here's the current bench result:

Test Time/op B/op allocs/op
Small 1809 960 19
Medium 6030 3100 64
Large 14726 7625 148

I ran fuzzy testing using go-fuzz and I didn't see any crashes. You are welcome to run by yourself and find potential failures.

LICENSE

I am providing code in the repository to you under MIT license. Because this is my personal repository, the license you receive to my code is from me and not my employer (Facebook)

Owner
Comments
  • Time Based Queries with Specific Format

    Time Based Queries with Specific Format

    First off, thanks for this awesome library! Some awesome work has been put into this project, and I'm going to be able to leverage a lot of it. 👍


    I'd like to be able to filter on time range based results with a specific time format. For example, I'm storing time in YYYY-MM-DD HH:MM format. If the following filter is provided, I'd like it to error.

    {
        "filter": {
            "startTime": {
                $gt: "2006-01-02T15:04:05Z07:00"
        }
    }
    

    It looks like time validation in the library right now just validates based on RFC3339 time format, so the filter expression above would work. What would be your recommendation to achieve this?

  • How to Express Multiple Predicates of the Same Type

    How to Express Multiple Predicates of the Same Type

    @a8m I want to get a filter that returns all object's whose status != X AND status != Y.

    I would expect something like:

    {"filter":{"status":{"$neq":"X", "$neq":"Y"}}}
    

    The RQL parser only takes the second predicate. So it results in:

    status != Y

    How can I express what I'm trying to express in RQL?

  • Question: How to prevent repeat of filters when making query of form

    Question: How to prevent repeat of filters when making query of form "(A or B) and (C or D)"?

    Currently if I want to build an SQL as follows:

    ... WHERE
    (status = 'INACTIVE' OR status = 'ARCHIVED')
    AND
    (content_category_id = 2 OR content_category_id = 3)
    

    I have to use following filter object:

    {
        "$or":[
           {
              "status":"INACTIVE",
              "$or":[
                    { "content_category_id":2 },
                    { "content_category_id":3 }
              ]
           },
           {
              "status":"ARCHIVED",
              "$or":[
                    { "content_category_id":2 },
                    { "content_category_id":3 }
              ]
           }
        ]
    }
    

    There is a repeat of snippet

                    { "content_category_id":2 },
                    { "content_category_id":3 }
    

    is there any way where I could write just the above snippet only once?

  • Add LICENSE file to enable go.dev

    Add LICENSE file to enable go.dev

    To view reference documentation on go.dev, it is necessary to include a LICENSE file that the site can detect.

    • https://pkg.go.dev/github.com/a8m/rql
    • https://pkg.go.dev/license-policy

    Would this be possible to add?

  • Help !! Cant figure the error, Unkmow field near offset ... error

    Help !! Cant figure the error, Unkmow field near offset ... error

    Hello, I tried to do simple parsing in the struct show below: image

    And the code that I use is this: image

    I have encoded a simple struct value like this: image

    "{"CatId":"6fdd779c-71c7-4c5d-9b6a-2fcb1e71b97d","UserId":null,"Created":"2020-11-02T14:20:36.890782516+05:45","Approved":null,"ApprovedBy":null,"Blocked":false,"Within":1000}"

    Which resulted in base64 encoding as follows: image

    I am able to successfully decode the base64 withing my code, but rql keeps on throwing me an error as such: image

    It keeps on throwing Unknown field, while the field is very much there in the struct. What is the issue that I ran into?, Can anyone help me on this please ?

  • Adding postgres syntax support for filter arguments

    Adding postgres syntax support for filter arguments

    This PR defines an optional Dialect configuration setting and interface to provide "custom" formatting for filter parameters.

    The provided implementation is for Postgres will generate arguments in the format of col = $1, col2 = $2, colN = $n.

    This is useful when not using an orm like gorm, etc. Configuration might look like this:

    parser := rql.MustNewParser(rql.Config{
    		Model:    Application{},
    		ColumnFn: swag.ToJSONName,
    		Dialect:  rql.Postgres(),
    	})
    
  • expose ParseQuery to parse query struct

    expose ParseQuery to parse query struct

    Just exposing a parse function that accepts the Query object to allow for greater control. Primary use case is to allow cleaner web framework integration and allow flatter query parameters ie ?query={limit=..,offset=..} vs ?limit=..&offset=..&filter={..}.

    On the web framework side this allows the framework to handle things like defaults and validation so that it can be consistent with the rest of the web project. Depending on the web framework (gin,goa) this may also allow it to auto document the structure better.

  • Build Query String

    Build Query String

    on your example: curl --request POST --data '{"filter": {"name": {"$like": "t%st"}}}' http://localhost:8080/users

    how if we want to use GET request using query string ? how is the query string looks like ?

  • Fix example for gorm

    Fix example for gorm

    without this fix, when using a filter using more than one column gorm would try to use the FilterArgs only on the first column. gorm api reference: https://github.com/go-gorm/gorm/blob/373bcf7aca01ef76c8ba5c3bc1ff191b020afc7b/chainable_api.go#L155

  • Question: Just parse without need to create a QueryParser (aka rql.MustNewParser)

    Question: Just parse without need to create a QueryParser (aka rql.MustNewParser)

    As I know, I need to create a parser to be able to parse the query into params. Like:

    QueryParser = rql.MustNewParser(rql.Config{
        Model:    User{},
        FieldSep: ".",
    })
    
    // and then ...
    
    params, err := QueryParser.Parse(query)
    

    What I want is a way to just parse the raw query without care about the other stuff. Something like:

    params, err := rql.RawQuery(query)
    

    So it will produce the same result as QueryParser.Parse but without any aditional steps validating the data, checking struct tags etc.

    There is any method to do something like that?

  • rql_test failes statistically

    rql_test failes statistically

    When running rql_test sometimes the split function gets stuck and gets to a point of out of memory. After a lot of debugging i found out that the way the equalExp function implemented is wrong.

    The check

    if s1[i][0] == '(' && s2[j][0] == '(' {
        found = equalExp(s1[i][1:len(s1[i])-1], s2[j][1:len(s2[j])-1])
    }
    

    is wrong because if we get ( at the start we are removing the end of the string, not the next ) found which is the problem there is a corruption of data (a = b AND c = d) OR (a = c AND (c = d OR d = f)) will be a = b AND c = d) OR (a = c AND (c = d OR d = f)

    The problem starts here and get to the split function which is not validating the given data and not breaking out of the while loop after it failes at the start with finding )

  • Positional Parameter Support

    Positional Parameter Support

    This PR provides support for:

    1. Alternate parameter symbols (i.e. $ rather than ?)
    2. Position parameters in the FilterExp, i.e. (name = $1, age = $2).

    There are set via two new Config parameters:

    {
    	ParamSymbol string
    
    	PositionalParams bool
    }
    

    Addionally, a Config parameter ParamOffset int is also provided.

    This supports the use case where the FilterExp is appended to a larger expression and is not the initial parameter set for example:

    	parser := rql.MustNewParser(rql.Config{
    		Model:            Application{},
    		ColumnFn:         swag.ToJSONName,
    		ParamSymbol:      "$",
    		PositionalParams: true,
    		ParamOffset:      9,
    	})
    
    	params, err := parser.ParseQuery(query)
    	if err != nil {
    		return err
    	}
    
    	row := b.db.QueryRowx(
    		`UPDATE applications SET
    		name=$1, description=$2, type=$3, login_uris=$4, redirect_uris=$5, logout_uris=$6, allowed_grants=$7, permissions=$8
    		WHERE `+params.FilterExp+
    			` RETURNING *`,
    		args...)
    

    where params.FilterExp would be id=$9

    Ultimately this allows for the use of this library to create statements directly compatible with Postgres and other dialects that use positional parameters.

    Test Cases

    Some of the internal testing methods had to be modified to support both of these use cases, namely the split function that only supported the ? character. This was modified to support any character as well as handle positional arguments by ignoring digits post the parameter symbol.

  • unrecognized key \

    unrecognized key \"$and\" for filtering

    1. unrecognized key "$and" for filtering; 2. unrecognized key "$between" for filtering; 3. $or: Error 1241: Operand should contain 1 column(s)
  • Specify a Filter Name That Differs from the Column Name

    Specify a Filter Name That Differs from the Column Name

    I'd like to have a property that is filterable but have the name of the filterable property be different than the name of the column that backs the property. For example

    type User struct {
        FirstName string `json:"firstName" rql:"filter,name=firstName,column=first_name"`
    }
    

    So a filter like this

    {
        "filter": {
            "firstName": "Bob"
        }
    }
    

    produces

    WHERE first_name="Bob"

    I don't see support for this currently. Is that correct? If so, would doing this be that difficult? I don't know the codebase very well, but this could be really useful for my team using this library.

GSQL is a structured query language code builder for golang.

GSQL is a structured query language code builder for golang.

Dec 12, 2022
Simple filter query language parser so that you can build SQL, Elasticsearch, etc. queries safely from user input.

fexpr fexpr is a filter query language parser that generates extremely easy to work with AST structure so that you can create safely SQL, Elasticsearc

Dec 29, 2022
Query, update and convert data structures from the command line. Comparable to jq/yq but supports JSON, TOML, YAML, XML and CSV with zero runtime dependencies.
Query, update and convert data structures from the command line. Comparable to jq/yq but supports JSON, TOML, YAML, XML and CSV with zero runtime dependencies.

dasel Dasel (short for data-selector) allows you to query and modify data structures using selector strings. Comparable to jq / yq, but supports JSON,

Jan 2, 2023
A simple Go package to Query over JSON/YAML/XML/CSV Data
A simple Go package to Query over JSON/YAML/XML/CSV Data

A simple Go package to Query over JSON Data. It provides simple, elegant and fast ODM like API to access, query JSON document Installation Install the

Jan 8, 2023
JSON query expression library in Golang.

jsonql JSON query expression library in Golang. This library enables query against JSON. Currently supported operators are: (precedences from low to h

Dec 31, 2022
Go monolith with embedded microservices including GRPC,REST,GraphQL and The Clean Architecture.
Go monolith with embedded microservices including GRPC,REST,GraphQL and The Clean Architecture.

GoArcc - Go monolith with embedded microservices including GRPC,REST, graphQL and The Clean Architecture. Description When you start writing a Go proj

Dec 21, 2022
A simple (yet effective) GraphQL to HTTP / REST router

ReGraphQL A simple (yet effective) GraphQL to REST / HTTP router. ReGraphQL helps you expose your GraphQL queries / mutations as REST / HTTP endpoints

Dec 12, 2022
Terraform Provider for Azure (Resource Manager)Terraform Provider for Azure (Resource Manager)
Terraform Provider for Azure (Resource Manager)Terraform Provider for Azure (Resource Manager)

Terraform Provider for Azure (Resource Manager) Version 2.x of the AzureRM Provider requires Terraform 0.12.x and later, but 1.0 is recommended. Terra

Oct 16, 2021
Fadvisor(FinOps Advisor) is a collection of exporters which collect cloud resource pricing and billing data guided by FinOps, insight cost allocation for containers and kubernetes resource
Fadvisor(FinOps Advisor) is a collection of exporters which collect cloud resource pricing and billing data guided by FinOps, insight cost allocation for containers and kubernetes resource

[TOC] Fadvisor: FinOps Advisor fadvisor(finops advisor) is used to solve the FinOps Observalibility, it can be integrated with Crane to help users to

Jan 3, 2023
Eos-resource-purchase-cal - Calculate eos resource fee with golang

eos-resource-purchase-cal calculate eos resource fee Info this rep complete eosi

Jan 20, 2022
Apachedist-resource - A concourse resource to track updates of an apache distribution, e.g. tomcat

Apache Distribution Resource A concourse resource that can track information abo

Feb 2, 2022
A REST framework for quickly writing resource based services in Golang.

What is Resoursea? A high productivity web framework for quickly writing resource based services fully implementing the REST architectural style. This

Sep 27, 2022
An Oracle Cloud (OCI) Pulumi resource package, providing multi-language access to OCI

Oracle Cloud Infrastructure Resource Provider The Oracle Cloud Infrastructure (OCI) Resource Provider lets you manage OCI resources. Installing This p

Dec 2, 2022
Go-Postgresql-Query-Builder - A query builder for Postgresql in Go

Postgresql Query Builder for Go This query builder aims to make complex queries

Nov 17, 2022
`go-redash-query` is a simple library to get structed data from `redash query` sources

go-redash-query go-redash-query is a simple library to get structed data from redash query sources Example Source table id name email 1 Dannyhann rhrn

May 22, 2022
Query Parser for REST

Query Parser for REST Query Parser is a library for easy building dynamic SQL queries to Database. It provides a simple API for web-applications which

Dec 24, 2022
💊 A git query language

Gitql Gitql is a Git query language. In a repository path... See more here Reading the code ⚠️ This project was created in 2014 as my first go project

Dec 29, 2022
JSONata in Go Package jsonata is a query and transformation language for JSON

JSONata in Go Package jsonata is a query and transformation language for JSON. It's a Go port of the JavaScript library JSONata.

Nov 8, 2022
hdq - HTML DOM Query Language for Go+

hdq - HTML DOM Query Language for Go+ Summary about hdq hdq is a Go+ package for processing HTML documents. Tutorials Collect links of a html page How

Dec 13, 2022
GSQL is a structured query language code builder for golang.

GSQL is a structured query language code builder for golang.

Dec 12, 2022