Skip to content
Merged
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
32 changes: 32 additions & 0 deletions async_postgres/pg_types.nim
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ type
PgTsVector* = distinct string ## PostgreSQL tsvector (full-text search document).
PgTsQuery* = distinct string ## PostgreSQL tsquery (full-text search query).

PgXml* = distinct string ## PostgreSQL xml type.

PgPoint* = object ## PostgreSQL point type: (x, y).
x*: float64
y*: float64
Expand Down Expand Up @@ -199,6 +201,8 @@ const
OidTsVector* = 3614'i32
OidTsQuery* = 3615'i32

OidXml* = 142'i32

rangeEmpty* = 0x01'u8 ## Range flag: range is empty.
rangeHasLower* = 0x02'u8 ## Range flag: lower bound present.
rangeHasUpper* = 0x04'u8 ## Range flag: upper bound present.
Expand All @@ -221,6 +225,9 @@ proc `==`*(a, b: PgTsVector): bool {.borrow.}
proc `$`*(v: PgTsQuery): string {.borrow.}
proc `==`*(a, b: PgTsQuery): bool {.borrow.}

proc `$`*(v: PgXml): string {.borrow.}
proc `==`*(a, b: PgXml): bool {.borrow.}

proc parsePgNumeric*(s: string): PgNumeric =
## Parse a decimal string (e.g. "123.45", "-0.001", "NaN") into PgNumeric.
if s.len == 0:
Expand Down Expand Up @@ -650,6 +657,9 @@ proc toPgParam*(v: PgTsVector): PgParam =
proc toPgParam*(v: PgTsQuery): PgParam =
PgParam(oid: OidTsQuery, format: 0, value: some(toBytes(string(v))))

proc toPgParam*(v: PgXml): PgParam =
PgParam(oid: OidXml, format: 0, value: some(toBytes(string(v))))

proc toPgParam*(v: PgPoint): PgParam =
PgParam(oid: OidPoint, format: 0, value: some(toBytes($v)))

Expand Down Expand Up @@ -979,6 +989,10 @@ proc toPgBinaryParam*(v: PgTsQuery): PgParam =
## Send as text format — PostgreSQL handles the parsing.
PgParam(oid: OidTsQuery, format: 0, value: some(toBytes(string(v))))

proc toPgBinaryParam*(v: PgXml): PgParam =
## Binary wire format for xml is the text representation itself.
PgParam(oid: OidXml, format: 1, value: some(toBytes(string(v))))

proc encodePointBinary(p: PgPoint): seq[byte] =
## Encode a point as 16 bytes (two float64 big-endian).
result = newSeq[byte](16)
Expand Down Expand Up @@ -2021,6 +2035,18 @@ proc getTsQuery*(row: Row, col: int): PgTsQuery =
return PgTsQuery(decodeBinaryTsQuery(row.data.buf.toOpenArray(off, off + clen - 1)))
PgTsQuery(row.getStr(col))

proc getXml*(row: Row, col: int): PgXml =
## Get a column value as PgXml. Handles both text and binary format.
if row.isBinaryCol(col):
let (off, clen) = cellInfo(row, col)
if clen == -1:
raise newException(PgTypeError, "Column " & $col & " is NULL")
var s = newString(clen)
for i in 0 ..< clen:
s[i] = char(row.data.buf[off + i])
return PgXml(s)
PgXml(row.getStr(col))

# Geometry text format parsers

proc parsePointText(s: string): PgPoint =
Expand Down Expand Up @@ -2256,6 +2282,7 @@ optAccessor(getMacAddr, getMacAddrOpt, PgMacAddr)
optAccessor(getMacAddr8, getMacAddr8Opt, PgMacAddr8)
optAccessor(getTsVector, getTsVectorOpt, PgTsVector)
optAccessor(getTsQuery, getTsQueryOpt, PgTsQuery)
optAccessor(getXml, getXmlOpt, PgXml)
optAccessor(getPoint, getPointOpt, PgPoint)
optAccessor(getLine, getLineOpt, PgLine)
optAccessor(getLseg, getLsegOpt, PgLseg)
Expand Down Expand Up @@ -3842,6 +3869,9 @@ proc get*(row: Row, col: int, T: typedesc[PgTsVector]): PgTsVector =
proc get*(row: Row, col: int, T: typedesc[PgTsQuery]): PgTsQuery =
row.getTsQuery(col)

proc get*(row: Row, col: int, T: typedesc[PgXml]): PgXml =
row.getXml(col)

proc get*(row: Row, col: int, T: typedesc[PgPoint]): PgPoint =
row.getPoint(col)

Expand Down Expand Up @@ -3964,6 +3994,7 @@ nameAccessor(getMacAddr, PgMacAddr)
nameAccessor(getMacAddr8, PgMacAddr8)
nameAccessor(getTsVector, PgTsVector)
nameAccessor(getTsQuery, PgTsQuery)
nameAccessor(getXml, PgXml)
nameAccessor(getPoint, PgPoint)
nameAccessor(getLine, PgLine)
nameAccessor(getLseg, PgLseg)
Expand All @@ -3986,6 +4017,7 @@ nameAccessor(getMacAddrOpt, Option[PgMacAddr])
nameAccessor(getMacAddr8Opt, Option[PgMacAddr8])
nameAccessor(getTsVectorOpt, Option[PgTsVector])
nameAccessor(getTsQueryOpt, Option[PgTsQuery])
nameAccessor(getXmlOpt, Option[PgXml])
nameAccessor(getPointOpt, Option[PgPoint])
nameAccessor(getLineOpt, Option[PgLine])
nameAccessor(getLsegOpt, Option[PgLseg])
Expand Down
45 changes: 45 additions & 0 deletions tests/test_e2e.nim
Original file line number Diff line number Diff line change
Expand Up @@ -6140,3 +6140,48 @@ suite "E2E: cancelNoWait":
await conn.close()

waitFor t()

test "xml roundtrip":
proc t() {.async.} =
let conn = await connect(plainConfig())
let v = PgXml("<root><item>hello</item></root>")
let res = await conn.query("SELECT $1::xml", @[toPgParam(v)])
doAssert res.rows.len == 1
let got = res.rows[0].getXml(0)
doAssert $got == "<root><item>hello</item></root>"
await conn.close()

waitFor t()

test "xmlparse function":
proc t() {.async.} =
let conn = await connect(plainConfig())
let res = await conn.query("SELECT xmlparse(CONTENT '<item>test</item>')")
doAssert res.rows.len == 1
let v = res.rows[0].getXml(0)
doAssert "<item>test</item>" == $v
await conn.close()

waitFor t()

test "NULL xml":
proc t() {.async.} =
let conn = await connect(plainConfig())
let res = await conn.query("SELECT NULL::xml")
doAssert res.rows.len == 1
doAssert res.rows[0].getXmlOpt(0).isNone
await conn.close()

waitFor t()

test "xml binary results":
proc t() {.async.} =
let conn = await connect(plainConfig())
let res =
await conn.query("SELECT '<root>data</root>'::xml", resultFormat = rfBinary)
doAssert res.rows.len == 1
let v = res.rows[0].getXml(0)
doAssert "<root>data</root>" == $v
await conn.close()

waitFor t()
50 changes: 50 additions & 0 deletions tests/test_types.nim
Original file line number Diff line number Diff line change
Expand Up @@ -3924,3 +3924,53 @@ suite "tsvector / tsquery":
let fields = @[mkField(OidTsQuery, 0'i16)]
let row = mkRow(@[none(seq[byte])], fields)
check row.getTsQueryOpt(0).isNone

suite "xml":
test "OID constant":
check OidXml == 142'i32

test "toPgParam PgXml":
let v = PgXml("<root><item>hello</item></root>")
let p = toPgParam(v)
check p.oid == OidXml
check p.format == 0
check toString(p.value.get) == "<root><item>hello</item></root>"

test "toPgBinaryParam PgXml sends binary format":
let v = PgXml("<root/>")
let p = toPgBinaryParam(v)
check p.oid == OidXml
check p.format == 1

test "$ PgXml":
let v = PgXml("<root/>")
check $v == "<root/>"

test "== PgXml":
check PgXml("<a/>") == PgXml("<a/>")
check PgXml("<a/>") != PgXml("<b/>")

test "getXml text format":
let data = toBytes("<root><item>test</item></root>")
let fields = @[mkField(OidXml, 0'i16)]
let row = mkRow(@[some(data)], fields)
check $row.getXml(0) == "<root><item>test</item></root>"

test "getXml binary format":
let data = toBytes("<root><item>test</item></root>")
let fields = @[mkField(OidXml, 1'i16)]
let row = mkRow(@[some(data)], fields)
check $row.getXml(0) == "<root><item>test</item></root>"

test "getXmlOpt some":
let data = toBytes("<root/>")
let fields = @[mkField(OidXml, 0'i16)]
let row = mkRow(@[some(data)], fields)
let r = row.getXmlOpt(0)
check r.isSome
check $r.get == "<root/>"

test "getXmlOpt none":
let fields = @[mkField(OidXml, 0'i16)]
let row = mkRow(@[none(seq[byte])], fields)
check row.getXmlOpt(0).isNone
Loading