diff --git a/async_postgres/pg_types.nim b/async_postgres/pg_types.nim index af231d7..d8ae668 100644 --- a/async_postgres/pg_types.nim +++ b/async_postgres/pg_types.nim @@ -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 @@ -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. @@ -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: @@ -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))) @@ -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) @@ -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 = @@ -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) @@ -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) @@ -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) @@ -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]) diff --git a/tests/test_e2e.nim b/tests/test_e2e.nim index edb0d4c..f61b57f 100644 --- a/tests/test_e2e.nim +++ b/tests/test_e2e.nim @@ -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("hello") + let res = await conn.query("SELECT $1::xml", @[toPgParam(v)]) + doAssert res.rows.len == 1 + let got = res.rows[0].getXml(0) + doAssert $got == "hello" + await conn.close() + + waitFor t() + + test "xmlparse function": + proc t() {.async.} = + let conn = await connect(plainConfig()) + let res = await conn.query("SELECT xmlparse(CONTENT 'test')") + doAssert res.rows.len == 1 + let v = res.rows[0].getXml(0) + doAssert "test" == $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 'data'::xml", resultFormat = rfBinary) + doAssert res.rows.len == 1 + let v = res.rows[0].getXml(0) + doAssert "data" == $v + await conn.close() + + waitFor t() diff --git a/tests/test_types.nim b/tests/test_types.nim index 49ba3b6..2b2e3f2 100644 --- a/tests/test_types.nim +++ b/tests/test_types.nim @@ -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("hello") + let p = toPgParam(v) + check p.oid == OidXml + check p.format == 0 + check toString(p.value.get) == "hello" + + test "toPgBinaryParam PgXml sends binary format": + let v = PgXml("") + let p = toPgBinaryParam(v) + check p.oid == OidXml + check p.format == 1 + + test "$ PgXml": + let v = PgXml("") + check $v == "" + + test "== PgXml": + check PgXml("") == PgXml("") + check PgXml("") != PgXml("") + + test "getXml text format": + let data = toBytes("test") + let fields = @[mkField(OidXml, 0'i16)] + let row = mkRow(@[some(data)], fields) + check $row.getXml(0) == "test" + + test "getXml binary format": + let data = toBytes("test") + let fields = @[mkField(OidXml, 1'i16)] + let row = mkRow(@[some(data)], fields) + check $row.getXml(0) == "test" + + test "getXmlOpt some": + let data = toBytes("") + let fields = @[mkField(OidXml, 0'i16)] + let row = mkRow(@[some(data)], fields) + let r = row.getXmlOpt(0) + check r.isSome + check $r.get == "" + + test "getXmlOpt none": + let fields = @[mkField(OidXml, 0'i16)] + let row = mkRow(@[none(seq[byte])], fields) + check row.getXmlOpt(0).isNone