Query type safety

The cql.Query method and its query system provide type safety in multiple cases.

Conditions of the model

cql will only allow us to add conditions on the model we are querying, prohibiting the use of conditions from other models in the wrong place:

Correct
1_, err := cql.Query[models.City](
2    context.Background(),
3    db,
4    conditions.City.Name.Is().Eq(cql.String("Paris")),
5).Find()
Incorrect
1_, err := cql.Query[models.City](
2    context.Background(),
3    db,
4    conditions.Country.Name.Is().Eq(cql.String("Paris")),
5).Find()

In this case, the compilation error will be:

cannot use conditions.Country.Name.Is().Eq(cql.String("Paris"))
(value of interface type condition.WhereCondition[models.Country]) as condition.Condition[models.City]...

Similarly, conditions are checked when making joins:

Correct
1_, err := cql.Query[models.City](
2    context.Background(),
3    db,
4    conditions.City.Country(
5        conditions.Country.Name.Is().Eq(cql.String("France")),
6    ),
7).Find()
Incorrect
1_, err := cql.Query[models.City](
2    context.Background(),
3    db,
4    conditions.City.Country(
5        conditions.City.Name.Is().Eq(cql.String("France")),
6    ),
7).Find()

Name of an attribute or operator

Since the conditions are made using the auto-generated code, the attributes and methods used on it will only allow us to use attributes and operators that exist:

Correct
1_, err := cql.Query[models.City](
2    context.Background(),
3    db,
4    conditions.City.Name.Is().Eq(cql.String("Paris")),
5).Find()
Incorrect
1_, err := cql.Query[models.City](
2    context.Background(),
3    db,
4    conditions.City.Namee.Is().Eq(cql.String("Paris")),
5).Find()

In this case, the compilation error will be:

conditions.City.Namee undefined (type conditions.cityConditions has no field or method Namee)

Type of an attribute

cql not only verifies that the attribute used exists but also verifies that the value compared to the attribute is of the correct type:

Correct
1_, err := cql.Query[models.City](
2    context.Background(),
3    db,
4    conditions.City.Name.Is().Eq(cql.String("Paris")),
5).Find()
Incorrect
1_, err := cql.Query[models.City](
2    context.Background(),
3    db,
4    conditions.City.Name.Is().Eq(cql.Int64(100)),
5).Find()

In this case, the compilation error will be:

cannot use cql.Int64(100) (value of struct type condition.NumericValue[int64]) as condition.ValueOfType[string]
value in argument to conditions.City.Name.Is().Eq: condition.NumericValue[int64] does not implement
condition.ValueOfType[string] (wrong type for method GetValue)

Type of an attribute (dynamic operator)

cql also checks that the type of the attributes is correct when using dynamic operators. In this case, the type of the two attributes being compared must be the same:

Correct
1_, err := cql.Query[models.City](
2    context.Background(),
3    db,
4    conditions.City.Country(
5        conditions.Country.Name.Is().Eq(conditions.City.Name),
6    ),
7).Find()
Incorrect
1_, err := cql.Query[models.City](
2    context.Background(),
3    db,
4    conditions.City.Country(
5        conditions.Country.Name.Is().Eq(conditions.City.Population),
6    ),
7).Find()

In this case, the compilation error will be:

cannot use conditions.City.Population (variable of type condition.UpdatableField[models.City, int]) as
condition.FieldOfType[string] value in argument to conditions.Country.Name.Is().Eq...

Type safety limitations and cqllint

Dynamic operators and functions

cql.Query is not safe at compile time to determine whether the values used in dynamic operators or functions, as in the following examples:

Correct
1_, err := cql.Query[models.City](
2    context.Background(),
3    db,
4    conditions.City.Country(
5        conditions.Country.Name.Is().Eq(conditions.City.Name),
6    ),
7).Find()
Not joined model in dynamic function
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()
Not joined model in dynamic operator
1_, err := cql.Query[models.City](
2    context.Background(),
3    db,
4    conditions.City.Name.Is().Eq(
5        conditions.Country.Name,
6    ),
7).Find()
Not joined model in dynamic function in dynamic operator
1_, err := cql.Query[models.City](
2    context.Background(),
3    db,
4    conditions.City.Name.Is().Eq(
5        conditions.City.Name.Concat(
6            conditions.Country.Name,
7        ),
8    ),
9).Find()

Which would generate the following error of type cql.ErrFieldModelNotConcerned at runtime:

field's model is not concerned by the query (not joined); not concerned model:
models.Country; operator: Eq; model: models.City, field: Name

Now, if we run cqllint we will see the following report:

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

Modifier methods

Similarly, the Descending and Ascending sorting modifier methods are not safe at compile time to terminate if the fields used are part of the query, as in the following examples:

Correct
1_, err := cql.Query[models.City](
2    context.Background(),
3    db,
4    conditions.City.Name.Is().Eq(cql.String("Paris")),
5).Descending(
6    conditions.City.Name,
7).Find()
Not joined model in order
1_, err := cql.Query[models.City](
2    context.Background(),
3    db,
4    conditions.City.Name.Is().Eq(cql.String("Paris")),
5).Descending(
6    conditions.Country.Name,
7).Find()

Which would generate the following error of type cql.ErrFieldModelNotConcerned at runtime:

field's model is not concerned by the query (not joined); not concerned model: models.Seller; method: Descending

Now, if we run cqllint we will see the following report:

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

Group by

Similarly, the GroupBy and Having methods are not safe at compile time to terminate if the fields used are part of the query, as in the following examples:

Correct
 1results, err := cql.Select(
 2    cql.Query[MyModel](
 3        context.Background(),
 4        db,
 5    ).GroupBy(
 6        conditions.MyModel.Name,
 7    ),
 8    cql.ValueInto(conditions.MyModel.Name, func(value string, result *Result) {
 9        result.Name = value
10    }),
11    cql.ValueInto(conditions.MyModel.Status.Aggregate().Sum(), func(value float64, result *Result) {
12        result.SumStatus = int(value)
13    }),
14)
Not joined model in group by
 1results, err := cql.Select(
 2    cql.Query[MyModel](
 3        context.Background(),
 4        db,
 5    ).GroupBy(
 6        conditions.MyOtherModel.Name,
 7    ),
 8    cql.ValueInto(conditions.MyModel.Name, func(value string, result *Result) {
 9        result.Name = value
10    }),
11    cql.ValueInto(conditions.MyModel.Status.Aggregate().Sum(), func(value float64, result *Result) {
12        result.SumStatus = int(value)
13    }),
14)

Which would generate the following error of type cql.ErrFieldModelNotConcerned at runtime:

field's model is not concerned by the query (not joined); not concerned model: MyOtherModel; method: GroupBy
Not joined model in having
 1results, err := cql.Select(
 2    cql.Query[MyModel](
 3        context.Background(),
 4        db,
 5    ).GroupBy(
 6        conditions.MyModel.Name,
 7    ).Having(
 8        conditions.MyOtherModel.Status.Aggregate().Count().Gt(cql.Int(2)),
 9    ),
10    cql.ValueInto(conditions.MyModel.Name, func(value string, result *Result) {
11        result.Name = value
12    }),
13    cql.ValueInto(conditions.MyModel.Status.Aggregate().Sum(), func(value float64, result *Result) {
14        result.SumStatus = int(value)
15    }),
16)

Which would generate the following error of type cql.ErrFieldModelNotConcerned at runtime:

field's model is not concerned by the query (not joined); not concerned model: MyOtherModel; method: Having

Now, if we run cqllint we will see the following report:

$ cqllint ./...
example.go:6: MyOtherModel is not joined by the query

Appearance

The selection of the Appearance can generate two runtime errors:

  • cql.ErrAppearanceMustBeSelected: generated when you try to use a model that appears (is joined) more than once in the query without selecting which one you want to use.

  • cql.ErrAppearanceOutOfRange: generated when you try select an appearance number (with the Appearance method) greater than the number of appearances of a model.

Both errors can be determined before runtime using cqllint.

ErrAppearanceMustBeSelected

To generate this error we must join the same model more than once and not select the appearance number:

example.go
 1_, err := cql.Query[models.Child](
 2    context.Background(),
 3    db,
 4    conditions.Child.Parent1(
 5        conditions.Parent1.ParentParent(),
 6    ),
 7    conditions.Child.Parent2(
 8        conditions.Parent2.ParentParent(),
 9    ),
10    conditions.Child.ID.Is().Eq(conditions.ParentParent.ID),
11).Find()

If we execute this query we will obtain an error of type cql.ErrAppearanceMustBeSelected with the following message:

field's model appears more than once, select which one you want to use with Appearance; model: models.ParentParent; operator: Eq; model: models.Child, field: ID

Now, if we run cqllint we will see the following report:

$ cqllint ./...
example.go:10: models.ParentParent appears more than once, select which one you want to use with Appearance

ErrAppearanceOutOfRange

To generate this error we must use the Appearance method with a value greater than the number of appearances of a model:

example.go
1_, err := cql.Query[models.Phone](
2    context.Background(),
3    db,
4    conditions.Phone.Brand(
5        conditions.Brand.Name.Is().Eq(conditions.Phone.Name.Appearance(1)),
6    ),
7).Find()

If we execute this query we will obtain an error of type cql.ErrAppearanceOutOfRange with the following message:

selected appearance is bigger than field's model number of appearances; model: models.Phone; operator: Eq; model: models.Brand, field: Name

Now, if we run cqllint we will see the following report:

$ cqllint ./...
example.go:5: selected appearance is bigger than models.Phone's number of appearances

Unnecessary Appearance selection

This is the case when the Appearance method is used without being necessary, i.e. when the model appears only once:

example.go
1_, err := cql.Query[models.Phone](
2    context.Background(),
3    db,
4    conditions.Phone.Brand(
5        conditions.Brand.Name.Is().Eq(conditions.Phone.Name.Appearance(0)),
6    ),
7).Find()

If we run cqllint we will see the following report:

$ cqllint ./...
example.go:5: Appearance call not necessary, models.Phone appears only once

Collections preloads

Another possible runtime error is cql.ErrOnlyPreloadsAllowed, generated when trying to use conditions within a preload of Collections:

Model
type Seller struct {
    model.UUIDModel

    Name string

    Company   *Company
    CompanyID *model.UUID // Company HasMany Seller (Company 0..1 -> 0..* Seller)
}

type Company struct {
    model.UUIDModel

    Sellers *[]Seller // Company HasMany Seller (Company 0..1 -> 0..* Seller)
}
Correct
1_, err := cql.Query[models.Company](
2    context.Background(),
3    db,
4    conditions.Company.Sellers.Preload(),
5).Find()
Conditions inside preload
1_, err := cql.Query[models.Company](
2    context.Background(),
3    db,
4    conditions.Company.Sellers.Preload(
5        conditions.Seller.ID.Is().Eq(cql.String("Franco")),
6    ),
7).Find()

This error has not yet been determined by cqllint.