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
70 changes: 60 additions & 10 deletions packages/core/realtime-js/src/lib/transformers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,21 +217,71 @@ export const toArray = (value: RecordValue, type: string): RecordValue => {

// Confirm value is a Postgres array by checking curly brackets
if (openBrace === '{' && closeBrace === '}') {
let arr
const valTrim = value.slice(1, lastIdx)
const elements = parseArrayElements(value.slice(1, lastIdx))
// `type` has already had its array marker stripped by the caller, so each
// element converts to a scalar.
return elements.map((element) =>
element === null ? null : (convertCell(type, element) as BaseValue)
)
}

// TODO: find a better solution to separate Postgres array data
try {
arr = JSON.parse('[' + valTrim + ']')
} catch (_) {
// WARNING: splitting on comma does not cover all edge cases
arr = valTrim ? valTrim.split(',') : []
return value
}

/**
* Splits the inside of a Postgres array literal into its elements.
*
* An element is double-quoted whenever it is empty, spells `NULL`, or contains a
* delimiter, brace, quote, backslash or whitespace; inside the quotes `\\` and
* `\"` are escapes. Unquoted `NULL` is the null element, whereas the quoted
* `"NULL"` is the four-character string.
*
* https://www.postgresql.org/docs/current/arrays.html#ARRAYS-IO
*
* @param inner - The literal with its outermost braces already removed
*/
const parseArrayElements = (inner: string): (string | null)[] => {
if (inner === '') {
return []
}

const elements: (string | null)[] = []
let index = 0

while (index <= inner.length) {
if (inner[index] === '"') {
let element = ''
index++

while (index < inner.length && inner[index] !== '"') {
if (inner[index] === '\\') {
index++
}
element += inner[index]
index++
}

index++
elements.push(element)
} else {
const start = index

while (index < inner.length && inner[index] !== ',') {
index++
}

const element = inner.slice(start, index)
elements.push(element.toUpperCase() === 'NULL' ? null : element)
}

return arr.map((val: BaseValue) => convertCell(type, val))
if (inner[index] !== ',') {
break
}

index++
}

return value
return elements
}

/**
Expand Down
27 changes: 27 additions & 0 deletions packages/core/realtime-js/test/array-literal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, expect, test } from 'vitest'
import { toArray } from '../src/lib/transformers'

// Postgres quotes an array element whenever it is empty, spells `NULL`, or
// contains a delimiter, brace, quote, backslash or whitespace — so a single
// literal routinely mixes quoted and unquoted elements.
describe('toArray with quoted and unquoted elements mixed', () => {
test.each([
['{"a,b",c}', ['a,b', 'c']],
['{"hello world",plain}', ['hello world', 'plain']],
['{plain,"hello world"}', ['plain', 'hello world']],
['{"x\\"y",z}', ['x"y', 'z']],
['{"e\\\\f",z}', ['e\\f', 'z']],
['{"p{q",z}', ['p{q', 'z']],
['{"",z}', ['', 'z']],
])('parses %s', (literal, expected) => {
expect(toArray(literal, 'text')).toEqual(expected)
})

test('an unquoted NULL is the null element', () => {
expect(toArray('{a,NULL,b}', 'text')).toEqual(['a', null, 'b'])
})

test('a quoted NULL is the four-character string', () => {
expect(toArray('{"NULL"}', 'text')).toEqual(['NULL'])
})
})