diff --git a/postgres_pgtype/date.go b/postgres_pgtype/date.go new file mode 100644 index 00000000..da9e2652 --- /dev/null +++ b/postgres_pgtype/date.go @@ -0,0 +1,356 @@ +// копия файла из https://github.com/jackc/pgtype/timestamptz.go +// чтоб не выдавала ошибку на null +// чтобы дата NULL = time.Time{} +package postgres_pgtype + +import ( + "database/sql/driver" + "encoding/binary" + "encoding/json" + "fmt" + "github.com/jackc/pgx/v5/pgtype" + "regexp" + "strconv" + "time" +) + +//type DateScanner interface { +// ScanDate(v Date) error +//} +// +//type DateValuer interface { +// DateValue() (Date, error) +//} + +type Date struct { + Time time.Time + InfinityModifier pgtype.InfinityModifier + Valid bool +} + +func (d *Date) ScanDate(v Date) error { + *d = v + return nil +} + +func (d Date) DateValue() (Date, error) { + return d, nil +} + +const ( + negativeInfinityDayOffset = -2147483648 + infinityDayOffset = 2147483647 +) + +// Scan implements the database/sql Scanner interface. +func (dst *Date) Scan(src any) error { + if src == nil { + *dst = Date{Valid: true} //sanek + //*dst = Date{} + return nil + } + + switch src := src.(type) { + case string: + return scanPlanTextAnyToDateScanner{}.Scan([]byte(src), dst) + case time.Time: + *dst = Date{Time: src, Valid: true} + return nil + } + + return fmt.Errorf("cannot scan %T", src) +} + +// Value implements the database/sql/driver Valuer interface. +func (src Date) Value() (driver.Value, error) { + if !src.Valid { + return nil, nil + } + + if src.InfinityModifier != pgtype.Finite { + return src.InfinityModifier.String(), nil + } + return src.Time, nil +} + +func (src Date) MarshalJSON() ([]byte, error) { + if !src.Valid { + return []byte("null"), nil + } + + var s string + + switch src.InfinityModifier { + case pgtype.Finite: + s = src.Time.Format("2006-01-02") + case pgtype.Infinity: + s = "infinity" + case pgtype.NegativeInfinity: + s = "-infinity" + } + + return json.Marshal(s) +} + +func (dst *Date) UnmarshalJSON(b []byte) error { + var s *string + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + if s == nil { + *dst = Date{} + return nil + } + + switch *s { + case "infinity": + *dst = Date{Valid: true, InfinityModifier: pgtype.Infinity} + case "-infinity": + *dst = Date{Valid: true, InfinityModifier: -pgtype.Infinity} + default: + t, err := time.ParseInLocation("2006-01-02", *s, time.UTC) + if err != nil { + return err + } + + *dst = Date{Time: t, Valid: true} + } + + return nil +} + +type DateCodec struct{} + +func (DateCodec) FormatSupported(format int16) bool { + return format == pgtype.TextFormatCode || format == pgtype.BinaryFormatCode +} + +func (DateCodec) PreferredFormat() int16 { + return pgtype.BinaryFormatCode +} + +func (DateCodec) PlanEncode(m *pgtype.Map, oid uint32, format int16, value any) pgtype.EncodePlan { + if _, ok := value.(pgtype.DateValuer); !ok { + return nil + } + + switch format { + case pgtype.BinaryFormatCode: + return encodePlanDateCodecBinary{} + case pgtype.TextFormatCode: + return encodePlanDateCodecText{} + } + + return nil +} + +type encodePlanDateCodecBinary struct{} + +func (encodePlanDateCodecBinary) Encode(value any, buf []byte) (newBuf []byte, err error) { + date, err := value.(pgtype.DateValuer).DateValue() + if err != nil { + return nil, err + } + + if !date.Valid { + return nil, nil + } + + var daysSinceDateEpoch int32 + switch date.InfinityModifier { + case pgtype.Finite: + tUnix := time.Date(date.Time.Year(), date.Time.Month(), date.Time.Day(), 0, 0, 0, 0, time.UTC).Unix() + dateEpoch := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC).Unix() + + secSinceDateEpoch := tUnix - dateEpoch + daysSinceDateEpoch = int32(secSinceDateEpoch / 86400) + case pgtype.Infinity: + daysSinceDateEpoch = infinityDayOffset + case pgtype.NegativeInfinity: + daysSinceDateEpoch = negativeInfinityDayOffset + } + + return AppendInt32(buf, daysSinceDateEpoch), nil +} + +type encodePlanDateCodecText struct{} + +func (encodePlanDateCodecText) Encode(value any, buf []byte) (newBuf []byte, err error) { + date, err := value.(pgtype.DateValuer).DateValue() + if err != nil { + return nil, err + } + + if !date.Valid { + return nil, nil + } + + switch date.InfinityModifier { + case pgtype.Finite: + // Year 0000 is 1 BC + bc := false + year := date.Time.Year() + if year <= 0 { + year = -year + 1 + bc = true + } + + yearBytes := strconv.AppendInt(make([]byte, 0, 6), int64(year), 10) + for i := len(yearBytes); i < 4; i++ { + buf = append(buf, '0') + } + buf = append(buf, yearBytes...) + buf = append(buf, '-') + if date.Time.Month() < 10 { + buf = append(buf, '0') + } + buf = strconv.AppendInt(buf, int64(date.Time.Month()), 10) + buf = append(buf, '-') + if date.Time.Day() < 10 { + buf = append(buf, '0') + } + buf = strconv.AppendInt(buf, int64(date.Time.Day()), 10) + + if bc { + buf = append(buf, " BC"...) + } + case pgtype.Infinity: + buf = append(buf, "infinity"...) + case pgtype.NegativeInfinity: + buf = append(buf, "-infinity"...) + } + + return buf, nil +} + +func (DateCodec) PlanScan(m *pgtype.Map, oid uint32, format int16, target any) pgtype.ScanPlan { + + switch format { + case pgtype.BinaryFormatCode: + name := getInterfaceName(target) //sanek + switch name { + case "*pgtype.timeWrapper": + return scanPlanBinaryDateToDateScanner{} + } + case pgtype.TextFormatCode: + name := getInterfaceName(target) //sanek + switch name { + case "*pgtype.timeWrapper": + return scanPlanTextAnyToDateScanner{} + } + } + + return nil +} + +type scanPlanBinaryDateToDateScanner struct{} + +func (scanPlanBinaryDateToDateScanner) Scan(src []byte, dst any) error { + scanner := (dst).(pgtype.DateScanner) + + if src == nil { + return scanner.ScanDate(pgtype.Date{}) + } + + if len(src) != 4 { + return fmt.Errorf("invalid length for date: %v", len(src)) + } + + dayOffset := int32(binary.BigEndian.Uint32(src)) + + switch dayOffset { + case infinityDayOffset: + return scanner.ScanDate(pgtype.Date{InfinityModifier: pgtype.Infinity, Valid: true}) + case negativeInfinityDayOffset: + return scanner.ScanDate(pgtype.Date{InfinityModifier: -pgtype.Infinity, Valid: true}) + default: + t := time.Date(2000, 1, int(1+dayOffset), 0, 0, 0, 0, time.UTC) + return scanner.ScanDate(pgtype.Date{Time: t, Valid: true}) + } +} + +type scanPlanTextAnyToDateScanner struct{} + +var dateRegexp = regexp.MustCompile(`^(\d{4,})-(\d\d)-(\d\d)( BC)?$`) + +func (scanPlanTextAnyToDateScanner) Scan(src []byte, dst any) error { + scanner := (dst).(pgtype.DateScanner) + + if src == nil { + return scanner.ScanDate(pgtype.Date{}) + } + + sbuf := string(src) + match := dateRegexp.FindStringSubmatch(sbuf) + if match != nil { + year, err := strconv.ParseInt(match[1], 10, 32) + if err != nil { + return fmt.Errorf("BUG: cannot parse date that regexp matched (year): %w", err) + } + + month, err := strconv.ParseInt(match[2], 10, 32) + if err != nil { + return fmt.Errorf("BUG: cannot parse date that regexp matched (month): %w", err) + } + + day, err := strconv.ParseInt(match[3], 10, 32) + if err != nil { + return fmt.Errorf("BUG: cannot parse date that regexp matched (month): %w", err) + } + + // BC matched + if len(match[4]) > 0 { + year = -year + 1 + } + + t := time.Date(int(year), time.Month(month), int(day), 0, 0, 0, 0, time.UTC) + return scanner.ScanDate(pgtype.Date{Time: t, Valid: true}) + } + + switch sbuf { + case "infinity": + return scanner.ScanDate(pgtype.Date{InfinityModifier: pgtype.Infinity, Valid: true}) + case "-infinity": + return scanner.ScanDate(pgtype.Date{InfinityModifier: -pgtype.Infinity, Valid: true}) + default: + return fmt.Errorf("invalid date format") + } +} + +func (c DateCodec) DecodeDatabaseSQLValue(m *pgtype.Map, oid uint32, format int16, src []byte) (driver.Value, error) { + if src == nil { + return nil, nil + } + + var date Date + err := codecScan(c, m, oid, format, src, &date) + if err != nil { + return nil, err + } + + if date.InfinityModifier != pgtype.Finite { + return date.InfinityModifier.String(), nil + } + + return date.Time, nil +} + +func (c DateCodec) DecodeValue(m *pgtype.Map, oid uint32, format int16, src []byte) (any, error) { + if src == nil { + return nil, nil + } + + var date Date + err := codecScan(c, m, oid, format, src, &date) + if err != nil { + return nil, err + } + + if date.InfinityModifier != pgtype.Finite { + return date.InfinityModifier, nil + } + + return date.Time, nil +} diff --git a/postgres_pgtype/interval.go b/postgres_pgtype/interval.go new file mode 100644 index 00000000..4bd799ee --- /dev/null +++ b/postgres_pgtype/interval.go @@ -0,0 +1,299 @@ +// копия файла из https://github.com/jackc/pgtype/interval.go +// чтоб не выдавала ошибку на null +// чтобы дата NULL = time.Time{} +package postgres_pgtype + +import ( + "database/sql/driver" + "encoding/binary" + "fmt" + "github.com/jackc/pgx/v5/pgtype" + "strconv" + "strings" +) + +const ( + microsecondsPerSecond = 1000000 + microsecondsPerMinute = 60 * microsecondsPerSecond + microsecondsPerHour = 60 * microsecondsPerMinute + microsecondsPerDay = 24 * microsecondsPerHour + microsecondsPerMonth = 30 * microsecondsPerDay +) + +type IntervalScanner interface { + ScanInterval(v Interval) error +} + +type IntervalValuer interface { + IntervalValue() (Interval, error) +} + +type Interval struct { + Microseconds int64 + Days int32 + Months int32 + Valid bool +} + +func (interval *Interval) ScanInterval(v Interval) error { + *interval = v + return nil +} + +func (interval Interval) IntervalValue() (Interval, error) { + return interval, nil +} + +// Scan implements the database/sql Scanner interface. +func (interval *Interval) Scan(src any) error { + if src == nil { + *interval = Interval{} + return nil + } + + switch src := src.(type) { + case string: + return scanPlanTextAnyToIntervalScanner{}.Scan([]byte(src), interval) + } + + return fmt.Errorf("cannot scan %T", src) +} + +// Value implements the database/sql/driver Valuer interface. +func (interval Interval) Value() (driver.Value, error) { + if !interval.Valid { + return nil, nil + } + + buf, err := pgtype.IntervalCodec{}.PlanEncode(nil, 0, pgtype.TextFormatCode, interval).Encode(interval, nil) + if err != nil { + return nil, err + } + return string(buf), err +} + +type IntervalCodec struct{} + +func (IntervalCodec) FormatSupported(format int16) bool { + return format == pgtype.TextFormatCode || format == pgtype.BinaryFormatCode +} + +func (IntervalCodec) PreferredFormat() int16 { + return pgtype.BinaryFormatCode +} + +func (IntervalCodec) PlanEncode(m *pgtype.Map, oid uint32, format int16, value any) pgtype.EncodePlan { + if _, ok := value.(IntervalValuer); !ok { + return nil + } + + switch format { + case pgtype.BinaryFormatCode: + return encodePlanIntervalCodecBinary{} + case pgtype.TextFormatCode: + return encodePlanIntervalCodecText{} + } + + return nil +} + +type encodePlanIntervalCodecBinary struct{} + +func (encodePlanIntervalCodecBinary) Encode(value any, buf []byte) (newBuf []byte, err error) { + interval, err := value.(IntervalValuer).IntervalValue() + if err != nil { + return nil, err + } + + if !interval.Valid { + return nil, nil + } + + buf = AppendInt64(buf, interval.Microseconds) + buf = AppendInt32(buf, interval.Days) + buf = AppendInt32(buf, interval.Months) + return buf, nil +} + +type encodePlanIntervalCodecText struct{} + +func (encodePlanIntervalCodecText) Encode(value any, buf []byte) (newBuf []byte, err error) { + interval, err := value.(IntervalValuer).IntervalValue() + if err != nil { + return nil, err + } + + if !interval.Valid { + return nil, nil + } + + if interval.Months != 0 { + buf = append(buf, strconv.FormatInt(int64(interval.Months), 10)...) + buf = append(buf, " mon "...) + } + + if interval.Days != 0 { + buf = append(buf, strconv.FormatInt(int64(interval.Days), 10)...) + buf = append(buf, " day "...) + } + + absMicroseconds := interval.Microseconds + if absMicroseconds < 0 { + absMicroseconds = -absMicroseconds + buf = append(buf, '-') + } + + hours := absMicroseconds / microsecondsPerHour + minutes := (absMicroseconds % microsecondsPerHour) / microsecondsPerMinute + seconds := (absMicroseconds % microsecondsPerMinute) / microsecondsPerSecond + + timeStr := fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds) + buf = append(buf, timeStr...) + + microseconds := absMicroseconds % microsecondsPerSecond + if microseconds != 0 { + buf = append(buf, fmt.Sprintf(".%06d", microseconds)...) + } + + return buf, nil +} + +func (IntervalCodec) PlanScan(m *pgtype.Map, oid uint32, format int16, target any) pgtype.ScanPlan { + + switch format { + case pgtype.BinaryFormatCode: + switch target.(type) { + case IntervalScanner: + return scanPlanBinaryIntervalToIntervalScanner{} + } + case pgtype.TextFormatCode: + switch target.(type) { + case IntervalScanner: + return scanPlanTextAnyToIntervalScanner{} + } + } + + return nil +} + +type scanPlanBinaryIntervalToIntervalScanner struct{} + +func (scanPlanBinaryIntervalToIntervalScanner) Scan(src []byte, dst any) error { + scanner := (dst).(IntervalScanner) + + if src == nil { + return scanner.ScanInterval(Interval{}) + } + + if len(src) != 16 { + return fmt.Errorf("Received an invalid size for an interval: %d", len(src)) + } + + microseconds := int64(binary.BigEndian.Uint64(src)) + days := int32(binary.BigEndian.Uint32(src[8:])) + months := int32(binary.BigEndian.Uint32(src[12:])) + + return scanner.ScanInterval(Interval{Microseconds: microseconds, Days: days, Months: months, Valid: true}) +} + +type scanPlanTextAnyToIntervalScanner struct{} + +func (scanPlanTextAnyToIntervalScanner) Scan(src []byte, dst any) error { + scanner := (dst).(IntervalScanner) + + if src == nil { + return scanner.ScanInterval(Interval{}) + } + + var microseconds int64 + var days int32 + var months int32 + + parts := strings.Split(string(src), " ") + + for i := 0; i < len(parts)-1; i += 2 { + scalar, err := strconv.ParseInt(parts[i], 10, 64) + if err != nil { + return fmt.Errorf("bad interval format") + } + + switch parts[i+1] { + case "year", "years": + months += int32(scalar * 12) + case "mon", "mons": + months += int32(scalar) + case "day", "days": + days = int32(scalar) + } + } + + if len(parts)%2 == 1 { + timeParts := strings.SplitN(parts[len(parts)-1], ":", 3) + if len(timeParts) != 3 { + return fmt.Errorf("bad interval format") + } + + var negative bool + if timeParts[0][0] == '-' { + negative = true + timeParts[0] = timeParts[0][1:] + } + + hours, err := strconv.ParseInt(timeParts[0], 10, 64) + if err != nil { + return fmt.Errorf("bad interval hour format: %s", timeParts[0]) + } + + minutes, err := strconv.ParseInt(timeParts[1], 10, 64) + if err != nil { + return fmt.Errorf("bad interval minute format: %s", timeParts[1]) + } + + sec, secFrac, secFracFound := strings.Cut(timeParts[2], ".") + + seconds, err := strconv.ParseInt(sec, 10, 64) + if err != nil { + return fmt.Errorf("bad interval second format: %s", sec) + } + + var uSeconds int64 + if secFracFound { + uSeconds, err = strconv.ParseInt(secFrac, 10, 64) + if err != nil { + return fmt.Errorf("bad interval decimal format: %s", secFrac) + } + + for i := 0; i < 6-len(secFrac); i++ { + uSeconds *= 10 + } + } + + microseconds = hours * microsecondsPerHour + microseconds += minutes * microsecondsPerMinute + microseconds += seconds * microsecondsPerSecond + microseconds += uSeconds + + if negative { + microseconds = -microseconds + } + } + + return scanner.ScanInterval(Interval{Months: months, Days: days, Microseconds: microseconds, Valid: true}) +} + +func (c IntervalCodec) DecodeDatabaseSQLValue(m *pgtype.Map, oid uint32, format int16, src []byte) (driver.Value, error) { + return codecDecodeToTextFormat(c, m, oid, format, src) +} + +func (c IntervalCodec) DecodeValue(m *pgtype.Map, oid uint32, format int16, src []byte) (any, error) { + if src == nil { + return nil, nil + } + + var interval Interval + err := codecScan(c, m, oid, format, src, &interval) + if err != nil { + return nil, err + } + return interval, nil +} diff --git a/postgres_pgtype/pgtype.go b/postgres_pgtype/pgtype.go new file mode 100644 index 00000000..9650e0b9 --- /dev/null +++ b/postgres_pgtype/pgtype.go @@ -0,0 +1,145 @@ +// копия файла из https://github.com/jackc/pgtype/pgtype.go +// чтоб не выдавала ошибку на null +// чтобы дата NULL = time.Time{} +package postgres_pgtype + +import ( + "database/sql/driver" + "github.com/jackc/pgx/v5/pgtype" +) + +// PostgreSQL oids for common types +const ( + BoolOID = 16 + ByteaOID = 17 + QCharOID = 18 + NameOID = 19 + Int8OID = 20 + Int2OID = 21 + Int4OID = 23 + TextOID = 25 + OIDOID = 26 + TIDOID = 27 + XIDOID = 28 + CIDOID = 29 + JSONOID = 114 + XMLOID = 142 + XMLArrayOID = 143 + JSONArrayOID = 199 + XID8ArrayOID = 271 + PointOID = 600 + LsegOID = 601 + PathOID = 602 + BoxOID = 603 + PolygonOID = 604 + LineOID = 628 + LineArrayOID = 629 + CIDROID = 650 + CIDRArrayOID = 651 + Float4OID = 700 + Float8OID = 701 + CircleOID = 718 + CircleArrayOID = 719 + UnknownOID = 705 + Macaddr8OID = 774 + MacaddrOID = 829 + InetOID = 869 + BoolArrayOID = 1000 + QCharArrayOID = 1002 + NameArrayOID = 1003 + Int2ArrayOID = 1005 + Int4ArrayOID = 1007 + TextArrayOID = 1009 + TIDArrayOID = 1010 + ByteaArrayOID = 1001 + XIDArrayOID = 1011 + CIDArrayOID = 1012 + BPCharArrayOID = 1014 + VarcharArrayOID = 1015 + Int8ArrayOID = 1016 + PointArrayOID = 1017 + LsegArrayOID = 1018 + PathArrayOID = 1019 + BoxArrayOID = 1020 + Float4ArrayOID = 1021 + Float8ArrayOID = 1022 + PolygonArrayOID = 1027 + OIDArrayOID = 1028 + ACLItemOID = 1033 + ACLItemArrayOID = 1034 + MacaddrArrayOID = 1040 + InetArrayOID = 1041 + BPCharOID = 1042 + VarcharOID = 1043 + DateOID = 1082 + TimeOID = 1083 + TimestampOID = 1114 + TimestampArrayOID = 1115 + DateArrayOID = 1182 + TimeArrayOID = 1183 + TimestamptzOID = 1184 + TimestamptzArrayOID = 1185 + IntervalOID = 1186 + IntervalArrayOID = 1187 + NumericArrayOID = 1231 + TimetzOID = 1266 + TimetzArrayOID = 1270 + BitOID = 1560 + BitArrayOID = 1561 + VarbitOID = 1562 + VarbitArrayOID = 1563 + NumericOID = 1700 + RecordOID = 2249 + RecordArrayOID = 2287 + UUIDOID = 2950 + UUIDArrayOID = 2951 + JSONBOID = 3802 + JSONBArrayOID = 3807 + DaterangeOID = 3912 + DaterangeArrayOID = 3913 + Int4rangeOID = 3904 + Int4rangeArrayOID = 3905 + NumrangeOID = 3906 + NumrangeArrayOID = 3907 + TsrangeOID = 3908 + TsrangeArrayOID = 3909 + TstzrangeOID = 3910 + TstzrangeArrayOID = 3911 + Int8rangeOID = 3926 + Int8rangeArrayOID = 3927 + JSONPathOID = 4072 + JSONPathArrayOID = 4073 + Int4multirangeOID = 4451 + NummultirangeOID = 4532 + TsmultirangeOID = 4533 + TstzmultirangeOID = 4534 + DatemultirangeOID = 4535 + Int8multirangeOID = 4536 + XID8OID = 5069 + Int4multirangeArrayOID = 6150 + NummultirangeArrayOID = 6151 + TsmultirangeArrayOID = 6152 + TstzmultirangeArrayOID = 6153 + DatemultirangeArrayOID = 6155 + Int8multirangeArrayOID = 6157 +) + +func codecDecodeToTextFormat(codec pgtype.Codec, m *pgtype.Map, oid uint32, format int16, src []byte) (driver.Value, error) { + if src == nil { + return nil, nil + } + + if format == pgtype.TextFormatCode { + return string(src), nil + } else { + value, err := codec.DecodeValue(m, oid, format, src) + if err != nil { + return nil, err + } + buf, err := m.Encode(oid, pgtype.TextFormatCode, value, nil) + if err != nil { + return nil, err + } + return string(buf), nil + } +} diff --git a/postgres_pgtype/pgtype_default.go b/postgres_pgtype/pgtype_default.go new file mode 100644 index 00000000..96056801 --- /dev/null +++ b/postgres_pgtype/pgtype_default.go @@ -0,0 +1,4 @@ +// копия файла из https://github.com/jackc/pgtype/pgtype_default.go +// чтоб не выдавала ошибку на null +// чтобы дата NULL = time.Time{} +package postgres_pgtype diff --git a/postgres_pgtype/sanek.go b/postgres_pgtype/sanek.go new file mode 100644 index 00000000..b974fdc7 --- /dev/null +++ b/postgres_pgtype/sanek.go @@ -0,0 +1,8 @@ +package postgres_pgtype + +import "reflect" + +// getInterfaceName - возвращает имя типа интерфейса +func getInterfaceName(v interface{}) string { + return reflect.TypeOf(v).String() +} diff --git a/postgres_pgtype/time.go b/postgres_pgtype/time.go new file mode 100644 index 00000000..e5ef0701 --- /dev/null +++ b/postgres_pgtype/time.go @@ -0,0 +1,279 @@ +// копия файла из https://github.com/jackc/pgtype/timestamptz.go +// чтоб не выдавала ошибку на null +// чтобы дата NULL = time.Time{} +package postgres_pgtype + +import ( + "database/sql/driver" + "encoding/binary" + "fmt" + "github.com/jackc/pgx/v5/pgtype" + "strconv" +) + +//type TimeScanner interface { +// ScanTime(v Time) error +//} +// +//type TimeValuer interface { +// TimeValue() (Time, error) +//} + +// Time represents the PostgreSQL time type. The PostgreSQL time is a time of day without time zone. +// +// Time is represented as the number of microseconds since midnight in the same way that PostgreSQL does. Other time and +// date types in pgtype can use time.Time as the underlying representation. However, pgtype.Time type cannot due to +// needing to handle 24:00:00. time.Time converts that to 00:00:00 on the following day. +// +// The time with time zone type is not supported. Use of time with time zone is discouraged by the PostgreSQL documentation. +type Time struct { + Microseconds int64 // Number of microseconds since midnight + Valid bool +} + +func (t *Time) ScanTime(v Time) error { + *t = v + return nil +} + +func (t Time) TimeValue() (Time, error) { + return t, nil +} + +// Scan implements the database/sql Scanner interface. +func (t *Time) Scan(src any) error { + if src == nil { + *t = Time{Valid: true} //sanek + //*t = Time{} + return nil + } + + switch src := src.(type) { + case string: + err := scanPlanTextAnyToTimeScanner{}.Scan([]byte(src), t) + if err != nil { + t.Microseconds = 0 + t.Valid = false + } + return err + } + + return fmt.Errorf("cannot scan %T", src) +} + +// Value implements the database/sql/driver Valuer interface. +func (t Time) Value() (driver.Value, error) { + if !t.Valid { + return nil, nil + } + + buf, err := pgtype.TimeCodec{}.PlanEncode(nil, 0, pgtype.TextFormatCode, t).Encode(t, nil) + if err != nil { + return nil, err + } + return string(buf), err +} + +type TimeCodec struct{} + +func (TimeCodec) FormatSupported(format int16) bool { + return format == pgtype.TextFormatCode || format == pgtype.BinaryFormatCode +} + +func (TimeCodec) PreferredFormat() int16 { + return pgtype.BinaryFormatCode +} + +func (TimeCodec) PlanEncode(m *pgtype.Map, oid uint32, format int16, value any) pgtype.EncodePlan { + if _, ok := value.(pgtype.TimeValuer); !ok { + return nil + } + + switch format { + case pgtype.BinaryFormatCode: + return encodePlanTimeCodecBinary{} + case pgtype.TextFormatCode: + return encodePlanTimeCodecText{} + } + + return nil +} + +type encodePlanTimeCodecBinary struct{} + +func (encodePlanTimeCodecBinary) Encode(value any, buf []byte) (newBuf []byte, err error) { + t, err := value.(pgtype.TimeValuer).TimeValue() + if err != nil { + return nil, err + } + + if !t.Valid { + return nil, nil + } + + return AppendInt64(buf, t.Microseconds), nil +} + +type encodePlanTimeCodecText struct{} + +func (encodePlanTimeCodecText) Encode(value any, buf []byte) (newBuf []byte, err error) { + t, err := value.(pgtype.TimeValuer).TimeValue() + if err != nil { + return nil, err + } + + if !t.Valid { + return nil, nil + } + + usec := t.Microseconds + hours := usec / microsecondsPerHour + usec -= hours * microsecondsPerHour + minutes := usec / microsecondsPerMinute + usec -= minutes * microsecondsPerMinute + seconds := usec / microsecondsPerSecond + usec -= seconds * microsecondsPerSecond + + s := fmt.Sprintf("%02d:%02d:%02d.%06d", hours, minutes, seconds, usec) + + return append(buf, s...), nil +} + +func (TimeCodec) PlanScan(m *pgtype.Map, oid uint32, format int16, target any) pgtype.ScanPlan { + + switch format { + case pgtype.BinaryFormatCode: + name := getInterfaceName(target) //sanek + switch name { + case "*pgtype.timeWrapper": + return scanPlanBinaryTimeToTimeScanner{} + case "*pgtype.stringWrapper": + return scanPlanBinaryTimeToTextScanner{} + } + case pgtype.TextFormatCode: + name := getInterfaceName(target) //sanek + switch name { + case "*pgtype.timeWrapper": + return scanPlanTextAnyToTimeScanner{} + } + } + + return nil +} + +type scanPlanBinaryTimeToTimeScanner struct{} + +func (scanPlanBinaryTimeToTimeScanner) Scan(src []byte, dst any) error { + scanner := (dst).(pgtype.TimeScanner) + + if src == nil { + return scanner.ScanTime(pgtype.Time{}) + } + + if len(src) != 8 { + return fmt.Errorf("invalid length for time: %v", len(src)) + } + + usec := int64(binary.BigEndian.Uint64(src)) + + return scanner.ScanTime(pgtype.Time{Microseconds: usec, Valid: true}) +} + +type scanPlanBinaryTimeToTextScanner struct{} + +func (scanPlanBinaryTimeToTextScanner) Scan(src []byte, dst any) error { + ts, ok := (dst).(pgtype.TextScanner) + if !ok { + return pgtype.ErrScanTargetTypeChanged + } + + if src == nil { + return ts.ScanText(pgtype.Text{}) + } + + if len(src) != 8 { + return fmt.Errorf("invalid length for time: %v", len(src)) + } + + usec := int64(binary.BigEndian.Uint64(src)) + + tim := Time{Microseconds: usec, Valid: true} + + buf, err := TimeCodec{}.PlanEncode(nil, 0, pgtype.TextFormatCode, tim).Encode(tim, nil) + if err != nil { + return err + } + + return ts.ScanText(pgtype.Text{String: string(buf), Valid: true}) +} + +type scanPlanTextAnyToTimeScanner struct{} + +func (scanPlanTextAnyToTimeScanner) Scan(src []byte, dst any) error { + scanner := (dst).(pgtype.TimeScanner) + + if src == nil { + return scanner.ScanTime(pgtype.Time{}) + } + + s := string(src) + + if len(s) < 8 || s[2] != ':' || s[5] != ':' { + return fmt.Errorf("cannot decode %v into Time", s) + } + + hours, err := strconv.ParseInt(s[0:2], 10, 64) + if err != nil { + return fmt.Errorf("cannot decode %v into Time", s) + } + usec := hours * microsecondsPerHour + + minutes, err := strconv.ParseInt(s[3:5], 10, 64) + if err != nil { + return fmt.Errorf("cannot decode %v into Time", s) + } + usec += minutes * microsecondsPerMinute + + seconds, err := strconv.ParseInt(s[6:8], 10, 64) + if err != nil { + return fmt.Errorf("cannot decode %v into Time", s) + } + usec += seconds * microsecondsPerSecond + + if len(s) > 9 { + if s[8] != '.' || len(s) > 15 { + return fmt.Errorf("cannot decode %v into Time", s) + } + + fraction := s[9:] + n, err := strconv.ParseInt(fraction, 10, 64) + if err != nil { + return fmt.Errorf("cannot decode %v into Time", s) + } + + for i := len(fraction); i < 6; i++ { + n *= 10 + } + + usec += n + } + + return scanner.ScanTime(pgtype.Time{Microseconds: usec, Valid: true}) +} + +func (c TimeCodec) DecodeDatabaseSQLValue(m *pgtype.Map, oid uint32, format int16, src []byte) (driver.Value, error) { + return codecDecodeToTextFormat(c, m, oid, format, src) +} + +func (c TimeCodec) DecodeValue(m *pgtype.Map, oid uint32, format int16, src []byte) (any, error) { + if src == nil { + return nil, nil + } + + var t Time + err := codecScan(c, m, oid, format, src, &t) + if err != nil { + return nil, err + } + return t, nil +} diff --git a/postgres_pgtype/timestamp.go b/postgres_pgtype/timestamp.go new file mode 100644 index 00000000..61ef871e --- /dev/null +++ b/postgres_pgtype/timestamp.go @@ -0,0 +1,361 @@ +// копия файла из https://github.com/jackc/pgtype/timestamp.go +// чтоб не выдавала ошибку на null +// чтобы дата NULL = time.Time{} +package postgres_pgtype + +import ( + "database/sql/driver" + "encoding/binary" + "encoding/json" + "fmt" + "github.com/jackc/pgx/v5/pgtype" + "strings" + "time" + //"github.com/jackc/pgx/v5/internal/pgio" +) + +const pgTimestampFormat = "2006-01-02 15:04:05.999999999" + +//type TimestampScanner interface { +// ScanTimestamp(v Timestamp) error +//} +// +//type TimestampValuer interface { +// TimestampValue() (Timestamp, error) +//} + +// Timestamp represents the PostgreSQL timestamp type. +type Timestamp struct { + Time time.Time // Time zone will be ignored when encoding to PostgreSQL. + InfinityModifier pgtype.InfinityModifier + Valid bool +} + +func (ts *Timestamp) ScanTimestamp(v Timestamp) error { + *ts = v + return nil +} + +func (ts Timestamp) TimestampValue() (Timestamp, error) { + return ts, nil +} + +// Scan implements the database/sql Scanner interface. +func (ts *Timestamp) Scan(src any) error { + if src == nil { + *ts = Timestamp{Valid: true} //sanek + //*ts = Timestamp{} + return nil + } + + switch src := src.(type) { + case string: + return (&scanPlanTextTimestampToTimestampScanner{}).Scan([]byte(src), ts) + case time.Time: + *ts = Timestamp{Time: src, Valid: true} + return nil + } + + return fmt.Errorf("cannot scan %T", src) +} + +// Value implements the database/sql/driver Valuer interface. +func (ts Timestamp) Value() (driver.Value, error) { + if !ts.Valid { + return nil, nil + } + + if ts.InfinityModifier != pgtype.Finite { + return ts.InfinityModifier.String(), nil + } + return ts.Time, nil +} + +func (ts Timestamp) MarshalJSON() ([]byte, error) { + if !ts.Valid { + return []byte("null"), nil + } + + var s string + + switch ts.InfinityModifier { + case pgtype.Finite: + s = ts.Time.Format(time.RFC3339Nano) + case pgtype.Infinity: + s = "infinity" + case pgtype.NegativeInfinity: + s = "-infinity" + } + + return json.Marshal(s) +} + +func (ts *Timestamp) UnmarshalJSON(b []byte) error { + var s *string + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + if s == nil { + *ts = Timestamp{} + return nil + } + + switch *s { + case "infinity": + *ts = Timestamp{Valid: true, InfinityModifier: pgtype.Infinity} + case "-infinity": + *ts = Timestamp{Valid: true, InfinityModifier: -pgtype.Infinity} + default: + // PostgreSQL uses ISO 8601 wihout timezone for to_json function and casting from a string to timestampt + tim, err := time.Parse(time.RFC3339Nano, *s+"Z") + if err != nil { + return err + } + + *ts = Timestamp{Time: tim, Valid: true} + } + + return nil +} + +type TimestampCodec struct { + // ScanLocation is the location that the time is assumed to be in for scanning. This is different from + // TimestamptzCodec.ScanLocation in that this setting does change the instant in time that the timestamp represents. + ScanLocation *time.Location +} + +func (*TimestampCodec) FormatSupported(format int16) bool { + return format == pgtype.TextFormatCode || format == pgtype.BinaryFormatCode +} + +func (*TimestampCodec) PreferredFormat() int16 { + return pgtype.BinaryFormatCode +} + +func (*TimestampCodec) PlanEncode(m *pgtype.Map, oid uint32, format int16, value any) pgtype.EncodePlan { + if _, ok := value.(pgtype.TimestampValuer); !ok { + return nil + } + + switch format { + case pgtype.BinaryFormatCode: + return encodePlanTimestampCodecBinary{} + case pgtype.TextFormatCode: + return encodePlanTimestampCodecText{} + } + + return nil +} + +type encodePlanTimestampCodecBinary struct{} + +func (encodePlanTimestampCodecBinary) Encode(value any, buf []byte) (newBuf []byte, err error) { + ts, err := value.(pgtype.TimestampValuer).TimestampValue() + if err != nil { + return nil, err + } + + if !ts.Valid { + return nil, nil + } + + var microsecSinceY2K int64 + switch ts.InfinityModifier { + case pgtype.Finite: + t := discardTimeZone(ts.Time) + microsecSinceUnixEpoch := t.Unix()*1000000 + int64(t.Nanosecond())/1000 + microsecSinceY2K = microsecSinceUnixEpoch - microsecFromUnixEpochToY2K + case pgtype.Infinity: + microsecSinceY2K = infinityMicrosecondOffset + case pgtype.NegativeInfinity: + microsecSinceY2K = negativeInfinityMicrosecondOffset + } + + buf = AppendInt64(buf, microsecSinceY2K) + + return buf, nil +} + +type encodePlanTimestampCodecText struct{} + +func (encodePlanTimestampCodecText) Encode(value any, buf []byte) (newBuf []byte, err error) { + ts, err := value.(pgtype.TimestampValuer).TimestampValue() + if err != nil { + return nil, err + } + + if !ts.Valid { + return nil, nil + } + + var s string + + switch ts.InfinityModifier { + case pgtype.Finite: + t := discardTimeZone(ts.Time) + + // Year 0000 is 1 BC + bc := false + if year := t.Year(); year <= 0 { + year = -year + 1 + t = time.Date(year, t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), time.UTC) + bc = true + } + + s = t.Truncate(time.Microsecond).Format(pgTimestampFormat) + + if bc { + s = s + " BC" + } + case pgtype.Infinity: + s = "infinity" + case pgtype.NegativeInfinity: + s = "-infinity" + } + + buf = append(buf, s...) + + return buf, nil +} + +func discardTimeZone(t time.Time) time.Time { + if t.Location() != time.UTC { + return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), time.UTC) + } + + return t +} + +func (c *TimestampCodec) PlanScan(m *pgtype.Map, oid uint32, format int16, target any) pgtype.ScanPlan { + switch format { + case pgtype.BinaryFormatCode: + name := getInterfaceName(target) //sanek + switch name { + case "*pgtype.timeWrapper": + return &scanPlanBinaryTimestampToTimestampScanner{location: c.ScanLocation} + } + case pgtype.TextFormatCode: + name := getInterfaceName(target) //sanek + switch name { + case "*pgtype.timeWrapper": + return &scanPlanTextTimestampToTimestampScanner{location: c.ScanLocation} + } + } + + return nil +} + +type scanPlanBinaryTimestampToTimestampScanner struct{ location *time.Location } + +func (plan *scanPlanBinaryTimestampToTimestampScanner) Scan(src []byte, dst any) error { + scanner := (dst).(pgtype.TimestampScanner) + + if src == nil { + return scanner.ScanTimestamp(pgtype.Timestamp{}) + } + + if len(src) != 8 { + return fmt.Errorf("invalid length for timestamp: %v", len(src)) + } + + var ts pgtype.Timestamp + microsecSinceY2K := int64(binary.BigEndian.Uint64(src)) + + switch microsecSinceY2K { + case infinityMicrosecondOffset: + ts = pgtype.Timestamp{Valid: true, InfinityModifier: pgtype.Infinity} + case negativeInfinityMicrosecondOffset: + ts = pgtype.Timestamp{Valid: true, InfinityModifier: -pgtype.Infinity} + default: + tim := time.Unix( + microsecFromUnixEpochToY2K/1000000+microsecSinceY2K/1000000, + (microsecFromUnixEpochToY2K%1000000*1000)+(microsecSinceY2K%1000000*1000), + ).UTC() + if plan.location != nil { + tim = time.Date(tim.Year(), tim.Month(), tim.Day(), tim.Hour(), tim.Minute(), tim.Second(), tim.Nanosecond(), plan.location) + } + ts = pgtype.Timestamp{Time: tim, Valid: true} + } + + return scanner.ScanTimestamp(ts) +} + +type scanPlanTextTimestampToTimestampScanner struct{ location *time.Location } + +func (plan *scanPlanTextTimestampToTimestampScanner) Scan(src []byte, dst any) error { + scanner := (dst).(pgtype.TimestampScanner) + + if src == nil { + return scanner.ScanTimestamp(pgtype.Timestamp{}) + } + + var ts pgtype.Timestamp + sbuf := string(src) + switch sbuf { + case "infinity": + ts = pgtype.Timestamp{Valid: true, InfinityModifier: pgtype.Infinity} + case "-infinity": + ts = pgtype.Timestamp{Valid: true, InfinityModifier: -pgtype.Infinity} + default: + bc := false + if strings.HasSuffix(sbuf, " BC") { + sbuf = sbuf[:len(sbuf)-3] + bc = true + } + tim, err := time.Parse(pgTimestampFormat, sbuf) + if err != nil { + return err + } + + if bc { + year := -tim.Year() + 1 + tim = time.Date(year, tim.Month(), tim.Day(), tim.Hour(), tim.Minute(), tim.Second(), tim.Nanosecond(), tim.Location()) + } + + if plan.location != nil { + tim = time.Date(tim.Year(), tim.Month(), tim.Day(), tim.Hour(), tim.Minute(), tim.Second(), tim.Nanosecond(), plan.location) + } + + ts = pgtype.Timestamp{Time: tim, Valid: true} + } + + return scanner.ScanTimestamp(ts) +} + +func (c *TimestampCodec) DecodeDatabaseSQLValue(m *pgtype.Map, oid uint32, format int16, src []byte) (driver.Value, error) { + if src == nil { + return nil, nil + } + + var ts Timestamp + err := codecScan(c, m, oid, format, src, &ts) + if err != nil { + return nil, err + } + + if ts.InfinityModifier != pgtype.Finite { + return ts.InfinityModifier.String(), nil + } + + return ts.Time, nil +} + +func (c *TimestampCodec) DecodeValue(m *pgtype.Map, oid uint32, format int16, src []byte) (any, error) { + if src == nil { + return nil, nil + } + + var ts Timestamp + err := codecScan(c, m, oid, format, src, &ts) + if err != nil { + return nil, err + } + + if ts.InfinityModifier != pgtype.Finite { + return ts.InfinityModifier, nil + } + + return ts.Time, nil +} diff --git a/postgres_pgx/timestamptz.go b/postgres_pgtype/timestamptz.go similarity index 98% rename from postgres_pgx/timestamptz.go rename to postgres_pgtype/timestamptz.go index 0a0360ec..678402d9 100644 --- a/postgres_pgx/timestamptz.go +++ b/postgres_pgtype/timestamptz.go @@ -1,7 +1,7 @@ // копия файла из https://github.com/jackc/pgtype/timestamptz.go // чтоб не выдавала ошибку на null // чтобы дата NULL = time.Time{} -package postgres_pgx +package postgres_pgtype import ( "database/sql/driver" @@ -9,7 +9,6 @@ import ( "encoding/json" "fmt" "github.com/jackc/pgx/v5/pgtype" - "reflect" "strings" "time" ) @@ -248,10 +247,6 @@ func (c *TimestamptzCodec) PlanScan(m *pgtype.Map, oid uint32, format int16, tar return nil } -func getInterfaceName(v interface{}) string { - return reflect.TypeOf(v).String() -} - type scanPlanBinaryTimestamptzToTimestamptzScanner struct{ location *time.Location } func (plan *scanPlanBinaryTimestamptzToTimestamptzScanner) Scan(src []byte, dst any) error { diff --git a/postgres_pgx/write.go b/postgres_pgtype/write.go similarity index 97% rename from postgres_pgx/write.go rename to postgres_pgtype/write.go index 6e11ea72..733f282f 100644 --- a/postgres_pgx/write.go +++ b/postgres_pgtype/write.go @@ -1,4 +1,4 @@ -package postgres_pgx +package postgres_pgtype import "encoding/binary" diff --git a/postgres_pgxpool/postgres_pgxpool.go b/postgres_pgxpool/postgres_pgxpool.go index 61cd1d5a..00d0949a 100644 --- a/postgres_pgxpool/postgres_pgxpool.go +++ b/postgres_pgxpool/postgres_pgxpool.go @@ -9,6 +9,7 @@ import ( "github.com/ManyakRus/starter/constants" "github.com/ManyakRus/starter/log" "github.com/ManyakRus/starter/port_checker" + "github.com/ManyakRus/starter/postgres_pgtype" "github.com/ManyakRus/starter/postgres_pgx" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" @@ -589,12 +590,44 @@ func ReplaceSchemaName(TextSQL, SchemaNameFrom string) string { return Otvet } +// AfterConnect_NoNull - регистрирует обработчики для нужных типов +// чтобы NULL=default value func AfterConnect_NoNull(ctx context.Context, conn *pgx.Conn) error { - // Регистрируем zeronull обработчики для нужных типов + // Регистрируем обработчики для нужных типов, + + //timestamptz conn.TypeMap().RegisterType(&pgtype.Type{ Name: "timestamptz", OID: pgtype.TimestamptzOID, - Codec: &postgres_pgx.TimestamptzCodec{}, + Codec: &postgres_pgtype.TimestamptzCodec{}, + }) + + //timestamp + conn.TypeMap().RegisterType(&pgtype.Type{ + Name: "timestamp", + OID: pgtype.TimestampOID, + Codec: &postgres_pgtype.TimestampCodec{}, + }) + + //timetz + conn.TypeMap().RegisterType(&pgtype.Type{ + Name: "timetz", + OID: pgtype.TimetzOID, + Codec: &postgres_pgtype.TimeCodec{}, + }) + + //time + conn.TypeMap().RegisterType(&pgtype.Type{ + Name: "time", + OID: pgtype.TimeOID, + Codec: &postgres_pgtype.TimeCodec{}, + }) + + //date + conn.TypeMap().RegisterType(&pgtype.Type{ + Name: "date", + OID: pgtype.DateOID, + Codec: &postgres_pgtype.DateCodec{}, }) return nil