Files
pad/internal/store/dialect_fieldvalue.go
T
xarmian ca158c22d8 feat(apps): app install staging: apps/1 manifest, fetch policy, pending reservations, preview (TASK-3397, U8a) (#1770)
SPEC-6 U8a (DOC-3371 §1, §2 steps 1-5, §9). Strict apps/1 parsing;
an https/no-redirect fetch policy screened at dial time with an admin
private-origin list; pending-install reservations under the owner lock
and the instance advisory lock, holding the full charge until the fetch
ends; the owner preview with every artifact normalized by the importer's
own code (normalizeArtifact, prepareCreateFields, both extracted
behaviour-preserving and pinned by a parity golden); apps_enabled and
the admin apps settings; a background sweep of expired pending
installs; migration 118/092. The preview is advisory: it may refuse
what the import would accept, never the reverse; U8b re-checks
atomically.

Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC
2026-10-04 07:19:07 -04:00

181 lines
7.3 KiB
Go

package store
import (
"fmt"
"math"
"regexp"
"strconv"
"strings"
)
// Field-value reads that answer the same on both dialects (BUG-3221).
//
// JSONExtractText is json_extract on SQLite, which yields the NATIVE type
// (true is INTEGER 1, 1e3 is REAL 1000.0, an array is compact JSON text), and
// ->> on Postgres, which yields jsonb's TEXT form ('true', '1000', an array
// with spaces). Compared to a caller's string, sorted, grouped or displayed,
// the same row therefore answered differently by dialect. The helpers here
// compare and render a field value by its JSON TYPE instead:
//
// - string: itself.
// - boolean: 'true' / 'false', never '1' / '0'.
// - number: equal to an argument that parses as a JSON number, NUMERICALLY
// (1000 = 1000.0 = 1e3); rendered with integer-valued numbers as integer
// digits.
// - array, object, null, missing: never equal to a scalar argument, and
// rendered as NULL. No membership: that would be a multi_select feature,
// not a dialect fix.
//
// Residual, pinned by TestFieldValueDialectResidual: a NON-integer number is
// rendered with each dialect's own number text, so one whose SQLite REAL form
// uses an exponent or needs more than 15 significant digits (1.5e-7) renders
// differently. Equality is numeric and does not depend on the rendering.
// Second residual, one class: SQLite holds a number as int64 or float64, so
// its numeric equality is exact only for numbers those types represent
// exactly, while Postgres compares numeric exactly. Every member is the same
// rounding: beyond int64 (2^63 equals 2^63+1), beyond float64 range (1e400
// equals 1e401, both +Inf), underflow (1e-400 equals 0), and more than 17
// significant digits (0.1 equals 0.10000000000000000001). Pinned, one case
// per member, by TestFieldValueNumberExtremes.
// jsonNumberRe is the JSON number grammar. An argument outside it never
// equals a stored number, so SQLite's CAST('abc' AS REAL) = 0 cannot fire.
var jsonNumberRe = regexp.MustCompile(`^-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?$`)
// numericArg returns the SQLite comparison value for a numeric argument: an
// int64 when the number is integer-valued and in range, so a stored INTEGER is
// compared exactly, else a float64. ok is false for a non-number.
func numericArg(arg string) (any, bool) {
if !jsonNumberRe.MatchString(arg) {
return nil, false
}
if n, err := strconv.ParseInt(arg, 10, 64); err == nil {
return n, true
}
// Out of float64 range, ParseFloat answers ±Inf with ErrRange, and that is
// the value SQLite stores for such a literal, so 1e400 still finds itself.
f, err := strconv.ParseFloat(arg, 64)
if err != nil && !math.IsInf(f, 0) {
return nil, false
}
if f == math.Trunc(f) && f >= -9.2e18 && f <= 9.2e18 {
return int64(f), true
}
return f, true
}
// joinFieldEquals ORs the two halves JSONFieldEqualsParts returns.
func joinFieldEquals(str string, strArgs []any, other string, otherArgs []any) (string, []any) {
if other == "" {
return "(" + str + ")", strArgs
}
return "(" + str + " OR " + other + ")", append(append([]any{}, strArgs...), otherArgs...)
}
// ---------- SQLite ----------
func (d *sqliteDialect) jsonType(column, key string) string {
return fmt.Sprintf("json_type(%s, '$.%s')", column, key)
}
func (d *sqliteDialect) JSONFieldText(column, key string) string {
v := d.JSONExtractText(column, key)
return fmt.Sprintf("(CASE %s"+
" WHEN 'text' THEN %s"+
" WHEN 'true' THEN 'true' WHEN 'false' THEN 'false'"+
" WHEN 'integer' THEN CAST(%s AS TEXT)"+
" WHEN 'real' THEN CASE WHEN %s = CAST(%s AS INTEGER) THEN CAST(CAST(%s AS INTEGER) AS TEXT) ELSE CAST(%s AS TEXT) END"+
" END)", d.jsonType(column, key), v, v, v, v, v, v)
}
func (d *sqliteDialect) JSONFieldEquals(column, key, arg string) (string, []any) {
return joinFieldEquals(d.JSONFieldEqualsParts(column, key, arg))
}
func (d *sqliteDialect) JSONFieldEqualsParts(column, key, arg string) (string, []any, string, []any) {
t := d.jsonType(column, key)
v := d.JSONExtractText(column, key)
str := fmt.Sprintf("(%s = ? AND %s = 'text')", v, t)
var other []string
var otherArgs []any
switch arg {
case "true", "false":
other = append(other, fmt.Sprintf("%s = '%s'", t, arg))
}
if n, ok := numericArg(arg); ok {
other = append(other, fmt.Sprintf("(%s IN ('integer','real') AND %s = ?)", t, v))
otherArgs = append(otherArgs, n)
}
return str, []any{arg}, strings.Join(other, " OR "), otherArgs
}
func (d *sqliteDialect) JSONFieldOrder(column, key, dir string) string {
t := d.jsonType(column, key)
rank := fmt.Sprintf("(CASE COALESCE(%s, 'null')"+
" WHEN 'integer' THEN 0 WHEN 'real' THEN 0"+
" WHEN 'text' THEN 1 WHEN 'true' THEN 2 WHEN 'false' THEN 2"+
" WHEN 'null' THEN 4 ELSE 3 END)", t)
num := fmt.Sprintf("(CASE WHEN %s IN ('integer','real') THEN %s END)", t, d.JSONExtractText(column, key))
return fmt.Sprintf("%s ASC, %s %s, %s %s", rank, num, dir, d.JSONFieldText(column, key), dir)
}
// ---------- PostgreSQL ----------
func (d *postgresDialect) JSONFieldText(column, key string) string {
return fmt.Sprintf("(CASE jsonb_typeof(%s->'%s')"+
" WHEN 'string' THEN %s->>'%s'"+
" WHEN 'boolean' THEN %s->>'%s'"+
" WHEN 'number' THEN trim_scale((%s->'%s')::numeric)::text"+
" END)", column, key, column, key, column, key, column, key)
}
func (d *postgresDialect) JSONFieldEquals(column, key, arg string) (string, []any) {
return joinFieldEquals(d.JSONFieldEqualsParts(column, key, arg))
}
func (d *postgresDialect) JSONFieldEqualsParts(column, key, arg string) (string, []any, string, []any) {
t := fmt.Sprintf("jsonb_typeof(%s->'%s')", column, key)
v := d.JSONExtractText(column, key)
str := fmt.Sprintf("(%s = ? AND %s = 'string')", v, t)
var other []string
var otherArgs []any
switch arg {
case "true", "false":
other = append(other, fmt.Sprintf("(%s = 'boolean' AND %s = '%s')", t, v, arg))
}
if _, ok := numericArg(arg); ok {
// The argument goes over as text and is cast by Postgres, so the
// comparison is exact numeric, not float.
other = append(other, fmt.Sprintf("(%s = 'number' AND (%s->'%s')::numeric = CAST(? AS numeric))", t, column, key))
otherArgs = append(otherArgs, arg)
}
return str, []any{arg}, strings.Join(other, " OR "), otherArgs
}
func (d *postgresDialect) JSONFieldOrder(column, key, dir string) string {
t := fmt.Sprintf("jsonb_typeof(%s->'%s')", column, key)
rank := fmt.Sprintf("(CASE COALESCE(%s, 'null')"+
" WHEN 'number' THEN 0 WHEN 'string' THEN 1 WHEN 'boolean' THEN 2"+
" WHEN 'null' THEN 4 ELSE 3 END)", t)
num := fmt.Sprintf("(CASE WHEN %s = 'number' THEN (%s->'%s')::numeric END)", t, column, key)
return fmt.Sprintf("%s ASC, %s %s, %s %s", rank, num, dir, d.JSONFieldText(column, key), dir)
}
// NumericClaimForms is the form under which JSONFieldEquals could find two
// numeric values equal, on either dialect, for arg when numericArg accepts
// it: its float64 value. SQLite compares an INTEGER and a REAL as doubles,
// and Postgres compares numeric EXACTLY; two exactly equal values round to
// the same double, so equal doubles is a superset of both. The app
// installer's in-pack uniqueness claims key numbers on it (TASK-3397, codex
// rounds 10-12).
func NumericClaimForms(arg string) []string {
if _, ok := numericArg(arg); !ok {
return nil
}
f, err := strconv.ParseFloat(arg, 64)
if err != nil && !math.IsInf(f, 0) {
return nil
}
return []string{"f:" + strconv.FormatFloat(f, 'g', -1, 64)}
}