From 8578021f73ced12ae7ddf5c0be5852ac8305b4e8 Mon Sep 17 00:00:00 2001 From: Franco Liberali Date: Mon, 15 Jan 2024 16:38:28 -0300 Subject: [PATCH] Improvement/appearance (#60) * join number to appearance in dynamic and order * join number to appearance in update * fix lint2 * update docs * cqlint not concerned support appearance * cqllint: support appearance * cqllint support true * cqllint: support connector * verify appearance is in range * update docs * fix lint * remove unused code --- condition/delete.go | 12 +- condition/dynamic_condition.go | 13 -- condition/errors.go | 22 ++- condition/field.go | 80 ++++++++- condition/field_condition.go | 11 +- condition/field_is_dynamic.go | 40 ++--- condition/field_is_unsafe.go | 20 +-- condition/gorm_query.go | 35 ++-- condition/operator.go | 10 -- condition/order_limit_returning.go | 12 +- condition/query.go | 14 +- condition/update.go | 50 ++---- condition/value_operator.go | 27 +-- cqllint/pkg/analyzer/analyzer.go | 166 ++++++++++++++---- cqllint/pkg/analyzer/analyzer_test.go | 5 + cqllint/pkg/analyzer/testdata/go.mod | 3 + .../analyzer/testdata/src/appearance/go.mod | 10 ++ .../analyzer/testdata/src/appearance/order.go | 49 ++++++ .../analyzer/testdata/src/appearance/query.go | 81 +++++++++ .../analyzer/testdata/src/appearance/set.go | 81 +++++++++ .../testdata/src/not_concerned/query.go | 44 +++++ .../testdata/src/not_concerned/set.go | 18 ++ .../pkg/analyzer/testdata/src/repeated/set.go | 9 + docs/cql/advanced_query.rst | 23 ++- docs/cql/cqllint.rst | 99 ++++++++++- docs/cql/type_safety.rst | 6 +- errors.go | 7 +- go.work | 1 + test/join_conditions_test.go | 16 +- test/query_test.go | 24 ++- test/update_test.go | 74 +++++++- 31 files changed, 817 insertions(+), 245 deletions(-) delete mode 100644 condition/dynamic_condition.go create mode 100644 cqllint/pkg/analyzer/testdata/src/appearance/go.mod create mode 100644 cqllint/pkg/analyzer/testdata/src/appearance/order.go create mode 100644 cqllint/pkg/analyzer/testdata/src/appearance/query.go create mode 100644 cqllint/pkg/analyzer/testdata/src/appearance/set.go diff --git a/condition/delete.go b/condition/delete.go index b290769..94c6ab4 100644 --- a/condition/delete.go +++ b/condition/delete.go @@ -12,22 +12,18 @@ type Delete[T model.Model] struct { // Ascending specify an ascending order when updating models // -// joinNumber can be used to select the join in case the field is joined more than once -// // available for: mysql -func (deleteS *Delete[T]) Ascending(field IField, joinNumber ...uint) *Delete[T] { - deleteS.OrderLimitReturning.Ascending(field, joinNumber...) +func (deleteS *Delete[T]) Ascending(field IField) *Delete[T] { + deleteS.OrderLimitReturning.Ascending(field) return deleteS } // Descending specify a descending order when updating models // -// joinNumber can be used to select the join in case the field is joined more than once -// // available for: mysql -func (deleteS *Delete[T]) Descending(field IField, joinNumber ...uint) *Delete[T] { - deleteS.OrderLimitReturning.Descending(field, joinNumber...) +func (deleteS *Delete[T]) Descending(field IField) *Delete[T] { + deleteS.OrderLimitReturning.Descending(field) return deleteS } diff --git a/condition/dynamic_condition.go b/condition/dynamic_condition.go deleted file mode 100644 index 6e385b0..0000000 --- a/condition/dynamic_condition.go +++ /dev/null @@ -1,13 +0,0 @@ -package condition - -import "github.com/FrancoLiberali/cql/model" - -type DynamicCondition[T model.Model] interface { - WhereCondition[T] - - // Allows to choose which number of join use - // for the operation in position "operationNumber" - // when the value is a field and its model is joined more than once. - // Does nothing if the operationNumber is bigger than the amount of operations. - SelectJoin(operationNumber, joinNumber uint) DynamicCondition[T] -} diff --git a/condition/errors.go b/condition/errors.go index 8268bce..127f6ee 100644 --- a/condition/errors.go +++ b/condition/errors.go @@ -11,9 +11,10 @@ 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") + ErrFieldModelNotConcerned = errors.New("field's model is not concerned by the query (not joined)") + ErrAppearanceMustBeSelected = errors.New("field's model appears more than once, select which one you want to use with Appearance") + ErrAppearanceOutOfRange = errors.New("selected appearance is bigger than field's model number of appearances") + ErrFieldIsRepeated = errors.New("field is repeated") // conditions @@ -45,11 +46,16 @@ func fieldModelNotConcernedError(field IField) error { ) } -func joinMustBeSelectedError(field IField) error { - return fmt.Errorf("%w; joined multiple times model: %s", - ErrJoinMustBeSelected, - field.getModelType(), - ) +func fieldModelError(err error, field IField) error { + return fmt.Errorf("%w; model: %s", err, field.getModelType()) +} + +func appearanceMustBeSelectedError(field IField) error { + return fieldModelError(ErrAppearanceMustBeSelected, field) +} + +func appearanceOutOfRangeError(field IField) error { + return fieldModelError(ErrAppearanceOutOfRange, field) } func fieldIsRepeatedError(field IField) error { diff --git a/condition/field.go b/condition/field.go index eda8ff0..7c80ee6 100644 --- a/condition/field.go +++ b/condition/field.go @@ -11,12 +11,15 @@ type IField interface { fieldName() string columnSQL(query *GormQuery, table Table) string getModelType() reflect.Type + getAppearance() int } type Field[TModel model.Model, TAttribute any] struct { - column string - name string - columnPrefix string + column string + name string + columnPrefix string + appearance uint + appearanceSelected bool } // Is allows creating conditions that include the field and a static value @@ -41,6 +44,27 @@ func (field Field[TModel, TAttribute]) Value() *FieldValue[TModel, TAttribute] { return NewFieldValue(field) } +// Appearance allows to choose which number of appearance use +// when field's model is joined more than once. +func (field Field[TModel, TAttribute]) Appearance(number uint) Field[TModel, TAttribute] { + newField := NewField[TModel, TAttribute]( + field.name, field.column, field.columnPrefix, + ) + + newField.appearanceSelected = true + newField.appearance = number + + return newField +} + +func (field Field[TModel, TAttribute]) getAppearance() int { + if !field.appearanceSelected { + return undefinedAppearance + } + + return int(field.appearance) +} + func (field Field[TModel, TAttribute]) getModelType() reflect.Type { return reflect.TypeOf(*new(TModel)) } @@ -82,6 +106,12 @@ func (field UpdatableField[TModel, TAttribute]) Set() FieldSet[TModel, TAttribut return FieldSet[TModel, TAttribute]{field: field} } +// Appearance allows to choose which number of appearance use +// when field's model is joined more than once. +func (field UpdatableField[TModel, TAttribute]) Appearance(number uint) UpdatableField[TModel, TAttribute] { + return UpdatableField[TModel, TAttribute]{Field: field.Field.Appearance(number)} +} + func NewUpdatableField[TModel model.Model, TAttribute any](name, column, columnPrefix string) UpdatableField[TModel, TAttribute] { return UpdatableField[TModel, TAttribute]{ Field: NewField[TModel, TAttribute](name, column, columnPrefix), @@ -96,6 +126,14 @@ func (field NullableField[TModel, TAttribute]) Set() NullableFieldSet[TModel, TA return NullableFieldSet[TModel, TAttribute]{FieldSet[TModel, TAttribute]{field: field.UpdatableField}} } +// Appearance allows to choose which number of appearance use +// when field's model is joined more than once. +func (field NullableField[TModel, TAttribute]) Appearance(number uint) NullableField[TModel, TAttribute] { + return NullableField[TModel, TAttribute]{ + UpdatableField: UpdatableField[TModel, TAttribute]{Field: field.Field.Appearance(number)}, + } +} + func NewNullableField[TModel model.Model, TAttribute any](name, column, columnPrefix string) NullableField[TModel, TAttribute] { return NullableField[TModel, TAttribute]{ UpdatableField: NewUpdatableField[TModel, TAttribute](name, column, columnPrefix), @@ -151,6 +189,14 @@ func (stringField StringField[TModel]) Value() *StringFieldValue[TModel] { return &StringFieldValue[TModel]{FieldValue: *stringField.UpdatableField.Value()} } +// Appearance allows to choose which number of appearance use +// when field's model is joined more than once. +func (stringField StringField[TModel]) Appearance(number uint) StringField[TModel] { + return StringField[TModel]{ + UpdatableField: UpdatableField[TModel, string]{Field: stringField.Field.Appearance(number)}, + } +} + func NewStringField[TModel model.Model](name, column, columnPrefix string) StringField[TModel] { return StringField[TModel]{ UpdatableField: NewUpdatableField[TModel, string](name, column, columnPrefix), @@ -174,6 +220,16 @@ func (stringField NullableStringField[TModel]) Value() *StringFieldValue[TModel] return &StringFieldValue[TModel]{FieldValue: *stringField.UpdatableField.Value()} } +// Appearance allows to choose which number of appearance use +// when field's model is joined more than once. +func (stringField NullableStringField[TModel]) Appearance(number uint) NullableStringField[TModel] { + return NullableStringField[TModel]{ + NullableField: NullableField[TModel, string]{ + UpdatableField: UpdatableField[TModel, string]{Field: stringField.Field.Appearance(number)}, + }, + } +} + func NewNullableStringField[TModel model.Model](name, column, columnPrefix string) NullableStringField[TModel] { return NullableStringField[TModel]{ NullableField: NewNullableField[TModel, string](name, column, columnPrefix), @@ -206,6 +262,14 @@ func (numericField NumericField[TModel, TAttribute]) Set() NumericFieldSet[TMode return NumericFieldSet[TModel, TAttribute]{field: numericField} } +// Appearance allows to choose which number of appearance use +// when field's model is joined more than once. +func (numericField NumericField[TModel, TAttribute]) Appearance(number uint) NumericField[TModel, TAttribute] { + return NumericField[TModel, TAttribute]{ + UpdatableField: UpdatableField[TModel, TAttribute]{Field: numericField.Field.Appearance(number)}, + } +} + type NullableNumericField[ TModel model.Model, TAttribute int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64, @@ -217,6 +281,16 @@ func (field NullableNumericField[TModel, TAttribute]) Set() NullableFieldSet[TMo return NullableFieldSet[TModel, TAttribute]{FieldSet[TModel, TAttribute]{field: field.UpdatableField}} } +// Appearance allows to choose which number of appearance use +// when field's model is joined more than once. +func (field NullableNumericField[TModel, TAttribute]) Appearance(number uint) NullableNumericField[TModel, TAttribute] { + return NullableNumericField[TModel, TAttribute]{ + NumericField: NumericField[TModel, TAttribute]{ + UpdatableField: UpdatableField[TModel, TAttribute]{Field: field.Field.Appearance(number)}, + }, + } +} + func NewNullableNumericField[ TModel model.Model, TAttribute int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64, diff --git a/condition/field_condition.go b/condition/field_condition.go index 35a7b96..2ee87de 100644 --- a/condition/field_condition.go +++ b/condition/field_condition.go @@ -45,19 +45,10 @@ func (condition fieldCondition[TObject, TAtribute]) getSQL(query *GormQuery, tab return sqlString, values, nil } -func (condition *fieldCondition[TObject, TAtribute]) SelectJoin(operationNumber, joinNumber uint) DynamicCondition[TObject] { - dynamicOperator, isDynamic := condition.Operator.(DynamicOperator[TAtribute]) - if isDynamic { - condition.Operator = dynamicOperator.SelectJoin(operationNumber, joinNumber) - } - - return condition -} - func NewFieldCondition[TObject model.Model, TAttribute any]( fieldIdentifier Field[TObject, TAttribute], operator Operator[TAttribute], -) DynamicCondition[TObject] { +) WhereCondition[TObject] { return &fieldCondition[TObject, TAttribute]{ FieldIdentifier: fieldIdentifier, Operator: operator, diff --git a/condition/field_is_dynamic.go b/condition/field_is_dynamic.go index c0edd90..200d4d3 100644 --- a/condition/field_is_dynamic.go +++ b/condition/field_is_dynamic.go @@ -16,32 +16,32 @@ type DynamicFieldIs[TObject model.Model, TAttribute any] struct { // - SQLite: https://www.sqlite.org/lang_expr.html // EqualTo -func (is DynamicFieldIs[TObject, TAttribute]) Eq(value ValueOfType[TAttribute]) DynamicCondition[TObject] { +func (is DynamicFieldIs[TObject, TAttribute]) Eq(value ValueOfType[TAttribute]) WhereCondition[TObject] { return NewFieldCondition(is.field, Eq[TAttribute](value)) } // NotEqualTo -func (is DynamicFieldIs[TObject, TAttribute]) NotEq(value ValueOfType[TAttribute]) DynamicCondition[TObject] { +func (is DynamicFieldIs[TObject, TAttribute]) NotEq(value ValueOfType[TAttribute]) WhereCondition[TObject] { return NewFieldCondition(is.field, NotEq[TAttribute](value)) } // LessThan -func (is DynamicFieldIs[TObject, TAttribute]) Lt(value ValueOfType[TAttribute]) DynamicCondition[TObject] { +func (is DynamicFieldIs[TObject, TAttribute]) Lt(value ValueOfType[TAttribute]) WhereCondition[TObject] { return NewFieldCondition(is.field, Lt[TAttribute](value)) } // LessThanOrEqualTo -func (is DynamicFieldIs[TObject, TAttribute]) LtOrEq(value ValueOfType[TAttribute]) DynamicCondition[TObject] { +func (is DynamicFieldIs[TObject, TAttribute]) LtOrEq(value ValueOfType[TAttribute]) WhereCondition[TObject] { return NewFieldCondition(is.field, LtOrEq[TAttribute](value)) } // GreaterThan -func (is DynamicFieldIs[TObject, TAttribute]) Gt(value ValueOfType[TAttribute]) DynamicCondition[TObject] { +func (is DynamicFieldIs[TObject, TAttribute]) Gt(value ValueOfType[TAttribute]) WhereCondition[TObject] { return NewFieldCondition(is.field, Gt[TAttribute](value)) } // GreaterThanOrEqualTo -func (is DynamicFieldIs[TObject, TAttribute]) GtOrEq(value ValueOfType[TAttribute]) DynamicCondition[TObject] { +func (is DynamicFieldIs[TObject, TAttribute]) GtOrEq(value ValueOfType[TAttribute]) WhereCondition[TObject] { return NewFieldCondition(is.field, GtOrEq[TAttribute](value)) } @@ -53,20 +53,20 @@ func (is DynamicFieldIs[TObject, TAttribute]) GtOrEq(value ValueOfType[TAttribut // - SQLite: https://www.sqlite.org/lang_expr.html // Equivalent to field1 < value < field2 -func (is DynamicFieldIs[TObject, TAttribute]) Between(value1, value2 ValueOfType[TAttribute]) DynamicCondition[TObject] { +func (is DynamicFieldIs[TObject, TAttribute]) Between(value1, value2 ValueOfType[TAttribute]) WhereCondition[TObject] { return NewFieldCondition(is.field, Between[TAttribute](value1, value2)) } // Equivalent to NOT (field1 < value < field2) -func (is DynamicFieldIs[TObject, TAttribute]) NotBetween(value1, value2 ValueOfType[TAttribute]) DynamicCondition[TObject] { +func (is DynamicFieldIs[TObject, TAttribute]) NotBetween(value1, value2 ValueOfType[TAttribute]) WhereCondition[TObject] { return NewFieldCondition(is.field, NotBetween[TAttribute](value1, value2)) } -func (is DynamicFieldIs[TObject, TAttribute]) Distinct(value ValueOfType[TAttribute]) DynamicCondition[TObject] { +func (is DynamicFieldIs[TObject, TAttribute]) Distinct(value ValueOfType[TAttribute]) WhereCondition[TObject] { return NewFieldCondition(is.field, IsDistinct[TAttribute](value)) } -func (is DynamicFieldIs[TObject, TAttribute]) NotDistinct(value ValueOfType[TAttribute]) DynamicCondition[TObject] { +func (is DynamicFieldIs[TObject, TAttribute]) NotDistinct(value ValueOfType[TAttribute]) WhereCondition[TObject] { return NewFieldCondition(is.field, IsNotDistinct[TAttribute](value)) } @@ -82,32 +82,32 @@ type NumericDynamicFieldIs[TObject model.Model, TAttribute any] struct { // - SQLite: https://www.sqlite.org/lang_expr.html // EqualTo -func (is NumericDynamicFieldIs[TObject, TAttribute]) Eq(value ValueOfType[numeric]) DynamicCondition[TObject] { +func (is NumericDynamicFieldIs[TObject, TAttribute]) Eq(value ValueOfType[numeric]) WhereCondition[TObject] { return NewFieldCondition(is.field, Eq[TAttribute](value)) } // NotEqualTo -func (is NumericDynamicFieldIs[TObject, TAttribute]) NotEq(value ValueOfType[numeric]) DynamicCondition[TObject] { +func (is NumericDynamicFieldIs[TObject, TAttribute]) NotEq(value ValueOfType[numeric]) WhereCondition[TObject] { return NewFieldCondition(is.field, NotEq[TAttribute](value)) } // LessThan -func (is NumericDynamicFieldIs[TObject, TAttribute]) Lt(value ValueOfType[numeric]) DynamicCondition[TObject] { +func (is NumericDynamicFieldIs[TObject, TAttribute]) Lt(value ValueOfType[numeric]) WhereCondition[TObject] { return NewFieldCondition(is.field, Lt[TAttribute](value)) } // LessThanOrEqualTo -func (is NumericDynamicFieldIs[TObject, TAttribute]) LtOrEq(value ValueOfType[numeric]) DynamicCondition[TObject] { +func (is NumericDynamicFieldIs[TObject, TAttribute]) LtOrEq(value ValueOfType[numeric]) WhereCondition[TObject] { return NewFieldCondition(is.field, LtOrEq[TAttribute](value)) } // GreaterThan -func (is NumericDynamicFieldIs[TObject, TAttribute]) Gt(value ValueOfType[numeric]) DynamicCondition[TObject] { +func (is NumericDynamicFieldIs[TObject, TAttribute]) Gt(value ValueOfType[numeric]) WhereCondition[TObject] { return NewFieldCondition(is.field, Gt[TAttribute](value)) } // GreaterThanOrEqualTo -func (is NumericDynamicFieldIs[TObject, TAttribute]) GtOrEq(value ValueOfType[numeric]) DynamicCondition[TObject] { +func (is NumericDynamicFieldIs[TObject, TAttribute]) GtOrEq(value ValueOfType[numeric]) WhereCondition[TObject] { return NewFieldCondition(is.field, GtOrEq[TAttribute](value)) } @@ -119,19 +119,19 @@ func (is NumericDynamicFieldIs[TObject, TAttribute]) GtOrEq(value ValueOfType[nu // - SQLite: https://www.sqlite.org/lang_expr.html // Equivalent to field1 < value < field2 -func (is NumericDynamicFieldIs[TObject, TAttribute]) Between(value1, value2 ValueOfType[numeric]) DynamicCondition[TObject] { +func (is NumericDynamicFieldIs[TObject, TAttribute]) Between(value1, value2 ValueOfType[numeric]) WhereCondition[TObject] { return NewFieldCondition(is.field, Between[TAttribute](value1, value2)) } // Equivalent to NOT (field1 < value < field2) -func (is NumericDynamicFieldIs[TObject, TAttribute]) NotBetween(value1, value2 ValueOfType[numeric]) DynamicCondition[TObject] { +func (is NumericDynamicFieldIs[TObject, TAttribute]) NotBetween(value1, value2 ValueOfType[numeric]) WhereCondition[TObject] { return NewFieldCondition(is.field, NotBetween[TAttribute](value1, value2)) } -func (is NumericDynamicFieldIs[TObject, TAttribute]) Distinct(value ValueOfType[numeric]) DynamicCondition[TObject] { +func (is NumericDynamicFieldIs[TObject, TAttribute]) Distinct(value ValueOfType[numeric]) WhereCondition[TObject] { return NewFieldCondition(is.field, IsDistinct[TAttribute](value)) } -func (is NumericDynamicFieldIs[TObject, TAttribute]) NotDistinct(value ValueOfType[numeric]) DynamicCondition[TObject] { +func (is NumericDynamicFieldIs[TObject, TAttribute]) NotDistinct(value ValueOfType[numeric]) WhereCondition[TObject] { return NewFieldCondition(is.field, IsNotDistinct[TAttribute](value)) } diff --git a/condition/field_is_unsafe.go b/condition/field_is_unsafe.go index d9d3d50..eb6f0f2 100644 --- a/condition/field_is_unsafe.go +++ b/condition/field_is_unsafe.go @@ -9,49 +9,49 @@ type UnsafeFieldIs[TObject model.Model, TAttribute any] struct { } // EqualTo -func (is UnsafeFieldIs[TObject, TAttribute]) Eq(value any) DynamicCondition[TObject] { +func (is UnsafeFieldIs[TObject, TAttribute]) Eq(value any) WhereCondition[TObject] { return NewFieldCondition(is.field, Eq[TAttribute](value)) } // NotEqualTo -func (is UnsafeFieldIs[TObject, TAttribute]) NotEq(value any) DynamicCondition[TObject] { +func (is UnsafeFieldIs[TObject, TAttribute]) NotEq(value any) WhereCondition[TObject] { return NewFieldCondition(is.field, NotEq[TAttribute](value)) } // LessThan -func (is UnsafeFieldIs[TObject, TAttribute]) Lt(value any) DynamicCondition[TObject] { +func (is UnsafeFieldIs[TObject, TAttribute]) Lt(value any) WhereCondition[TObject] { return NewFieldCondition(is.field, Lt[TAttribute](value)) } // LessThanOrEqualTo -func (is UnsafeFieldIs[TObject, TAttribute]) LtOrEq(value any) DynamicCondition[TObject] { +func (is UnsafeFieldIs[TObject, TAttribute]) LtOrEq(value any) WhereCondition[TObject] { return NewFieldCondition(is.field, LtOrEq[TAttribute](value)) } // GreaterThan -func (is UnsafeFieldIs[TObject, TAttribute]) Gt(value any) DynamicCondition[TObject] { +func (is UnsafeFieldIs[TObject, TAttribute]) Gt(value any) WhereCondition[TObject] { return NewFieldCondition(is.field, Gt[TAttribute](value)) } // GreaterThanOrEqualTo -func (is UnsafeFieldIs[TObject, TAttribute]) GtOrEq(value any) DynamicCondition[TObject] { +func (is UnsafeFieldIs[TObject, TAttribute]) GtOrEq(value any) WhereCondition[TObject] { return NewFieldCondition(is.field, GtOrEq[TAttribute](value)) } // Equivalent to field1 < value < field2 -func (is UnsafeFieldIs[TObject, TAttribute]) Between(v1, v2 any) DynamicCondition[TObject] { +func (is UnsafeFieldIs[TObject, TAttribute]) Between(v1, v2 any) WhereCondition[TObject] { return NewFieldCondition(is.field, Between[TAttribute](v1, v2)) } // Equivalent to NOT (field1 < value < field2) -func (is UnsafeFieldIs[TObject, TAttribute]) NotBetween(v1, v2 any) DynamicCondition[TObject] { +func (is UnsafeFieldIs[TObject, TAttribute]) NotBetween(v1, v2 any) WhereCondition[TObject] { return NewFieldCondition(is.field, NotBetween[TAttribute](v1, v2)) } -func (is UnsafeFieldIs[TObject, TAttribute]) Distinct(value any) DynamicCondition[TObject] { +func (is UnsafeFieldIs[TObject, TAttribute]) Distinct(value any) WhereCondition[TObject] { return NewFieldCondition(is.field, IsDistinct[TAttribute](value)) } -func (is UnsafeFieldIs[TObject, TAttribute]) NotDistinct(value any) DynamicCondition[TObject] { +func (is UnsafeFieldIs[TObject, TAttribute]) NotDistinct(value any) WhereCondition[TObject] { return NewFieldCondition(is.field, IsNotDistinct[TAttribute](value)) } diff --git a/condition/gorm_query.go b/condition/gorm_query.go index 0370f73..fe456e3 100644 --- a/condition/gorm_query.go +++ b/condition/gorm_query.go @@ -25,10 +25,8 @@ type GormQuery struct { // Order specify order when retrieving models from database. // // if descending is true, the ordering is in descending direction. -// -// joinNumber can be used to select the join in case the field is joined more than once. -func (query *GormQuery) Order(field IField, descending bool, joinNumber int) error { - table, err := query.GetModelTable(field, joinNumber) +func (query *GormQuery) Order(field IField, descending bool) error { + table, err := query.GetModelTable(field) if err != nil { return err } @@ -168,9 +166,9 @@ func (query *GormQuery) GetTables(modelType reflect.Type) []Table { return tableList } -const UndefinedJoinNumber = -1 +const undefinedAppearance = -1 -func (query *GormQuery) GetModelTable(field IField, joinNumber int) (Table, error) { +func (query *GormQuery) GetModelTable(field IField) (Table, error) { modelTables := query.GetTables(field.getModelType()) if modelTables == nil { return Table{}, fieldModelNotConcernedError(field) @@ -180,11 +178,17 @@ func (query *GormQuery) GetModelTable(field IField, joinNumber int) (Table, erro return modelTables[0], nil } - if joinNumber == UndefinedJoinNumber { - return Table{}, joinMustBeSelectedError(field) + appearance := field.getAppearance() + + if appearance == undefinedAppearance { + return Table{}, appearanceMustBeSelectedError(field) + } + + if appearance > len(modelTables)-1 { + return Table{}, appearanceOutOfRangeError(field) } - return modelTables[joinNumber], nil + return modelTables[appearance], nil } func (query GormQuery) ColumnName(table Table, fieldName string) string { @@ -352,7 +356,7 @@ func getUpdateTablesAndValues(query *GormQuery, sets []ISet) (map[IField]TableAn for _, set := range sets { field := set.getField() - table, err := query.GetModelTable(field, 0) + table, err := query.GetModelTable(field) if err != nil { return nil, err } @@ -380,7 +384,7 @@ func getUpdateValue(query *GormQuery, set ISet) (any, error) { value := set.getValue() if iValue, isIValue := value.(IValue); isIValue { - table, err := query.GetModelTable(iValue.getField(), set.getJoinNumber()) + table, err := query.GetModelTable(iValue.getField()) if err != nil { return nil, err } @@ -439,12 +443,3 @@ func (query *GormQuery) Delete() (int64, error) { return deleteTx.RowsAffected, deleteTx.Error } - -// from a list of uint, return the first or UndefinedJoinNumber in case the list is empty -func GetJoinNumber(joinNumberList []uint) int { - if len(joinNumberList) == 0 { - return UndefinedJoinNumber - } - - return int(joinNumberList[0]) -} diff --git a/condition/operator.go b/condition/operator.go index ba7af56..31d86d9 100644 --- a/condition/operator.go +++ b/condition/operator.go @@ -11,13 +11,3 @@ type Operator[T any] interface { // any other Operator[T2] would also be considered a Operator[T]. InterfaceVerificationMethod(t T) } - -type DynamicOperator[T any] interface { - Operator[T] - - // Allows to choose which number of join use - // for the value in position "valueNumber" - // when the value is a field and its model is joined more than once. - // Does nothing if the valueNumber is bigger than the amount of values. - SelectJoin(valueNumber, joinNumber uint) DynamicOperator[T] -} diff --git a/condition/order_limit_returning.go b/condition/order_limit_returning.go index c5d428e..676d608 100644 --- a/condition/order_limit_returning.go +++ b/condition/order_limit_returning.go @@ -12,30 +12,26 @@ type OrderLimitReturning[T model.Model] struct { // Ascending specify an ascending order when updating models // -// joinNumber can be used to select the join in case the field is joined more than once -// // available for: mysql -func (olr *OrderLimitReturning[T]) Ascending(field IField, joinNumber ...uint) { +func (olr *OrderLimitReturning[T]) Ascending(field IField) { if olr.query.gormQuery.Dialector() != sql.MySQL { olr.query.addError(methodError(ErrUnsupportedByDatabase, "Ascending")) } olr.orderByCalled = true - olr.query.order(field, false, joinNumber) + olr.query.order(field, false) } // Descending specify a descending order when updating models // -// joinNumber can be used to select the join in case the field is joined more than once -// // available for: mysql -func (olr *OrderLimitReturning[T]) Descending(field IField, joinNumber ...uint) { +func (olr *OrderLimitReturning[T]) Descending(field IField) { if olr.query.gormQuery.Dialector() != sql.MySQL { olr.query.addError(methodError(ErrUnsupportedByDatabase, "Descending")) } olr.orderByCalled = true - olr.query.order(field, true, joinNumber) + olr.query.order(field, true) } // Limit specify the number of models to be updated diff --git a/condition/query.go b/condition/query.go index c64adc6..619c809 100644 --- a/condition/query.go +++ b/condition/query.go @@ -12,21 +12,19 @@ type Query[T model.Model] struct { } // Ascending specify an ascending order when retrieving models from database -// joinNumber can be used to select the join in case the field is joined more than once -func (query *Query[T]) Ascending(field IField, joinNumber ...uint) *Query[T] { - return query.order(field, false, joinNumber) +func (query *Query[T]) Ascending(field IField) *Query[T] { + return query.order(field, false) } // Descending specify a descending order when retrieving models from database -// joinNumber can be used to select the join in case the field is joined more than once -func (query *Query[T]) Descending(field IField, joinNumber ...uint) *Query[T] { - return query.order(field, true, joinNumber) +func (query *Query[T]) Descending(field IField) *Query[T] { + return query.order(field, true) } // Order specify order when retrieving models from database // if descending is true, the ordering is in descending direction -func (query *Query[T]) order(field IField, descending bool, joinNumberList []uint) *Query[T] { - err := query.gormQuery.Order(field, descending, GetJoinNumber(joinNumberList)) +func (query *Query[T]) order(field IField, descending bool) *Query[T] { + err := query.gormQuery.Order(field, descending) if err != nil { methodName := "Ascending" if descending { diff --git a/condition/update.go b/condition/update.go index 6c26f75..bf2ecfd 100644 --- a/condition/update.go +++ b/condition/update.go @@ -18,28 +18,30 @@ func (update *Update[T]) Set(sets ...*Set[T]) (int64, error) { setsAsInterface = append(setsAsInterface, set) } - return update.unsafeSet(setsAsInterface) + return update.unsafeSet(setsAsInterface, "Set") } // SetMultiple allows updating multiple tables in the same query. // // available for: mysql func (update *Update[T]) SetMultiple(sets ...ISet) (int64, error) { + methodName := "SetMultiple" + if update.query.gormQuery.Dialector() != sql.MySQL { - update.query.addError(methodError(ErrUnsupportedByDatabase, "SetMultiple")) + update.query.addError(methodError(ErrUnsupportedByDatabase, methodName)) } - return update.unsafeSet(sets) + return update.unsafeSet(sets, methodName) } -func (update *Update[T]) unsafeSet(sets []ISet) (int64, error) { +func (update *Update[T]) unsafeSet(sets []ISet, methodName string) (int64, error) { if update.query.err != nil { return 0, update.query.err } updated, err := update.query.gormQuery.Update(sets) if err != nil { - return 0, methodError(err, "Set") + return 0, methodError(err, methodName) } return updated, nil @@ -47,22 +49,18 @@ func (update *Update[T]) unsafeSet(sets []ISet) (int64, error) { // Ascending specify an ascending order when updating models // -// joinNumber can be used to select the join in case the field is joined more than once -// // available for: mysql -func (update *Update[T]) Ascending(field IField, joinNumber ...uint) *Update[T] { - update.OrderLimitReturning.Ascending(field, joinNumber...) +func (update *Update[T]) Ascending(field IField) *Update[T] { + update.OrderLimitReturning.Ascending(field) return update } // Descending specify a descending order when updating models // -// joinNumber can be used to select the join in case the field is joined more than once -// // available for: mysql -func (update *Update[T]) Descending(field IField, joinNumber ...uint) *Update[T] { - update.OrderLimitReturning.Descending(field, joinNumber...) +func (update *Update[T]) Descending(field IField) *Update[T] { + update.OrderLimitReturning.Descending(field) return update } @@ -111,13 +109,11 @@ func NewUpdate[T model.Model](tx *gorm.DB, conditions []Condition[T]) *Update[T] type ISet interface { getField() IField getValue() any - getJoinNumber() int } type Set[T model.Model] struct { - field IField - value any - joinNumber int + field IField + value any } func (set Set[T]) getField() IField { @@ -128,10 +124,6 @@ func (set Set[T]) getValue() any { return set.value } -func (set Set[T]) getJoinNumber() int { - return set.joinNumber -} - type FieldSet[TModel model.Model, TAttribute any] struct { field UpdatableField[TModel, TAttribute] } @@ -143,12 +135,10 @@ func (set FieldSet[TModel, TAttribute]) Eq(value TAttribute) *Set[TModel] { } } -// joinNumber can be used to select the join in case the field is joined more than once -func (set FieldSet[TModel, TAttribute]) Dynamic(value ValueOfType[TAttribute], joinNumber ...uint) *Set[TModel] { +func (set FieldSet[TModel, TAttribute]) Dynamic(value ValueOfType[TAttribute]) *Set[TModel] { return &Set[TModel]{ - field: set.field, - value: value, - joinNumber: GetJoinNumber(joinNumber), + field: set.field, + value: value, } } @@ -181,12 +171,10 @@ func (set NumericFieldSet[TModel, TAttribute]) Eq(value TAttribute) *Set[TModel] } } -// joinNumber can be used to select the join in case the field is joined more than once -func (set NumericFieldSet[TModel, TAttribute]) Dynamic(value ValueOfType[numeric], joinNumber ...uint) *Set[TModel] { +func (set NumericFieldSet[TModel, TAttribute]) Dynamic(value ValueOfType[numeric]) *Set[TModel] { return &Set[TModel]{ - field: set.field, - value: value, - joinNumber: GetJoinNumber(joinNumber), + field: set.field, + value: value, } } diff --git a/condition/value_operator.go b/condition/value_operator.go index dacf02b..071bdfb 100644 --- a/condition/value_operator.go +++ b/condition/value_operator.go @@ -19,7 +19,6 @@ type operation struct { SQLOperator sql.Operator SQLOperatorByDialector map[sql.Dialector]sql.Operator Value any - JoinNumber int } func NewValueOperator[T any](sqlOperator sql.Operator, value any) *ValueOperator[T] { @@ -31,22 +30,6 @@ func (operator ValueOperator[T]) InterfaceVerificationMethod(_ T) { // that an object is of type Operator[T] } -// Allows to choose which number of join use -// for the operation in position "operationNumber" -// when the value is a field and its model is joined more than once. -// Does nothing if the operationNumber is bigger than the amount of operations. -func (operator *ValueOperator[T]) SelectJoin(operationNumber, joinNumber uint) DynamicOperator[T] { - if operationNumber >= uint(len(operator.Operations)) { - return operator - } - - operationSaved := operator.Operations[operationNumber] - operationSaved.JoinNumber = int(joinNumber) - operator.Operations[operationNumber] = operationSaved - - return operator -} - func (operator ValueOperator[T]) ToSQL(query *GormQuery, columnName string) (string, []any, error) { operationString := columnName @@ -73,7 +56,9 @@ func (operator ValueOperator[T]) ToSQL(query *GormQuery, columnName string) (str // verify that this field is concerned by the query // (a join was performed with the model to which this field belongs) // and get the alias of the table of this model. - modelTable, err := getModelTable(query, iValue.getField(), operation.JoinNumber, sqlOperator) + field := iValue.getField() + + modelTable, err := getModelTable(query, field, sqlOperator) if err != nil { return "", nil, err } @@ -99,8 +84,8 @@ func (operator ValueOperator[T]) ToSQL(query *GormQuery, columnName string) (str return operationString, values, nil } -func getModelTable(query *GormQuery, field IField, joinNumber int, sqlOperator sql.Operator) (Table, error) { - table, err := query.GetModelTable(field, joinNumber) +func getModelTable(query *GormQuery, field IField, sqlOperator sql.Operator) (Table, error) { + table, err := query.GetModelTable(field) if err != nil { return Table{}, operatorError(err, sqlOperator) } @@ -115,13 +100,11 @@ func (operator *ValueOperator[T]) AddOperation(sqlOperator any, value any) *Valu newOperation = operation{ Value: value, SQLOperator: sqlOperatorTyped, - JoinNumber: UndefinedJoinNumber, } case map[sql.Dialector]sql.Operator: newOperation = operation{ Value: value, SQLOperatorByDialector: sqlOperatorTyped, - JoinNumber: UndefinedJoinNumber, } default: return operator diff --git a/cqllint/pkg/analyzer/analyzer.go b/cqllint/pkg/analyzer/analyzer.go index 6bbd32b..d37b488 100644 --- a/cqllint/pkg/analyzer/analyzer.go +++ b/cqllint/pkg/analyzer/analyzer.go @@ -4,6 +4,7 @@ import ( "go/ast" "go/token" "go/types" + "strconv" "github.com/elliotchance/pie/v2" "golang.org/x/tools/go/analysis" @@ -26,10 +27,16 @@ var Analyzer = &analysis.Analyzer{ var ( cqlMethods = []string{"Query", "Update", "Delete"} cqlOrder = []string{"Descending", "Ascending"} + cqlConnectors = []string{"And", "Or", "Not"} cqlSetMultiple = "SetMultiple" cqlSets = []string{cqlSetMultiple, "Set"} cqlSelectors = append(cqlOrder, cqlSets...) dynamicMethod = "Dynamic" + + notJoinedMessage = "%s is not joined by the query" + appearanceNotNecessaryMessage = "Appearance call not necessary, %s appears only once" + appearanceMoreThanOnceMessage = "%s appears more than once, select which one you want to use with Appearance" + appearanceOutOfRangeMessage = "selected appearance is bigger than %s's number of appearances" ) type Model struct { @@ -37,6 +44,16 @@ type Model struct { Name string } +type Report struct { + message string + model Model +} + +type Appearance struct { + selected bool + number int +} + var passG *analysis.Pass func run(pass *analysis.Pass) (interface{}, error) { @@ -46,7 +63,7 @@ func run(pass *analysis.Pass) (interface{}, error) { (*ast.CallExpr)(nil), } - positionsToReport := []Model{} + positionsToReport := []Report{} inspector.Preorder(nodeFilter, func(node ast.Node) { defer func() { @@ -70,11 +87,11 @@ func run(pass *analysis.Pass) (interface{}, error) { } }) - for _, position := range positionsToReport { + for _, report := range positionsToReport { pass.Reportf( - position.Pos, - "%s is not joined by the query", - position.Name, + report.model.Pos, + report.message, + report.model.Name, ) } @@ -82,7 +99,7 @@ func run(pass *analysis.Pass) (interface{}, error) { } // Finds NotConcerned and Repeated errors in selector functions: Descending, Ascending, SetMultiple, Set -func findForSelector(callExpr *ast.CallExpr, positionsToReport []Model) []Model { +func findForSelector(callExpr *ast.CallExpr, positionsToReport []Report) []Report { selectorExpr := callExpr.Fun.(*ast.SelectorExpr) if !pie.Contains(cqlSelectors, selectorExpr.Sel.Name) { @@ -95,37 +112,55 @@ func findForSelector(callExpr *ast.CallExpr, positionsToReport []Model) []Model } // Finds NotConcerned errors in selector functions: Descending, Ascending, SetMultiple, Set -func fieldNotConcerned(callExpr *ast.CallExpr, selectorExpr *ast.SelectorExpr, positionsToReport []Model) []Model { +func fieldNotConcerned(callExpr *ast.CallExpr, selectorExpr *ast.SelectorExpr, positionsToReport []Report) []Report { _, models := findNotConcernedForIndex(selectorExpr.X.(*ast.CallExpr), positionsToReport) for _, arg := range callExpr.Args { - var model Model - methodName := selectorExpr.Sel.Name if pie.Contains(cqlOrder, methodName) { - model = getModel(arg.(*ast.SelectorExpr).X.(*ast.SelectorExpr)) - positionsToReport = addPositionsToReport(positionsToReport, models, model) + positionsToReport = findForOrder(arg, positionsToReport, models) } else { - argCallExpr := arg.(*ast.CallExpr) + positionsToReport = findForSet(arg, positionsToReport, models, methodName) + } + } - setFunction := argCallExpr.Fun.(*ast.SelectorExpr).Sel.Name + return positionsToReport +} - if setFunction == dynamicMethod { - model = getModel(argCallExpr.Args[0].(*ast.CallExpr).Fun.(*ast.SelectorExpr).X.(*ast.SelectorExpr).X.(*ast.SelectorExpr)) - positionsToReport = addPositionsToReport(positionsToReport, models, model) - } +func findForSet(set ast.Expr, positionsToReport []Report, models []string, methodName string) []Report { + setCall := set.(*ast.CallExpr) - if methodName == cqlSetMultiple { - model = getModel(argCallExpr.Fun.(*ast.SelectorExpr).X.(*ast.CallExpr).Fun.(*ast.SelectorExpr).X.(*ast.SelectorExpr).X.(*ast.SelectorExpr)) - positionsToReport = addPositionsToReport(positionsToReport, models, model) - } - } + setFunction := setCall.Fun.(*ast.SelectorExpr).Sel.Name + + if setFunction == dynamicMethod { + model, appearance := getModelFromCall(setCall.Args[0].(*ast.CallExpr)) + positionsToReport = addPositionsToReport(positionsToReport, models, model, appearance) + } + + if methodName == cqlSetMultiple { + model, appearance := getModelFromCall(setCall.Fun.(*ast.SelectorExpr).X.(*ast.CallExpr)) + positionsToReport = addPositionsToReport(positionsToReport, models, model, appearance) } return positionsToReport } +func findForOrder(order ast.Expr, positionsToReport []Report, models []string) []Report { + var model Model + + appearance := Appearance{selected: false} + + orderCall, isCall := order.(*ast.CallExpr) + if isCall { + model, appearance = getModelFromCall(orderCall) + } else { + model = getModel(order.(*ast.SelectorExpr).X.(*ast.SelectorExpr)) + } + + return addPositionsToReport(positionsToReport, models, model, appearance) +} + func findRepeatedFields(call *ast.CallExpr, selectorExpr *ast.SelectorExpr) { if !pie.Contains(cqlSets, selectorExpr.Sel.Name) { return @@ -149,7 +184,11 @@ func findRepeatedFields(call *ast.CallExpr, selectorExpr *ast.SelectorExpr) { } if argSelector.Sel.Name == dynamicMethod && len(argCall.Args) == 1 { - comparedField := argCall.Args[0].(*ast.CallExpr).Fun.(*ast.SelectorExpr).X.(*ast.SelectorExpr) + comparedField, isSelector := argCall.Args[0].(*ast.CallExpr).Fun.(*ast.SelectorExpr).X.(*ast.SelectorExpr) + if !isSelector { + continue + } + comparedFieldName := getFieldName(comparedField) if comparedFieldName == fieldName { @@ -182,7 +221,7 @@ func getFieldName(condition *ast.SelectorExpr) string { } // Finds NotConcerned errors in index functions: cql.Query, cql.Update, cql.Delete -func findNotConcernedForIndex(callExpr *ast.CallExpr, positionsToReport []Model) ([]Model, []string) { +func findNotConcernedForIndex(callExpr *ast.CallExpr, positionsToReport []Report) ([]Report, []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 @@ -220,10 +259,19 @@ func findNotConcernedForIndex(callExpr *ast.CallExpr, positionsToReport []Model) return findErrorIsDynamic(positionsToReport, models, callExpr.Args[1:]) // first parameters is ignored as it's the db object } -func findErrorIsDynamic(positionsToReport []Model, models []string, conditions []ast.Expr) ([]Model, []string) { +func findErrorIsDynamic(positionsToReport []Report, models []string, conditions []ast.Expr) ([]Report, []string) { for _, condition := range conditions { conditionCall := condition.(*ast.CallExpr) - conditionSelector := conditionCall.Fun.(*ast.SelectorExpr) + conditionSelector, isSelector := conditionCall.Fun.(*ast.SelectorExpr) + + if !isSelector { + // cql.True + continue + } + + if pie.Contains(cqlConnectors, conditionSelector.Sel.Name) { + positionsToReport, models = findErrorIsDynamic(positionsToReport, models, conditionCall.Args) + } if conditionSelector.Sel.Name == "Preload" { conditionCall = conditionSelector.X.(*ast.CallExpr) @@ -258,24 +306,49 @@ func getFirstGenericType(parent *types.Named) string { } func findErrorIsDynamicWhereCondition( - positionsToReport []Model, models []string, + positionsToReport []Report, models []string, conditionCall *ast.CallExpr, conditionSelector *ast.SelectorExpr, -) []Model { +) []Report { whereCondition, isWhereCondition := conditionSelector.X.(*ast.CallExpr) if isWhereCondition && getFieldIsMethodName(whereCondition) == "IsDynamic" { - isDynamicModel := getModelFromWhereCondition(conditionCall.Args[0].(*ast.CallExpr)) - return addPositionsToReport(positionsToReport, models, isDynamicModel) + isDynamicModel, appearance := getModelFromCall(conditionCall.Args[0].(*ast.CallExpr)) + return addPositionsToReport(positionsToReport, models, isDynamicModel, appearance) } return positionsToReport } -func addPositionsToReport(positionsToReport []Model, models []string, model Model) []Model { +func addPositionsToReport(positionsToReport []Report, models []string, model Model, appearance Appearance) []Report { if !pie.Contains(models, model.Name) { - return append(positionsToReport, Model{ - Pos: model.Pos, - Name: model.Name, + return append(positionsToReport, Report{ + model: model, + message: notJoinedMessage, + }) + } + + joinedTimes := len(pie.Filter(models, func(modelName string) bool { + return modelName == model.Name + })) + + if appearance.selected { + if joinedTimes == 1 { + return append(positionsToReport, Report{ + model: model, + message: appearanceNotNecessaryMessage, + }) + } + + if appearance.number > joinedTimes-1 { + return append(positionsToReport, Report{ + model: model, + message: appearanceOutOfRangeMessage, + }) + } + } else if joinedTimes > 1 { + return append(positionsToReport, Report{ + model: model, + message: appearanceMoreThanOnceMessage, }) } @@ -286,9 +359,28 @@ func getFieldIsMethodName(whereCondition *ast.CallExpr) string { return whereCondition.Fun.(*ast.SelectorExpr).Sel.Name } -// Returns model's package the model name -func getModelFromWhereCondition(whereCondition *ast.CallExpr) Model { - return getModel(whereCondition.Fun.(*ast.SelectorExpr).X.(*ast.SelectorExpr).X.(*ast.SelectorExpr)) +// Returns model's package the model name and true if Appearance method is called +func getModelFromCall(call *ast.CallExpr) (Model, Appearance) { + fun := call.Fun.(*ast.SelectorExpr) + + funX, isXSelector := fun.X.(*ast.SelectorExpr) + if isXSelector { + model := getModel(funX.X.(*ast.SelectorExpr)) + + if fun.Sel.Name == "Appearance" { + appearanceNumber, err := strconv.Atoi(call.Args[0].(*ast.BasicLit).Value) + if err != nil { + panic(err) + } + + return model, Appearance{selected: true, number: appearanceNumber} + } + + return model, Appearance{selected: false} + } + + // x is not a selector, so Appearance method or a function is called + return getModelFromCall(fun.X.(*ast.CallExpr)) } // Returns model's package the model name diff --git a/cqllint/pkg/analyzer/analyzer_test.go b/cqllint/pkg/analyzer/analyzer_test.go index 604ce79..881b698 100644 --- a/cqllint/pkg/analyzer/analyzer_test.go +++ b/cqllint/pkg/analyzer/analyzer_test.go @@ -17,3 +17,8 @@ func TestErrRepeated(t *testing.T) { testdata := analysistest.TestData() analysistest.Run(t, testdata, analyzer.Analyzer, "repeated") } + +func TestErrAppearance(t *testing.T) { + testdata := analysistest.TestData() + analysistest.Run(t, testdata, analyzer.Analyzer, "appearance") +} diff --git a/cqllint/pkg/analyzer/testdata/go.mod b/cqllint/pkg/analyzer/testdata/go.mod index c45b2a7..109992e 100644 --- a/cqllint/pkg/analyzer/testdata/go.mod +++ b/cqllint/pkg/analyzer/testdata/go.mod @@ -5,8 +5,11 @@ go 1.18 require ( not_concerned v0.0.1 repeated v0.0.1 + appearance v0.0.1 ) replace not_concerned => ./src/not_concerned replace repeated => ./src/repeated + +replace appearance => ./src/appearance diff --git a/cqllint/pkg/analyzer/testdata/src/appearance/go.mod b/cqllint/pkg/analyzer/testdata/src/appearance/go.mod new file mode 100644 index 0000000..641a8fd --- /dev/null +++ b/cqllint/pkg/analyzer/testdata/src/appearance/go.mod @@ -0,0 +1,10 @@ +module github.com/FrancoLiberali/cql/cqllint/pkg/analyzer/testdata/src/appearance + +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/appearance/order.go b/cqllint/pkg/analyzer/testdata/src/appearance/order.go new file mode 100644 index 0000000..da231b5 --- /dev/null +++ b/cqllint/pkg/analyzer/testdata/src/appearance/order.go @@ -0,0 +1,49 @@ +package appearance + +import ( + "github.com/FrancoLiberali/cql" + "github.com/FrancoLiberali/cql/test/conditions" + "github.com/FrancoLiberali/cql/test/models" +) + +func testOrderNotNecessary() { + cql.Query[models.Brand]( + db, + ).Descending(conditions.Brand.Name.Appearance(0)).Find() // want "Appearance call not necessary, github.com/FrancoLiberali/cql/test/models.Brand appears only once" +} + +func testOrderNecessaryNotCalled() { + cql.Query[models.Child]( + db, + conditions.Child.Parent1( + conditions.Parent1.ParentParent(), + ), + conditions.Child.Parent2( + conditions.Parent2.ParentParent(), + ), + ).Descending(conditions.ParentParent.ID).Find() // want "github.com/FrancoLiberali/cql/test/models.ParentParent appears more than once, select which one you want to use with Appearance" +} + +func testOrderNecessaryCalled() { + cql.Query[models.Child]( + db, + conditions.Child.Parent1( + conditions.Parent1.ParentParent(), + ), + conditions.Child.Parent2( + conditions.Parent2.ParentParent(), + ), + ).Descending(conditions.ParentParent.ID.Appearance(0)).Find() +} + +func testOrderOutOfRange() { + cql.Query[models.Child]( + db, + conditions.Child.Parent1( + conditions.Parent1.ParentParent(), + ), + conditions.Child.Parent2( + conditions.Parent2.ParentParent(), + ), + ).Descending(conditions.ParentParent.ID.Appearance(2)).Find() // want "selected appearance is bigger than github.com/FrancoLiberali/cql/test/models.ParentParent's number of appearances" +} diff --git a/cqllint/pkg/analyzer/testdata/src/appearance/query.go b/cqllint/pkg/analyzer/testdata/src/appearance/query.go new file mode 100644 index 0000000..b9250b9 --- /dev/null +++ b/cqllint/pkg/analyzer/testdata/src/appearance/query.go @@ -0,0 +1,81 @@ +package appearance + +import ( + "gorm.io/gorm" + + "github.com/FrancoLiberali/cql" + "github.com/FrancoLiberali/cql/test/conditions" + "github.com/FrancoLiberali/cql/test/models" +) + +var db *gorm.DB + +func testQueryNotNecessary() { + cql.Query[models.Phone]( + db, + conditions.Phone.Brand( + conditions.Brand.Name.IsDynamic().Eq(conditions.Phone.Name.Appearance(0).Value()), // want "Appearance call not necessary, github.com/FrancoLiberali/cql/test/models.Phone appears only once" + ), + ).Find() +} + +func testQueryNotNecessaryWithFunction() { + cql.Query[models.Phone]( + db, + conditions.Phone.Brand( + conditions.Brand.Name.IsDynamic().Eq(conditions.Phone.Name.Appearance(0).Value().Concat("asd")), // want "Appearance call not necessary, github.com/FrancoLiberali/cql/test/models.Phone appears only once" + ), + ).Find() +} + +func testQueryNecessaryNotCalled() { + cql.Query[models.Child]( + db, + conditions.Child.Parent1( + conditions.Parent1.ParentParent(), + ), + conditions.Child.Parent2( + conditions.Parent2.ParentParent(), + ), + conditions.Child.ID.IsDynamic().Eq(conditions.ParentParent.ID.Value()), // want "github.com/FrancoLiberali/cql/test/models.ParentParent appears more than once, select which one you want to use with Appearance" + ).Find() +} + +func testQueryNecessaryNotCalledWithFunction() { + cql.Query[models.Child]( + db, + conditions.Child.Parent1( + conditions.Parent1.ParentParent(), + ), + conditions.Child.Parent2( + conditions.Parent2.ParentParent(), + ), + conditions.Child.Number.IsDynamic().Eq(conditions.ParentParent.Number.Value().Plus(1)), // want "github.com/FrancoLiberali/cql/test/models.ParentParent appears more than once, select which one you want to use with Appearance" + ).Find() +} + +func testQueryNecessaryCalled() { + cql.Query[models.Child]( + db, + conditions.Child.Parent1( + conditions.Parent1.ParentParent(), + ), + conditions.Child.Parent2( + conditions.Parent2.ParentParent(), + ), + conditions.Child.ID.IsDynamic().Eq(conditions.ParentParent.ID.Appearance(0).Value()), + ).Find() +} + +func testQueryOutOfRange() { + cql.Query[models.Child]( + db, + conditions.Child.Parent1( + conditions.Parent1.ParentParent(), + ), + conditions.Child.Parent2( + conditions.Parent2.ParentParent(), + ), + conditions.Child.ID.IsDynamic().Eq(conditions.ParentParent.ID.Appearance(2).Value()), // want "selected appearance is bigger than github.com/FrancoLiberali/cql/test/models.ParentParent's number of appearances" + ).Find() +} diff --git a/cqllint/pkg/analyzer/testdata/src/appearance/set.go b/cqllint/pkg/analyzer/testdata/src/appearance/set.go new file mode 100644 index 0000000..b8961cd --- /dev/null +++ b/cqllint/pkg/analyzer/testdata/src/appearance/set.go @@ -0,0 +1,81 @@ +package appearance + +import ( + "github.com/FrancoLiberali/cql" + "github.com/FrancoLiberali/cql/test/conditions" + "github.com/FrancoLiberali/cql/test/models" +) + +func testSetNotNecessary() { + cql.Update[models.Phone]( + db, + conditions.Phone.Name.Is().Eq("asd"), + ).Set( + conditions.Phone.Name.Set().Dynamic(conditions.Phone.Name.Appearance(0).Value()), // want "Appearance call not necessary, github.com/FrancoLiberali/cql/test/models.Phone appears only once" + ) +} + +func testSetNotNecessaryWithFunction() { + cql.Update[models.Phone]( + db, + conditions.Phone.Name.Is().Eq("asd"), + ).Set( + conditions.Phone.Name.Set().Dynamic(conditions.Phone.Name.Appearance(0).Value().Concat("asd")), // want "Appearance call not necessary, github.com/FrancoLiberali/cql/test/models.Phone appears only once" + ) +} + +func testSetNecessaryNotCalled() { + cql.Update[models.Child]( + db, + conditions.Child.Parent1( + conditions.Parent1.ParentParent(), + ), + conditions.Child.Parent2( + conditions.Parent2.ParentParent(), + ), + ).Set( + conditions.Child.Name.Set().Dynamic(conditions.ParentParent.Name.Value()), // want "github.com/FrancoLiberali/cql/test/models.ParentParent appears more than once, select which one you want to use with Appearance" + ) +} + +func testSetNecessaryNotCalledWithFunction() { + cql.Update[models.Child]( + db, + conditions.Child.Parent1( + conditions.Parent1.ParentParent(), + ), + conditions.Child.Parent2( + conditions.Parent2.ParentParent(), + ), + ).Set( + conditions.Child.Name.Set().Dynamic(conditions.ParentParent.Name.Value().Concat("asd")), // want "github.com/FrancoLiberali/cql/test/models.ParentParent appears more than once, select which one you want to use with Appearance" + ) +} + +func testSetNecessaryCalled() { + cql.Update[models.Child]( + db, + conditions.Child.Parent1( + conditions.Parent1.ParentParent(), + ), + conditions.Child.Parent2( + conditions.Parent2.ParentParent(), + ), + ).Set( + conditions.Child.Name.Set().Dynamic(conditions.ParentParent.Name.Appearance(0).Value()), + ) +} + +func testSetOutOfRange() { + cql.Update[models.Child]( + db, + conditions.Child.Parent1( + conditions.Parent1.ParentParent(), + ), + conditions.Child.Parent2( + conditions.Parent2.ParentParent(), + ), + ).Set( + conditions.Child.Name.Set().Dynamic(conditions.ParentParent.Name.Appearance(2).Value()), // want "selected appearance is bigger than github.com/FrancoLiberali/cql/test/models.ParentParent's number of appearances" + ) +} diff --git a/cqllint/pkg/analyzer/testdata/src/not_concerned/query.go b/cqllint/pkg/analyzer/testdata/src/not_concerned/query.go index bd4383b..6190014 100644 --- a/cqllint/pkg/analyzer/testdata/src/not_concerned/query.go +++ b/cqllint/pkg/analyzer/testdata/src/not_concerned/query.go @@ -26,6 +26,23 @@ func testNotJoinedInDifferentLines() { ).Find() } +func testNotJoinedWithTrue() { + cql.Query[models.Brand]( + db, + cql.True[models.Brand](), + conditions.Brand.Name.IsDynamic().Eq(conditions.City.Name.Value()), // want "github.com/FrancoLiberali/cql/test/models.City is not joined by the query" + ).Find() +} + +func testNotJoinedInsideConnector() { + cql.Query[models.Brand]( + db, + cql.And( + conditions.Brand.Name.IsDynamic().Eq(conditions.City.Name.Value()), // want "github.com/FrancoLiberali/cql/test/models.City is not joined by the query" + ), + ).Find() +} + func testNotJoinedInsideJoinCondition() { cql.Query[models.Phone]( db, @@ -169,3 +186,30 @@ func testJoinedWithDifferentRelationNameWithoutConditionsWithPreload() { conditions.Bicycle.Name.IsDynamic().Eq(conditions.City.Name.Value()), // want "github.com/FrancoLiberali/cql/test/models.City is not joined by the query" ).Find() } + +func testNotJoinedWithAppearance() { + cql.Query[models.Phone]( + db, + conditions.Phone.Brand( + conditions.Brand.Name.IsDynamic().Eq(conditions.City.Name.Appearance(0).Value()), // want "github.com/FrancoLiberali/cql/test/models.City is not joined by the query" + ), + ).Find() +} + +func testNotJoinedWithFunction() { + cql.Query[models.Phone]( + db, + conditions.Phone.Brand( + conditions.Brand.Name.IsDynamic().Eq(conditions.City.Name.Value().Concat("asd")), // want "github.com/FrancoLiberali/cql/test/models.City is not joined by the query" + ), + ).Find() +} + +func testNotJoinedWithTwoFunctions() { + cql.Query[models.Phone]( + db, + conditions.Phone.Brand( + conditions.Brand.Name.IsDynamic().Eq(conditions.City.Name.Value().Concat("asd").Concat("asd")), // want "github.com/FrancoLiberali/cql/test/models.City is not joined by the query" + ), + ).Find() +} diff --git a/cqllint/pkg/analyzer/testdata/src/not_concerned/set.go b/cqllint/pkg/analyzer/testdata/src/not_concerned/set.go index c73fe47..a6c6398 100644 --- a/cqllint/pkg/analyzer/testdata/src/not_concerned/set.go +++ b/cqllint/pkg/analyzer/testdata/src/not_concerned/set.go @@ -128,3 +128,21 @@ func testSetMultipleNestedJoinedModel() { conditions.ParentParent.Name.Set().Eq("asd"), ) } + +func testSetDynamicNotJoinedWithFunction() { + cql.Update[models.Brand]( + db, + conditions.Brand.Name.Is().Eq("asd"), + ).Set(conditions.Brand.Name.Set().Dynamic( + conditions.City.Name.Value().Concat("asd"), // want "github.com/FrancoLiberali/cql/test/models.City is not joined by the query" + )) +} + +func testSetDynamicNotJoinedWithTwoFunction() { + cql.Update[models.Brand]( + db, + conditions.Brand.Name.Is().Eq("asd"), + ).Set(conditions.Brand.Name.Set().Dynamic( + conditions.City.Name.Value().Concat("asd").Concat("asd"), // want "github.com/FrancoLiberali/cql/test/models.City is not joined by the query" + )) +} diff --git a/cqllint/pkg/analyzer/testdata/src/repeated/set.go b/cqllint/pkg/analyzer/testdata/src/repeated/set.go index 99640a1..c690869 100644 --- a/cqllint/pkg/analyzer/testdata/src/repeated/set.go +++ b/cqllint/pkg/analyzer/testdata/src/repeated/set.go @@ -74,3 +74,12 @@ func testSetDynamicSameValue() { conditions.Product.Int.Set().Dynamic(conditions.Product.Int.Value()), // want "conditions.Product.Int is set to itself" ) } + +func testSetDynamicSameValueWithFunction() { + cql.Update[models.Product]( + db, + conditions.Product.Int.Is().Eq(0), + ).Set( + conditions.Product.Int.Set().Dynamic(conditions.Product.Int.Value().Plus(1)), + ) +} diff --git a/docs/cql/advanced_query.rst b/docs/cql/advanced_query.rst index 2467cb7..5bc6daf 100644 --- a/docs/cql/advanced_query.rst +++ b/docs/cql/advanced_query.rst @@ -159,15 +159,16 @@ For example, if we seek to obtain the cities whose population represents at leas ).Find() -Select join -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Appearance +------------------------- -In case the attribute to be used by the dynamic operator is present more -than once in the query, it will be necessary to select the join to be used, -to avoid getting the error cql.ErrJoinMustBeSelected. -To do this, you must use the SelectJoin method, as in the following example: +In case the attribute to be used is present more +than once in the query, it will be necessary to select select its appearance number, +to avoid getting the error cql.ErrAppearanceMustBeSelected. +To do this, you must use the Appearance method of the field, as in the following example: .. code-block:: go + :caption: Example model type ParentParent struct { model.UUIDModel @@ -197,6 +198,11 @@ To do this, you must use the SelectJoin method, as in the following example: Parent2ID model.UUID } +.. code-block:: go + :caption: Query + :linenos: + :emphasize-lines: 10 + models, err := cql.Query[Child]( gormDB, conditions.Child.Parent1( @@ -205,9 +211,8 @@ To do this, you must use the SelectJoin method, as in the following example: conditions.Child.Parent2( conditions.Parent2.ParentParent(), ), - conditions.Child.Name.IsDynamic().Eq(conditions.ParentParent.Name.Value()).SelectJoin( - 0, // for the parameter in position 0 of the operator (conditions.ParentParent.Name), - 0, // choose the first (0) join (made by conditions.Child.Parent1()) + conditions.Child.Name.IsDynamic().Eq( + conditions.ParentParent.Name.Appearance(0).Value(), // choose the first (0) appearance (made by conditions.Child.Parent1()) ), ).Find() diff --git a/docs/cql/cqllint.rst b/docs/cql/cqllint.rst index 7af3cd7..ddc5568 100644 --- a/docs/cql/cqllint.rst +++ b/docs/cql/cqllint.rst @@ -12,7 +12,8 @@ It also adds other detections that would not generate runtime errors but are pos .. note:: - At the moment, only the errors cql.ErrFieldModelNotConcerned and cql.ErrFieldIsRepeated are detected. + At the moment, only 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. @@ -79,8 +80,6 @@ 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. - ErrFieldIsRepeated ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -114,7 +113,71 @@ Now, if we run cqllint we will see the following report: 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. +ErrAppearanceMustBeSelected +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To generate this error we must join the same model more than once and not select the appearance number: + +.. code-block:: go + :caption: example.go + :class: with-errors + :emphasize-lines: 9 + :linenos: + + _, err := cql.Query[models.Child]( + db, + conditions.Child.Parent1( + conditions.Parent1.ParentParent(), + ), + conditions.Child.Parent2( + conditions.Parent2.ParentParent(), + ), + conditions.Child.ID.IsDynamic().Eq(conditions.ParentParent.ID.Value()), + ).Find() + +If we execute this query we will obtain an error of type `cql.ErrAppearanceMustBeSelected` with the following message: + +.. code-block:: none + + 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: + +.. code-block:: none + + $ cqllint ./... + example.go:9: 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: + +.. code-block:: go + :caption: example.go + :class: with-errors + :emphasize-lines: 4 + :linenos: + + _, err := cql.Query[models.Phone]( + db, + conditions.Phone.Brand( + conditions.Brand.Name.IsDynamic().Eq(conditions.Phone.Name.Appearance(1).Value()), + ), + ).Find() + +If we execute this query we will obtain an error of type `cql.ErrAppearanceOutOfRange` with the following message: + +.. code-block:: none + + 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: + +.. code-block:: none + + $ cqllint ./... + example.go:4: selected appearance is bigger than models.Phone's number of appearances Misuses ------------------------- @@ -144,4 +207,30 @@ 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 + example.go:5: conditions.Brand.Name is set to itself + +Unnecessary Appearance selection +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This is the case when the Appearance method is used without being necessary, +i.e. when the model appears only once: + +.. code-block:: go + :caption: example.go + :class: with-errors + :emphasize-lines: 4 + :linenos: + + _, err := cql.Query[models.Phone]( + db, + conditions.Phone.Brand( + conditions.Brand.Name.IsDynamic().Eq(conditions.Phone.Name.Appearance(0).Value()), + ), + ).Find() + +If we run cqllint we will see the following report: + +.. code-block:: none + + $ cqllint ./... + example.go:4: Appearance call not necessary, models.Phone appears only once \ No newline at end of file diff --git a/docs/cql/type_safety.rst b/docs/cql/type_safety.rst index c1ce1a0..313066d 100644 --- a/docs/cql/type_safety.rst +++ b/docs/cql/type_safety.rst @@ -189,8 +189,10 @@ there are still some possible cases that generate the following run-time errors: - cql.ErrFieldModelNotConcerned **(1)**: generated when trying to use a model that is not related 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.ErrAppearanceMustBeSelected **(1)**: 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 (see :ref:`cql/advanced_query:appearance`). +- cql.ErrAppearanceOutOfRange **(1)**: generated when you try select an appearance number (with the Appearance method) + greater than the number of appearances of a model. (see :ref:`cql/advanced_query:appearance`). - 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. diff --git a/errors.go b/errors.go index 60b7354..a3c5eba 100644 --- a/errors.go +++ b/errors.go @@ -8,9 +8,10 @@ import ( var ( // query - ErrFieldModelNotConcerned = condition.ErrFieldModelNotConcerned - ErrJoinMustBeSelected = condition.ErrJoinMustBeSelected - ErrFieldIsRepeated = condition.ErrFieldIsRepeated + ErrFieldModelNotConcerned = condition.ErrFieldModelNotConcerned + ErrAppearanceMustBeSelected = condition.ErrAppearanceMustBeSelected + ErrAppearanceOutOfRange = condition.ErrAppearanceOutOfRange + ErrFieldIsRepeated = condition.ErrFieldIsRepeated // crud diff --git a/go.work b/go.work index 7d33f8c..f9060ef 100644 --- a/go.work +++ b/go.work @@ -8,4 +8,5 @@ use ( ./cqllint/pkg/analyzer/testdata ./cqllint/pkg/analyzer/testdata/src/not_concerned ./cqllint/pkg/analyzer/testdata/src/repeated + ./cqllint/pkg/analyzer/testdata/src/appearance ) diff --git a/test/join_conditions_test.go b/test/join_conditions_test.go index 11c2032..03383b4 100644 --- a/test/join_conditions_test.go +++ b/test/join_conditions_test.go @@ -438,7 +438,7 @@ func (ts *JoinConditionsIntTestSuite) TestDynamicOperatorWithJoinedInTheFutureMo ts.ErrorContains(err, "not concerned model: models.Parent1; operator: Eq; model: models.Child, field: ID") } -func (ts *JoinConditionsIntTestSuite) TestDynamicOperatorJoinMoreThanOnceWithoutSelectJoinReturnsError() { +func (ts *JoinConditionsIntTestSuite) TestDynamicOperatorJoinMoreThanOnceWithoutAppearanceReturnsError() { _, err := cql.Query[models.Child]( ts.db, conditions.Child.Parent1( @@ -449,11 +449,11 @@ func (ts *JoinConditionsIntTestSuite) TestDynamicOperatorJoinMoreThanOnceWithout ), conditions.Child.ID.IsDynamic().Eq(conditions.ParentParent.ID.Value()), ).Find() - ts.ErrorIs(err, cql.ErrJoinMustBeSelected) - ts.ErrorContains(err, "joined multiple times model: models.ParentParent; operator: Eq; model: models.Child, field: ID") + ts.ErrorIs(err, cql.ErrAppearanceMustBeSelected) + ts.ErrorContains(err, "model: models.ParentParent; operator: Eq; model: models.Child, field: ID") } -func (ts *JoinConditionsIntTestSuite) TestDynamicOperatorJoinMoreThanOnceWithSelectJoin() { +func (ts *JoinConditionsIntTestSuite) TestDynamicOperatorJoinMoreThanOnceWithAppearance() { parentParent := &models.ParentParent{Name: "franco"} parent1 := &models.Parent1{ParentParent: *parentParent} parent2 := &models.Parent2{ParentParent: *parentParent} @@ -469,14 +469,14 @@ func (ts *JoinConditionsIntTestSuite) TestDynamicOperatorJoinMoreThanOnceWithSel conditions.Child.Parent2( conditions.Parent2.ParentParent(), ), - conditions.Child.Name.IsDynamic().Eq(conditions.ParentParent.Name.Value()).SelectJoin(0, 0), + conditions.Child.Name.IsDynamic().Eq(conditions.ParentParent.Name.Appearance(0).Value()), ).Find() ts.Require().NoError(err) EqualList(&ts.Suite, []*models.Child{child}, entities) } -func (ts *JoinConditionsIntTestSuite) TestDynamicOperatorJoinMoreThanOnceWithoutSelectJoinOnMultivalueOperatorReturnsError() { +func (ts *JoinConditionsIntTestSuite) TestDynamicOperatorJoinMoreThanOnceWithoutAppearanceOnMultivalueOperatorReturnsError() { _, err := cql.Query[models.Child]( ts.db, conditions.Child.Parent1( @@ -490,8 +490,8 @@ func (ts *JoinConditionsIntTestSuite) TestDynamicOperatorJoinMoreThanOnceWithout conditions.ParentParent.ID.Value(), ), ).Find() - ts.ErrorIs(err, cql.ErrJoinMustBeSelected) - ts.ErrorContains(err, "joined multiple times model: models.ParentParent; operator: Between; model: models.Child, field: ID") + ts.ErrorIs(err, cql.ErrAppearanceMustBeSelected) + ts.ErrorContains(err, "model: models.ParentParent; operator: Between; model: models.Child, field: ID") } func (ts *JoinConditionsIntTestSuite) TestCollectionAnyReturnsEmptyWhenNothingMatches() { diff --git a/test/query_test.go b/test/query_test.go index d28ec54..effa4f3 100644 --- a/test/query_test.go +++ b/test/query_test.go @@ -228,7 +228,7 @@ func (ts *QueryIntTestSuite) TestOrderReturnsErrorIfFieldIsNotConcerned() { ts.ErrorContains(err, "not concerned model: models.Seller; method: Descending") } -func (ts *QueryIntTestSuite) TestOrderReturnsErrorIfFieldIsJoinedMoreThanOnceAndJoinIsNotSelected() { +func (ts *QueryIntTestSuite) TestOrderReturnsErrorIfFieldIsJoinedMoreThanOnceAndAppearanceIsNotSelected() { _, err := cql.Query[models.Child]( ts.db, conditions.Child.Parent1( @@ -238,11 +238,25 @@ func (ts *QueryIntTestSuite) TestOrderReturnsErrorIfFieldIsJoinedMoreThanOnceAnd conditions.Parent2.ParentParent(), ), ).Descending(conditions.ParentParent.ID).Find() - ts.ErrorIs(err, cql.ErrJoinMustBeSelected) - ts.ErrorContains(err, "joined multiple times model: models.ParentParent; method: Descending") + ts.ErrorIs(err, cql.ErrAppearanceMustBeSelected) + ts.ErrorContains(err, "model: models.ParentParent; method: Descending") } -func (ts *QueryIntTestSuite) TestOrderWorksIfFieldIsJoinedMoreThanOnceAndJoinIsSelected() { +func (ts *QueryIntTestSuite) TestOrderReturnsErrorIfAppearanceIfOutOfRange() { + _, err := cql.Query[models.Child]( + ts.db, + conditions.Child.Parent1( + conditions.Parent1.ParentParent(), + ), + conditions.Child.Parent2( + conditions.Parent2.ParentParent(), + ), + ).Descending(conditions.ParentParent.ID.Appearance(3)).Find() + ts.ErrorIs(err, cql.ErrAppearanceOutOfRange) + ts.ErrorContains(err, "model: models.ParentParent; method: Descending") +} + +func (ts *QueryIntTestSuite) TestOrderWorksIfFieldIsJoinedMoreThanOnceAndAppearanceIsSelected() { parentParent1 := &models.ParentParent{Name: "a"} parent11 := &models.Parent1{ParentParent: *parentParent1} parent12 := &models.Parent2{ParentParent: *parentParent1} @@ -265,7 +279,7 @@ func (ts *QueryIntTestSuite) TestOrderWorksIfFieldIsJoinedMoreThanOnceAndJoinIsS conditions.Child.Parent2( conditions.Parent2.ParentParent(), ), - ).Ascending(conditions.ParentParent.Name, 0).Find() + ).Ascending(conditions.ParentParent.Name.Appearance(0)).Find() ts.Require().NoError(err) ts.Len(children, 2) diff --git a/test/update_test.go b/test/update_test.go index 50c14b9..936e3de 100644 --- a/test/update_test.go +++ b/test/update_test.go @@ -395,7 +395,7 @@ func (ts *UpdateIntTestSuite) TestUpdateDynamicNotJoinedReturnsError() { ts.ErrorContains(err, "not concerned model: models.City; method: Set") } -func (ts *UpdateIntTestSuite) TestUpdateDynamicWithoutJoinNumberReturnsErrorIfJoinedMoreThanOnce() { +func (ts *UpdateIntTestSuite) TestUpdateDynamicWithoutAppearanceReturnsErrorIfJoinedMoreThanOnce() { _, err := cql.Update[models.Child]( ts.db, conditions.Child.Parent1( @@ -408,11 +408,11 @@ func (ts *UpdateIntTestSuite) TestUpdateDynamicWithoutJoinNumberReturnsErrorIfJo conditions.Child.Name.Set().Dynamic(conditions.ParentParent.Name.Value()), ) - ts.ErrorIs(err, cql.ErrJoinMustBeSelected) - ts.ErrorContains(err, "joined multiple times model: models.ParentParent; method: Set") + ts.ErrorIs(err, cql.ErrAppearanceMustBeSelected) + ts.ErrorContains(err, "model: models.ParentParent; method: Set") } -func (ts *UpdateIntTestSuite) TestUpdateDynamicWithJoinNumber() { +func (ts *UpdateIntTestSuite) TestUpdateDynamicWithAppearance() { parentParent := &models.ParentParent{Name: "franco"} parent1 := &models.Parent1{ParentParent: *parentParent} parent2 := &models.Parent2{ParentParent: *parentParent} @@ -429,7 +429,7 @@ func (ts *UpdateIntTestSuite) TestUpdateDynamicWithJoinNumber() { conditions.Parent2.ParentParent(), ), ).Set( - conditions.Child.Name.Set().Dynamic(conditions.ParentParent.Name.Value(), 0), + conditions.Child.Name.Set().Dynamic(conditions.ParentParent.Name.Appearance(0).Value()), ) ts.Require().NoError(err) ts.Equal(int64(1), updated) @@ -683,6 +683,70 @@ func (ts *UpdateIntTestSuite) TestUpdateMultipleTablesReturnsErrorIfTableNotJoin ts.ErrorContains(err, "not concerned model: models.Brand; method: Set") } +func (ts *UpdateIntTestSuite) TestUpdateMultipleTablesReturnsErrorIfTableJoinedMultipleTimesAndNoAppearance() { + // update join only supported for mysql + if getDBDialector() != cqlSQL.MySQL { + return + } + + _, err := cql.Update[models.Child]( + ts.db, + conditions.Child.Parent1( + conditions.Parent1.ParentParent(), + ), + conditions.Child.Parent2( + conditions.Parent2.ParentParent(), + ), + ).SetMultiple( + conditions.ParentParent.Name.Set().Dynamic(conditions.Child.Name.Value()), + ) + + ts.ErrorIs(err, cql.ErrAppearanceMustBeSelected) + ts.ErrorContains(err, "model: models.ParentParent; method: SetMultiple") +} + +func (ts *UpdateIntTestSuite) TestUpdateMultipleTablesTableJoinedMultipleTimesAndAppearance() { + // update join only supported for mysql + if getDBDialector() != cqlSQL.MySQL { + return + } + + parentParent1 := &models.ParentParent{Name: "franco"} + parentParent2 := &models.ParentParent{Name: "ruben"} + err := ts.db.Create(parentParent2).Error + ts.Require().NoError(err) + + parent1 := &models.Parent1{ParentParent: *parentParent1} + parent2 := &models.Parent2{ParentParent: *parentParent2} + child := &models.Child{Parent1: *parent1, Parent2: *parent2, Name: "not_franco"} + err = ts.db.Create(child).Error + ts.Require().NoError(err) + + updated, err := cql.Update[models.Child]( + ts.db, + conditions.Child.Parent1( + conditions.Parent1.ParentParent(), + ), + conditions.Child.Parent2( + conditions.Parent2.ParentParent(), + ), + ).SetMultiple( + conditions.ParentParent.Name.Appearance(1).Set().Dynamic(conditions.Child.Name.Value()), + ) + + ts.Require().NoError(err) + ts.Equal(int64(1), updated) + + parentParentReturned, err := cql.Query[models.ParentParent]( + ts.db, + conditions.ParentParent.Name.Is().Eq("not_franco"), + ).FindOne() + ts.Require().NoError(err) + + ts.Equal(parentParent2.ID, parentParentReturned.ID) + ts.NotEqual(parentParent2.UpdatedAt.UnixMicro(), parentParentReturned.UpdatedAt.UnixMicro()) +} + func (ts *UpdateIntTestSuite) TestUpdateOrderByLimit() { // update order by limit only supported for mysql if getDBDialector() != cqlSQL.MySQL {