diff --git a/dialect_sqlite.go b/dialect_sqlite.go index 2769ae0..e3217ab 100644 --- a/dialect_sqlite.go +++ b/dialect_sqlite.go @@ -3,7 +3,9 @@ package lasebuche import ( "database/sql" "fmt" + "reflect" "strings" + "time" ) type SqliteDialect struct{} @@ -59,3 +61,229 @@ func (d *SqliteDialect) ColumnSpec(f dbfield) (string, error) { } return fmt.Sprintf("%s %s not null default %s", f.dbname, convertType(f), defaultValue(f)), nil } + +// ScanResult scans a single row into the provided fields slice. +// For SQLite, this handles time.Time fields which are returned as strings. +func (d *SqliteDialect) ScanResult(row interface{ Scan(dest ...any) error }, fields []any, fieldTypes []dbfield) error { + // SQLite returns time.Time as strings, so we need special handling + // Check if any field is a time type + hasTimeField := false + for _, f := range fieldTypes { + if f.stype == "time.Time" || f.stype == "*time.Time" { + hasTimeField = true + break + } + } + + if !hasTimeField { + // No time fields, use standard scan + return row.Scan(fields...) + } + + // We have time fields - need custom handling + // Create temp destinations for all fields + tempFields := make([]any, len(fields)) + fieldInfos := make([]struct { + index int + dest any + isTime bool + isTimePtr bool + }, len(fields)) + + for i, f := range fieldTypes { + isTime := f.stype == "time.Time" + isTimePtr := f.stype == "*time.Time" + + fieldInfos[i] = struct { + index int + dest any + isTime bool + isTimePtr bool + }{ + index: i, + dest: fields[i], + isTime: isTime, + isTimePtr: isTimePtr, + } + + if isTime || isTimePtr { + var s sql.NullString + tempFields[i] = &s + } else { + tempFields[i] = fields[i] + } + } + + if err := row.Scan(tempFields...); err != nil { + return err + } + + // Copy time values back to original destinations + for _, info := range fieldInfos { + if info.isTime || info.isTimePtr { + s := tempFields[info.index].(*sql.NullString) + if s.Valid { + // Try to parse the string as time + t, err := tryParseTime(s.String) + if err != nil { + return fmt.Errorf("failed to parse time field: %w", err) + } + + // Set the value in the original destination + destVal := reflect.ValueOf(info.dest).Elem() + if info.isTime { + // dest is *time.Time, set the value + destVal.Set(reflect.ValueOf(t)) + } else if info.isTimePtr { + // dest is **time.Time, allocate and set + ptr := reflect.New(destVal.Type().Elem()) + ptr.Elem().Set(reflect.ValueOf(t)) + destVal.Set(ptr) + } + } + } + } + + return nil +} + +// ScanRows scans multiple rows using the dialect-specific logic +func (d *SqliteDialect) ScanRows(rows *sql.Rows, fieldDest func() []any, fieldTypes []dbfield) ([]any, error) { + // Check if any field is a time type + hasTimeField := false + for _, f := range fieldTypes { + if f.stype == "time.Time" || f.stype == "*time.Time" { + hasTimeField = true + break + } + } + + if !hasTimeField { + // No time fields, use standard scan + var results []any + for rows.Next() { + fields := fieldDest() + if err := rows.Scan(fields...); err != nil { + return nil, err + } + results = append(results, fields...) + } + if err := rows.Err(); err != nil { + return nil, err + } + return results, nil + } + + // We have time fields - need custom handling + var results []any + for rows.Next() { + fields := fieldDest() + + // Create temp destinations for all fields + tempFields := make([]any, len(fields)) + fieldInfos := make([]struct { + index int + dest any + isTime bool + isTimePtr bool + }, len(fields)) + + for i, f := range fieldTypes { + isTime := f.stype == "time.Time" + isTimePtr := f.stype == "*time.Time" + + fieldInfos[i] = struct { + index int + dest any + isTime bool + isTimePtr bool + }{ + index: i, + dest: fields[i], + isTime: isTime, + isTimePtr: isTimePtr, + } + + if isTime || isTimePtr { + var s sql.NullString + tempFields[i] = &s + } else { + tempFields[i] = fields[i] + } + } + + if err := rows.Scan(tempFields...); err != nil { + return nil, err + } + + // Copy time values back to original destinations + for _, info := range fieldInfos { + if info.isTime || info.isTimePtr { + s := tempFields[info.index].(*sql.NullString) + if s.Valid { + t, err := tryParseTime(s.String) + if err != nil { + return nil, fmt.Errorf("failed to parse time field: %w", err) + } + + // Set the value in the original destination + destVal := reflect.ValueOf(info.dest).Elem() + if info.isTime { + // dest is *time.Time, set the value + destVal.Set(reflect.ValueOf(t)) + } else if info.isTimePtr { + // dest is **time.Time, allocate and set + ptr := reflect.New(destVal.Type().Elem()) + ptr.Elem().Set(reflect.ValueOf(t)) + destVal.Set(ptr) + } + } + } + } + + results = append(results, fields...) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return results, nil +} + +// tryParseTime attempts to parse a time string in various common formats +// SQLite driver returns time as "2006-01-02 15:04:05.999999999 -0700 MST m=+0.000000000" +// We need to strip the monotonic clock part (m=...) before parsing +func tryParseTime(s string) (time.Time, error) { + // Remove monotonic clock part if present + // Format: "2006-01-02 15:04:05.999999999 -0700 MST m=+0.000000000" + if idx := strings.Index(s, " m="); idx != -1 { + s = s[:idx] + } + + // Try common formats + formats := []string{ + time.RFC3339, + time.RFC3339Nano, + "2006-01-02T15:04:05Z07:00", + "2006-01-02 15:04:05.999999999-07:00", + "2006-01-02 15:04:05", + "2006-01-02T15:04:05", + "2006-01-02 15:04:05+00:00", + // Go's default time.String() format (without monotonic clock) + "2006-01-02 15:04:05.999999999 -0700 MST", + // SQLite without timezone + "2006-01-02 15:04:05.999999", + } + + var err error + var t time.Time + for _, format := range formats { + t, err = time.Parse(format, s) + if err == nil { + return t, nil + } + } + + return time.Time{}, fmt.Errorf("unable to parse time: %s", s) +} diff --git a/orm.go b/orm.go index b16b746..b70ffca 100644 --- a/orm.go +++ b/orm.go @@ -17,6 +17,9 @@ type dbfield struct { type Dialect interface { TableExists(db *sql.DB, tableName string) (bool, error) ColumnSpec(f dbfield) (string, error) + // ScanResult scans a single row into the provided destination slice + // The row parameter can be *sql.Row or *sql.Rows, both implement Scan(dest ...any) error + ScanResult(row interface{ Scan(dest ...any) error }, fields []any, fieldTypes []dbfield) error } type Table[T any] struct { @@ -89,27 +92,21 @@ func (t Table[T]) Get(id string) (*T, error) { } var obj T - // Use a slice of pointers to struct fields for scanning - // We need to pass pointers to each field in the order of the SQL columns + // Create field destinations ptrVal := reflect.ValueOf(&obj).Elem() fields := make([]any, len(t.fields)) - for i, f := range t.fields { fieldValue := ptrVal.FieldByName(f.name) if !fieldValue.IsValid() { return nil, fmt.Errorf("field %s not found in struct", f.name) } - - // For all fields, pass the address so Scan can set the value - // For pointer fields (e.g., *string), this gives **T - // For value fields (e.g., string), this gives *T fields[i] = fieldValue.Addr().Interface() } - if err := row.Scan(fields...); err != nil { - // Handle time scanning manually for SQLite which returns dates as strings - if strings.Contains(err.Error(), "unsupported Scan, storing driver.Value type string into type *time.Time") { - return t.getWithTimeHandling(id) + // Use dialect to scan with proper type handling + if err := t.dialect.ScanResult(row, fields, t.fields); err != nil { + if err == sql.ErrNoRows { + return nil, sql.ErrNoRows } return nil, err } @@ -117,89 +114,6 @@ func (t Table[T]) Get(id string) (*T, error) { return &obj, nil } -func (t Table[T]) getWithTimeHandling(id string) (*T, error) { - row := t.db.QueryRow(t.selectbyid, id) - if row == nil { - return nil, sql.ErrNoRows - } - var obj T - - ptrVal := reflect.ValueOf(&obj).Elem() - - // Create a slice to hold all values, using string for time fields - tempFields := make([]any, len(t.fields)) - fieldInfos := make([]struct { - index int - fieldValue reflect.Value - isTime bool - isTimePtr bool - }, len(t.fields)) - - for i, f := range t.fields { - fieldValue := ptrVal.FieldByName(f.name) - if !fieldValue.IsValid() { - return nil, fmt.Errorf("field %s not found in struct", f.name) - } - - // Check if this is a time.Time or *time.Time field - isTime := fieldValue.Type() == reflect.TypeOf(time.Time{}) - isTimePtr := fieldValue.Kind() == reflect.Ptr && fieldValue.Type().Elem() == reflect.TypeOf(time.Time{}) - - fieldInfos[i] = struct { - index int - fieldValue reflect.Value - isTime bool - isTimePtr bool - }{ - index: i, - fieldValue: fieldValue, - isTime: isTime, - isTimePtr: isTimePtr, - } - - // For time fields, use a string to capture the raw value - if isTime || isTimePtr { - var s sql.NullString - tempFields[i] = &s - } else { - tempFields[i] = fieldValue.Addr().Interface() - } - } - - if err := row.Scan(tempFields...); err != nil { - return nil, err - } - - // Copy values back, parsing time fields - for _, info := range fieldInfos { - if info.isTime || info.isTimePtr { - s := tempFields[info.index].(*sql.NullString) - if s.Valid { - t, err := time.Parse(time.RFC3339, s.String) - if err != nil { - // Try other common formats - t, err = time.Parse("2006-01-02 15:04:05", s.String) - if err != nil { - t, err = time.Parse("2006-01-02T15:04:05Z", s.String) - if err != nil { - continue // Skip if we can't parse - } - } - } - if info.isTime { - info.fieldValue.Set(reflect.ValueOf(t)) - } else if info.isTimePtr { - ptr := reflect.New(info.fieldValue.Type().Elem()) - ptr.Elem().Set(reflect.ValueOf(t)) - info.fieldValue.Set(ptr) - } - } - } - } - - return &obj, nil -} - func (t Table[T]) SelectOne(where string, args ...any) (*T, error) { query := t.selectwhere + " where " + where + " limit 1" row := t.db.QueryRow(query, args...) @@ -210,7 +124,6 @@ func (t Table[T]) SelectOne(where string, args ...any) (*T, error) { var obj T ptrVal := reflect.ValueOf(&obj).Elem() fields := make([]any, len(t.fields)) - for i, f := range t.fields { fieldValue := ptrVal.FieldByName(f.name) if !fieldValue.IsValid() { @@ -219,9 +132,9 @@ func (t Table[T]) SelectOne(where string, args ...any) (*T, error) { fields[i] = fieldValue.Addr().Interface() } - if err := row.Scan(fields...); err != nil { - if strings.Contains(err.Error(), "unsupported Scan, storing driver.Value type string into type *time.Time") { - return t.selectOneWithTimeHandling(where, args...) + if err := t.dialect.ScanResult(row, fields, t.fields); err != nil { + if err == sql.ErrNoRows { + return nil, sql.ErrNoRows } return nil, err } @@ -229,16 +142,6 @@ func (t Table[T]) SelectOne(where string, args ...any) (*T, error) { return &obj, nil } -func (t Table[T]) selectOneWithTimeHandling(where string, args ...any) (*T, error) { - query := t.selectwhere + " where " + where + " limit 1" - row := t.db.QueryRow(query, args...) - if row == nil { - return nil, sql.ErrNoRows - } - - return t.scanWithTimeHandling(row) -} - func (t Table[T]) SelectWhere(where string, args ...any) ([]*T, error) { query := t.selectwhere if where != "" { @@ -265,12 +168,9 @@ func (t Table[T]) SelectWhere(where string, args ...any) ([]*T, error) { fields[i] = fieldValue.Addr().Interface() } - if err := rows.Scan(fields...); err != nil { - if strings.Contains(err.Error(), "unsupported Scan, storing driver.Value type string into type *time.Time") { - // Need to re-execute with time handling - rows.Close() - return t.selectWhereWithTimeHandling(where, args...) - } + // Use dialect to scan with proper type handling + // *sql.Rows implements Scanner interface + if err := t.dialect.ScanResult(rows, fields, t.fields); err != nil { return nil, err } results = append(results, &obj) @@ -283,135 +183,6 @@ func (t Table[T]) SelectWhere(where string, args ...any) ([]*T, error) { return results, nil } -func (t Table[T]) selectWhereWithTimeHandling(where string, args ...any) ([]*T, error) { - query := t.selectwhere - if where != "" { - query += " where " + where - } - - rows, err := t.db.Query(query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - var results []*T - - for rows.Next() { - obj, err := t.scanWithTimeHandling(rows) - if err != nil { - return nil, err - } - results = append(results, obj) - } - - if err := rows.Err(); err != nil { - return nil, err - } - - return results, nil -} - -func (t Table[T]) scanWithTimeHandling(scanner any) (*T, error) { - var obj T - - ptrVal := reflect.ValueOf(&obj).Elem() - - // Create a slice to hold all values, using string for time fields - tempFields := make([]any, len(t.fields)) - fieldInfos := make([]struct { - index int - fieldValue reflect.Value - isTime bool - isTimePtr bool - }, len(t.fields)) - - for i, f := range t.fields { - fieldValue := ptrVal.FieldByName(f.name) - if !fieldValue.IsValid() { - return nil, fmt.Errorf("field %s not found in struct", f.name) - } - - // Check if this is a time.Time or *time.Time field - isTime := fieldValue.Type() == reflect.TypeOf(time.Time{}) - var isTimePtr bool - if fieldValue.Kind() == reflect.Ptr { - isTimePtr = fieldValue.Type().Elem() == reflect.TypeOf(time.Time{}) - } - - fieldInfos[i] = struct { - index int - fieldValue reflect.Value - isTime bool - isTimePtr bool - }{ - index: i, - fieldValue: fieldValue, - isTime: isTime, - isTimePtr: isTimePtr, - } - - // For time fields, use a string to capture the raw value - if isTime || isTimePtr { - var s sql.NullString - tempFields[i] = &s - } else { - tempFields[i] = fieldValue.Addr().Interface() - } - } - - var err error - switch s := scanner.(type) { - case *sql.Row: - err = s.Scan(tempFields...) - case *sql.Rows: - err = s.Scan(tempFields...) - default: - return nil, fmt.Errorf("unsupported scanner type") - } - - if err != nil { - return nil, err - } - - // Copy values back, parsing time fields - for _, info := range fieldInfos { - if info.isTime || info.isTimePtr { - s := tempFields[info.index].(*sql.NullString) - if s.Valid { - t, err := time.Parse(time.RFC3339, s.String) - if err != nil { - // Try other common formats - t, err = time.Parse("2006-01-02 15:04:05", s.String) - if err != nil { - t, err = time.Parse("2006-01-02T15:04:05Z", s.String) - if err != nil { - // Try SQLite format: YYYY-MM-DD HH:MM:SS - t, err = time.Parse("2006-01-02 15:04:05.999999999-07:00", s.String) - if err != nil { - // Try simpler format - t, err = time.Parse("2006-01-02 15:04:05", s.String) - if err != nil { - continue - } - } - } - } - } - if info.isTime { - info.fieldValue.Set(reflect.ValueOf(t)) - } else if info.isTimePtr { - ptr := reflect.New(info.fieldValue.Type().Elem()) - ptr.Elem().Set(reflect.ValueOf(t)) - info.fieldValue.Set(ptr) - } - } - } - } - - return &obj, nil -} - func (t Table[T]) Delete(id string) error { _, err := t.db.Exec(t.deletebyid, id) return err