cqllint

cqllint is a Go linter that checks that cql queries will not generate run-time errors.

While, in most cases, queries created using cql are checked at compile time, there are still some cases that can generate run-time errors (see Type safety).

cqllint analyses the Go code written to detect these cases and fix them without the need to execute the query. It also adds other detections that would not generate runtime errors but are possible misuses of cql.

Note

At the moment, the errors cql.ErrFieldModelNotConcerned, cql.ErrFieldIsRepeated, cql.ErrAppearanceMustBeSelected and cql.ErrAppearanceOutOfRange are detected.

We recommend integrating cqllint into your CI so that the use of cql ensures 100% that your queries will be executed correctly.

Installation

For simply installing it, use:

go install github.com/FrancoLiberali/cql/cqllint@latest

Warning

The version of cqllint used must be the same as the version of cql. You can install a specific version using go install github.com/FrancoLiberali/cql/cqllint@vX.Y.Z, where X.Y.Z is the version number.

Execution

cqllint can be used independently by running:

cqllint ./...

or using go vet:

go vet -vettool=$(which cqllint) ./...

Detections

cqllint has two types of detections: errors and misuses. Errors are those that would generate an error at runtime, while misuses would not generate an error but are an indication that the code is incorrect.

An example of an error is the detection of cql.ErrFieldModelNotConcerned in cql.Query.

On the contrary, an example of misuse is the use of Repeated sets in cql.Update.

The list of each of the detections performed by cqllint can be found at:

Scope and limitations

cqllint analyzes the entire scope of a CQL method call, so detection works both outside and inside the function call:

Inside function call
1_, err := cql.Query[models.City](
2    context.Background(),
3    db,
4    conditions.City.Name.Concat(
5        conditions.Country.Name,
6    ).Is().Eq(cql.String("error")),
7).Find()
In variable
1countryName := conditions.Country.Name
2
3_, err := cql.Query[models.City](
4    context.Background(),
5    db,
6    conditions.City.Name.Concat(
7        countryName,
8    ).Is().Eq(cql.String("error")),
9).Find()

In these cases the detection will be:

$ cqllint ./...
example.go:5: models.Country is not joined by the query

It also works within lists:

In list
 1conditions := []condition.Condition[models.City]{
 2    conditions.City.Name.Is().Eq(
 3        conditions.Country.Name,
 4    ),
 5}
 6
 7_, err := cql.Query[models.City](
 8    context.Background(),
 9    db,
10    conditions...,
11).Find()
$ cqllint ./...
example.go:3: models.Country is not joined by the query

On the contrary, it cannot go beyond the current scope, so, for example, it will not be able to detect parameters that a function receives.

In parameter
 1countryName := conditions.Country.Name
 2
 3func doQuery(conditions []condition.Condition[models.City]) error {
 4    _, err := cql.Query[models.City](
 5        context.Background(),
 6        db,
 7        conditions...,
 8    ).Find()
 9
10    return err
11}

On the other hand, it also cannot analyze the conditions that our code has on the conditions to be used, considering that every condition present in the code will be used, for example:

In list
 1conditions := []condition.Condition[models.City]{}
 2
 3joinCountry := false
 4
 5if joinCountry {
 6    conditions := append(conditions, conditions.City.Country())
 7}
 8
 9conditions := append(conditions, conditions.City.Name.Is().Eq(
10    conditions.Country.Name,
11))
12
13_, err := cql.Query[models.City](
14    context.Background(),
15    db,
16    conditions...,
17).Find()

In this case, since joinCountry is false, at runtime the join with country will not be performed and will result in an error, but cqllint will consider the join to be present.