From c10244aeab5b4bf9789bc3018802873d3c99fc3e Mon Sep 17 00:00:00 2001 From: go-jet Date: Fri, 4 Feb 2022 12:31:08 +0100 Subject: [PATCH 1/5] Improve Rows scan performance ScanContext reused between rows.Scan calls. Simplified assign value logic. Use complex destination for Rows test. --- internal/jet/statement.go | 15 +++++++- qrm/qrm.go | 40 +++++-------------- qrm/scan_context.go | 23 ++++++----- qrm/utill.go | 79 +++++++++++++++++++++----------------- tests/mysql/select_test.go | 39 ++++++++++++++++--- 5 files changed, 113 insertions(+), 83 deletions(-) diff --git a/internal/jet/statement.go b/internal/jet/statement.go index b2058017..183aaae8 100644 --- a/internal/jet/statement.go +++ b/internal/jet/statement.go @@ -33,11 +33,13 @@ type Statement interface { // Rows wraps sql.Rows type to add query result mapping for Scan method type Rows struct { *sql.Rows + + scanContext *qrm.ScanContext } // Scan will map the Row values into struct destination func (r *Rows) Scan(destination interface{}) error { - return qrm.ScanOneRowToDest(r.Rows, destination) + return qrm.ScanOneRowToDest(r.scanContext, r.Rows, destination) } // SerializerStatement interface @@ -161,7 +163,16 @@ func (s *serializerStatementInterfaceImpl) Rows(ctx context.Context, db qrm.DB) return nil, err } - return &Rows{rows}, nil + scanContext, err := qrm.NewScanContext(rows) + + if err != nil { + return nil, err + } + + return &Rows{ + Rows: rows, + scanContext: scanContext, + }, nil } func duration(f func()) time.Duration { diff --git a/qrm/qrm.go b/qrm/qrm.go index 3731c687..1ffd36f4 100644 --- a/qrm/qrm.go +++ b/qrm/qrm.go @@ -63,48 +63,28 @@ func Query(ctx context.Context, db DB, query string, args []interface{}, destPtr } // ScanOneRowToDest will scan one row into struct destination -func ScanOneRowToDest(rows *sql.Rows, destPtr interface{}) error { +func ScanOneRowToDest(scanContext *ScanContext, rows *sql.Rows, destPtr interface{}) error { utils.MustBeInitializedPtr(destPtr, "jet: destination is nil") utils.MustBe(destPtr, reflect.Ptr, "jet: destination has to be a pointer to slice or pointer to struct") - scanContext, err := newScanContext(rows) - - if err != nil { - return fmt.Errorf("failed to create scan context, %w", err) - } - if len(scanContext.row) == 0 { return errors.New("empty row slice") } - err = rows.Scan(scanContext.row...) + err := rows.Scan(scanContext.row...) if err != nil { return fmt.Errorf("rows scan error, %w", err) } - destinationPtrType := reflect.TypeOf(destPtr) - tempSlicePtrValue := reflect.New(reflect.SliceOf(destinationPtrType)) - tempSliceValue := tempSlicePtrValue.Elem() + destValue := reflect.ValueOf(destPtr) - _, err = mapRowToSlice(scanContext, "", newTypeStack(), tempSlicePtrValue, nil) + _, err = mapRowToStruct(scanContext, "", newTypeStack(), destValue, nil) if err != nil { return fmt.Errorf("failed to map a row, %w", err) } - // edge case when row result set contains only NULLs. - if tempSliceValue.Len() == 0 { - return nil - } - - destValue := reflect.ValueOf(destPtr).Elem() - firstTempSliceValue := tempSliceValue.Index(0).Elem() - - if destValue.Type().AssignableTo(firstTempSliceValue.Type()) { - destValue.Set(tempSliceValue.Index(0).Elem()) - } - return nil } @@ -120,7 +100,7 @@ func queryToSlice(ctx context.Context, db DB, query string, args []interface{}, } defer rows.Close() - scanContext, err := newScanContext(rows) + scanContext, err := NewScanContext(rows) if err != nil { return @@ -157,7 +137,7 @@ func queryToSlice(ctx context.Context, db DB, query string, args []interface{}, } func mapRowToSlice( - scanContext *scanContext, + scanContext *ScanContext, groupKey string, typesVisited *typeStack, slicePtrValue reflect.Value, @@ -204,7 +184,7 @@ func mapRowToSlice( return } -func mapRowToBaseTypeSlice(scanContext *scanContext, slicePtrValue reflect.Value, field *reflect.StructField) (updated bool, err error) { +func mapRowToBaseTypeSlice(scanContext *ScanContext, slicePtrValue reflect.Value, field *reflect.StructField) (updated bool, err error) { index := 0 if field != nil { typeName, columnName := getTypeAndFieldName("", *field) @@ -226,7 +206,7 @@ func mapRowToBaseTypeSlice(scanContext *scanContext, slicePtrValue reflect.Value } func mapRowToStruct( - scanContext *scanContext, + scanContext *ScanContext, groupKey string, typesVisited *typeStack, // to prevent circular dependency scan structPtrValue reflect.Value, @@ -308,7 +288,7 @@ func mapRowToStruct( } func mapRowToDestinationValue( - scanContext *scanContext, + scanContext *ScanContext, groupKey string, typesVisited *typeStack, dest reflect.Value, @@ -340,7 +320,7 @@ func mapRowToDestinationValue( } func mapRowToDestinationPtr( - scanContext *scanContext, + scanContext *ScanContext, groupKey string, typesVisited *typeStack, destPtrValue reflect.Value, diff --git a/qrm/scan_context.go b/qrm/scan_context.go index 61feb759..01cfe539 100644 --- a/qrm/scan_context.go +++ b/qrm/scan_context.go @@ -7,7 +7,9 @@ import ( "strings" ) -type scanContext struct { +// ScanContext contains information about current row processed, mapping from the row to the +// destination types and type grouping information. +type ScanContext struct { rowNum int64 row []interface{} uniqueDestObjectsMap map[string]int @@ -16,7 +18,8 @@ type scanContext struct { typeInfoMap map[string]typeInfo } -func newScanContext(rows *sql.Rows) (*scanContext, error) { +// NewScanContext creates new ScanContext from rows +func NewScanContext(rows *sql.Rows) (*ScanContext, error) { aliases, err := rows.Columns() if err != nil { @@ -42,7 +45,7 @@ func newScanContext(rows *sql.Rows) (*scanContext, error) { commonIdentToColumnIndex[commonIdentifier] = i } - return &scanContext{ + return &ScanContext{ row: createScanSlice(len(columnTypes)), uniqueDestObjectsMap: make(map[string]int), @@ -74,7 +77,7 @@ type fieldMapping struct { implementsScanner bool } -func (s *scanContext) getTypeInfo(structType reflect.Type, parentField *reflect.StructField) typeInfo { +func (s *ScanContext) getTypeInfo(structType reflect.Type, parentField *reflect.StructField) typeInfo { typeMapKey := structType.String() @@ -120,7 +123,7 @@ type groupKeyInfo struct { subTypes []groupKeyInfo } -func (s *scanContext) getGroupKey(structType reflect.Type, structField *reflect.StructField) string { +func (s *ScanContext) getGroupKey(structType reflect.Type, structField *reflect.StructField) string { mapKey := structType.Name() @@ -139,7 +142,7 @@ func (s *scanContext) getGroupKey(structType reflect.Type, structField *reflect. return s.constructGroupKey(groupKeyInfo) } -func (s *scanContext) constructGroupKey(groupKeyInfo groupKeyInfo) string { +func (s *ScanContext) constructGroupKey(groupKeyInfo groupKeyInfo) string { if len(groupKeyInfo.indexes) == 0 && len(groupKeyInfo.subTypes) == 0 { return fmt.Sprintf("|ROW:%d|", s.rowNum) } @@ -161,7 +164,7 @@ func (s *scanContext) constructGroupKey(groupKeyInfo groupKeyInfo) string { return groupKeyInfo.typeName + "(" + strings.Join(groupKeys, ",") + strings.Join(subTypesGroupKeys, ",") + ")" } -func (s *scanContext) getGroupKeyInfo( +func (s *ScanContext) getGroupKeyInfo( structType reflect.Type, parentField *reflect.StructField, typeVisited *typeStack) groupKeyInfo { @@ -210,7 +213,7 @@ func (s *scanContext) getGroupKeyInfo( return ret } -func (s *scanContext) typeToColumnIndex(typeName, fieldName string) int { +func (s *ScanContext) typeToColumnIndex(typeName, fieldName string) int { var key string if typeName != "" { @@ -228,7 +231,7 @@ func (s *scanContext) typeToColumnIndex(typeName, fieldName string) int { return index } -func (s *scanContext) rowElem(index int) interface{} { +func (s *ScanContext) rowElem(index int) interface{} { cellValue := reflect.ValueOf(s.row[index]) if cellValue.IsValid() && !cellValue.IsNil() { @@ -238,7 +241,7 @@ func (s *scanContext) rowElem(index int) interface{} { return nil } -func (s *scanContext) rowElemValuePtr(index int) reflect.Value { +func (s *ScanContext) rowElemValuePtr(index int) reflect.Value { rowElem := s.rowElem(index) rowElemValue := reflect.ValueOf(rowElem) diff --git a/qrm/utill.go b/qrm/utill.go index 6926c423..ca0db619 100644 --- a/qrm/utill.go +++ b/qrm/utill.go @@ -201,23 +201,38 @@ func isFloatType(value reflect.Type) bool { return false } +func assignIfAssignable(source, destination reflect.Value) bool { + if source.Type().AssignableTo(destination.Type()) { + switch source.Type() { + case byteArrayType: + destination.SetBytes(cloneBytes(source.Interface().([]byte))) + default: + destination.Set(source) + } + return true + } + + return false +} + func tryAssign(source, destination reflect.Value) error { - if source.Type() != destination.Type() && - !isFloatType(destination.Type()) && // to preserve precision during conversion - !(isIntegerType(source.Type()) && destination.Kind() == reflect.String) && // default conversion will convert int to 1 rune string - source.Type().ConvertibleTo(destination.Type()) { + if assignIfAssignable(source, destination) { + return nil + } + + sourceType := source.Type() + destinationType := destination.Type() + + if sourceType != destinationType && + !isFloatType(destinationType) && // to preserve precision during conversion + !(isIntegerType(sourceType) && destination.Kind() == reflect.String) && // default conversion will convert int to 1 rune string + sourceType.ConvertibleTo(destinationType) { - source = source.Convert(destination.Type()) + source = source.Convert(destinationType) } - if source.Type().AssignableTo(destination.Type()) { - switch b := source.Interface().(type) { - case []byte: - destination.SetBytes(cloneBytes(b)) - default: - destination.Set(source) - } + if assignIfAssignable(source, destination) { return nil } @@ -302,38 +317,32 @@ func tryAssign(source, destination reflect.Value) error { return nil } +func setZeroValue(value reflect.Value) { + if !value.IsZero() { + value.Set(reflect.Zero(value.Type())) + } +} + func setReflectValue(source, destination reflect.Value) error { + if source.Kind() == reflect.Ptr { + if source.IsNil() { + // source is nil, destination should be its zero value + setZeroValue(destination) + return nil + } + source = source.Elem() + } + if destination.Kind() == reflect.Ptr { if destination.IsNil() { initializeValueIfNilPtr(destination) } - if source.Kind() == reflect.Ptr { - if source.IsNil() { - return nil // source is nil, destination should keep its zero value - } - source = source.Elem() - } - - if err := tryAssign(source, destination.Elem()); err != nil { - return err - } - - } else { - if source.Kind() == reflect.Ptr { - if source.IsNil() { - return nil // source is nil, destination should keep its zero value - } - source = source.Elem() - } - - if err := tryAssign(source, destination); err != nil { - return err - } + destination = destination.Elem() } - return nil + return tryAssign(source, destination) } func isPrimaryKey(field reflect.StructField, primaryKeyOverwrites []string) bool { diff --git a/tests/mysql/select_test.go b/tests/mysql/select_test.go index 39f0e431..e60a7d43 100644 --- a/tests/mysql/select_test.go +++ b/tests/mysql/select_test.go @@ -951,8 +951,12 @@ func TestRowsScan(t *testing.T) { stmt := SELECT( Inventory.AllColumns, + Film.AllColumns, + Store.AllColumns, ).FROM( - Inventory, + Inventory. + INNER_JOIN(Film, Film.FilmID.EQ(Inventory.FilmID)). + INNER_JOIN(Store, Store.StoreID.EQ(Inventory.StoreID)), ).ORDER_BY( Inventory.InventoryID.ASC(), ) @@ -961,19 +965,42 @@ func TestRowsScan(t *testing.T) { require.NoError(t, err) for rows.Next() { - var inventory model.Inventory + var inventory struct { + model.Inventory + + Film model.Film + Store model.Store + } + err = rows.Scan(&inventory) require.NoError(t, err) - require.NotEqual(t, inventory.InventoryID, uint32(0)) - require.NotEqual(t, inventory.FilmID, uint16(0)) - require.NotEqual(t, inventory.StoreID, uint16(0)) - require.NotEqual(t, inventory.LastUpdate, time.Time{}) + require.NotEmpty(t, inventory.InventoryID) + require.NotEmpty(t, inventory.FilmID) + require.NotEmpty(t, inventory.StoreID) + require.NotEmpty(t, inventory.LastUpdate) + + require.NotEmpty(t, inventory.Film.FilmID) + require.NotEmpty(t, inventory.Film.Title) + require.NotEmpty(t, inventory.Film.Description) + + require.NotEmpty(t, inventory.Store.StoreID) + require.NotEmpty(t, inventory.Store.AddressID) + require.NotEmpty(t, inventory.Store.ManagerStaffID) if inventory.InventoryID == 2103 { require.Equal(t, inventory.FilmID, uint16(456)) require.Equal(t, inventory.StoreID, uint8(2)) require.Equal(t, inventory.LastUpdate.Format(time.RFC3339), "2006-02-15T05:09:17Z") + + require.Equal(t, inventory.Film.FilmID, uint16(456)) + require.Equal(t, inventory.Film.Title, "INCH JET") + require.Equal(t, *inventory.Film.Description, "A Fateful Saga of a Womanizer And a Student who must Defeat a Butler in A Monastery") + require.Equal(t, *inventory.Film.ReleaseYear, int16(2006)) + + require.Equal(t, inventory.Store.StoreID, uint8(2)) + require.Equal(t, inventory.Store.ManagerStaffID, uint8(2)) + require.Equal(t, inventory.Store.AddressID, uint16(2)) } } From c86903fd1d7e2be2b866a7acfd39001ade420737 Mon Sep 17 00:00:00 2001 From: go-jet Date: Wed, 9 Feb 2022 12:34:10 +0100 Subject: [PATCH 2/5] Additional scan performance improvements Move typeStack to ScanContext, so it is shared between rows.Scan calls. Use string.Builder for string concatenations. Simplify value assign logic. Move convert value to the last assign step (needs for type conversions are rare). --- qrm/qrm.go | 60 ++++++------- qrm/scan_context.go | 66 +++++++------- qrm/type_stack.go | 4 +- qrm/utill.go | 161 +++++++++++++-------------------- tests/mysql/select_test.go | 61 +++++++++++-- tests/postgres/scan_test.go | 165 ++++++++++++++++++++++++++++++++++ tests/postgres/select_test.go | 73 +++++++++++++++ tests/postgres/with_test.go | 2 - 8 files changed, 423 insertions(+), 169 deletions(-) diff --git a/qrm/qrm.go b/qrm/qrm.go index 1ffd36f4..50597cd9 100644 --- a/qrm/qrm.go +++ b/qrm/qrm.go @@ -74,15 +74,15 @@ func ScanOneRowToDest(scanContext *ScanContext, rows *sql.Rows, destPtr interfac err := rows.Scan(scanContext.row...) if err != nil { - return fmt.Errorf("rows scan error, %w", err) + return fmt.Errorf("jet: rows scan error, %w", err) } - destValue := reflect.ValueOf(destPtr) + destValuePtr := reflect.ValueOf(destPtr) - _, err = mapRowToStruct(scanContext, "", newTypeStack(), destValue, nil) + _, err = mapRowToStruct(scanContext, "", destValuePtr, nil) if err != nil { - return fmt.Errorf("failed to map a row, %w", err) + return fmt.Errorf("jet: failed to scan a row into destination, %w", err) } return nil @@ -121,7 +121,7 @@ func queryToSlice(ctx context.Context, db DB, query string, args []interface{}, scanContext.rowNum++ - _, err = mapRowToSlice(scanContext, "", newTypeStack(), slicePtrValue, nil) + _, err = mapRowToSlice(scanContext, "", slicePtrValue, nil) if err != nil { return scanContext.rowNum, err @@ -139,7 +139,6 @@ func queryToSlice(ctx context.Context, db DB, query string, args []interface{}, func mapRowToSlice( scanContext *ScanContext, groupKey string, - typesVisited *typeStack, slicePtrValue reflect.Value, field *reflect.StructField) (updated bool, err error) { @@ -154,19 +153,19 @@ func mapRowToSlice( structGroupKey := scanContext.getGroupKey(sliceElemType, field) - groupKey = groupKey + "," + structGroupKey + groupKey = concat(groupKey, ",", structGroupKey) index, ok := scanContext.uniqueDestObjectsMap[groupKey] if ok { structPtrValue := getSliceElemPtrAt(slicePtrValue, index) - return mapRowToStruct(scanContext, groupKey, typesVisited, structPtrValue, field, true) + return mapRowToStruct(scanContext, groupKey, structPtrValue, field, true) } destinationStructPtr := newElemPtrValueForSlice(slicePtrValue) - updated, err = mapRowToStruct(scanContext, groupKey, typesVisited, destinationStructPtr, field) + updated, err = mapRowToStruct(scanContext, groupKey, destinationStructPtr, field) if err != nil { return @@ -192,7 +191,7 @@ func mapRowToBaseTypeSlice(scanContext *ScanContext, slicePtrValue reflect.Value return } } - rowElemPtr := scanContext.rowElemValuePtr(index) + rowElemPtr := scanContext.rowElemValueClonePtr(index) if rowElemPtr.IsValid() && !rowElemPtr.IsNil() { updated = true @@ -208,7 +207,6 @@ func mapRowToBaseTypeSlice(scanContext *ScanContext, slicePtrValue reflect.Value func mapRowToStruct( scanContext *ScanContext, groupKey string, - typesVisited *typeStack, // to prevent circular dependency scan structPtrValue reflect.Value, parentField *reflect.StructField, onlySlices ...bool, // small optimization, not to assign to already assigned struct fields @@ -217,12 +215,12 @@ func mapRowToStruct( mapOnlySlices := len(onlySlices) > 0 structType := structPtrValue.Type().Elem() - if typesVisited.contains(&structType) { + if scanContext.typesVisited.contains(&structType) { return false, nil } - typesVisited.push(&structType) - defer typesVisited.pop() + scanContext.typesVisited.push(&structType) + defer scanContext.typesVisited.pop() typeInf := scanContext.getTypeInfo(structType, parentField) @@ -240,7 +238,7 @@ func mapRowToStruct( if fieldMap.complexType { var changed bool - changed, err = mapRowToDestinationValue(scanContext, groupKey, typesVisited, fieldValue, &field) + changed, err = mapRowToDestinationValue(scanContext, groupKey, fieldValue, &field) if err != nil { return @@ -251,34 +249,36 @@ func mapRowToStruct( } } else { - if mapOnlySlices || fieldMap.columnIndex == -1 { + if mapOnlySlices || fieldMap.rowIndex == -1 { continue } - cellValue := scanContext.rowElem(fieldMap.columnIndex) + scannedValue := scanContext.rowElemValue(fieldMap.rowIndex) - if cellValue == nil { + if !scannedValue.IsValid() { + setZeroValue(fieldValue) // scannedValue is nil, destination should be set to zero value continue } - initializeValueIfNilPtr(fieldValue) updated = true if fieldMap.implementsScanner { - scanner := getScanner(fieldValue) + initializeValueIfNilPtr(fieldValue) + fieldScanner := getScanner(fieldValue) - err = scanner.Scan(cellValue) + value := scannedValue.Interface() + + err := fieldScanner.Scan(value) if err != nil { - err = fmt.Errorf(`can't scan %T(%q) to '%s %s': %w`, cellValue, cellValue, field.Name, field.Type.String(), err) - return + return updated, fmt.Errorf(`can't scan %T(%q) to '%s %s': %w`, value, value, field.Name, field.Type.String(), err) } } else { - err = setReflectValue(reflect.ValueOf(cellValue), fieldValue) + err := assign(scannedValue, fieldValue) if err != nil { - err = fmt.Errorf(`can't assign %T(%q) to '%s %s': %w`, cellValue, cellValue, field.Name, field.Type.String(), err) - return + return updated, fmt.Errorf(`can't assign %T(%q) to '%s %s': %w`, scannedValue.Interface(), scannedValue.Interface(), + field.Name, field.Type.String(), err) } } } @@ -290,7 +290,6 @@ func mapRowToStruct( func mapRowToDestinationValue( scanContext *ScanContext, groupKey string, - typesVisited *typeStack, dest reflect.Value, structField *reflect.StructField) (updated bool, err error) { @@ -306,7 +305,7 @@ func mapRowToDestinationValue( } } - updated, err = mapRowToDestinationPtr(scanContext, groupKey, typesVisited, destPtrValue, structField) + updated, err = mapRowToDestinationPtr(scanContext, groupKey, destPtrValue, structField) if err != nil { return @@ -322,7 +321,6 @@ func mapRowToDestinationValue( func mapRowToDestinationPtr( scanContext *ScanContext, groupKey string, - typesVisited *typeStack, destPtrValue reflect.Value, structField *reflect.StructField) (updated bool, err error) { @@ -331,9 +329,9 @@ func mapRowToDestinationPtr( destValueKind := destPtrValue.Elem().Kind() if destValueKind == reflect.Struct { - return mapRowToStruct(scanContext, groupKey, typesVisited, destPtrValue, structField) + return mapRowToStruct(scanContext, groupKey, destPtrValue, structField) } else if destValueKind == reflect.Slice { - return mapRowToSlice(scanContext, groupKey, typesVisited, destPtrValue, structField) + return mapRowToSlice(scanContext, groupKey, destPtrValue, structField) } else { panic("jet: unsupported dest type: " + structField.Name + " " + structField.Type.String()) } diff --git a/qrm/scan_context.go b/qrm/scan_context.go index 01cfe539..fa99b5ad 100644 --- a/qrm/scan_context.go +++ b/qrm/scan_context.go @@ -16,6 +16,8 @@ type ScanContext struct { commonIdentToColumnIndex map[string]int groupKeyInfoCache map[string]groupKeyInfo typeInfoMap map[string]typeInfo + + typesVisited typeStack // to prevent circular dependency scan } // NewScanContext creates new ScanContext from rows @@ -39,7 +41,7 @@ func NewScanContext(rows *sql.Rows) (*ScanContext, error) { commonIdentifier := toCommonIdentifier(names[0]) if len(names) > 1 { - commonIdentifier += "." + toCommonIdentifier(names[1]) + commonIdentifier = concat(commonIdentifier, ".", toCommonIdentifier(names[1])) } commonIdentToColumnIndex[commonIdentifier] = i @@ -53,15 +55,17 @@ func NewScanContext(rows *sql.Rows) (*ScanContext, error) { commonIdentToColumnIndex: commonIdentToColumnIndex, typeInfoMap: make(map[string]typeInfo), + + typesVisited: newTypeStack(), }, nil } func createScanSlice(columnCount int) []interface{} { - scanSlice := make([]interface{}, columnCount) scanPtrSlice := make([]interface{}, columnCount) for i := range scanPtrSlice { - scanPtrSlice[i] = &scanSlice[i] // if destination is pointer to interface sql.Scan will just forward driver value + var a interface{} + scanPtrSlice[i] = &a // if destination is pointer to interface sql.Scan will just forward driver value } return scanPtrSlice @@ -72,8 +76,8 @@ type typeInfo struct { } type fieldMapping struct { - complexType bool // slice or struct - columnIndex int + complexType bool // slice and struct are complex types + rowIndex int // index in ScanContext.row implementsScanner bool } @@ -82,7 +86,7 @@ func (s *ScanContext) getTypeInfo(structType reflect.Type, parentField *reflect. typeMapKey := structType.String() if parentField != nil { - typeMapKey += string(parentField.Tag) + typeMapKey = concat(typeMapKey, string(parentField.Tag)) } if typeInfo, ok := s.typeInfoMap[typeMapKey]; ok { @@ -100,7 +104,7 @@ func (s *ScanContext) getTypeInfo(structType reflect.Type, parentField *reflect. columnIndex := s.typeToColumnIndex(newTypeName, fieldName) fieldMap := fieldMapping{ - columnIndex: columnIndex, + rowIndex: columnIndex, } if implementsScannerType(field.Type) { @@ -128,14 +132,15 @@ func (s *ScanContext) getGroupKey(structType reflect.Type, structField *reflect. mapKey := structType.Name() if structField != nil { - mapKey += structField.Type.String() + mapKey = concat(mapKey, structField.Type.String()) } if groupKeyInfo, ok := s.groupKeyInfoCache[mapKey]; ok { return s.constructGroupKey(groupKeyInfo) } - groupKeyInfo := s.getGroupKeyInfo(structType, structField, newTypeStack()) + tempTypeStack := newTypeStack() + groupKeyInfo := s.getGroupKeyInfo(structType, structField, &tempTypeStack) s.groupKeyInfoCache[mapKey] = groupKeyInfo @@ -150,10 +155,7 @@ func (s *ScanContext) constructGroupKey(groupKeyInfo groupKeyInfo) string { var groupKeys []string for _, index := range groupKeyInfo.indexes { - cellValue := s.rowElem(index) - subKey := valueToString(reflect.ValueOf(cellValue)) - - groupKeys = append(groupKeys, subKey) + groupKeys = append(groupKeys, s.rowElemToString(index)) } var subTypesGroupKeys []string @@ -161,7 +163,7 @@ func (s *ScanContext) constructGroupKey(groupKeyInfo groupKeyInfo) string { subTypesGroupKeys = append(subTypesGroupKeys, s.constructGroupKey(subType)) } - return groupKeyInfo.typeName + "(" + strings.Join(groupKeys, ",") + strings.Join(subTypesGroupKeys, ",") + ")" + return concat(groupKeyInfo.typeName, "(", strings.Join(groupKeys, ","), strings.Join(subTypesGroupKeys, ","), ")") } func (s *ScanContext) getGroupKeyInfo( @@ -231,32 +233,36 @@ func (s *ScanContext) typeToColumnIndex(typeName, fieldName string) int { return index } -func (s *ScanContext) rowElem(index int) interface{} { - cellValue := reflect.ValueOf(s.row[index]) +// rowElemValue always returns non-ptr value, +// invalid value is nil +func (s *ScanContext) rowElemValue(index int) reflect.Value { + scannedValue := reflect.ValueOf(s.row[index]) + return scannedValue.Elem().Elem() // no need to check validity of Elem, because s.row[index] always contains interface in interface +} + +func (s *ScanContext) rowElemToString(index int) string { + value := s.rowElemValue(index) + + if !value.IsValid() { + return "nil" + } + + valueInterface := value.Interface() - if cellValue.IsValid() && !cellValue.IsNil() { - return cellValue.Elem().Interface() + if t, ok := valueInterface.(fmt.Stringer); ok { + return t.String() } - return nil + return fmt.Sprintf("%#v", valueInterface) } -func (s *ScanContext) rowElemValuePtr(index int) reflect.Value { - rowElem := s.rowElem(index) - rowElemValue := reflect.ValueOf(rowElem) +func (s *ScanContext) rowElemValueClonePtr(index int) reflect.Value { + rowElemValue := s.rowElemValue(index) if !rowElemValue.IsValid() { return reflect.Value{} } - if rowElemValue.Kind() == reflect.Ptr { - return rowElemValue - } - - if rowElemValue.CanAddr() { - return rowElemValue.Addr() - } - newElem := reflect.New(rowElemValue.Type()) newElem.Elem().Set(rowElemValue) return newElem diff --git a/qrm/type_stack.go b/qrm/type_stack.go index 235c06ea..2bdf799b 100644 --- a/qrm/type_stack.go +++ b/qrm/type_stack.go @@ -4,9 +4,9 @@ import "reflect" type typeStack []*reflect.Type -func newTypeStack() *typeStack { +func newTypeStack() typeStack { stack := make(typeStack, 0, 20) - return &stack + return stack } func (s *typeStack) isEmpty() bool { diff --git a/qrm/utill.go b/qrm/utill.go index ca0db619..dfb9a694 100644 --- a/qrm/utill.go +++ b/qrm/utill.go @@ -18,9 +18,9 @@ func implementsScannerType(fieldType reflect.Type) bool { return true } - typePtr := reflect.New(fieldType).Type() + fieldTypePtr := reflect.New(fieldType).Type() - return typePtr.Implements(scannerInterfaceType) + return fieldTypePtr.Implements(scannerInterfaceType) } func getScanner(value reflect.Value) sql.Scanner { @@ -68,9 +68,9 @@ func appendElemToSlice(slicePtrValue reflect.Value, objPtrValue reflect.Value) e if newSliceElemValue.Kind() == reflect.Ptr { newSliceElemValue.Set(reflect.New(newSliceElemValue.Type().Elem())) - err = tryAssign(objPtrValue.Elem(), newSliceElemValue.Elem()) + err = assign(objPtrValue.Elem(), newSliceElemValue.Elem()) } else { - err = tryAssign(objPtrValue.Elem(), newSliceElemValue) + err = assign(objPtrValue.Elem(), newSliceElemValue) } if err != nil { @@ -138,29 +138,6 @@ func initializeValueIfNilPtr(value reflect.Value) { } } -func valueToString(value reflect.Value) string { - - if !value.IsValid() { - return "nil" - } - - var valueInterface interface{} - if value.Kind() == reflect.Ptr { - if value.IsNil() { - return "nil" - } - valueInterface = value.Elem().Interface() - } else { - valueInterface = value.Interface() - } - - if t, ok := valueInterface.(fmt.Stringer); ok { - return t.String() - } - - return fmt.Sprintf("%#v", valueInterface) -} - var timeType = reflect.TypeOf(time.Now()) var uuidType = reflect.TypeOf(uuid.New()) var byteArrayType = reflect.TypeOf([]byte("")) @@ -180,30 +157,35 @@ func isSimpleModelType(objType reflect.Type) bool { return objType == timeType || objType == uuidType || objType == byteArrayType } -func isIntegerType(objType reflect.Type) bool { - objType = indirectType(objType) +// source can't be pointer +// destination can be pointer +func assign(source, destination reflect.Value) error { + if destination.Kind() == reflect.Ptr { + if destination.IsNil() { + initializeValueIfNilPtr(destination) + } - switch objType.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - return true + destination = destination.Elem() } - return false -} + err := tryAssign(source, destination) -func isFloatType(value reflect.Type) bool { - switch value.Kind() { - case reflect.Float32, reflect.Float64: - return true + if err != nil { + // needs for the type conversions are rare, so we leave conversion as a last assign step if everything else fails + if tryConvert(source, destination) { + return nil + } + + return err } - return false + return nil } func assignIfAssignable(source, destination reflect.Value) bool { - if source.Type().AssignableTo(destination.Type()) { - switch source.Type() { + sourceType := source.Type() + if sourceType.AssignableTo(destination.Type()) { + switch sourceType { case byteArrayType: destination.SetBytes(cloneBytes(source.Interface().([]byte))) default: @@ -215,31 +197,17 @@ func assignIfAssignable(source, destination reflect.Value) bool { return false } +// source and destination are non-ptr values func tryAssign(source, destination reflect.Value) error { if assignIfAssignable(source, destination) { return nil } - sourceType := source.Type() - destinationType := destination.Type() - - if sourceType != destinationType && - !isFloatType(destinationType) && // to preserve precision during conversion - !(isIntegerType(sourceType) && destination.Kind() == reflect.String) && // default conversion will convert int to 1 rune string - sourceType.ConvertibleTo(destinationType) { - - source = source.Convert(destinationType) - } - - if assignIfAssignable(source, destination) { - return nil - } - sourceInterface := source.Interface() - switch destination.Interface().(type) { - case bool: + switch destination.Type().Kind() { + case reflect.Bool: var nullBool internal.NullBool err := nullBool.Scan(sourceInterface) @@ -250,7 +218,7 @@ func tryAssign(source, destination reflect.Value) error { destination.SetBool(nullBool.Bool) - case float32, float64: + case reflect.Float32, reflect.Float64: var nullFloat sql.NullFloat64 err := nullFloat.Scan(sourceInterface) @@ -261,7 +229,7 @@ func tryAssign(source, destination reflect.Value) error { if nullFloat.Valid { destination.SetFloat(nullFloat.Float64) } - case int, int8, int16, int32, int64: + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: var integer sql.NullInt64 err := integer.Scan(sourceInterface) @@ -273,7 +241,7 @@ func tryAssign(source, destination reflect.Value) error { destination.SetInt(integer.Int64) } - case uint, uint8, uint16, uint32, uint64: + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: var uInt internal.NullUInt64 err := uInt.Scan(sourceInterface) @@ -286,7 +254,7 @@ func tryAssign(source, destination reflect.Value) error { destination.SetUint(uInt.UInt64) } - case string: + case reflect.String: var str sql.NullString err := str.Scan(sourceInterface) @@ -298,51 +266,42 @@ func tryAssign(source, destination reflect.Value) error { destination.SetString(str.String) } - case time.Time: - var nullTime internal.NullTime - - err := nullTime.Scan(sourceInterface) - if err != nil { - return err - } - - if nullTime.Valid { - destination.Set(reflect.ValueOf(nullTime.Time)) - } - default: - return fmt.Errorf("can't assign %T to %T", sourceInterface, destination.Interface()) + switch destination.Interface().(type) { + case time.Time: + var nullTime internal.NullTime + + err := nullTime.Scan(sourceInterface) + if err != nil { + return err + } + + if nullTime.Valid { + destination.Set(reflect.ValueOf(nullTime.Time)) + } + default: + return fmt.Errorf("can't assign %T to %T", sourceInterface, destination.Interface()) + } } return nil } -func setZeroValue(value reflect.Value) { - if !value.IsZero() { - value.Set(reflect.Zero(value.Type())) - } -} - -func setReflectValue(source, destination reflect.Value) error { +func tryConvert(source, destination reflect.Value) bool { + destinationType := destination.Type() - if source.Kind() == reflect.Ptr { - if source.IsNil() { - // source is nil, destination should be its zero value - setZeroValue(destination) - return nil - } - source = source.Elem() + if source.Type().ConvertibleTo(destinationType) { + source = source.Convert(destinationType) + return assignIfAssignable(source, destination) } - if destination.Kind() == reflect.Ptr { - if destination.IsNil() { - initializeValueIfNilPtr(destination) - } + return false +} - destination = destination.Elem() +func setZeroValue(value reflect.Value) { + if !value.IsZero() { + value.Set(reflect.Zero(value.Type())) } - - return tryAssign(source, destination) } func isPrimaryKey(field reflect.StructField, primaryKeyOverwrites []string) bool { @@ -398,3 +357,11 @@ func cloneBytes(b []byte) []byte { copy(c, b) return c } + +func concat(stringList ...string) string { + var b strings.Builder + for _, str := range stringList { + b.WriteString(str) + } + return b.String() +} diff --git a/tests/mysql/select_test.go b/tests/mysql/select_test.go index e60a7d43..8bd028ab 100644 --- a/tests/mysql/select_test.go +++ b/tests/mysql/select_test.go @@ -206,7 +206,7 @@ GROUP BY payment.customer_id; "RentalID": null, "Amount": 0, "PaymentDate": "0001-01-01T00:00:00Z", - "LastUpdate": "0001-01-01T00:00:00Z", + "LastUpdate": null, "Count": 8, "Sum": 38.92, "Avg": 4.865, @@ -964,14 +964,14 @@ func TestRowsScan(t *testing.T) { rows, err := stmt.Rows(context.Background(), db) require.NoError(t, err) - for rows.Next() { - var inventory struct { - model.Inventory + var inventory struct { + model.Inventory - Film model.Film - Store model.Store - } + Film model.Film + Store model.Store + } + for rows.Next() { err = rows.Scan(&inventory) require.NoError(t, err) @@ -1056,3 +1056,50 @@ func TestScanNumericToNumber(t *testing.T) { require.Equal(t, number.Float32, float32(1.234568e+09)) require.Equal(t, number.Float64, float64(1.23456789e+09)) } + +// scan into custom base types should be equivalent to the scan into base go types +func TestScanIntoCustomBaseTypes(t *testing.T) { + + type MyUint8 uint8 + type MyUint16 uint16 + type MyUint32 uint32 + type MyInt16 int16 + type MyFloat32 float32 + type MyFloat64 float64 + type MyString string + type MyTime = time.Time + + type film struct { + FilmID MyUint16 `sql:"primary_key"` + Title MyString + Description *MyString + ReleaseYear *MyInt16 + LanguageID MyUint8 + OriginalLanguageID *MyUint8 + RentalDuration MyUint8 + RentalRate MyFloat32 + Length *MyUint32 + ReplacementCost MyFloat64 + Rating *model.FilmRating + SpecialFeatures *MyString + LastUpdate MyTime + } + + stmt := SELECT( + Film.AllColumns, + ).FROM( + Film, + ).ORDER_BY( + Film.FilmID.ASC(), + ).LIMIT(3) + + var films []model.Film + err := stmt.Query(db, &films) + require.NoError(t, err) + + var myFilms []film + err = stmt.Query(db, &myFilms) + require.NoError(t, err) + + require.Equal(t, testutils.ToJSON(films), testutils.ToJSON(myFilms)) +} diff --git a/tests/postgres/scan_test.go b/tests/postgres/scan_test.go index 61b7becc..30787090 100644 --- a/tests/postgres/scan_test.go +++ b/tests/postgres/scan_test.go @@ -786,6 +786,123 @@ func TestRowsScan(t *testing.T) { requireQueryLogged(t, stmt, 0) } +func TestScanNullColumn(t *testing.T) { + stmt := SELECT( + Address.AllColumns, + ).FROM( + Address, + ).WHERE( + Address.Address2.IS_NULL(), + ) + + var dest []model.Address + + err := stmt.Query(db, &dest) + require.NoError(t, err) + testutils.AssertJSON(t, dest, ` +[ + { + "AddressID": 1, + "Address": "47 MySakila Drive", + "Address2": null, + "District": "Alberta", + "CityID": 300, + "PostalCode": "", + "Phone": "", + "LastUpdate": "2006-02-15T09:45:30Z" + }, + { + "AddressID": 2, + "Address": "28 MySQL Boulevard", + "Address2": null, + "District": "QLD", + "CityID": 576, + "PostalCode": "", + "Phone": "", + "LastUpdate": "2006-02-15T09:45:30Z" + }, + { + "AddressID": 3, + "Address": "23 Workhaven Lane", + "Address2": null, + "District": "Alberta", + "CityID": 300, + "PostalCode": "", + "Phone": "14033335568", + "LastUpdate": "2006-02-15T09:45:30Z" + }, + { + "AddressID": 4, + "Address": "1411 Lillydale Drive", + "Address2": null, + "District": "QLD", + "CityID": 576, + "PostalCode": "", + "Phone": "6172235589", + "LastUpdate": "2006-02-15T09:45:30Z" + } +] +`) +} + +func TestRowsScanSetZeroValue(t *testing.T) { + stmt := SELECT( + Rental.AllColumns, + ).FROM( + Rental, + ).WHERE( + Rental.RentalID.IN(Int(16049), Int(15966)), + ).ORDER_BY( + Rental.RentalID.DESC(), + ) + + rows, err := stmt.Rows(context.Background(), db) + require.NoError(t, err) + + defer rows.Close() + + // destination object is used as destination for all rows scan. + // this tests checks that ReturnedDate is set to nil with the second call + // check qrm.setZeroValue + var dest model.Rental + + for rows.Next() { + err := rows.Scan(&dest) + require.NoError(t, err) + + if dest.RentalID == 16049 { + testutils.AssertJSON(t, dest, ` +{ + "RentalID": 16049, + "RentalDate": "2005-08-23T22:50:12Z", + "InventoryID": 2666, + "CustomerID": 393, + "ReturnDate": "2005-08-30T01:01:12Z", + "StaffID": 2, + "LastUpdate": "2006-02-16T02:30:53Z" +} +`) + } else { + testutils.AssertJSON(t, dest, ` +{ + "RentalID": 15966, + "RentalDate": "2006-02-14T15:16:03Z", + "InventoryID": 4472, + "CustomerID": 374, + "ReturnDate": null, + "StaffID": 1, + "LastUpdate": "2006-02-16T02:30:53Z" +} +`) + } + } + + err = rows.Close() + require.NoError(t, err) + err = rows.Err() + require.NoError(t, err) +} + func TestScanNumericToFloat(t *testing.T) { type Number struct { Float32 float32 @@ -826,6 +943,54 @@ func TestScanNumericToIntegerError(t *testing.T) { } +func TestScanIntoCustomBaseTypes(t *testing.T) { + + type MyUint8 uint8 + type MyUint16 uint16 + type MyUint32 uint32 + type MyInt16 int16 + type MyFloat32 float32 + type MyFloat64 float64 + type MyString string + type MyTime = time.Time + + type film struct { + FilmID MyUint16 `sql:"primary_key"` + Title MyString + Description *MyString + ReleaseYear *MyInt16 + LanguageID MyUint8 + RentalDuration MyUint8 + RentalRate MyFloat32 + Length *MyUint32 + ReplacementCost MyFloat64 + Rating *model.MpaaRating + LastUpdate MyTime + SpecialFeatures *MyString + Fulltext MyString + } + + stmt := SELECT( + Film.AllColumns, + ).FROM( + Film, + ).ORDER_BY( + Film.FilmID.ASC(), + ).LIMIT(3) + + var films []model.Film + + err := stmt.Query(db, &films) + require.NoError(t, err) + + var myFilms []film + + err = stmt.Query(db, &myFilms) + require.NoError(t, err) + + require.Equal(t, testutils.ToJSON(films), testutils.ToJSON(myFilms)) +} + // QueryContext panic when the scanned value is nil and the destination is a slice of primitive // https://github.com/go-jet/jet/issues/91 func TestScanToPrimitiveElementsSlice(t *testing.T) { diff --git a/tests/postgres/select_test.go b/tests/postgres/select_test.go index b3d3e63d..e95d92ad 100644 --- a/tests/postgres/select_test.go +++ b/tests/postgres/select_test.go @@ -2521,6 +2521,79 @@ func TestRecursionScanNx1(t *testing.T) { }) } +type StoreInfo struct { + model.Store + + Staffs ManagerInfo +} + +type ManagerInfo struct { + model.Staff + Store *StoreInfo +} + +func TestRecursionScan1x1(t *testing.T) { + + stmt := SELECT( + Store.AllColumns, + Staff.AllColumns, + ).FROM( + Store. + INNER_JOIN(Staff, Staff.StaffID.EQ(Store.ManagerStaffID)), + ).ORDER_BY( + Store.StoreID, + ) + + var dest []StoreInfo + + err := stmt.Query(db, &dest) + require.NoError(t, err) + testutils.AssertJSON(t, dest, ` +[ + { + "StoreID": 1, + "ManagerStaffID": 1, + "AddressID": 1, + "LastUpdate": "2006-02-15T09:57:12Z", + "Staffs": { + "StaffID": 1, + "FirstName": "Mike", + "LastName": "Hillyer", + "AddressID": 3, + "Email": "Mike.Hillyer@sakilastaff.com", + "StoreID": 1, + "Active": true, + "Username": "Mike", + "Password": "8cb2237d0679ca88db6464eac60da96345513964", + "LastUpdate": "2006-05-16T16:13:11.79328Z", + "Picture": "iVBORw0KWgo=", + "Store": null + } + }, + { + "StoreID": 2, + "ManagerStaffID": 2, + "AddressID": 2, + "LastUpdate": "2006-02-15T09:57:12Z", + "Staffs": { + "StaffID": 2, + "FirstName": "Jon", + "LastName": "Stephens", + "AddressID": 4, + "Email": "Jon.Stephens@sakilastaff.com", + "StoreID": 2, + "Active": true, + "Username": "Jon", + "Password": "8cb2237d0679ca88db6464eac60da96345513964", + "LastUpdate": "2006-05-16T16:13:11.79328Z", + "Picture": null, + "Store": null + } + } +] +`) +} + // In parameterized statements integer literals, like Int(num), are replaced with a placeholders. For some expressions, // postgres interpreter will not have enough information to deduce the type. If this is the case postgres returns an error. // Int8, Int16, .... functions will add automatic type cast over placeholder, so type deduction is always possible. diff --git a/tests/postgres/with_test.go b/tests/postgres/with_test.go index 0b47e9aa..e6d23ee8 100644 --- a/tests/postgres/with_test.go +++ b/tests/postgres/with_test.go @@ -2,7 +2,6 @@ package postgres import ( "context" - "fmt" "github.com/go-jet/jet/v2/internal/testutils" . "github.com/go-jet/jet/v2/postgres" "github.com/go-jet/jet/v2/tests/.gentestdata/jetdb/northwind/model" @@ -864,5 +863,4 @@ WHERE orders1."orders.order_id" < $1; err := stmt.Query(db, &dest) require.NoError(t, err) require.Len(t, dest, 72) - fmt.Println(len(dest)) } From dc557390b0186018303718e8ca25a97eee556b56 Mon Sep 17 00:00:00 2001 From: go-jet Date: Wed, 9 Feb 2022 12:41:58 +0100 Subject: [PATCH 3/5] Test fix. --- internal/testutils/test_utils.go | 3 ++- tests/mysql/select_test.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/testutils/test_utils.go b/internal/testutils/test_utils.go index cac4a623..37c1665a 100644 --- a/internal/testutils/test_utils.go +++ b/internal/testutils/test_utils.go @@ -67,7 +67,8 @@ func AssertJSON(t *testing.T, data interface{}, expectedJSON string) { jsonData, err := json.MarshalIndent(data, "", "\t") require.NoError(t, err) - require.Equal(t, "\n"+string(jsonData)+"\n", expectedJSON) + dataJson := "\n" + string(jsonData) + "\n" + require.Equal(t, dataJson, expectedJSON) } // SaveJSONFile saves v as json at testRelativePath diff --git a/tests/mysql/select_test.go b/tests/mysql/select_test.go index 8bd028ab..024d9417 100644 --- a/tests/mysql/select_test.go +++ b/tests/mysql/select_test.go @@ -206,7 +206,7 @@ GROUP BY payment.customer_id; "RentalID": null, "Amount": 0, "PaymentDate": "0001-01-01T00:00:00Z", - "LastUpdate": null, + "LastUpdate": "0001-01-01T00:00:00Z", "Count": 8, "Sum": 38.92, "Avg": 4.865, From 8ffbe38993af6b455a7fa90126cacb635c1fc99f Mon Sep 17 00:00:00 2001 From: go-jet Date: Fri, 11 Feb 2022 12:49:54 +0100 Subject: [PATCH 4/5] Update Readme. --- README.md | 12 +- examples/quick-start/dest.json | 374 ++++++++++++++-------------- examples/quick-start/quick-start.go | 2 +- 3 files changed, 194 insertions(+), 194 deletions(-) diff --git a/README.md b/README.md index b53dfe1a..a6288f30 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ https://medium.com/@go.jet/jet-5f3667efa0cc * [WITH](https://github.com/go-jet/jet/wiki/WITH) 2) Auto-generated Data Model types - Go types mapped to database type (table, view or enum), used to store - result of database queries. Can be combined to create desired query result destination. + result of database queries. Can be combined to create complex query result destination. 3) Query execution with result mapping to arbitrary destination. ## Getting Started @@ -164,11 +164,11 @@ import ( ``` Let's say we want to retrieve the list of all _actors_ that acted in _films_ longer than 180 minutes, _film language_ is 'English' and _film category_ is not 'Action'. -```java +```golang stmt := SELECT( Actor.ActorID, Actor.FirstName, Actor.LastName, Actor.LastUpdate, // or just Actor.AllColumns Film.AllColumns, - Language.AllColumns, + Language.AllColumns.Except(Language.LastUpdate), Category.AllColumns, ).FROM( Actor. @@ -358,7 +358,7 @@ fmt.Println(string(jsonText)) "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -393,7 +393,7 @@ fmt.Println(string(jsonText)) "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -580,5 +580,5 @@ To run the tests, additional dependencies are required: ## License -Copyright 2019-2021 Goran Bjelanovic +Copyright 2019-2022 Goran Bjelanovic Licensed under the Apache License, Version 2.0. diff --git a/examples/quick-start/dest.json b/examples/quick-start/dest.json index 050a6fb4..d23e5881 100644 --- a/examples/quick-start/dest.json +++ b/examples/quick-start/dest.json @@ -22,7 +22,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -57,7 +57,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -92,7 +92,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -127,7 +127,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -154,7 +154,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -181,7 +181,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -208,7 +208,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -243,7 +243,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -270,7 +270,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -305,7 +305,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -340,7 +340,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -367,7 +367,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -402,7 +402,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -429,7 +429,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -464,7 +464,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -499,7 +499,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -526,7 +526,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -553,7 +553,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -588,7 +588,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -623,7 +623,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -658,7 +658,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -693,7 +693,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -720,7 +720,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -755,7 +755,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -782,7 +782,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -817,7 +817,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -852,7 +852,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -879,7 +879,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -914,7 +914,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -949,7 +949,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -984,7 +984,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1019,7 +1019,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1046,7 +1046,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1081,7 +1081,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1116,7 +1116,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1151,7 +1151,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1186,7 +1186,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1221,7 +1221,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1248,7 +1248,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1283,7 +1283,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1318,7 +1318,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1345,7 +1345,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1380,7 +1380,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1407,7 +1407,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1434,7 +1434,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1469,7 +1469,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1504,7 +1504,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1539,7 +1539,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1574,7 +1574,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1609,7 +1609,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1644,7 +1644,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1679,7 +1679,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1714,7 +1714,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1741,7 +1741,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1776,7 +1776,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1803,7 +1803,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1838,7 +1838,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1865,7 +1865,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1900,7 +1900,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1927,7 +1927,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1954,7 +1954,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -1989,7 +1989,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2024,7 +2024,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2059,7 +2059,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2094,7 +2094,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2129,7 +2129,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2164,7 +2164,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2199,7 +2199,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2234,7 +2234,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2269,7 +2269,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2304,7 +2304,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2331,7 +2331,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2366,7 +2366,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2393,7 +2393,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2428,7 +2428,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2463,7 +2463,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2498,7 +2498,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2533,7 +2533,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2568,7 +2568,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2595,7 +2595,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2622,7 +2622,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2657,7 +2657,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2684,7 +2684,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2719,7 +2719,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2754,7 +2754,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2789,7 +2789,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2816,7 +2816,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2851,7 +2851,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2878,7 +2878,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2913,7 +2913,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2948,7 +2948,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -2975,7 +2975,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3010,7 +3010,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3045,7 +3045,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3080,7 +3080,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3107,7 +3107,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3134,7 +3134,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3169,7 +3169,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3204,7 +3204,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3239,7 +3239,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3274,7 +3274,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3309,7 +3309,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3344,7 +3344,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3379,7 +3379,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3414,7 +3414,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3441,7 +3441,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3476,7 +3476,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3511,7 +3511,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3538,7 +3538,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3573,7 +3573,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3608,7 +3608,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3643,7 +3643,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3678,7 +3678,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3713,7 +3713,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3748,7 +3748,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3783,7 +3783,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3818,7 +3818,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3853,7 +3853,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3880,7 +3880,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3915,7 +3915,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3942,7 +3942,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3969,7 +3969,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -3996,7 +3996,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4031,7 +4031,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4066,7 +4066,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4101,7 +4101,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4136,7 +4136,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4163,7 +4163,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4198,7 +4198,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4225,7 +4225,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4260,7 +4260,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4295,7 +4295,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4322,7 +4322,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4349,7 +4349,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4384,7 +4384,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4411,7 +4411,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4438,7 +4438,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4465,7 +4465,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4500,7 +4500,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4535,7 +4535,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4570,7 +4570,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4605,7 +4605,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4632,7 +4632,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4659,7 +4659,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4694,7 +4694,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4729,7 +4729,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4764,7 +4764,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4799,7 +4799,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4826,7 +4826,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4861,7 +4861,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4896,7 +4896,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4923,7 +4923,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4950,7 +4950,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -4985,7 +4985,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5020,7 +5020,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5055,7 +5055,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5082,7 +5082,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5117,7 +5117,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5152,7 +5152,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5187,7 +5187,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5222,7 +5222,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5249,7 +5249,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5284,7 +5284,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5311,7 +5311,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5346,7 +5346,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5381,7 +5381,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5416,7 +5416,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5443,7 +5443,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5478,7 +5478,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5505,7 +5505,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5540,7 +5540,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5575,7 +5575,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5610,7 +5610,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5645,7 +5645,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5680,7 +5680,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5715,7 +5715,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5742,7 +5742,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5777,7 +5777,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5804,7 +5804,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5839,7 +5839,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5874,7 +5874,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5909,7 +5909,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5936,7 +5936,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5963,7 +5963,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -5990,7 +5990,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -6017,7 +6017,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { @@ -6052,7 +6052,7 @@ "Language": { "LanguageID": 1, "Name": "English ", - "LastUpdate": "2006-02-15T10:02:19Z" + "LastUpdate": "0001-01-01T00:00:00Z" }, "Categories": [ { diff --git a/examples/quick-start/quick-start.go b/examples/quick-start/quick-start.go index 5bdc424c..e453f51b 100644 --- a/examples/quick-start/quick-start.go +++ b/examples/quick-start/quick-start.go @@ -36,7 +36,7 @@ func main() { stmt := SELECT( Actor.ActorID, Actor.FirstName, Actor.LastName, Actor.LastUpdate, Film.AllColumns, - Language.AllColumns, + Language.AllColumns.Except(Language.LastUpdate), Category.AllColumns, ).FROM( Actor. From 9f91fd705aae7864b635bfea8beecb56fa68216c Mon Sep 17 00:00:00 2001 From: go-jet Date: Fri, 11 Feb 2022 13:09:49 +0100 Subject: [PATCH 5/5] Global `AND` and `OR` functions for better indentation of a complex condition in the Go code and in the generated SQL. --- internal/jet/clause.go | 4 +- internal/jet/expression.go | 63 +++++++++++- internal/jet/func_expression.go | 12 +++ internal/jet/func_expression_test.go | 22 ++++ internal/jet/sql_builder.go | 1 + internal/jet/utils.go | 11 ++ mysql/functions.go | 9 ++ mysql/select_statement_test.go | 8 +- postgres/functions.go | 9 ++ sqlite/functions.go | 9 ++ sqlite/select_statement_test.go | 8 +- tests/mysql/with_test.go | 6 +- tests/postgres/chinook_db_test.go | 146 +++++++++++++++++++++++++++ tests/postgres/delete_test.go | 14 ++- tests/postgres/select_test.go | 22 ++-- tests/postgres/with_test.go | 26 ++--- tests/sqlite/with_test.go | 12 +-- 17 files changed, 338 insertions(+), 44 deletions(-) diff --git a/internal/jet/clause.go b/internal/jet/clause.go index ce99a6b5..aa450055 100644 --- a/internal/jet/clause.go +++ b/internal/jet/clause.go @@ -98,9 +98,9 @@ func (c *ClauseWhere) Serialize(statementType StatementType, out *SQLBuilder, op } out.WriteString("WHERE") - out.IncreaseIdent() + out.IncreaseIdent(6) c.Condition.serialize(statementType, out, NoWrap.WithFallTrough(options)...) - out.DecreaseIdent() + out.DecreaseIdent(6) } // ClauseGroupBy struct diff --git a/internal/jet/expression.go b/internal/jet/expression.go index e657f30a..0fe78df0 100644 --- a/internal/jet/expression.go +++ b/internal/jet/expression.go @@ -123,6 +123,65 @@ func (c *binaryOperatorExpression) serialize(statement StatementType, out *SQLBu } } +type expressionListOperator struct { + ExpressionInterfaceImpl + + operator string + expressions []Expression +} + +func newExpressionListOperator(operator string, expressions ...Expression) *expressionListOperator { + ret := &expressionListOperator{ + operator: operator, + expressions: expressions, + } + + ret.ExpressionInterfaceImpl.Parent = ret + + return ret +} + +func newBoolExpressionListOperator(operator string, expressions ...BoolExpression) BoolExpression { + return BoolExp(newExpressionListOperator(operator, BoolExpressionListToExpressionList(expressions)...)) +} + +func (elo *expressionListOperator) serialize(statement StatementType, out *SQLBuilder, options ...SerializeOption) { + if len(elo.expressions) == 0 { + panic("jet: syntax error, expression list empty") + } + + shouldWrap := len(elo.expressions) > 1 + if shouldWrap { + out.WriteByte('(') + out.IncreaseIdent(tabSize) + out.NewLine() + } + + for i, expression := range elo.expressions { + if i == 1 { + out.IncreaseIdent(tabSize) + } + if i > 0 { + out.NewLine() + out.WriteString(elo.operator) + } + + out.IncreaseIdent(len(elo.operator) + 1) + expression.serialize(statement, out, FallTrough(options)...) + out.DecreaseIdent(len(elo.operator) + 1) + } + + if len(elo.expressions) > 1 { + out.DecreaseIdent(tabSize) + } + + if shouldWrap { + out.DecreaseIdent(tabSize) + out.NewLine() + out.WriteByte(')') + } +} + // A prefix operator Expression type prefixExpression struct { ExpressionInterfaceImpl @@ -209,8 +268,8 @@ type complexExpression struct { expressions Expression } -func complexExpr(expressions Expression) Expression { - complexExpression := &complexExpression{expressions: expressions} +func complexExpr(expression Expression) Expression { + complexExpression := &complexExpression{expressions: expression} complexExpression.ExpressionInterfaceImpl.Parent = complexExpression return complexExpression diff --git a/internal/jet/func_expression.go b/internal/jet/func_expression.go index 3e40edfe..cfac71f8 100644 --- a/internal/jet/func_expression.go +++ b/internal/jet/func_expression.go @@ -1,5 +1,17 @@ package jet +// AND function adds AND operator between expressions. This function can be used, instead of method AND, +// to have a better inlining of a complex condition in the Go code and in the generated SQL. +func AND(expressions ...BoolExpression) BoolExpression { + return newBoolExpressionListOperator("AND", expressions...) +} + +// OR function adds OR operator between expressions. This function can be used, instead of method OR, +// to have a better inlining of a complex condition in the Go code and in the generated SQL. +func OR(expressions ...BoolExpression) BoolExpression { + return newBoolExpressionListOperator("OR", expressions...) +} + // ROW is construct one table row from list of expressions. func ROW(expressions ...Expression) Expression { return NewFunc("ROW", expressions, nil) diff --git a/internal/jet/func_expression_test.go b/internal/jet/func_expression_test.go index 264be956..048ade29 100644 --- a/internal/jet/func_expression_test.go +++ b/internal/jet/func_expression_test.go @@ -4,6 +4,28 @@ import ( "testing" ) +func TestAND(t *testing.T) { + assertClauseSerializeErr(t, AND(), "jet: syntax error, expression list empty") + assertClauseSerialize(t, AND(table1ColInt.IS_NULL()), `table1.col_int IS NULL`) // IS NULL doesn't add parenthesis + assertClauseSerialize(t, AND(table1ColInt.LT(Int(11))), `(table1.col_int < $1)`, int64(11)) + assertClauseSerialize(t, AND(table1ColInt.GT(Int(11)), table1ColFloat.EQ(Float(0))), + `( + (table1.col_int > $1) + AND (table1.col_float = $2) +)`, int64(11), 0.0) +} + +func TestOR(t *testing.T) { + assertClauseSerializeErr(t, OR(), "jet: syntax error, expression list empty") + assertClauseSerialize(t, OR(table1ColInt.IS_NULL()), `table1.col_int IS NULL`) // IS NULL doesn't add parenthesis + assertClauseSerialize(t, OR(table1ColInt.LT(Int(11))), `(table1.col_int < $1)`, int64(11)) + assertClauseSerialize(t, OR(table1ColInt.GT(Int(11)), table1ColFloat.EQ(Float(0))), + `( + (table1.col_int > $1) + OR (table1.col_float = $2) +)`, int64(11), 0.0) +} + func TestFuncAVG(t *testing.T) { assertClauseSerialize(t, AVG(table1ColFloat), "AVG(table1.col_float)") assertClauseSerialize(t, AVG(table1ColInt), "AVG(table1.col_int)") diff --git a/internal/jet/sql_builder.go b/internal/jet/sql_builder.go index 6241feec..e3fb61b2 100644 --- a/internal/jet/sql_builder.go +++ b/internal/jet/sql_builder.go @@ -26,6 +26,7 @@ type SQLBuilder struct { Debug bool } +const tabSize = 4 const defaultIdent = 5 // IncreaseIdent adds ident or defaultIdent number of spaces to each new line diff --git a/internal/jet/utils.go b/internal/jet/utils.go index 524c2c53..4ab3fae9 100644 --- a/internal/jet/utils.go +++ b/internal/jet/utils.go @@ -113,6 +113,17 @@ func ExpressionListToSerializerList(expressions []Expression) []Serializer { return ret } +// BoolExpressionListToExpressionList converts list of bool expressions to list of expressions +func BoolExpressionListToExpressionList(expressions []BoolExpression) []Expression { + var ret []Expression + + for _, expression := range expressions { + ret = append(ret, expression) + } + + return ret +} + // ColumnListToProjectionList func func ColumnListToProjectionList(columns []ColumnExpression) []Projection { var ret []Projection diff --git a/mysql/functions.go b/mysql/functions.go index 6d8193d9..b794ef70 100644 --- a/mysql/functions.go +++ b/mysql/functions.go @@ -2,6 +2,15 @@ package mysql import "github.com/go-jet/jet/v2/internal/jet" +// This functions can be used, instead of its method counterparts, to have a better indentation of a complex condition +// in the Go code and in the generated SQL. +var ( + // AND function adds AND operator between expressions. + AND = jet.AND + // OR function adds OR operator between expressions. + OR = jet.OR +) + // ROW is construct one table row from list of expressions. var ROW = jet.ROW diff --git a/mysql/select_statement_test.go b/mysql/select_statement_test.go index 37827d5f..bd3a0e9c 100644 --- a/mysql/select_statement_test.go +++ b/mysql/select_statement_test.go @@ -148,9 +148,9 @@ func TestSelect_NOT_EXISTS(t *testing.T) { SELECT table1.col_int AS "table1.col_int" FROM db.table1 WHERE NOT (EXISTS ( - SELECT table2.col_int AS "table2.col_int" - FROM db.table2 - WHERE table1.col_int = table2.col_int - )); + SELECT table2.col_int AS "table2.col_int" + FROM db.table2 + WHERE table1.col_int = table2.col_int + )); `) } diff --git a/postgres/functions.go b/postgres/functions.go index a20d1e11..cd2c130a 100644 --- a/postgres/functions.go +++ b/postgres/functions.go @@ -2,6 +2,15 @@ package postgres import "github.com/go-jet/jet/v2/internal/jet" +// This functions can be used, instead of its method counterparts, to have a better indentation of a complex condition +// in the Go code and in the generated SQL. +var ( + // AND function adds AND operator between expressions. + AND = jet.AND + // OR function adds OR operator between expressions. + OR = jet.OR +) + // ROW is construct one table row from list of expressions. var ROW = jet.ROW diff --git a/sqlite/functions.go b/sqlite/functions.go index 2b70714b..d7142747 100644 --- a/sqlite/functions.go +++ b/sqlite/functions.go @@ -6,6 +6,15 @@ import ( "time" ) +// This functions can be used, instead of its method counterparts, to have a better indentation of a complex condition +// in the Go code and in the generated SQL. +var ( + // AND function adds AND operator between expressions. + AND = jet.AND + // OR function adds OR operator between expressions. + OR = jet.OR +) + // ROW is construct one table row from list of expressions. func ROW(expressions ...Expression) Expression { return jet.NewFunc("", expressions, nil) diff --git a/sqlite/select_statement_test.go b/sqlite/select_statement_test.go index a42fe06d..5c4f9c3c 100644 --- a/sqlite/select_statement_test.go +++ b/sqlite/select_statement_test.go @@ -148,9 +148,9 @@ func TestSelect_NOT_EXISTS(t *testing.T) { SELECT table1.col_int AS "table1.col_int" FROM db.table1 WHERE NOT (EXISTS ( - SELECT table2.col_int AS "table2.col_int" - FROM db.table2 - WHERE table1.col_int = table2.col_int - )); + SELECT table2.col_int AS "table2.col_int" + FROM db.table2 + WHERE table1.col_int = table2.col_int + )); `) } diff --git a/tests/mysql/with_test.go b/tests/mysql/with_test.go index cc8dfd6f..d7d8d3d7 100644 --- a/tests/mysql/with_test.go +++ b/tests/mysql/with_test.go @@ -165,9 +165,9 @@ WITH payments_to_delete AS ( ) DELETE FROM dvds.payment WHERE payment.payment_id IN ( - SELECT payments_to_delete.''payment.payment_id'' AS "payment.payment_id" - FROM payments_to_delete - ); + SELECT payments_to_delete.''payment.payment_id'' AS "payment.payment_id" + FROM payments_to_delete + ); `, "''", "`")) tx, err := db.Begin() diff --git a/tests/postgres/chinook_db_test.go b/tests/postgres/chinook_db_test.go index 684abc2c..2d9821cb 100644 --- a/tests/postgres/chinook_db_test.go +++ b/tests/postgres/chinook_db_test.go @@ -38,6 +38,152 @@ ORDER BY "Album"."AlbumId" ASC; requireQueryLogged(t, stmt, 347) } +func TestComplex_AND_OR(t *testing.T) { + stmt := SELECT( + Artist.AllColumns, + Album.AllColumns, + Track.AllColumns, + ).FROM( + Artist. + LEFT_JOIN(Album, Artist.ArtistId.EQ(Album.ArtistId)). + LEFT_JOIN(Track, Track.AlbumId.EQ(Album.AlbumId)), + ).WHERE( + AND( + Artist.ArtistId.BETWEEN(Int(5), Int(11)), + Album.AlbumId.GT_EQ(Int(7)), + Track.TrackId.GT(Int(74)), + OR( + Track.GenreId.EQ(Int(2)), + Track.UnitPrice.GT(Float(1.01)), + ), + Track.TrackId.LT(Int(125)), + ), + ).ORDER_BY( + Artist.ArtistId, + Album.AlbumId, + Track.TrackId, + ) + + testutils.AssertDebugStatementSql(t, stmt, ` +SELECT "Artist"."ArtistId" AS "Artist.ArtistId", + "Artist"."Name" AS "Artist.Name", + "Album"."AlbumId" AS "Album.AlbumId", + "Album"."Title" AS "Album.Title", + "Album"."ArtistId" AS "Album.ArtistId", + "Track"."TrackId" AS "Track.TrackId", + "Track"."Name" AS "Track.Name", + "Track"."AlbumId" AS "Track.AlbumId", + "Track"."MediaTypeId" AS "Track.MediaTypeId", + "Track"."GenreId" AS "Track.GenreId", + "Track"."Composer" AS "Track.Composer", + "Track"."Milliseconds" AS "Track.Milliseconds", + "Track"."Bytes" AS "Track.Bytes", + "Track"."UnitPrice" AS "Track.UnitPrice" +FROM chinook."Artist" + LEFT JOIN chinook."Album" ON ("Artist"."ArtistId" = "Album"."ArtistId") + LEFT JOIN chinook."Track" ON ("Track"."AlbumId" = "Album"."AlbumId") +WHERE ( + ("Artist"."ArtistId" BETWEEN 5 AND 11) + AND ("Album"."AlbumId" >= 7) + AND ("Track"."TrackId" > 74) + AND ( + ("Track"."GenreId" = 2) + OR ("Track"."UnitPrice" > 1.01) + ) + AND ("Track"."TrackId" < 125) + ) +ORDER BY "Artist"."ArtistId", "Album"."AlbumId", "Track"."TrackId"; +`) + + var dest []struct { + model.Artist + + Albums []struct { + model.Album + + Tracks []model.Track + } + } + + err := stmt.Query(db, &dest) + require.NoError(t, err) + + testutils.AssertJSON(t, dest, ` +[ + { + "ArtistId": 6, + "Name": "Ant�nio Carlos Jobim", + "Albums": [ + { + "AlbumId": 8, + "Title": "Warner 25 Anos", + "ArtistId": 6, + "Tracks": [ + { + "TrackId": 75, + "Name": "O Boto (B�to)", + "AlbumId": 8, + "MediaTypeId": 1, + "GenreId": 2, + "Composer": null, + "Milliseconds": 366837, + "Bytes": 12089673, + "UnitPrice": 0.99 + }, + { + "TrackId": 76, + "Name": "Canta, Canta Mais", + "AlbumId": 8, + "MediaTypeId": 1, + "GenreId": 2, + "Composer": null, + "Milliseconds": 271856, + "Bytes": 8719426, + "UnitPrice": 0.99 + } + ] + } + ] + }, + { + "ArtistId": 10, + "Name": "Billy Cobham", + "Albums": [ + { + "AlbumId": 13, + "Title": "The Best Of Billy Cobham", + "ArtistId": 10, + "Tracks": [ + { + "TrackId": 123, + "Name": "Quadrant", + "AlbumId": 13, + "MediaTypeId": 1, + "GenreId": 2, + "Composer": "Billy Cobham", + "Milliseconds": 261851, + "Bytes": 8538199, + "UnitPrice": 0.99 + }, + { + "TrackId": 124, + "Name": "Snoopy's search-Red baron", + "AlbumId": 13, + "MediaTypeId": 1, + "GenreId": 2, + "Composer": "Billy Cobham", + "Milliseconds": 456071, + "Bytes": 15075616, + "UnitPrice": 0.99 + } + ] + } + ] + } +] +`) +} + func TestJoinEverything(t *testing.T) { manager := Employee.AS("Manager") diff --git a/tests/postgres/delete_test.go b/tests/postgres/delete_test.go index 47637e1e..abbb3449 100644 --- a/tests/postgres/delete_test.go +++ b/tests/postgres/delete_test.go @@ -124,9 +124,11 @@ func TestDeleteFrom(t *testing.T) { table.Actor, ). WHERE( - table.Staff.StaffID.EQ(table.Rental.StaffID). - AND(table.Staff.StaffID.EQ(Int(2))). - AND(table.Rental.RentalID.LT(Int(10))), + AND( + table.Staff.StaffID.EQ(table.Rental.StaffID), + table.Store.StoreID.EQ(Int(2)), + table.Rental.RentalID.LT(Int(10)), + ), ). RETURNING( table.Rental.AllColumns, @@ -138,7 +140,11 @@ DELETE FROM dvds.rental USING dvds.staff INNER JOIN dvds.store ON (store.store_id = staff.staff_id), dvds.actor -WHERE ((staff.staff_id = rental.staff_id) AND (staff.staff_id = $1)) AND (rental.rental_id < $2) +WHERE ( + (staff.staff_id = rental.staff_id) + AND (store.store_id = $1) + AND (rental.rental_id < $2) + ) RETURNING rental.rental_id AS "rental.rental_id", rental.rental_date AS "rental.rental_date", rental.inventory_id AS "rental.inventory_id", diff --git a/tests/postgres/select_test.go b/tests/postgres/select_test.go index e95d92ad..304acb8c 100644 --- a/tests/postgres/select_test.go +++ b/tests/postgres/select_test.go @@ -395,8 +395,15 @@ func TestExecution1(t *testing.T) { Customer.CustomerID, Customer.LastName, ). - WHERE(City.City.EQ(String("London")).OR(City.City.EQ(String("York")))). - ORDER_BY(City.CityID, Address.AddressID, Customer.CustomerID) + WHERE( + OR( + City.City.EQ(String("London")), + City.City.EQ(String("York")), + ), + ). + ORDER_BY( + City.CityID, Address.AddressID, Customer.CustomerID, + ) testutils.AssertDebugStatementSql(t, stmt, ` SELECT city.city_id AS "city.city_id", @@ -408,7 +415,10 @@ SELECT city.city_id AS "city.city_id", FROM dvds.city INNER JOIN dvds.address ON (address.city_id = city.city_id) INNER JOIN dvds.customer ON (customer.address_id = address.address_id) -WHERE (city.city = 'London') OR (city.city = 'York') +WHERE ( + (city.city = 'London') + OR (city.city = 'York') + ) ORDER BY city.city_id, address.address_id, customer.customer_id; `, "London", "York") @@ -1073,9 +1083,9 @@ SELECT film.film_id AS "film.film_id", film.fulltext AS "film.fulltext" FROM dvds.film WHERE film.rental_rate = ( - SELECT MAX(film.rental_rate) - FROM dvds.film - ) + SELECT MAX(film.rental_rate) + FROM dvds.film + ) ORDER BY film.film_id ASC; ` diff --git a/tests/postgres/with_test.go b/tests/postgres/with_test.go index e6d23ee8..c78ca8a4 100644 --- a/tests/postgres/with_test.go +++ b/tests/postgres/with_test.go @@ -73,9 +73,9 @@ WITH regional_sales AS ( SELECT regional_sales."orders.ship_region" AS "orders.ship_region" FROM regional_sales WHERE regional_sales.total_sales > (( - SELECT SUM(regional_sales.total_sales) - FROM regional_sales - ) / 50) + SELECT SUM(regional_sales.total_sales) + FROM regional_sales + ) / 50) ) SELECT orders.ship_region AS "orders.ship_region", order_details.product_id AS "order_details.product_id", @@ -84,9 +84,9 @@ SELECT orders.ship_region AS "orders.ship_region", FROM northwind.orders INNER JOIN northwind.order_details ON (orders.order_id = order_details.order_id) WHERE orders.ship_region IN ( - SELECT top_region."orders.ship_region" AS "orders.ship_region" - FROM top_region - ) + SELECT top_region."orders.ship_region" AS "orders.ship_region" + FROM top_region + ) GROUP BY orders.ship_region, order_details.product_id ORDER BY SUM(order_details.quantity) DESC; `) @@ -150,18 +150,18 @@ func TestWithStatementDeleteAndInsert(t *testing.T) { WITH remove_discontinued_orders AS ( DELETE FROM northwind.order_details WHERE order_details.product_id IN ( - SELECT products.product_id AS "products.product_id" - FROM northwind.products - WHERE products.discontinued = $1 - ) + SELECT products.product_id AS "products.product_id" + FROM northwind.products + WHERE products.discontinued = $1 + ) RETURNING order_details.product_id AS "order_details.product_id" ),update_discontinued_price AS ( UPDATE northwind.products SET unit_price = $2 WHERE products.product_id IN ( - SELECT remove_discontinued_orders."order_details.product_id" AS "order_details.product_id" - FROM remove_discontinued_orders - ) + SELECT remove_discontinued_orders."order_details.product_id" AS "order_details.product_id" + FROM remove_discontinued_orders + ) RETURNING products.product_id AS "products.product_id", products.product_name AS "products.product_name", products.supplier_id AS "products.supplier_id", diff --git a/tests/sqlite/with_test.go b/tests/sqlite/with_test.go index 92cd331e..402df2f0 100644 --- a/tests/sqlite/with_test.go +++ b/tests/sqlite/with_test.go @@ -154,9 +154,9 @@ WITH payments_to_update AS ( UPDATE payment SET amount = 0 WHERE payment.payment_id IN ( - SELECT payments_to_update.''payment.payment_id'' AS "payment.payment_id" - FROM payments_to_update - ); + SELECT payments_to_update.''payment.payment_id'' AS "payment.payment_id" + FROM payments_to_update + ); `, "''", "`", -1)) tx := beginDBTx(t) @@ -206,9 +206,9 @@ WITH payments_to_delete AS ( ) DELETE FROM payment WHERE payment.payment_id IN ( - SELECT payments_to_delete.''payment.payment_id'' AS "payment.payment_id" - FROM payments_to_delete - ); + SELECT payments_to_delete.''payment.payment_id'' AS "payment.payment_id" + FROM payments_to_delete + ); `, "''", "`", -1)) tx := beginDBTx(t)