// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. package pgserver import ( "fmt" "strconv" "time" "five/hbrt" ) // PostgreSQL OID constants for the types FiveSql2 surfaces. Values // match the canonical pg_type entries — psql and most drivers key // their decoders off these. const ( oidBool = 16 oidInt4 = 23 oidInt8 = 20 oidNumeric = 1700 oidText = 25 oidDate = 1082 oidTimestamp = 1114 ) // pgTypeFor returns (OID, declared-size). The declared size is -1 // for variable-width types (per PG convention). Sample is one // representative value from the column; NIL falls back to TEXT // because we don't have schema info at this layer. func pgTypeFor(sample hbrt.Value) (oid uint32, size int16) { switch { case sample.IsLogical(): return oidBool, 1 case sample.IsNumeric(): // Distinguish integer-ish from decimal so int columns // transit as INT4/INT8 (faster, BI tools render nicely). if isIntegerNumeric(sample) { n := sample.AsNumInt() if n >= -2147483648 && n <= 2147483647 { return oidInt4, 4 } return oidInt8, 8 } return oidNumeric, -1 case sample.IsDate(): return oidDate, 4 case sample.IsTimestamp(): return oidTimestamp, 8 case sample.IsString(): return oidText, -1 default: // NIL or unknown — claim TEXT so the client decodes // whatever we send as a string. Real schema-aware OID // dispatch lands with extended-protocol Describe support // in v1.1. return oidText, -1 } } // isIntegerNumeric distinguishes a whole-number Numeric from one // that's been declared with decimals. Five's hbrt.Value carries // the length/dec hints separately for printf-style formatting; if // `dec` is 0, the source field declared no decimal places. func isIntegerNumeric(v hbrt.Value) bool { // Fast path: tag-level integer subtype. if v.IsInt() || v.IsLong() { return true } // AsNumDouble rounding check — within 1e-9 of an integer, no // declared decimal places. d := v.AsNumDouble() if d != float64(int64(d)) { return false } return v.Decimal() == 0 } // encodeText writes one cell as a text-format PG byte slice. NULL // is signalled by returning nil (DataRow distinguishes nil from // empty []byte and sends length=-1 vs length=0 accordingly). func encodeText(v hbrt.Value) []byte { if v.IsNil() { return nil } switch { case v.IsLogical(): if v.AsBool() { return []byte{'t'} } return []byte{'f'} case v.IsNumeric(): if isIntegerNumeric(v) { return []byte(strconv.FormatInt(v.AsNumInt(), 10)) } precision := int(v.Decimal()) if precision <= 0 || precision > 30 { precision = 10 } return []byte(strconv.FormatFloat(v.AsNumDouble(), 'f', precision, 64)) case v.IsDate(): y, m, d := julianToYMD(v.AsJulian()) return []byte(fmt.Sprintf("%04d-%02d-%02d", y, m, d)) case v.IsTimestamp(): // Convert Julian + ms to a Go time and format. jul := v.AsJulian() y, mo, d := julianToYMD(jul) ms := v.AsTimeMs() hh := ms / 3600000 ms -= hh * 3600000 mm := ms / 60000 ms -= mm * 60000 ss := ms / 1000 ms -= ss * 1000 return []byte(fmt.Sprintf("%04d-%02d-%02d %02d:%02d:%02d.%03d", y, mo, d, hh, mm, ss, ms)) case v.IsString(): return []byte(v.AsString()) default: // Fallback: best-effort string conversion. return []byte(fmt.Sprintf("%v", v)) } } // julianToYMD converts an integer Julian day number to (year, month, // day). Matches Harbour's hb_dateDecode algorithm used elsewhere in // the runtime; duplicated here to avoid pulling in hbrtl which // would create an import cycle. func julianToYMD(j int64) (year, month, day int) { if j <= 0 { return 0, 0, 0 } a := j + 32044 b := (4*a + 3) / 146097 c := a - (b*146097)/4 d := (4*c + 3) / 1461 e := c - (1461*d)/4 m := (5*e + 2) / 153 day = int(e - (153*m+2)/5 + 1) month = int(m + 3 - 12*(m/10)) year = int(b*100 + d - 4800 + (m / 10)) return } // Force time package import — we'll need it for Timestamp parsing // when extended protocol lands. Stub function keeps the import // from being pruned in v1.0-skeleton. var _ = time.Date