Stricter floats.

This commit is contained in:
Nuno Cruces
2025-12-12 17:21:33 +00:00
parent 5b78823416
commit 8dca850bee
8 changed files with 109 additions and 24 deletions

View File

@@ -16,6 +16,7 @@ import (
"github.com/ncruces/go-sqlite3" "github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/internal/util" "github.com/ncruces/go-sqlite3/internal/util"
"github.com/ncruces/go-sqlite3/util/sql3util"
) )
// Register registers the bloom_filter virtual table: // Register registers the bloom_filter virtual table:
@@ -55,11 +56,9 @@ func create(db *sqlite3.Conn, _, schema, table string, arg ...string) (_ *bloom,
} }
if len(arg) > 1 { if len(arg) > 1 {
b.prob, err = strconv.ParseFloat(arg[1], 64) var ok bool
if err != nil { b.prob, ok = sql3util.ParseFloat(arg[1])
return nil, err if !ok || b.prob <= 0 || b.prob >= 1 {
}
if b.prob <= 0 || b.prob >= 1 {
return nil, util.ErrorString("bloom: probability must be in the range (0,1)") return nil, util.ErrorString("bloom: probability must be in the range (0,1)")
} }
} else { } else {

View File

@@ -254,19 +254,15 @@ func (c *cursor) Column(ctx sqlite3.Context, col int) error {
switch typ { switch typ {
case numeric, integer: case numeric, integer:
if strings.TrimLeft(txt, "+-0123456789") == "" { if i, err := strconv.ParseInt(txt, 10, 64); err == nil {
if i, err := strconv.ParseInt(txt, 10, 64); err == nil { ctx.ResultInt64(i)
ctx.ResultInt64(i) return nil
return nil
}
} }
fallthrough fallthrough
case real: case real:
if strings.TrimLeft(txt, "+-.0123456789Ee") == "" { if f, ok := sql3util.ParseFloat(txt); ok {
if f, err := strconv.ParseFloat(txt, 64); err == nil { ctx.ResultFloat(f)
ctx.ResultFloat(f) return nil
return nil
}
} }
fallthrough fallthrough
default: default:

View File

@@ -4,3 +4,8 @@ This package implements the **EXPERIMENTAL** `"litestream"` SQLite VFS
that offers Litestream [lightweight read-replicas](https://fly.io/blog/litestream-revamped/#lightweight-read-replicas). that offers Litestream [lightweight read-replicas](https://fly.io/blog/litestream-revamped/#lightweight-read-replicas).
See the [example](example_test.go) for how to use. See the [example](example_test.go) for how to use.
Our `PRAGMA litestream_time` accepts:
- Go [duration strings](https://pkg.go.dev/time#ParseDuration)
- SQLite [time values](https://sqlite.org/lang_datefunc.html#time_values)
- SQLite [time modifiers 1 through 13](https://sqlite.org/lang_datefunc.html#modifiers)

63
litestream/time.go Normal file
View File

@@ -0,0 +1,63 @@
package litestream
import (
"math"
"strings"
"time"
"github.com/ncruces/go-sqlite3/util/sql3util"
)
func parseTimeDelta(s string) (years, months, days int, duration time.Duration, ok bool) {
duration, err := time.ParseDuration(s)
if err == nil {
return 0, 0, 0, duration, true
}
if strings.EqualFold(s, "now") {
return 0, 0, 0, 0, true
}
ss := strings.TrimSuffix(strings.ToLower(s), "s")
switch {
case strings.HasSuffix(ss, " year"):
years, duration, ok = parseDateUnit(ss, " year", 365*86400)
case strings.HasSuffix(ss, " month"):
months, duration, ok = parseDateUnit(ss, " month", 30*86400)
case strings.HasSuffix(ss, " day"):
months, duration, ok = parseDateUnit(ss, " day", 86400)
case strings.HasSuffix(ss, " hour"):
duration, ok = parseTimeUnit(ss, " hour", time.Hour)
case strings.HasSuffix(ss, " minute"):
duration, ok = parseTimeUnit(ss, " minute", time.Minute)
case strings.HasSuffix(ss, " second"):
duration, ok = parseTimeUnit(ss, " second", time.Second)
default:
return sql3util.ParseTimeShift(s)
}
return
}
func parseDateUnit(s, unit string, seconds float64) (int, time.Duration, bool) {
f, ok := sql3util.ParseFloat(s[:len(s)-len(unit)])
if !ok {
return 0, 0, false
}
i, f := math.Modf(f)
if math.MinInt <= i && i <= math.MaxInt {
return int(i), time.Duration(f * seconds * float64(time.Second)), true
}
return 0, 0, false
}
func parseTimeUnit(s, unit string, scale time.Duration) (time.Duration, bool) {
f, ok := sql3util.ParseFloat(s[:len(s)-len(unit)])
return time.Duration(f * float64(scale)), ok
}

View File

@@ -15,7 +15,6 @@ import (
"github.com/superfly/ltx" "github.com/superfly/ltx"
"github.com/ncruces/go-sqlite3" "github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/util/sql3util"
"github.com/ncruces/go-sqlite3/util/vfsutil" "github.com/ncruces/go-sqlite3/util/vfsutil"
"github.com/ncruces/go-sqlite3/vfs" "github.com/ncruces/go-sqlite3/vfs"
"github.com/ncruces/wbt" "github.com/ncruces/wbt"
@@ -224,7 +223,7 @@ func (f *liteFile) Pragma(name, value string) (string, error) {
} }
var syncTime time.Time var syncTime time.Time
if years, months, days, duration, ok := sql3util.ParseTimeShift(value); ok { if years, months, days, duration, ok := parseTimeDelta(value); ok {
syncTime = time.Now().AddDate(years, months, days).Add(duration) syncTime = time.Now().AddDate(years, months, days).Add(duration)
} else { } else {
syncTime, _ = sqlite3.TimeFormatAuto.Decode(value) syncTime, _ = sqlite3.TimeFormatAuto.Decode(value)

View File

@@ -91,7 +91,17 @@ func Test_integration(t *testing.T) {
t.Errorf("got %q", txid) t.Errorf("got %q", txid)
} }
_, err = replica.ExecContext(t.Context(), `PRAGMA litestream_time='00:01'`) _, err = replica.ExecContext(t.Context(), `PRAGMA litestream_time='-1.5h'`)
if err != nil {
t.Fatal(err)
}
_, err = replica.ExecContext(t.Context(), `PRAGMA litestream_time='-00:01'`)
if err != nil {
t.Fatal(err)
}
_, err = replica.ExecContext(t.Context(), `PRAGMA litestream_time='-2.5 years'`)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

15
time.go
View File

@@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/ncruces/go-sqlite3/internal/util" "github.com/ncruces/go-sqlite3/internal/util"
"github.com/ncruces/go-sqlite3/util/sql3util"
"github.com/ncruces/julianday" "github.com/ncruces/julianday"
) )
@@ -157,11 +158,13 @@ func (f TimeFormat) Decode(v any) (time.Time, error) {
case TimeFormatUnix, TimeFormatUnixFrac: case TimeFormatUnix, TimeFormatUnixFrac:
if s, ok := v.(string); ok { if s, ok := v.(string); ok {
f, err := strconv.ParseFloat(s, 64) if i, err := strconv.ParseInt(s, 10, 64); err == nil {
if err != nil { v = i
return time.Time{}, err } else if f, ok := sql3util.ParseFloat(s); ok {
v = f
} else {
return time.Time{}, util.TimeErr
} }
v = f
} }
switch v := v.(type) { switch v := v.(type) {
case float64: case float64:
@@ -234,8 +237,8 @@ func (f TimeFormat) Decode(v any) (time.Time, error) {
v = i v = i
break break
} }
f, err := strconv.ParseFloat(s, 64) f, ok := sql3util.ParseFloat(s)
if err == nil { if ok {
v = f v = f
break break
} }

View File

@@ -1,6 +1,7 @@
package sql3util package sql3util
import ( import (
"strconv"
"strings" "strings"
"time" "time"
) )
@@ -67,6 +68,15 @@ func ParseBool(s string) (b, ok bool) {
return false, false return false, false
} }
// ParseFloat parses a decimal floating point number.
func ParseFloat(s string) (f float64, ok bool) {
if strings.TrimLeft(s, "+-.0123456789Ee") != "" {
return
}
f, err := strconv.ParseFloat(s, 64)
return f, err == nil
}
// ParseTimeShift parses a time shift modifier, // ParseTimeShift parses a time shift modifier,
// also the output of timediff. // also the output of timediff.
// //