Skip to content

Row inspector rewrites JSON whitespace on columns whose server stores the text #3057

Description

@datlechin

Found while investigating #3051. Separate from the hang, which #3056 fixes.

The defect

MultiRowEditState.resolvePendingValue compacts every JSON value before it reaches the database:

if isJson, let value, !value.isEmpty {
    let normalized = JsonReindenter.normalize(value)
    ...
    return normalized
}

and isJson is columnTypeEnum.isJsonType || (originalValue ?? "").looksLikeJson (MultiRowEditState.swift:177).

That rewrites whitespace the server would have kept:

  • PostgreSQL json (not jsonb). The docs: "Because the json type stores an exact copy of the input text, it will preserve semantically-insignificant white space between tokens, as well as the order of keys within JSON objects. Also, if a JSON object within the value contains the same key more than once, all the key/value pairs are kept."
  • MariaDB, whose JSON is a LONGTEXT alias. docs/databases/mariadb.mdx:21 already says so.
  • SQLite, libSQL, Turso, Cloudflare D1, DuckDB, which all give a column declared JSON text storage. ColumnTypeClassifier maps the name to .json on every engine.
  • Any TEXT/VARCHAR column whose current value happens to parse, on every engine, through looksLikeJson.

Editing one number rewrites every space in the document. The grid's inline editor does not, so the same cell commits different bytes depending on which surface you edit it from.

Why it is not in #3056

I built a per-engine policy for this and reverted it. Three review rounds each found new real defects in that code, and the last one is structural rather than a slip:

The policy answered "does this server preserve JSON source text?" from the saved DatabaseType. That is not answerable there. MariaDB reports its JSON columns as "JSON", byte-identical to MySQL, and a connection saved as MySQL can reach a MariaDB server, whose flavor the driver detects from the server banner. So a static table keyed on the connection label silently compacts MariaDB text.

Earlier rounds also found MariaDB, ClickHouse and Databend misclassified; the read-only cell popover never receiving the policy; and a Format JSON command offered in states where it could do nothing.

What a fix needs

  1. Resolve storage semantics from the live server, not the saved connection type. MySQLServerFlavor already detects MariaDB from the banner and liveDriver.serverVersion is reachable (ServerDashboardViewModel.swift:140). Regressions for MySQL-labelled MariaDB, TiDB-labelled MariaDB, and genuine MySQL, through both the inspector and the grid.
  2. Cover the json[] path. ArrayJsonElementEditor.swift:93 constructs JSONViewerView with no policy, and ArrayValueEditorModel normalizes each changed element independently. Paired json[] and jsonb[] byte-level tests.
  3. Re-key the editing model when a field's storage identity changes. MultiRowEditState.configure can preserve a field UUID across tables when the column name, index, original text and multiplicity all match, so a jsonb field and a json field showing the same text can share editor state.
  4. Decide what the editor opens on. Every comparable client gates this on the declared column type: TablePlus shows raw with an explicit PRETTY selector; Postico autoformats jsonb only ("The normal json type is basically just a text field, so I don't want to mess with formatting there; especially since formatting might hide issues like duplicate keys"); Sequel Ace pretty-prints on open and unformats on commit, gated on fieldType == JSON; DataGrip has an explicit Toggle Formatting; Beekeeper splits its commit into "Minify & Apply" and "Apply (no minify)".
  5. Do not round-trip through JSONSerialization. JsonReindenter's token model already preserves key order, duplicate keys and number literals, which is the choice Sequel Ace made deliberately for the same reason.

A Format JSON command belongs with this, replacing today's Pretty Print, which copies to the clipboard and silently does nothing when the document does not parse, is over 500,000 UTF-16 units, or nests past 512 (FieldMenuContent.swift:35-41).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions