2222 */
2323
2424import { numericValueInSettingUnits } from './make-conf.js' ;
25+ import { sizeTo , SYS_PG } from './units.js' ;
2526
2627const NAME_PATTERN = / ^ [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ . ] * $ / ;
2728const CONF_ASSIGNMENT = / ^ ( [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ . ] * ) \s * (?: = \s * | \s + ) ( .* ) $ / ;
@@ -85,33 +86,50 @@ export function parseConfText(text) {
8586 return { entries, skipped } ;
8687}
8788
88- /** One delimited line into cells; double quotes wrap a cell, `""` is one quote. */
89+ /** Logical CSV records: embedded newlines belong to a quoted value. */
90+ function delimitedRecords ( text ) {
91+ const records = [ ] ;
92+ let start = 0 ;
93+ let quoted = false ;
94+ for ( let i = 0 ; i < text . length ; i += 1 ) {
95+ if ( text [ i ] === '"' ) {
96+ if ( quoted && text [ i + 1 ] === '"' ) i += 1 ;
97+ else quoted = ! quoted ;
98+ } else if ( ( text [ i ] === '\n' || text [ i ] === '\r' ) && ! quoted ) {
99+ records . push ( text . slice ( start , i ) ) ;
100+ if ( text [ i ] === '\r' && text [ i + 1 ] === '\n' ) i += 1 ;
101+ start = i + 1 ;
102+ }
103+ }
104+ if ( start < text . length ) records . push ( text . slice ( start ) ) ;
105+ return records ;
106+ }
107+
108+ /** One logical record into cells; quoted whitespace is part of the value. */
89109function splitDelimited ( line , delimiter ) {
90110 const cells = [ ] ;
91111 let cell = '' ;
92112 let quoted = false ;
113+ let wasQuoted = false ;
114+ const finish = ( ) => {
115+ cells . push ( wasQuoted ? cell : cell . trim ( ) ) ;
116+ cell = '' ; wasQuoted = false ;
117+ } ;
93118 for ( let index = 0 ; index < line . length ; index += 1 ) {
94119 const char = line [ index ] ;
95120 if ( quoted ) {
96121 if ( char === '"' && line [ index + 1 ] === '"' ) {
97- cell += '"' ;
98- index += 1 ;
99- } else if ( char === '"' ) {
100- quoted = false ;
101- } else {
102- cell += char ;
103- }
122+ cell += '"' ; index += 1 ;
123+ } else if ( char === '"' ) quoted = false ;
124+ else cell += char ;
104125 } else if ( char === '"' ) {
105- quoted = true ;
106- } else if ( char === delimiter ) {
107- cells . push ( cell ) ;
108- cell = '' ;
109- } else {
110- cell += char ;
111- }
126+ quoted = true ; wasQuoted = true ;
127+ } else if ( char === delimiter ) finish ( ) ;
128+ else cell += char ;
112129 }
113- cells . push ( cell ) ;
114- return cells . map ( ( item ) => item . trim ( ) ) ;
130+ if ( quoted ) return null ; // An incomplete record is not a partial setting.
131+ finish ( ) ;
132+ return cells ;
115133}
116134
117135/** A pg_settings export, with or without a header row, into name → value. */
@@ -121,9 +139,10 @@ export function parseDelimitedText(text, delimiter) {
121139 let header = false ;
122140 let columns = { name : 0 , value : 1 , unit : null } ;
123141 let first = true ;
124- for ( const rawLine of text . split ( / \r ? \n / ) ) {
142+ for ( const rawLine of delimitedRecords ( text ) ) {
125143 if ( ! rawLine . trim ( ) ) continue ;
126144 const cells = splitDelimited ( rawLine , delimiter ) ;
145+ if ( cells === null ) { skipped += 1 ; continue ; }
127146 if ( first ) {
128147 first = false ;
129148 const lowered = cells . map ( ( cell ) => cell . toLowerCase ( ) ) ;
@@ -156,12 +175,21 @@ export function parseDelimitedText(text, delimiter) {
156175export function detectFormat ( text ) {
157176 const lines = text . split ( / \r ? \n / ) . filter ( ( line ) => line . trim ( ) && ! line . trim ( ) . startsWith ( '#' ) ) ;
158177 if ( ! lines . length ) return { format : 'empty' , delimiter : null } ;
178+ // A named CSV header is stronger evidence than the contents of multiline
179+ // values, which may themselves contain assignments or other delimiters.
180+ for ( const delimiter of DELIMITERS ) {
181+ const header = splitDelimited ( lines [ 0 ] , delimiter ) ?. map ( cell => cell . toLowerCase ( ) ) ;
182+ if ( header ?. includes ( 'name' ) && HEADER_VALUE_COLUMNS . some ( name => header . includes ( name ) ) ) {
183+ return { format : 'csv' , delimiter } ;
184+ }
185+ }
159186 const assignments = lines . filter ( ( line ) => / ^ \s * [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ . ] * \s * = \s * \S / . test ( line ) ) ;
160187 if ( assignments . length * 2 >= lines . length ) return { format : 'conf' , delimiter : null } ;
161188 let best = null ;
189+ const records = delimitedRecords ( text ) . filter ( record => record . trim ( ) ) ;
162190 for ( const delimiter of DELIMITERS ) {
163- const count = lines . filter ( ( line ) => line . includes ( delimiter ) ) . length ;
164- if ( count * 2 >= lines . length && ( best === null || count > best . count ) ) {
191+ const count = records . filter ( record => ( splitDelimited ( record , delimiter ) ?. length ?? 0 ) > 1 ) . length ;
192+ if ( count * 2 >= records . length && ( best === null || count > best . count ) ) {
165193 best = { delimiter, count } ;
166194 }
167195 }
@@ -241,6 +269,36 @@ export function normalizeSettingValue(rawValue, sourceUnit, metadata) {
241269 }
242270}
243271
272+ /** Display current values in the calculated configuration's PostgreSQL units. */
273+ export function formatCurrentSettingValue ( rawValue , sourceUnit , metadata ) {
274+ const raw = String ( rawValue ?? '' ) ;
275+ if ( [ 'string' , 'enum' , 'bool' ] . includes ( metadata ?. vartype ) ) return raw ;
276+ const match = NUMERIC_PATTERN . exec ( stripSingleQuotes ( raw . trim ( ) ) ) ;
277+ if ( match === null ) return raw ;
278+ const amount = Number ( match [ 1 ] ) ;
279+ const unit = match [ 2 ] || sourceUnit || metadata ?. unit || '' ;
280+ if ( ! Number . isFinite ( amount ) || ! unit ) return raw ;
281+ // Disabled, unlimited and automatic values are not negative durations/sizes.
282+ if ( amount <= 0 ) return String ( amount ) ;
283+ try {
284+ if ( / ^ ( u s | m s | s | m i n | h | d ) $ / . test ( unit ) ) {
285+ for ( const suffix of [ 'd' , 'h' , 'min' , 's' , 'ms' , 'us' ] ) {
286+ const value = numericValueInSettingUnits ( amount , unit , suffix ) ;
287+ if ( value >= 1 && Number . isInteger ( value ) ) return `${ value } ${ suffix } ` ;
288+ }
289+ return `${ match [ 1 ] } ${ unit } ` ;
290+ }
291+ const bytes = numericValueInSettingUnits ( amount , unit , 'B' ) ;
292+ // Reuse the engine's unit selection, but do not truncate collected values
293+ // as sizeTo does when constructing an integer-valued candidate setting.
294+ const suffix = sizeTo ( bytes , SYS_PG ) . match ( / [ A - Z a - z ] + $ / ) ?. [ 0 ] || 'B' ;
295+ return `${ numericValueInSettingUnits ( bytes , 'B' , suffix ) } ${ suffix } ` ;
296+ } catch {
297+ // Unknown units or non-numeric/redacted values retain the original text.
298+ return raw ;
299+ }
300+ }
301+
244302/**
245303 * The settings this run calculated that the pasted configuration sets to a
246304 * different value. `matching` counts agreement, `missing` the calculated
@@ -267,7 +325,7 @@ export function diffConfigurations(parameters, entries, metadata) {
267325 rows . push ( {
268326 name,
269327 calculated : detail . value ,
270- yours : entry . unit ? ` ${ entry . value } ( ${ entry . unit } )` : entry . value ,
328+ yours : formatCurrentSettingValue ( entry . value , entry . unit , meta ) ,
271329 apply_mode : detail . apply_mode ,
272330 context : detail . context ,
273331 source : detail . source ,
0 commit comments