Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/en_US/release_notes_9_16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Bug fixes
| `Issue #9595 <https://github.com/pgadmin-org/pgadmin4/issues/9595>`_ - Fix missing ALTER ... SET DEFAULT statements for inherited columns in the generated table SQL/EDIT script.
| `Issue #9677 <https://github.com/pgadmin-org/pgadmin4/issues/9677>`_ - Fix the Unlogged table toggle in table properties not generating any ALTER TABLE ... SET LOGGED/UNLOGGED statement.
| `Issue #9828 <https://github.com/pgadmin-org/pgadmin4/issues/9828>`_ - Fix tool calls failing against OpenAI-compatible providers that emit empty/null name, arguments, or id fields in streaming continuation deltas.
| `Issue #9854 <https://github.com/pgadmin-org/pgadmin4/issues/9854>`_ - Fix the JSON editor stripping trailing fractional zeros (e.g. 10.00) and rewriting large integers in jsonb values, which corrupted unmodified numbers when saving.
| `Issue #9875 <https://github.com/pgadmin-org/pgadmin4/issues/9875>`_ - Fixed an issue where EXPLAIN and EXPLAIN ANALYZE failed to execute when blank lines separated clauses in the SQL query.
| `Issue #9810 <https://github.com/pgadmin-org/pgadmin4/issues/9810>`_ - Use the ServerManager's passfile for the credential gate in connect() so the check matches the passfile actually used for the connection, and warn on conflicting passfile/passexec settings.
| `Issue #9892 <https://github.com/pgadmin-org/pgadmin4/issues/9892>`_ - Fix blank difference counts on the top-level group rows in Schema Diff.
Expand Down
1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
"json-bignumber": "^1.1.1",
"leaflet": "^1.9.4",
"lodash": "4.*",
"lossless-json": "^4.3.0",
"marked": "^18.0.2",
"moment": "^2.30.1",
"moment-timezone": "^0.6.2",
Expand Down
7 changes: 6 additions & 1 deletion web/pgadmin/static/js/components/JsonEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ export default function JsonEditor({setJsonEditorSize, value, options, className
...defaultOptions,
...options,
onChange: (updatedContent) => {
options.onChange(currentMode.current == 'text' ? updatedContent.text : JSON.stringify(updatedContent.json));
// Serialize non-text modes (tree/table) with the configured parser
// so a lossless parser preserves the original number representation
// (big integers, trailing fractional zeros) instead of being
// rewritten by the native JSON serializer.
const serializer = options.parser ?? JSON;
options.onChange(currentMode.current == 'text' ? updatedContent.text : serializer.stringify(updatedContent.json));
options.onValidationError(editor.current.validate());
},
onChangeMode: (mode) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { DefaultButton, PrimaryButton } from '../../../../../../static/js/compon
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
import CloseIcon from '@mui/icons-material/Close';
import gettext from 'sources/gettext';
import JSONBigNumber from 'json-bignumber';
import { losslessJSONParser, prettifyJSONString, prettifyJSONValue } from './json_utils';
import JsonEditor from '../../../../../../static/js/components/JsonEditor';
import PropTypes from 'prop-types';
import { RowInfoContext } from '.';
Expand Down Expand Up @@ -394,13 +394,17 @@ export function JsonTextEditor({row, column, onRowChange, onClose}) {

const value = React.useMemo(()=>{
let newVal = row[column.key] ?? null;
/* If jsonb or array */
/* If jsonb or array. Pretty-print with a lossless JSON parser so that the
* original numeric representation (e.g. big integers and trailing
* fractional zeros like 10.00) is preserved. A plain JSON.parse/stringify
* round-trip would silently rewrite such numbers and corrupt unmodified
* values on save (#9854). */
if(column.column_type_internal === 'jsonb' && !Array.isArray(newVal) && newVal != null) {
newVal = JSONBigNumber.stringify(JSONBigNumber.parse(newVal), null, 2);
newVal = prettifyJSONString(newVal);
} else if (Array.isArray(newVal)) {
let temp = newVal.map((ele)=>{
if (typeof ele === 'object') {
return JSONBigNumber.stringify(ele, null, 2);
return prettifyJSONValue(ele);
}
return ele;
});
Expand Down Expand Up @@ -456,6 +460,7 @@ export function JsonTextEditor({row, column, onRowChange, onClose}) {
setJsonEditorSize={setJsonEditorSize}
value={localVal??''}
options={{
parser: losslessJSONParser,
onChange: onChange,
onValidationError: (errors)=>{setHasError(Boolean(errors));}
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2026, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////

import * as LosslessJSON from 'lossless-json';

// A lossless JSON parser, with the same { parse, stringify } interface as the
// native JSON object. It preserves the original numeric representation of a
// value - including big integers and trailing fractional zeros such as 10.00 -
// instead of normalizing it the way JSON.parse/JSON.stringify does.
//
// This matters for the JSON cell editor: jsonb values arrive from the server
// as their canonical text, and a normalizing round-trip would silently rewrite
// numbers (e.g. 10.00 -> 10), corrupting unmodified values when the document is
// saved back. See issue #9854.
export const losslessJSONParser = LosslessJSON;

// Pretty-print a JSON string for the editor, preserving the exact numeric
// representation of every value.
export function prettifyJSONString(value) {
return LosslessJSON.stringify(LosslessJSON.parse(value), null, 2);
}

// Pretty-print an already-parsed JSON value (e.g. an element of an array
// column) for the editor.
export function prettifyJSONValue(value) {
return LosslessJSON.stringify(value, null, 2);
}
53 changes: 53 additions & 0 deletions web/regression/javascript/sqleditor/json_utils.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2026, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////

import {
losslessJSONParser,
prettifyJSONString,
prettifyJSONValue,
} from '../../../pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/json_utils';

describe('QueryToolDataGrid json_utils', () => {
describe('prettifyJSONString', () => {
it('preserves trailing fractional zeros (#9854)', () => {
// A native JSON.parse/stringify round-trip would turn 10.00 into 10.
expect(prettifyJSONString('10.00')).toBe('10.00');
expect(prettifyJSONString('{"a":10.00,"b":3.140}')).toBe(
'{\n "a": 10.00,\n "b": 3.140\n}'
);
});

it('preserves big integers beyond Number precision', () => {
expect(prettifyJSONString('{"id":1234567890123456789}')).toBe(
'{\n "id": 1234567890123456789\n}'
);
});

it('pretty-prints with two-space indentation', () => {
expect(prettifyJSONString('{"a":[1,2]}')).toBe(
'{\n "a": [\n 1,\n 2\n ]\n}'
);
});
});

describe('prettifyJSONValue', () => {
it('pretty-prints an already-parsed value', () => {
expect(prettifyJSONValue({ a: 1 })).toBe('{\n "a": 1\n}');
});
});

describe('losslessJSONParser', () => {
it('exposes a JSON-compatible parse/stringify interface', () => {
expect(typeof losslessJSONParser.parse).toBe('function');
expect(typeof losslessJSONParser.stringify).toBe('function');
// Round-trips a trailing-zero number without normalizing it.
expect(losslessJSONParser.stringify(losslessJSONParser.parse('10.50'))).toBe('10.50');
});
});
});
8 changes: 8 additions & 0 deletions web/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10286,6 +10286,13 @@ __metadata:
languageName: node
linkType: hard

"lossless-json@npm:^4.3.0":
version: 4.3.0
resolution: "lossless-json@npm:4.3.0"
checksum: 10c0/b561eb7f77ef99dbaa0df32fa2407ddcc2c61a0d53297240f0fdd6c2e5f2e0f7d224fb055f84e76467f5ec65547540662c265fcf7a1705497a2d44e323c517df
languageName: node
linkType: hard

"lower-case@npm:^2.0.2":
version: 2.0.2
resolution: "lower-case@npm:2.0.2"
Expand Down Expand Up @@ -13052,6 +13059,7 @@ __metadata:
leaflet: "npm:^1.9.4"
loader-utils: "npm:^3.3.1"
lodash: "npm:4.*"
lossless-json: "npm:^4.3.0"
marked: "npm:^18.0.2"
mini-css-extract-plugin: "npm:^2.10.2"
moment: "npm:^2.30.1"
Expand Down
Loading