Update

While update operations can still be performed using gorm’s Save method (see gorm documentation), this is useful only if the model(s) to be updated have already been loaded from the database.

On the contrary, cql’s Update method allows the update of all the models that meet the conditions entered without the need to load the information (via the direct execution of an UPDATE statement).

Update methods

Update operations are divided into two parts: the Update method and the Set method. In the first one, we must define the conditions that will determine which models will be updated. Here, the whole system of compilable queries is valid (for details visit Query). In the second one, we define the updates to be performed.

The object obtained using cql.Update has different methods that will allow you to modify the query:

Modifier methods

Modifier methods are those that modify the query in a certain way, affecting the models updated:

  • Limit: specifies the number of models to be updated.

  • Ascending: specifies an ascending order when updating models.

  • Descending: specifies a descending order when updating models.

  • Returning: specifies that the updated models must be fetched from the database after being updated (not supported by MySQL). Preload of related data is also possible (not supported by SQLite).

Finishing methods

Finishing methods are those that cause the query to be executed:

  • Set: defines the updates to be performed.

  • SetMultiple: (only supported by MySQL) allows updates to be made to different tables at the same time.

Example

type MyModel struct {
    model.UUIDModel

    Name string
}

updatedCount, err := cql.Update[MyModel](
    context.Background(),
    db,
    conditions.MyModel.Name.Is().Eq(cql.String("a_string")),
).Set(
    conditions.MyModel.Name.Set().Eq(cql.String("a_string_2")),
)

As you can see, the syntax for the Set method is similar to the queries system with the difference that the Set method must be used instead of Is.

For attributes that allow null (nullable values, pointers, nullable relations) the .Set().Null() method will also be available.

Joins

It is also possible to perform joins in the first part of the update (Update method):

type MyOtherModel struct {
    model.UUIDModel

    Name string
}

type MyModel struct {
    model.UUIDModel

    Name string

    Related   *MyOtherModel
    RelatedID *model.UUID
}

updatedCount, err := cql.Update[MyModel](
    context.Background(),
    db,
    conditions.MyModel.Related(
        conditions.MyOtherModel.Name.Is().Eq(cql.String("a_string")),
    ),
).Set(
    conditions.MyModel.Name.Set().Eq(cql.String("a_string_2")),
)

Here the only limitation is that in the Set part, only the values of the initial model can be updated (not of the joined models).

This limitation is imposed by the database engines, with the exception of MySQL, which allows multiple tables to be updated at the same time. To do this, you use the SetMultiple method:

updatedCount, err := cql.Update[MyModel](
    context.Background(),
    db,
    conditions.MyModel.Related(
        conditions.MyOtherModel.Name.Is().Eq(cql.String("a_string")),
    ),
).SetMultiple(
    conditions.MyModel.Name.Set().Eq(cql.String("a_string_2")),
    conditions.MyOtherModel.Name.Set().Eq(cql.String("a_string_2")),
)

Dynamic updates

Updates can also be dynamic, meaning that the set can be a value from the same entity or another entity. Functions can also be used on the values.

For example:

type MyModel struct {
    model.UUIDModel

    Value1 int
    Value2 int
}

updatedCount, err := cql.Update[MyModel](
    context.Background(),
    db,
    conditions.MyModel.Value1.Is().Eq(cql.Int64(2)),
).Set(
    conditions.MyModel.Value1.Set().Eq(conditions.MyModel.Value2.Divided(cql.Int64(2))),
)

Updated at

If your model contains a base model with timestamps (model.UUIDModelWithTimestamps or model.UIntModelWithTimestamps), cql will automatically add updated_at = now() to entities that are updated.

Type safety

Update uses the same system of compilable conditions as cql.Query, so it shares its features and limitations in terms of type safety at compile time.

For more details, see Query type safety.

Set

In addition, Update also provides the same type safety in Set methods, ensuring that the value to be set is of the same type as the attribute to be modified:

Model
1type MyModel struct {
2    model.UUIDModel
3
4    ValueInt    int
5    ValueString string
6}
Correct
1updatedCount, err := cql.Update[MyModel](
2    context.Background(),
3    db,
4    conditions.MyModel.ValueInt.Is().Eq(cql.Int64(2)),
5).Set(
6    conditions.MyModel.ValueInt.Set().Eq(conditions.MyModel.ValueInt.Divided(cql.Int64(2))),
7)
Incorrect
1updatedCount, err := cql.Update[MyModel](
2    context.Background(),
3    db,
4    conditions.MyModel.ValueInt.Is().Eq(cql.Int64(2)),
5).Set(
6    conditions.MyModel.ValueInt.Set().Eq(conditions.MyModel.ValueString),
7)

In this case, the compilation error will be:

cannot use conditions.MyModel.ValueString (variable of struct type condition.StringField[MyModel]) as
condition.ValueOfType[float64] value in argument to conditions.MyModel.ValueInt.Set().Eq:
condition.StringField[MyModel] does not implement condition.ValueOfType[float64] (wrong type for method GetValue)

Returning

In cql.Update, the Returning method is also safe at compile time, allowing you to only obtain results in a list of the correct type:

Correct
1myModelsUpdated := []MyModel{}
2
3updatedCount, err := cql.Update[MyModel](
4    context.Background(),
5    db,
6    conditions.MyModel.ValueInt.Is().Eq(cql.Int64(2)),
7).Returning(&myModelsUpdated).Set(
8    conditions.MyModel.ValueInt.Set().Eq(cql.Int64(3)),
9)
Incorrect
1myModelsUpdated := []MyOtherModel{}
2
3updatedCount, err := cql.Update[MyModel](
4    context.Background(),
5    db,
6    conditions.MyModel.ValueInt.Is().Eq(cql.Int64(2)),
7).Returning(&myModelsUpdated).Set(
8    conditions.MyModel.ValueInt.Set().Eq(cql.Int64(3)),
9)

In this case, the compilation error will be:

cannot use &myModelsUpdated (value of type *[]MyOtherModel) as *[]MyModel value in argument to
cql.Update[MyModel](context.Background(), db, conditions.MyModel.ValueInt.Is().Eq(cql.Int64(2))).Returning

Null update

For fields that are nullable, such as pointers or null.* types, cql.Update will allow you to safely set their value to null at compile time, i.e., giving a compile-time error if you try to update a non-nullable attribute to null:

Model
1type MyModel struct {
2    model.UUIDModel
3
4    ValueInt        int
5    ValueIntPointer *int
6}
Correct
1updatedCount, err := cql.Update[MyModel](
2    context.Background(),
3    db,
4    conditions.MyModel.ValueInt.Is().Eq(cql.Int64(2)),
5).Set(
6    conditions.MyModel.ValueIntPointer.Set().Null(),
7)
Incorrect
1updatedCount, err := cql.Update[MyModel](
2    context.Background(),
3    db,
4    conditions.MyModel.ValueInt.Is().Eq(cql.Int64(2)),
5).Set(
6    conditions.MyModel.ValueInt.Set().Null(),
7)

In this case, the compilation error will be:

conditions.MyModel.ValueInt.Set().Null undefined
(type condition.FieldSet[MyModel, int] has no field or method Null)

Type safety limitations and cqllint

Dynamic sets

Once again, similar to cql.Query, Set is not safe at compile time to determine whether the values used in Eq or used in functions in Eq are joined in the query, as in the following examples:

Incorrect
1updatedCount, err := cql.Update[MyModel](
2    context.Background(),
3    db,
4    conditions.MyModel.ValueInt.Is().Eq(cql.Int64(2)),
5).Set(
6    conditions.MyModel.ValueInt.Set().Eq(conditions.MyOtherModel.Value1),
7)
Incorrect
1updatedCount, err := cql.Update[MyModel](
2    context.Background(),
3    db,
4    conditions.MyModel.ValueInt.Is().Eq(cql.Int64(2)),
5).Set(
6    conditions.MyModel.ValueInt.Set().Eq(conditions.MyModel.ValueInt.Plus(conditions.MyOtherModel.Value1)),
7)

Which would generate the following error at runtime:

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

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

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

Repeated sets

While databases allow it, cql detects and generates a runtime error when attempting to set more than one value to the same attribute within an update:

Correct
1_, err := cql.Update[models.Brand](
2    context.Background(),
3    db,
4    conditions.Brand.Name.Is().Eq(cql.String("nike")),
5).Set(
6    conditions.Brand.Name.Set().Eq(cql.String("puma")),
7)
Incorrect example.go
1_, err := cql.Update[models.Brand](
2    context.Background(),
3    db,
4    conditions.Brand.Name.Is().Eq(cql.String("nike")),
5).Set(
6    conditions.Brand.Name.Set().Eq(cql.String("adidas")),
7    conditions.Brand.Name.Set().Eq(cql.String("puma")),
8)

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

field is repeated; field: models.Brand.Name; method: Set

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

$ cqllint ./...
example.go:5: conditions.Brand.Name is repeated
example.go:6: conditions.Brand.Name is repeated

Set the same value

Although this case does not generate a runtime error, making a Set of exactly the same value is considered misuse and detected by cqllint:

example.go
1_, err := cql.Update[models.Brand](
2    context.Background(),
3    db,
4    conditions.Brand.Name.Is().Eq(cql.String("nike")),
5).Set(
6    conditions.Brand.Name.Set().Eq(conditions.Brand.Name),
7)

If we run cqllint we will see the following report:

$ cqllint ./...
example.go:6: conditions.Brand.Name is set to itself

Limit and order

As mentioned above, in MySQL it is possible to limit the number of rows updated using the Limit method. For it to work correctly, it is also necessary to add a call to a sorting method:

Correct
1_, err := cql.Update[models.Brand](
2    context.Background(),
3    db,
4    conditions.Brand.Name.Is().Eq(cql.String("nike")),
5).Ascending(
6    conditions.Brand.Name,
7).Limit(1).Set(
8    conditions.Brand.Name.Set().Eq(cql.String("puma")),
9)
Incorrect example.go
1_, err := cql.Update[models.Brand](
2    context.Background(),
3    db,
4    conditions.Brand.Name.Is().Eq(cql.String("nike")),
5).Limit(1).Set(
6    conditions.Brand.Name.Set().Eq(cql.String("puma")),
7)

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

order by must be called before limit in an update statement; method: Limit

This error is not yet supported by cqllint.