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:
Select: Type safety limitations and cqllint.
Insert: Type safety limitations and cqllint.
Update: Type safety limitations and cqllint.
Delete: Type safety.
Scope and limitations
cqllint analyzes the entire scope of a CQL method call, so detection works both outside and inside the 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()
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:
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.
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:
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.