mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 14:30:13 +00:00
ca158c22d8
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
181 lines
7.3 KiB
Go
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)}
|
|
}
|