diff --git a/condition/errors.go b/condition/errors.go index 72e45d1..8268bce 100644 --- a/condition/errors.go +++ b/condition/errors.go @@ -10,19 +10,28 @@ import ( var ( // query + ErrFieldModelNotConcerned = errors.New("field's model is not concerned by the query (not joined)") ErrJoinMustBeSelected = errors.New("field's model is joined more than once, select which one you want to use") + ErrFieldIsRepeated = errors.New("field is repeated") // conditions - ErrEmptyConditions = errors.New("at least one condition is required") - ErrOnlyPreloadsAllowed = errors.New("only conditions that do a preload are allowed") + + ErrEmptyConditions = errors.New("at least one condition is required") // crud + ErrMoreThanOneObjectFound = errors.New("found more that one object that meet the requested conditions") ErrObjectNotFound = errors.New("no object exists that meets the requested conditions") + // database + ErrUnsupportedByDatabase = errors.New("method not supported by database") ErrOrderByMustBeCalled = errors.New("order by must be called before limit in an update statement") + + // preload + + ErrOnlyPreloadsAllowed = errors.New("only conditions that do a preload are allowed") ) func methodError(err error, method string) error { @@ -43,6 +52,14 @@ func joinMustBeSelectedError(field IField) error { ) } +func fieldIsRepeatedError(field IField) error { + return fmt.Errorf("%w; field: %s.%s", + ErrFieldIsRepeated, + field.getModelType(), + field.fieldName(), + ) +} + func preloadsInReturningNotAllowed(dialector sql.Dialector) error { return fmt.Errorf("%w; preloads in returning are not allowed for database: %s", ErrUnsupportedByDatabase, diff --git a/condition/gorm_query.go b/condition/gorm_query.go index 0da072c..0370f73 100644 --- a/condition/gorm_query.go +++ b/condition/gorm_query.go @@ -362,6 +362,11 @@ func getUpdateTablesAndValues(query *GormQuery, sets []ISet) (map[IField]TableAn return nil, err } + _, fieldAlreadyPresent := tables[field] + if fieldAlreadyPresent { + return nil, fieldIsRepeatedError(field) + } + tables[field] = TableAndValue{ table: table, value: updateValue, diff --git a/cqllint/pkg/analyzer/analyzer.go b/cqllint/pkg/analyzer/analyzer.go index e8dd258..6bbd32b 100644 --- a/cqllint/pkg/analyzer/analyzer.go +++ b/cqllint/pkg/analyzer/analyzer.go @@ -21,15 +21,15 @@ var Analyzer = &analysis.Analyzer{ URL: "compiledquerylenguage.readthedocs.io", Run: run, Requires: []*analysis.Analyzer{inspect.Analyzer}, - // Version: cql.Version, } var ( cqlMethods = []string{"Query", "Update", "Delete"} cqlOrder = []string{"Descending", "Ascending"} cqlSetMultiple = "SetMultiple" - cqlSet = "Set" - cqlSelectors = append(cqlOrder, cqlSetMultiple, cqlSet) + cqlSets = []string{cqlSetMultiple, "Set"} + cqlSelectors = append(cqlOrder, cqlSets...) + dynamicMethod = "Dynamic" ) type Model struct { @@ -66,7 +66,7 @@ func run(pass *analysis.Pass) (interface{}, error) { if isSelector { positionsToReport = findForSelector(callExpr, positionsToReport) } else { - positionsToReport, _ = findForIndex(callExpr, positionsToReport) + positionsToReport, _ = findNotConcernedForIndex(callExpr, positionsToReport) } }) @@ -81,7 +81,7 @@ func run(pass *analysis.Pass) (interface{}, error) { return nil, nil //nolint:nilnil // is necessary } -// Finds errors in selector functions: Descending, Ascending, SetMultiple, Set +// Finds NotConcerned and Repeated errors in selector functions: Descending, Ascending, SetMultiple, Set func findForSelector(callExpr *ast.CallExpr, positionsToReport []Model) []Model { selectorExpr := callExpr.Fun.(*ast.SelectorExpr) @@ -89,7 +89,14 @@ func findForSelector(callExpr *ast.CallExpr, positionsToReport []Model) []Model return positionsToReport } - _, models := findForIndex(selectorExpr.X.(*ast.CallExpr), positionsToReport) + findRepeatedFields(callExpr, selectorExpr) + + return fieldNotConcerned(callExpr, selectorExpr, positionsToReport) +} + +// Finds NotConcerned errors in selector functions: Descending, Ascending, SetMultiple, Set +func fieldNotConcerned(callExpr *ast.CallExpr, selectorExpr *ast.SelectorExpr, positionsToReport []Model) []Model { + _, models := findNotConcernedForIndex(selectorExpr.X.(*ast.CallExpr), positionsToReport) for _, arg := range callExpr.Args { var model Model @@ -101,9 +108,10 @@ func findForSelector(callExpr *ast.CallExpr, positionsToReport []Model) []Model positionsToReport = addPositionsToReport(positionsToReport, models, model) } else { argCallExpr := arg.(*ast.CallExpr) + setFunction := argCallExpr.Fun.(*ast.SelectorExpr).Sel.Name - if setFunction == "Dynamic" { + if setFunction == dynamicMethod { model = getModel(argCallExpr.Args[0].(*ast.CallExpr).Fun.(*ast.SelectorExpr).X.(*ast.SelectorExpr).X.(*ast.SelectorExpr)) positionsToReport = addPositionsToReport(positionsToReport, models, model) } @@ -118,8 +126,63 @@ func findForSelector(callExpr *ast.CallExpr, positionsToReport []Model) []Model return positionsToReport } -// Finds errors in index functions: cql.Query, cql.Update, cql.Delete -func findForIndex(callExpr *ast.CallExpr, positionsToReport []Model) ([]Model, []string) { +func findRepeatedFields(call *ast.CallExpr, selectorExpr *ast.SelectorExpr) { + if !pie.Contains(cqlSets, selectorExpr.Sel.Name) { + return + } + + fields := map[string][]token.Pos{} + + for _, arg := range call.Args { + argCall := arg.(*ast.CallExpr) + argSelector := argCall.Fun.(*ast.SelectorExpr) + condition := argSelector.X.(*ast.CallExpr).Fun.(*ast.SelectorExpr).X.(*ast.SelectorExpr) + + fieldName := getFieldName(condition) + fieldPos := condition.Sel.NamePos + + _, isPresent := fields[fieldName] + if !isPresent { + fields[fieldName] = []token.Pos{fieldPos} + } else { + fields[fieldName] = append(fields[fieldName], fieldPos) + } + + if argSelector.Sel.Name == dynamicMethod && len(argCall.Args) == 1 { + comparedField := argCall.Args[0].(*ast.CallExpr).Fun.(*ast.SelectorExpr).X.(*ast.SelectorExpr) + comparedFieldName := getFieldName(comparedField) + + if comparedFieldName == fieldName { + passG.Reportf( + comparedField.Sel.NamePos, + "%s is set to itself", + comparedFieldName, + ) + } + } + } + + for fieldName, positions := range fields { + if len(positions) > 1 { + for _, pos := range positions { + passG.Reportf( + pos, + "%s is repeated", + fieldName, + ) + } + } + } +} + +func getFieldName(condition *ast.SelectorExpr) string { + conditionModel := condition.X.(*ast.SelectorExpr) + + return conditionModel.X.(*ast.Ident).Name + "." + conditionModel.Sel.Name + "." + condition.Sel.Name +} + +// Finds NotConcerned errors in index functions: cql.Query, cql.Update, cql.Delete +func findNotConcernedForIndex(callExpr *ast.CallExpr, positionsToReport []Model) ([]Model, []string) { indexExpr, isIndex := callExpr.Fun.(*ast.IndexExpr) if !isIndex { // other functions may be between callExpr and the cql method, example: cql.Query(...).Limit(1).Descending @@ -127,7 +190,7 @@ func findForIndex(callExpr *ast.CallExpr, positionsToReport []Model) ([]Model, [ if isSelector { internalCallExpr, isCall := selectorExpr.X.(*ast.CallExpr) if isCall { - return findForIndex(internalCallExpr, positionsToReport) + return findNotConcernedForIndex(internalCallExpr, positionsToReport) } } diff --git a/cqllint/pkg/analyzer/analyzer_test.go b/cqllint/pkg/analyzer/analyzer_test.go index fb7c47c..604ce79 100644 --- a/cqllint/pkg/analyzer/analyzer_test.go +++ b/cqllint/pkg/analyzer/analyzer_test.go @@ -8,7 +8,12 @@ import ( "github.com/FrancoLiberali/cql/cqllint/pkg/analyzer" ) -func TestAll(t *testing.T) { +func TestErrNotConcerned(t *testing.T) { testdata := analysistest.TestData() - analysistest.Run(t, testdata, analyzer.Analyzer, "a") + analysistest.Run(t, testdata, analyzer.Analyzer, "not_concerned") +} + +func TestErrRepeated(t *testing.T) { + testdata := analysistest.TestData() + analysistest.Run(t, testdata, analyzer.Analyzer, "repeated") } diff --git a/cqllint/pkg/analyzer/testdata/go.mod b/cqllint/pkg/analyzer/testdata/go.mod index f681976..c45b2a7 100644 --- a/cqllint/pkg/analyzer/testdata/go.mod +++ b/cqllint/pkg/analyzer/testdata/go.mod @@ -3,7 +3,10 @@ module github.com/FrancoLiberali/cql/cqllint/pkg/analyzer/testdata go 1.18 require ( - a v0.0.1 + not_concerned v0.0.1 + repeated v0.0.1 ) -replace a => ./src/a +replace not_concerned => ./src/not_concerned + +replace repeated => ./src/repeated diff --git a/cqllint/pkg/analyzer/testdata/src/not_concerned/go.mod b/cqllint/pkg/analyzer/testdata/src/not_concerned/go.mod new file mode 100644 index 0000000..281d4fb --- /dev/null +++ b/cqllint/pkg/analyzer/testdata/src/not_concerned/go.mod @@ -0,0 +1,10 @@ +module github.com/FrancoLiberali/cql/cqllint/pkg/analyzer/testdata/src/not_concerned + +go 1.18 + +require ( + gorm.io/gorm v1.25.6 + github.com/FrancoLiberali/cql v0.0.1 +) + +replace github.com/FrancoLiberali/cql => ./../../../../../.. diff --git a/cqllint/pkg/analyzer/testdata/src/a/order.go b/cqllint/pkg/analyzer/testdata/src/not_concerned/order.go similarity index 99% rename from cqllint/pkg/analyzer/testdata/src/a/order.go rename to cqllint/pkg/analyzer/testdata/src/not_concerned/order.go index 97dad0d..9465399 100644 --- a/cqllint/pkg/analyzer/testdata/src/a/order.go +++ b/cqllint/pkg/analyzer/testdata/src/not_concerned/order.go @@ -1,4 +1,4 @@ -package a +package not_concerned import ( "github.com/FrancoLiberali/cql" diff --git a/cqllint/pkg/analyzer/testdata/src/a/query.go b/cqllint/pkg/analyzer/testdata/src/not_concerned/query.go similarity index 99% rename from cqllint/pkg/analyzer/testdata/src/a/query.go rename to cqllint/pkg/analyzer/testdata/src/not_concerned/query.go index 9e60726..bd4383b 100644 --- a/cqllint/pkg/analyzer/testdata/src/a/query.go +++ b/cqllint/pkg/analyzer/testdata/src/not_concerned/query.go @@ -1,4 +1,4 @@ -package a +package not_concerned import ( "gorm.io/gorm" diff --git a/cqllint/pkg/analyzer/testdata/src/a/set.go b/cqllint/pkg/analyzer/testdata/src/not_concerned/set.go similarity index 76% rename from cqllint/pkg/analyzer/testdata/src/a/set.go rename to cqllint/pkg/analyzer/testdata/src/not_concerned/set.go index 7220f62..c73fe47 100644 --- a/cqllint/pkg/analyzer/testdata/src/a/set.go +++ b/cqllint/pkg/analyzer/testdata/src/not_concerned/set.go @@ -1,4 +1,4 @@ -package a +package not_concerned import ( "github.com/FrancoLiberali/cql" @@ -23,26 +23,26 @@ func testSetDynamicNotJoinedInDifferentLines() { } func testSetDynamicMainModel() { - cql.Update[models.Brand]( + cql.Update[models.Product]( db, - conditions.Brand.Name.IsDynamic().Eq(conditions.City.Name.Value()), // want "github.com/FrancoLiberali/cql/test/models.City is not joined by the query" + conditions.Product.String.IsDynamic().Eq(conditions.City.Name.Value()), // want "github.com/FrancoLiberali/cql/test/models.City is not joined by the query" ).Set( - conditions.Brand.Name.Set().Dynamic( - conditions.Brand.Name.Value(), + conditions.Product.Int.Set().Dynamic( + conditions.Product.IntPointer.Value(), ), ) } func testSetDynamicMainModelMultipleTimes() { - cql.Update[models.Brand]( + cql.Update[models.Product]( db, - conditions.Brand.Name.IsDynamic().Eq(conditions.City.Name.Value()), // want "github.com/FrancoLiberali/cql/test/models.City is not joined by the query" + conditions.Product.String.IsDynamic().Eq(conditions.City.Name.Value()), // want "github.com/FrancoLiberali/cql/test/models.City is not joined by the query" ).Set( - conditions.Brand.Name.Set().Dynamic( - conditions.Brand.Name.Value(), + conditions.Product.String.Set().Dynamic( + conditions.Product.String2.Value(), ), - conditions.Brand.Name.Set().Dynamic( - conditions.Brand.Name.Value(), + conditions.Product.Int.Set().Dynamic( + conditions.Product.IntPointer.Value(), ), ) } @@ -54,7 +54,6 @@ func testSetDynamicJoinedModel() { conditions.Phone.Name.IsDynamic().Eq(conditions.City.Name.Value()), // want "github.com/FrancoLiberali/cql/test/models.City is not joined by the query" ).Set( conditions.Phone.Name.Set().Dynamic(conditions.Brand.Name.Value()), - conditions.Phone.Name.Set().Dynamic(conditions.Brand.Name.Value()), ) } @@ -67,7 +66,7 @@ func testSetDynamicNestedJoinedModel() { conditions.Child.Name.IsDynamic().Eq(conditions.City.Name.Value()), // want "github.com/FrancoLiberali/cql/test/models.City is not joined by the query" ).Set( conditions.Child.Name.Set().Dynamic(conditions.Parent1.Name.Value()), - conditions.Child.Name.Set().Dynamic(conditions.ParentParent.Name.Value()), + conditions.Child.Number.Set().Dynamic(conditions.ParentParent.Number.Value()), ) } @@ -97,12 +96,12 @@ func testSetMultipleMainModel() { } func testSetMultipleMainModelMultipleTimes() { - cql.Update[models.Brand]( + cql.Update[models.Product]( db, - conditions.Brand.Name.IsDynamic().Eq(conditions.City.Name.Value()), // want "github.com/FrancoLiberali/cql/test/models.City is not joined by the query" + conditions.Product.String.IsDynamic().Eq(conditions.City.Name.Value()), // want "github.com/FrancoLiberali/cql/test/models.City is not joined by the query" ).SetMultiple( - conditions.Brand.Name.Set().Eq("asd"), - conditions.Brand.Name.Set().Eq("asd"), + conditions.Product.String.Set().Eq("asd"), + conditions.Product.Int.Set().Eq(1), ) } diff --git a/cqllint/pkg/analyzer/testdata/src/a/go.mod b/cqllint/pkg/analyzer/testdata/src/repeated/go.mod similarity index 92% rename from cqllint/pkg/analyzer/testdata/src/a/go.mod rename to cqllint/pkg/analyzer/testdata/src/repeated/go.mod index 32a7f0d..ddec206 100644 --- a/cqllint/pkg/analyzer/testdata/src/a/go.mod +++ b/cqllint/pkg/analyzer/testdata/src/repeated/go.mod @@ -1,4 +1,4 @@ -module github.com/FrancoLiberali/cql/cqllint/pkg/analyzer/testdata/src/a +module github.com/FrancoLiberali/cql/cqllint/pkg/analyzer/testdata/src/repeated go 1.18 diff --git a/cqllint/pkg/analyzer/testdata/src/repeated/set.go b/cqllint/pkg/analyzer/testdata/src/repeated/set.go new file mode 100644 index 0000000..99640a1 --- /dev/null +++ b/cqllint/pkg/analyzer/testdata/src/repeated/set.go @@ -0,0 +1,76 @@ +package not_concerned + +import ( + "github.com/FrancoLiberali/cql" + "github.com/FrancoLiberali/cql/test/conditions" + "github.com/FrancoLiberali/cql/test/models" + "gorm.io/gorm" +) + +var db *gorm.DB + +func testSetRepeated() { + cql.Update[models.Product]( + db, + conditions.Product.Int.Is().Eq(0), + ).Set( + conditions.Product.Int.Set().Eq(1), // want "conditions.Product.Int is repeated" + conditions.Product.Int.Set().Eq(2), // want "conditions.Product.Int is repeated" + ) +} + +func testSetNotRepeated() { + cql.Update[models.Product]( + db, + conditions.Product.Int.Is().Eq(0), + ).Set( + conditions.Product.Int.Set().Eq(2), + ) +} + +func testSetDynamicRepeated() { + cql.Update[models.Product]( + db, + conditions.Product.Int.Is().Eq(0), + ).Set( + conditions.Product.Int.Set().Dynamic(conditions.Product.IntPointer.Value()), // want "conditions.Product.Int is repeated" + conditions.Product.Int.Set().Dynamic(conditions.Product.IntPointer.Value()), // want "conditions.Product.Int is repeated" + ) +} + +func testSetDynamicNotRepeated() { + cql.Update[models.Product]( + db, + conditions.Product.Int.Is().Eq(0), + ).Set( + conditions.Product.Int.Set().Dynamic(conditions.Product.IntPointer.Value()), + ) +} + +func testSetMultipleRepeated() { + cql.Update[models.Product]( + db, + conditions.Product.Int.Is().Eq(0), + ).SetMultiple( + conditions.Product.Int.Set().Eq(1), // want "conditions.Product.Int is repeated" + conditions.Product.Int.Set().Eq(2), // want "conditions.Product.Int is repeated" + ) +} + +func testSetMultipleNotRepeated() { + cql.Update[models.Product]( + db, + conditions.Product.Int.Is().Eq(0), + ).SetMultiple( + conditions.Product.Int.Set().Eq(2), + ) +} + +func testSetDynamicSameValue() { + cql.Update[models.Product]( + db, + conditions.Product.Int.Is().Eq(0), + ).Set( + conditions.Product.Int.Set().Dynamic(conditions.Product.Int.Value()), // want "conditions.Product.Int is set to itself" + ) +} diff --git a/docs/cql/concepts.rst b/docs/cql/concepts.rst index 923ea56..1e4b409 100644 --- a/docs/cql/concepts.rst +++ b/docs/cql/concepts.rst @@ -132,7 +132,7 @@ The set of conditions that are received by the `cql.Query`, `cql.Update` and `cql.Delete` methods form the cql compiled query system. It is so named because the conditions will verify at compile time that the query to be executed is correct. -For details visit :ref:`cql/query:conditions` and :doc:`/cql/compile_time_safety`. +For details visit :ref:`cql/query:conditions` and :doc:`/cql/type_safety`. Conditions generation ---------------------------- diff --git a/docs/cql/cqllint.rst b/docs/cql/cqllint.rst index fcafac5..7af3cd7 100644 --- a/docs/cql/cqllint.rst +++ b/docs/cql/cqllint.rst @@ -7,11 +7,12 @@ cqllint 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 :ref:`cql/type_safety:Runtime errors`). -cqllint analyses the Go code written to detect these cases and fix them without the need to execute the query. +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, only the error cql.ErrFieldModelNotConcerned is detected. + At the moment, only the errors cql.ErrFieldModelNotConcerned and cql.ErrFieldIsRepeated are detected. We recommend integrating cqllint into your CI so that the use of cql ensures 100% that your queries will be executed correctly. @@ -45,10 +46,13 @@ or using `go vet`: go vet -vettool=$(which cqllint) ./... -Example ----------------------------- +Errors +------------------------------- + +ErrFieldModelNotConcerned +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The simplest example of an error case is trying to make a comparison +The simplest example this error case is trying to make a comparison with an attribute of a model that is not joined by the query: .. code-block:: go @@ -75,4 +79,69 @@ Now, if we run cqllint we will see the following report: $ cqllint ./... example.go:3: models.City is not joined by the query -In this way, we will be able to correct this error without having to execute the query. \ No newline at end of file +In this way, we will be able to correct this error without having to execute the query. + +ErrFieldIsRepeated +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The simplest example this error case is trying to set the value of an attribute twice: + +.. code-block:: go + :caption: example.go + :class: with-errors + :emphasize-lines: 5,6 + :linenos: + + _, err := cql.Update[models.Brand]( + db, + conditions.Brand.Name.Is().Eq("nike"), + ).Set( + conditions.Brand.Name.Set().Eq("adidas"), + conditions.Brand.Name.Set().Eq("puma"), + ) + +If we execute this query we will obtain an error of type `cql.ErrFieldIsRepeated` with the following message: + +.. code-block:: none + + field is repeated; field: models.Brand.Name; method: Set + +Now, if we run cqllint we will see the following report: + +.. code-block:: none + + $ cqllint ./... + example.go:5: conditions.Brand.Name is repeated + example.go:6: conditions.Brand.Name is repeated + +In this way, we will be able to correct this error without having to execute the query. + +Misuses +------------------------- + +Although some cases would not generate runtime errors, cqllint will detect them as they are possible misuses of cql. + +Set the same value +^^^^^^^^^^^^^^^^^^^^^^^^^ + +This case occurs when making a Set of exactly the same value: + +.. code-block:: go + :caption: example.go + :class: with-errors + :emphasize-lines: 5 + :linenos: + + _, err := cql.Update[models.Brand]( + db, + conditions.Brand.Name.Is().Eq("nike"), + ).Set( + conditions.Brand.Name.Set().Dynamic(conditions.Brand.Name.Value()), + ) + +If we run cqllint we will see the following report: + +.. code-block:: none + + $ cqllint ./... + example.go:5: conditions.Brand.Name is set to itself \ No newline at end of file diff --git a/docs/cql/tutorial.rst b/docs/cql/tutorial.rst index 9780d4b..179e60b 100644 --- a/docs/cql/tutorial.rst +++ b/docs/cql/tutorial.rst @@ -178,8 +178,7 @@ We can run this tutorial with `make tutorial_4` and we will obtain the following As you can see, again we get only the Paris in France. -In this tutorial we have used a condition that performs a join, -for more details you can read :ref:`cql/query:Use of the conditions`. +In this tutorial we have used a condition that performs a join. Tutorial 5: preloading ------------------------------- diff --git a/docs/cql/type_safety.rst b/docs/cql/type_safety.rst index a82c10e..c1ce1a0 100644 --- a/docs/cql/type_safety.rst +++ b/docs/cql/type_safety.rst @@ -191,6 +191,7 @@ there are still some possible cases that generate the following run-time errors: to the rest of the query (not joined). - cql.ErrJoinMustBeSelected: generated when you try to use a model that is included (joined) more than once in the query without selecting which one you want to use (see :ref:`cql/advanced_query:select join`). +- cql.ErrFieldIsRepeated **(1)**: generated when a field is repeated inside a Set call (see :doc:`/cql/update`). - cql.ErrOnlyPreloadsAllowed: generated when trying to use conditions within a preload of collections (see :ref:`cql/advanced_query:collections`). - cql.ErrUnsupportedByDatabase: generated when an attempt is made to use a method or function that is not supported by the database engine used. - cql.ErrOrderByMustBeCalled: generated when in MySQL you try to do a delete/update with Limit but without using OrderBy. diff --git a/errors.go b/errors.go index 2ac7449..60b7354 100644 --- a/errors.go +++ b/errors.go @@ -7,20 +7,23 @@ import ( var ( // query + ErrFieldModelNotConcerned = condition.ErrFieldModelNotConcerned ErrJoinMustBeSelected = condition.ErrJoinMustBeSelected - - // conditions - ErrOnlyPreloadsAllowed = condition.ErrOnlyPreloadsAllowed + ErrFieldIsRepeated = condition.ErrFieldIsRepeated // crud + ErrMoreThanOneObjectFound = condition.ErrMoreThanOneObjectFound ErrObjectNotFound = condition.ErrObjectNotFound // database + ErrUnsupportedByDatabase = condition.ErrUnsupportedByDatabase ErrOrderByMustBeCalled = condition.ErrOrderByMustBeCalled // preload - ErrRelationNotLoaded = preload.ErrRelationNotLoaded + + ErrOnlyPreloadsAllowed = condition.ErrOnlyPreloadsAllowed + ErrRelationNotLoaded = preload.ErrRelationNotLoaded ) diff --git a/go.work b/go.work index e7dbb2e..7d33f8c 100644 --- a/go.work +++ b/go.work @@ -6,5 +6,6 @@ use ( ./cql-gen/tests ./cqllint ./cqllint/pkg/analyzer/testdata - ./cqllint/pkg/analyzer/testdata/src/a + ./cqllint/pkg/analyzer/testdata/src/not_concerned + ./cqllint/pkg/analyzer/testdata/src/repeated ) diff --git a/test/update_test.go b/test/update_test.go index f9017cb..50c14b9 100644 --- a/test/update_test.go +++ b/test/update_test.go @@ -135,6 +135,19 @@ func (ts *UpdateIntTestSuite) TestUpdateMultipleFieldsAtTheSameTime() { ts.NotEqual(product.UpdatedAt.UnixMicro(), productReturned.UpdatedAt.UnixMicro()) } +func (ts *UpdateIntTestSuite) TestUpdateSameFieldTwiceReturnsError() { + _, err := cql.Update[models.Product]( + ts.db, + conditions.Product.Int.Is().Eq(0), + ).Set( + conditions.Product.Int.Set().Eq(1), + conditions.Product.Int.Set().Eq(2), + ) + ts.ErrorIs(err, cql.ErrFieldIsRepeated) + ts.ErrorContains(err, "method: Set") + ts.ErrorContains(err, "field: models.Product.Int") +} + func (ts *UpdateIntTestSuite) TestUpdateWithJoinInConditions() { brand1 := ts.createBrand("google") brand2 := ts.createBrand("apple")