diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 3acc11f..9a75de4 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -30,7 +30,7 @@ jobs:
strategy:
matrix:
nim-version:
- - '2.2.0'
+ - '2.2.4'
- 'stable'
- 'devel'
diff --git a/README.md b/README.md
index 3f5f8d9..d35daf0 100644
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@ Async PostgreSQL client in Nim.
## Requirements
-- Nim >= 2.2.0
+- Nim >= 2.2.4
## Installation
diff --git a/async_postgres.nimble b/async_postgres.nimble
index 698daf9..6ec6fee 100644
--- a/async_postgres.nimble
+++ b/async_postgres.nimble
@@ -7,7 +7,7 @@ license = "MIT"
# Dependencies
-requires "nim >= 2.2.0"
+requires "nim >= 2.2.4"
requires "nimcrypto >= 0.6.0"
requires "checksums >= 0.2.0"
diff --git a/async_postgres/pg_protocol.nim b/async_postgres/pg_protocol.nim
index b426784..1678a2b 100644
--- a/async_postgres/pg_protocol.nim
+++ b/async_postgres/pg_protocol.nim
@@ -174,25 +174,69 @@ const
21, # int2 / smallint
23, # int4 / integer
25, # text
+ 114, # json
+ 142, # xml
+ 143, # xml[]
600, # point
601, # lseg
602, # path
603, # box
604, # polygon
628, # line
+ 629, # line[]
+ 650, # cidr
+ 651, # cidr[]
700, # float4
701, # float8
718, # circle
+ 719, # circle[]
+ 774, # macaddr8
+ 775, # macaddr8[]
+ 829, # macaddr
+ 869, # inet
+ 1000, # bool[]
+ 1001, # bytea[]
+ 1005, # int2[]
+ 1007, # int4[]
+ 1009, # text[]
+ 1015, # varchar[]
+ 1016, # int8[]
+ 1017, # point[]
+ 1018, # lseg[]
+ 1019, # path[]
+ 1020, # box[]
+ 1021, # float4[]
+ 1022, # float8[]
+ 1027, # polygon[]
+ 1040, # macaddr[]
+ 1041, # inet[]
1043, # varchar
1082, # date
1083, # time
1114, # timestamp
+ 1115, # timestamp[]
+ 1182, # date[]
+ 1183, # time[]
1184, # timestamptz
+ 1185, # timestamptz[]
+ 1186, # interval
+ 1187, # interval[]
+ 1231, # numeric[]
1266, # timetz
+ 1270, # timetz[]
1560, # bit
1561, # bit[]
1562, # varbit
1563, # varbit[]
+ 1700, # numeric
+ 2950, # uuid
+ 2951, # uuid[]
+ 3614, # tsvector
+ 3615, # tsquery
+ 3643, # tsvector[]
+ 3645, # tsquery[]
+ 3802, # jsonb
+ 3807, # jsonb[]
3904, # int4range
3905, # int4range[]
3906, # numrange
@@ -211,9 +255,15 @@ const
4534, # tstzmultirange
4535, # datemultirange
4536, # int8multirange
+ 6150, # int4multirange[]
+ 6151, # nummultirange[]
+ 6152, # tsmultirange[]
+ 6153, # tstzmultirange[]
+ 6155, # datemultirange[]
+ 6157, # int8multirange[]
]
- BinarySafeMaxOid = 4536
+ BinarySafeMaxOid = 6157
pgCopyBinaryHeader*: array[19, byte] = [
## PGCOPY binary format header (signature + flags + extension length).
diff --git a/async_postgres/pg_types.nim b/async_postgres/pg_types.nim
index 3bf19fe..523c5b6 100644
--- a/async_postgres/pg_types.nim
+++ b/async_postgres/pg_types.nim
@@ -226,6 +226,14 @@ const
OidDateMultirange* = 4535'i32
OidInt8Multirange* = 4536'i32
+ # Multirange array types (PostgreSQL 14+)
+ OidInt4MultirangeArray* = 6150'i32
+ OidNumMultirangeArray* = 6151'i32
+ OidTsMultirangeArray* = 6152'i32
+ OidTsTzMultirangeArray* = 6153'i32
+ OidDateMultirangeArray* = 6155'i32
+ OidInt8MultirangeArray* = 6157'i32
+
# Full-text search types
OidTsVector* = 3614'i32
OidTsQuery* = 3615'i32
@@ -234,6 +242,30 @@ const
OidBit* = 1560'i32
OidVarbit* = 1562'i32
+ OidByteaArray* = 1001'i32
+ OidTimestampArray* = 1115'i32
+ OidDateArray* = 1182'i32
+ OidTimeArray* = 1183'i32
+ OidTimestampTzArray* = 1185'i32
+ OidIntervalArray* = 1187'i32
+ OidNumericArray* = 1231'i32
+ OidTimeTzArray* = 1270'i32
+ OidUuidArray* = 2951'i32
+ OidJsonbArray* = 3807'i32
+ OidInetArray* = 1041'i32
+ OidCidrArray* = 651'i32
+ OidMacAddrArray* = 1040'i32
+ OidMacAddr8Array* = 775'i32
+ OidPointArray* = 1017'i32
+ OidLsegArray* = 1018'i32
+ OidPathArray* = 1019'i32
+ OidBoxArray* = 1020'i32
+ OidPolygonArray* = 1027'i32
+ OidLineArray* = 629'i32
+ OidCircleArray* = 719'i32
+ OidXmlArray* = 143'i32
+ OidTsVectorArray* = 3643'i32
+ OidTsQueryArray* = 3645'i32
OidBitArray* = 1561'i32
OidVarbitArray* = 1563'i32
@@ -692,6 +724,16 @@ proc fromBE64*(data: openArray[byte]): int64 =
int64(data[3]) shl 32 or int64(data[4]) shl 24 or int64(data[5]) shl 16 or
int64(data[6]) shl 8 or int64(data[7])
+proc decodeFloat64BE*(data: openArray[byte], offset: int = 0): float64 =
+ ## Decode a big-endian 64-bit float from bytes at the given offset.
+ var bits: uint64
+ bits =
+ (uint64(data[offset]) shl 56) or (uint64(data[offset + 1]) shl 48) or
+ (uint64(data[offset + 2]) shl 40) or (uint64(data[offset + 3]) shl 32) or
+ (uint64(data[offset + 4]) shl 24) or (uint64(data[offset + 5]) shl 16) or
+ (uint64(data[offset + 6]) shl 8) or uint64(data[offset + 7])
+ copyMem(addr result, addr bits, 8)
+
proc toPgParam*(v: string): PgParam =
## Convert a Nim value to a PgParam for use as a query parameter.
## Uses text format for strings, binary for numeric types.
@@ -1188,6 +1230,196 @@ proc toPgBinaryParam*(v: seq[PgBit]): PgParam =
proc toPgParam*(v: seq[PgBit]): PgParam =
toPgBinaryParam(v)
+# --- Temporal array encoders ---
+
+proc toPgTimestampArrayParam*(v: seq[DateTime]): PgParam =
+ if v.len == 0:
+ return PgParam(
+ oid: OidTimestampArray,
+ format: 1,
+ value: some(encodeBinaryArrayEmpty(OidTimestamp)),
+ )
+ var elements = newSeq[seq[byte]](v.len)
+ for i, x in v:
+ elements[i] = toPgBinaryParam(x).value.get
+ PgParam(
+ oid: OidTimestampArray,
+ format: 1,
+ value: some(encodeBinaryArray(OidTimestamp, elements)),
+ )
+
+proc toPgTimestampTzArrayParam*(v: seq[DateTime]): PgParam =
+ if v.len == 0:
+ return PgParam(
+ oid: OidTimestampTzArray,
+ format: 1,
+ value: some(encodeBinaryArrayEmpty(OidTimestampTz)),
+ )
+ var elements = newSeq[seq[byte]](v.len)
+ for i, x in v:
+ elements[i] = toPgBinaryTimestampTzParam(x).value.get
+ PgParam(
+ oid: OidTimestampTzArray,
+ format: 1,
+ value: some(encodeBinaryArray(OidTimestampTz, elements)),
+ )
+
+proc toPgDateArrayParam*(v: seq[DateTime]): PgParam =
+ if v.len == 0:
+ return PgParam(
+ oid: OidDateArray, format: 1, value: some(encodeBinaryArrayEmpty(OidDate))
+ )
+ var elements = newSeq[seq[byte]](v.len)
+ for i, x in v:
+ elements[i] = toPgBinaryDateParam(x).value.get
+ PgParam(
+ oid: OidDateArray, format: 1, value: some(encodeBinaryArray(OidDate, elements))
+ )
+
+proc toPgParam*(v: seq[PgTime]): PgParam =
+ if v.len == 0:
+ return PgParam(
+ oid: OidTimeArray, format: 1, value: some(encodeBinaryArrayEmpty(OidTime))
+ )
+ var elements = newSeq[seq[byte]](v.len)
+ for i, x in v:
+ elements[i] = toPgBinaryParam(x).value.get
+ PgParam(
+ oid: OidTimeArray, format: 1, value: some(encodeBinaryArray(OidTime, elements))
+ )
+
+proc toPgParam*(v: seq[PgTimeTz]): PgParam =
+ if v.len == 0:
+ return PgParam(
+ oid: OidTimeTzArray, format: 1, value: some(encodeBinaryArrayEmpty(OidTimeTz))
+ )
+ var elements = newSeq[seq[byte]](v.len)
+ for i, x in v:
+ elements[i] = toPgBinaryParam(x).value.get
+ PgParam(
+ oid: OidTimeTzArray, format: 1, value: some(encodeBinaryArray(OidTimeTz, elements))
+ )
+
+proc toPgParam*(v: seq[PgInterval]): PgParam =
+ if v.len == 0:
+ return PgParam(
+ oid: OidIntervalArray, format: 1, value: some(encodeBinaryArrayEmpty(OidInterval))
+ )
+ var elements = newSeq[seq[byte]](v.len)
+ for i, x in v:
+ elements[i] = toPgBinaryParam(x).value.get
+ PgParam(
+ oid: OidIntervalArray,
+ format: 1,
+ value: some(encodeBinaryArray(OidInterval, elements)),
+ )
+
+# --- Identifier / network array encoders ---
+
+proc toPgParam*(v: seq[PgUuid]): PgParam =
+ if v.len == 0:
+ return PgParam(
+ oid: OidUuidArray, format: 1, value: some(encodeBinaryArrayEmpty(OidUuid))
+ )
+ var elements = newSeq[seq[byte]](v.len)
+ for i, x in v:
+ elements[i] = toPgBinaryParam(x).value.get
+ PgParam(
+ oid: OidUuidArray, format: 1, value: some(encodeBinaryArray(OidUuid, elements))
+ )
+
+proc toPgParam*(v: seq[PgInet]): PgParam =
+ if v.len == 0:
+ return PgParam(
+ oid: OidInetArray, format: 1, value: some(encodeBinaryArrayEmpty(OidInet))
+ )
+ var elements = newSeq[seq[byte]](v.len)
+ for i, x in v:
+ elements[i] = toPgBinaryParam(x).value.get
+ PgParam(
+ oid: OidInetArray, format: 1, value: some(encodeBinaryArray(OidInet, elements))
+ )
+
+proc toPgParam*(v: seq[PgCidr]): PgParam =
+ if v.len == 0:
+ return PgParam(
+ oid: OidCidrArray, format: 1, value: some(encodeBinaryArrayEmpty(OidCidr))
+ )
+ var elements = newSeq[seq[byte]](v.len)
+ for i, x in v:
+ elements[i] = toPgBinaryParam(x).value.get
+ PgParam(
+ oid: OidCidrArray, format: 1, value: some(encodeBinaryArray(OidCidr, elements))
+ )
+
+proc toPgParam*(v: seq[PgMacAddr]): PgParam =
+ if v.len == 0:
+ return PgParam(
+ oid: OidMacAddrArray, format: 1, value: some(encodeBinaryArrayEmpty(OidMacAddr))
+ )
+ var elements = newSeq[seq[byte]](v.len)
+ for i, x in v:
+ elements[i] = toPgBinaryParam(x).value.get
+ PgParam(
+ oid: OidMacAddrArray,
+ format: 1,
+ value: some(encodeBinaryArray(OidMacAddr, elements)),
+ )
+
+proc toPgParam*(v: seq[PgMacAddr8]): PgParam =
+ if v.len == 0:
+ return PgParam(
+ oid: OidMacAddr8Array, format: 1, value: some(encodeBinaryArrayEmpty(OidMacAddr8))
+ )
+ var elements = newSeq[seq[byte]](v.len)
+ for i, x in v:
+ elements[i] = toPgBinaryParam(x).value.get
+ PgParam(
+ oid: OidMacAddr8Array,
+ format: 1,
+ value: some(encodeBinaryArray(OidMacAddr8, elements)),
+ )
+
+# --- Numeric / binary / JSON array encoders ---
+
+proc toPgParam*(v: seq[PgNumeric]): PgParam =
+ if v.len == 0:
+ return PgParam(
+ oid: OidNumericArray, format: 1, value: some(encodeBinaryArrayEmpty(OidNumeric))
+ )
+ var elements = newSeq[seq[byte]](v.len)
+ for i, x in v:
+ elements[i] = encodeNumericBinary(x)
+ PgParam(
+ oid: OidNumericArray,
+ format: 1,
+ value: some(encodeBinaryArray(OidNumeric, elements)),
+ )
+
+proc toPgByteaArrayParam*(v: seq[seq[byte]]): PgParam =
+ if v.len == 0:
+ return PgParam(
+ oid: OidByteaArray, format: 1, value: some(encodeBinaryArrayEmpty(OidBytea))
+ )
+ PgParam(oid: OidByteaArray, format: 1, value: some(encodeBinaryArray(OidBytea, v)))
+
+proc toPgParam*(v: seq[JsonNode]): PgParam =
+ if v.len == 0:
+ return PgParam(
+ oid: OidJsonbArray, format: 1, value: some(encodeBinaryArrayEmpty(OidJsonb))
+ )
+ var elements = newSeq[seq[byte]](v.len)
+ for i, x in v:
+ let jsonBytes = toBytes($x)
+ var data = newSeq[byte](1 + jsonBytes.len)
+ data[0] = 1 # jsonb version byte
+ for j in 0 ..< jsonBytes.len:
+ data[j + 1] = jsonBytes[j]
+ elements[i] = data
+ PgParam(
+ oid: OidJsonbArray, format: 1, value: some(encodeBinaryArray(OidJsonb, elements))
+ )
+
proc encodePointBinary(p: PgPoint): seq[byte] =
## Encode a point as 16 bytes (two float64 big-endian).
result = newSeq[byte](16)
@@ -1267,6 +1499,130 @@ proc toPgBinaryParam*(v: JsonNode): PgParam =
data[i + 1] = jsonBytes[i]
PgParam(oid: OidJsonb, format: 1, value: some(data))
+# --- Geometric array encoders ---
+
+proc toPgParam*(v: seq[PgPoint]): PgParam =
+ if v.len == 0:
+ return PgParam(
+ oid: OidPointArray, format: 1, value: some(encodeBinaryArrayEmpty(OidPoint))
+ )
+ var elements = newSeq[seq[byte]](v.len)
+ for i, x in v:
+ elements[i] = toPgBinaryParam(x).value.get
+ PgParam(
+ oid: OidPointArray, format: 1, value: some(encodeBinaryArray(OidPoint, elements))
+ )
+
+proc toPgParam*(v: seq[PgLine]): PgParam =
+ if v.len == 0:
+ return PgParam(
+ oid: OidLineArray, format: 1, value: some(encodeBinaryArrayEmpty(OidLine))
+ )
+ var elements = newSeq[seq[byte]](v.len)
+ for i, x in v:
+ elements[i] = toPgBinaryParam(x).value.get
+ PgParam(
+ oid: OidLineArray, format: 1, value: some(encodeBinaryArray(OidLine, elements))
+ )
+
+proc toPgParam*(v: seq[PgLseg]): PgParam =
+ if v.len == 0:
+ return PgParam(
+ oid: OidLsegArray, format: 1, value: some(encodeBinaryArrayEmpty(OidLseg))
+ )
+ var elements = newSeq[seq[byte]](v.len)
+ for i, x in v:
+ elements[i] = toPgBinaryParam(x).value.get
+ PgParam(
+ oid: OidLsegArray, format: 1, value: some(encodeBinaryArray(OidLseg, elements))
+ )
+
+proc toPgParam*(v: seq[PgBox]): PgParam =
+ if v.len == 0:
+ return
+ PgParam(oid: OidBoxArray, format: 1, value: some(encodeBinaryArrayEmpty(OidBox)))
+ var elements = newSeq[seq[byte]](v.len)
+ for i, x in v:
+ elements[i] = toPgBinaryParam(x).value.get
+ PgParam(oid: OidBoxArray, format: 1, value: some(encodeBinaryArray(OidBox, elements)))
+
+proc toPgParam*(v: seq[PgPath]): PgParam =
+ if v.len == 0:
+ return PgParam(
+ oid: OidPathArray, format: 1, value: some(encodeBinaryArrayEmpty(OidPath))
+ )
+ var elements = newSeq[seq[byte]](v.len)
+ for i, x in v:
+ elements[i] = toPgBinaryParam(x).value.get
+ PgParam(
+ oid: OidPathArray, format: 1, value: some(encodeBinaryArray(OidPath, elements))
+ )
+
+proc toPgParam*(v: seq[PgPolygon]): PgParam =
+ if v.len == 0:
+ return PgParam(
+ oid: OidPolygonArray, format: 1, value: some(encodeBinaryArrayEmpty(OidPolygon))
+ )
+ var elements = newSeq[seq[byte]](v.len)
+ for i, x in v:
+ elements[i] = toPgBinaryParam(x).value.get
+ PgParam(
+ oid: OidPolygonArray,
+ format: 1,
+ value: some(encodeBinaryArray(OidPolygon, elements)),
+ )
+
+proc toPgParam*(v: seq[PgCircle]): PgParam =
+ if v.len == 0:
+ return PgParam(
+ oid: OidCircleArray, format: 1, value: some(encodeBinaryArrayEmpty(OidCircle))
+ )
+ var elements = newSeq[seq[byte]](v.len)
+ for i, x in v:
+ elements[i] = toPgBinaryParam(x).value.get
+ PgParam(
+ oid: OidCircleArray, format: 1, value: some(encodeBinaryArray(OidCircle, elements))
+ )
+
+# --- Other array encoders ---
+
+proc toPgParam*(v: seq[PgXml]): PgParam =
+ if v.len == 0:
+ return
+ PgParam(oid: OidXmlArray, format: 1, value: some(encodeBinaryArrayEmpty(OidXml)))
+ var elements = newSeq[seq[byte]](v.len)
+ for i, x in v:
+ elements[i] = toBytes(string(x))
+ PgParam(oid: OidXmlArray, format: 1, value: some(encodeBinaryArray(OidXml, elements)))
+
+proc toPgParam*(v: seq[PgTsVector]): PgParam =
+ if v.len == 0:
+ return PgParam(
+ oid: OidTsVectorArray, format: 1, value: some(encodeBinaryArrayEmpty(OidTsVector))
+ )
+ var elements = newSeq[seq[byte]](v.len)
+ for i, x in v:
+ elements[i] = toBytes(string(x))
+ PgParam(
+ oid: OidTsVectorArray,
+ format: 1,
+ value: some(encodeBinaryArray(OidTsVector, elements)),
+ )
+
+proc toPgParam*(v: seq[PgTsQuery]): PgParam =
+ if v.len == 0:
+ return PgParam(
+ oid: OidTsQueryArray, format: 1, value: some(encodeBinaryArrayEmpty(OidTsQuery))
+ )
+ var elements = newSeq[seq[byte]](v.len)
+ for i, x in v:
+ elements[i] = toBytes(string(x))
+ PgParam(
+ oid: OidTsQueryArray,
+ format: 1,
+ value: some(encodeBinaryArray(OidTsQuery, elements)),
+ )
+
proc toPgBinaryParam*[T](v: seq[T]): PgParam =
toPgParam(v)
@@ -2026,14 +2382,7 @@ proc getBytes*(row: Row, col: int): seq[byte] =
if clen > 0:
copyMem(addr result[0], unsafeAddr row.data.buf[off], clen)
-proc getTimestamp*(row: Row, col: int): DateTime =
- ## Get a column value as DateTime. Handles binary timestamp format.
- if row.isBinaryCol(col):
- let (off, clen) = cellInfo(row, col)
- if clen == -1:
- raise newException(PgTypeError, "Column " & $col & " is NULL")
- return decodeBinaryTimestamp(row.data.buf.toOpenArray(off, off + 7))
- let s = row.getStr(col)
+proc parseTimestampText(s: string): DateTime =
const formats = [
"yyyy-MM-dd HH:mm:ss'.'ffffffzzz", "yyyy-MM-dd HH:mm:ss'.'ffffffzz",
"yyyy-MM-dd HH:mm:ss'.'ffffff", "yyyy-MM-dd HH:mm:sszzz", "yyyy-MM-dd HH:mm:sszz",
@@ -2046,6 +2395,16 @@ proc getTimestamp*(row: Row, col: int): DateTime =
discard
raise newException(PgTypeError, "Invalid timestamp: " & s)
+proc getTimestamp*(row: Row, col: int): DateTime =
+ ## Get a column value as DateTime. Handles binary timestamp format.
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ return decodeBinaryTimestamp(row.data.buf.toOpenArray(off, off + 7))
+ let s = row.getStr(col)
+ return parseTimestampText(s)
+
proc getDate*(row: Row, col: int): DateTime =
## Get a column value as DateTime. Handles binary date format.
if row.isBinaryCol(col):
@@ -2067,17 +2426,7 @@ proc getTimestampTz*(row: Row, col: int): DateTime =
raise newException(PgTypeError, "Column " & $col & " is NULL")
return decodeBinaryTimestamp(row.data.buf.toOpenArray(off, off + 7))
let s = row.getStr(col)
- const formats = [
- "yyyy-MM-dd HH:mm:ss'.'ffffffzzz", "yyyy-MM-dd HH:mm:ss'.'ffffffzz",
- "yyyy-MM-dd HH:mm:ss'.'ffffff", "yyyy-MM-dd HH:mm:sszzz", "yyyy-MM-dd HH:mm:sszz",
- "yyyy-MM-dd HH:mm:ss",
- ]
- for fmt in formats:
- try:
- return parse(s, fmt)
- except TimeParseError, IndexDefect:
- discard
- raise newException(PgTypeError, "Invalid timestamptz: " & s)
+ return parseTimestampText(s)
proc parseTimeText(s: string): PgTime =
## Parse PostgreSQL time text format: "HH:mm:ss" or "HH:mm:ss.ffffff".
@@ -2105,25 +2454,7 @@ proc parseTimeText(s: string): PgTime =
us *= 10
PgTime(hour: int32(h), minute: int32(m), second: int32(sec), microsecond: int32(us))
-proc getTime*(row: Row, col: int): PgTime =
- ## Get a column value as PgTime. Handles binary time format.
- if row.isBinaryCol(col):
- let (off, clen) = cellInfo(row, col)
- if clen == -1:
- raise newException(PgTypeError, "Column " & $col & " is NULL")
- return decodeBinaryTime(row.data.buf.toOpenArray(off, off + 7))
- let s = row.getStr(col)
- return parseTimeText(s)
-
-proc getTimeTz*(row: Row, col: int): PgTimeTz =
- ## Get a column value as PgTimeTz. Handles binary timetz format.
- if row.isBinaryCol(col):
- let (off, clen) = cellInfo(row, col)
- if clen == -1:
- raise newException(PgTypeError, "Column " & $col & " is NULL")
- return decodeBinaryTimeTz(row.data.buf.toOpenArray(off, off + 11))
- let s = row.getStr(col)
- # Find the timezone offset separator (+ or - after the time part)
+proc parseTimeTzText(s: string): PgTimeTz =
var tzPos = -1
for i in 8 ..< s.len:
if s[i] == '+' or s[i] == '-':
@@ -2133,7 +2464,6 @@ proc getTimeTz*(row: Row, col: int): PgTimeTz =
raise newException(PgTypeError, "Invalid timetz (no offset): " & s)
let timePart = s[0 ..< tzPos]
let t = parseTimeText(timePart)
- # Parse offset: "+HH", "+HH:MM", "+HH:MM:SS"
let sign = if s[tzPos] == '+': 1 else: -1
let offStr = s[tzPos + 1 .. ^1]
var offH, offM, offS: int
@@ -2160,6 +2490,26 @@ proc getTimeTz*(row: Row, col: int): PgTimeTz =
utcOffset: int32(utcOff),
)
+proc getTime*(row: Row, col: int): PgTime =
+ ## Get a column value as PgTime. Handles binary time format.
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ return decodeBinaryTime(row.data.buf.toOpenArray(off, off + 7))
+ let s = row.getStr(col)
+ return parseTimeText(s)
+
+proc getTimeTz*(row: Row, col: int): PgTimeTz =
+ ## Get a column value as PgTimeTz. Handles binary timetz format.
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ return decodeBinaryTimeTz(row.data.buf.toOpenArray(off, off + 11))
+ let s = row.getStr(col)
+ return parseTimeTzText(s)
+
proc parseHstoreText*(s: string): PgHstore =
## Parse PostgreSQL hstore text format: ``"key1"=>"val1", "key2"=>NULL``.
result = initTable[string, Option[string]]()
@@ -2661,26 +3011,9 @@ proc getLine*(row: Row, col: int): PgLine =
raise newException(PgTypeError, "Column " & $col & " is NULL")
if clen != 24:
raise newException(PgTypeError, "Invalid binary line length: " & $clen)
- let b = row.data.buf
- var aBits, bBits, cBits: uint64
- aBits =
- (uint64(b[off]) shl 56) or (uint64(b[off + 1]) shl 48) or
- (uint64(b[off + 2]) shl 40) or (uint64(b[off + 3]) shl 32) or
- (uint64(b[off + 4]) shl 24) or (uint64(b[off + 5]) shl 16) or
- (uint64(b[off + 6]) shl 8) or uint64(b[off + 7])
- bBits =
- (uint64(b[off + 8]) shl 56) or (uint64(b[off + 9]) shl 48) or
- (uint64(b[off + 10]) shl 40) or (uint64(b[off + 11]) shl 32) or
- (uint64(b[off + 12]) shl 24) or (uint64(b[off + 13]) shl 16) or
- (uint64(b[off + 14]) shl 8) or uint64(b[off + 15])
- cBits =
- (uint64(b[off + 16]) shl 56) or (uint64(b[off + 17]) shl 48) or
- (uint64(b[off + 18]) shl 40) or (uint64(b[off + 19]) shl 32) or
- (uint64(b[off + 20]) shl 24) or (uint64(b[off + 21]) shl 16) or
- (uint64(b[off + 22]) shl 8) or uint64(b[off + 23])
- copyMem(addr result.a, addr aBits, 8)
- copyMem(addr result.b, addr bBits, 8)
- copyMem(addr result.c, addr cBits, 8)
+ result.a = decodeFloat64BE(row.data.buf, off)
+ result.b = decodeFloat64BE(row.data.buf, off + 8)
+ result.c = decodeFloat64BE(row.data.buf, off + 16)
return
let s = row.getStr(col)
var inner = s.strip()
@@ -2780,14 +3113,7 @@ proc getCircle*(row: Row, col: int): PgCircle =
if clen != 24:
raise newException(PgTypeError, "Invalid binary circle length: " & $clen)
result.center = decodePointBinary(row.data.buf, off)
- var rBits: uint64
- let b = row.data.buf
- rBits =
- (uint64(b[off + 16]) shl 56) or (uint64(b[off + 17]) shl 48) or
- (uint64(b[off + 18]) shl 40) or (uint64(b[off + 19]) shl 32) or
- (uint64(b[off + 20]) shl 24) or (uint64(b[off + 21]) shl 16) or
- (uint64(b[off + 22]) shl 8) or uint64(b[off + 23])
- copyMem(addr result.radius, addr rBits, 8)
+ result.radius = decodeFloat64BE(row.data.buf, off + 16)
return
let s = row.getStr(col).strip()
if s.len < 2 or s[0] != '<' or s[^1] != '>':
@@ -3002,95 +3328,726 @@ proc getFloat32Array*(row: Row, col: int): seq[float32] =
if clen == -1:
raise newException(PgTypeError, "Column " & $col & " is NULL")
let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
- result = newSeq[float32](decoded.elements.len)
+ result = newSeq[float32](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in float32 array")
+ result[i] = cast[float32](cast[uint32](fromBE32(
+ row.data.buf.toOpenArray(off + e.off, off + e.off + e.len - 1)
+ )))
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in float32 array")
+ result.add(float32(parseFloat(e.get)))
+
+proc getBoolArray*(row: Row, col: int): seq[bool] =
+ ## Get a column value as a seq of bool. Handles binary array format.
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[bool](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in bool array")
+ result[i] = row.data.buf[off + e.off] == 1'u8
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in bool array")
+ case e.get
+ of "t", "true", "1":
+ result.add(true)
+ of "f", "false", "0":
+ result.add(false)
+ else:
+ raise newException(PgTypeError, "Invalid boolean: " & e.get)
+
+proc getStrArray*(row: Row, col: int): seq[string] =
+ ## Get a column value as a seq of strings. Handles binary array format.
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[string](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in string array")
+ result[i] = newString(e.len)
+ if e.len > 0:
+ copyMem(addr result[i][0], unsafeAddr row.data.buf[off + e.off], e.len)
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in string array")
+ result.add(e.get)
+
+proc getBitArray*(row: Row, col: int): seq[PgBit] =
+ ## Get a column value as a seq of PgBit. 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")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[PgBit](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in bit array")
+ if e.len < 4:
+ raise newException(PgTypeError, "Invalid binary bit element: too short")
+ let nbits = fromBE32(row.data.buf.toOpenArray(off + e.off, off + e.off + 3))
+ let dataLen = e.len - 4
+ var data = newSeq[byte](dataLen)
+ for j in 0 ..< dataLen:
+ data[j] = row.data.buf[off + e.off + 4 + j]
+ result[i] = PgBit(nbits: nbits, data: data)
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in bit array")
+ result.add(parseBitString(e.get))
+
+# --- Temporal array decoders ---
+
+proc getTimestampArray*(row: Row, col: int): seq[DateTime] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[DateTime](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in timestamp array")
+ result[i] =
+ decodeBinaryTimestamp(row.data.buf.toOpenArray(off + e.off, off + e.off + 7))
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in timestamp array")
+ result.add(parseTimestampText(e.get))
+
+proc getTimestampTzArray*(row: Row, col: int): seq[DateTime] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[DateTime](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in timestamptz array")
+ result[i] =
+ decodeBinaryTimestamp(row.data.buf.toOpenArray(off + e.off, off + e.off + 7))
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in timestamptz array")
+ result.add(parseTimestampText(e.get))
+
+proc getDateArray*(row: Row, col: int): seq[DateTime] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[DateTime](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in date array")
+ result[i] =
+ decodeBinaryDate(row.data.buf.toOpenArray(off + e.off, off + e.off + 3))
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in date array")
+ try:
+ result.add(parse(e.get, "yyyy-MM-dd"))
+ except TimeParseError:
+ raise newException(PgTypeError, "Invalid date: " & e.get)
+
+proc getTimeArray*(row: Row, col: int): seq[PgTime] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[PgTime](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in time array")
+ result[i] =
+ decodeBinaryTime(row.data.buf.toOpenArray(off + e.off, off + e.off + 7))
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in time array")
+ result.add(parseTimeText(e.get))
+
+proc getTimeTzArray*(row: Row, col: int): seq[PgTimeTz] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[PgTimeTz](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in timetz array")
+ result[i] =
+ decodeBinaryTimeTz(row.data.buf.toOpenArray(off + e.off, off + e.off + 11))
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in timetz array")
+ result.add(parseTimeTzText(e.get))
+
+proc getIntervalArray*(row: Row, col: int): seq[PgInterval] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[PgInterval](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in interval array")
+ if e.len != 16:
+ raise
+ newException(PgTypeError, "Invalid binary interval element length: " & $e.len)
+ result[i].microseconds =
+ fromBE64(row.data.buf.toOpenArray(off + e.off, off + e.off + 7))
+ result[i].days =
+ fromBE32(row.data.buf.toOpenArray(off + e.off + 8, off + e.off + 11))
+ result[i].months =
+ fromBE32(row.data.buf.toOpenArray(off + e.off + 12, off + e.off + 15))
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in interval array")
+ result.add(parseIntervalText(e.get))
+
+# --- Identifier / network array decoders ---
+
+proc getUuidArray*(row: Row, col: int): seq[PgUuid] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[PgUuid](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in uuid array")
+ if e.len != 16:
+ raise newException(PgTypeError, "Invalid binary uuid element length: " & $e.len)
+ const hexChars = "0123456789abcdef"
+ var s = newString(36)
+ var pos = 0
+ for j in 0 ..< 16:
+ if j == 4 or j == 6 or j == 8 or j == 10:
+ s[pos] = '-'
+ inc pos
+ let b = row.data.buf[off + e.off + j]
+ s[pos] = hexChars[int(b shr 4)]
+ s[pos + 1] = hexChars[int(b and 0x0F)]
+ pos += 2
+ result[i] = PgUuid(s)
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in uuid array")
+ result.add(PgUuid(e.get))
+
+proc getInetArray*(row: Row, col: int): seq[PgInet] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[PgInet](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in inet array")
+ let (ip, mask) =
+ decodeInetBinary(row.data.buf.toOpenArray(off + e.off, off + e.off + e.len - 1))
+ result[i] = PgInet(address: ip, mask: mask)
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in inet array")
+ let (ip, mask) = parseInetText(e.get)
+ result.add(PgInet(address: ip, mask: mask))
+
+proc getCidrArray*(row: Row, col: int): seq[PgCidr] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[PgCidr](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in cidr array")
+ let (ip, mask) =
+ decodeInetBinary(row.data.buf.toOpenArray(off + e.off, off + e.off + e.len - 1))
+ result[i] = PgCidr(address: ip, mask: mask)
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in cidr array")
+ let (ip, mask) = parseInetText(e.get)
+ result.add(PgCidr(address: ip, mask: mask))
+
+proc getMacAddrArray*(row: Row, col: int): seq[PgMacAddr] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[PgMacAddr](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in macaddr array")
+ if e.len != 6:
+ raise
+ newException(PgTypeError, "Invalid binary macaddr element length: " & $e.len)
+ var parts = newSeq[string](6)
+ for j in 0 ..< 6:
+ parts[j] = toHex(row.data.buf[off + e.off + j], 2).toLowerAscii()
+ result[i] = PgMacAddr(parts.join(":"))
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in macaddr array")
+ result.add(PgMacAddr(e.get))
+
+proc getMacAddr8Array*(row: Row, col: int): seq[PgMacAddr8] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[PgMacAddr8](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in macaddr8 array")
+ if e.len != 8:
+ raise
+ newException(PgTypeError, "Invalid binary macaddr8 element length: " & $e.len)
+ var parts = newSeq[string](8)
+ for j in 0 ..< 8:
+ parts[j] = toHex(row.data.buf[off + e.off + j], 2).toLowerAscii()
+ result[i] = PgMacAddr8(parts.join(":"))
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in macaddr8 array")
+ result.add(PgMacAddr8(e.get))
+
+# --- Numeric / binary / JSON array decoders ---
+
+proc getNumericArray*(row: Row, col: int): seq[PgNumeric] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[PgNumeric](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in numeric array")
+ result[i] = decodeNumericBinary(
+ row.data.buf.toOpenArray(off + e.off, off + e.off + e.len - 1)
+ )
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in numeric array")
+ result.add(parsePgNumeric(e.get))
+
+proc getBytesArray*(row: Row, col: int): seq[seq[byte]] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[seq[byte]](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in bytea array")
+ result[i] = newSeq[byte](e.len)
+ if e.len > 0:
+ copyMem(addr result[i][0], unsafeAddr row.data.buf[off + e.off], e.len)
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in bytea array")
+ let v = e.get
+ if v.len >= 2 and v[0] == '\\' and v[1] == 'x':
+ let hexStr = v[2 ..^ 1]
+ var bytes = newSeq[byte](hexStr.len div 2)
+ for j in 0 ..< bytes.len:
+ bytes[j] = byte(parseHexInt(hexStr[j * 2 .. j * 2 + 1]))
+ result.add(bytes)
+ else:
+ result.add(toBytes(v))
+
+proc getJsonArray*(row: Row, col: int): seq[JsonNode] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[JsonNode](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in json array")
+ var jsonStr: string
+ if decoded.elemOid == OidJsonb and e.len > 0 and row.data.buf[off + e.off] == 1:
+ jsonStr = newString(e.len - 1)
+ for j in 1 ..< e.len:
+ jsonStr[j - 1] = char(row.data.buf[off + e.off + j])
+ else:
+ jsonStr = newString(e.len)
+ for j in 0 ..< e.len:
+ jsonStr[j] = char(row.data.buf[off + e.off + j])
+ try:
+ result[i] = parseJson(jsonStr)
+ except JsonParsingError:
+ raise newException(PgTypeError, "Invalid JSON element: " & jsonStr)
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in json array")
+ try:
+ result.add(parseJson(e.get))
+ except JsonParsingError:
+ raise newException(PgTypeError, "Invalid JSON element: " & e.get)
+
+# --- Geometric array decoders ---
+
+proc getPointArray*(row: Row, col: int): seq[PgPoint] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[PgPoint](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in point array")
+ result[i] = decodePointBinary(row.data.buf, off + e.off)
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in point array")
+ result.add(parsePointText(e.get))
+
+proc getLineArray*(row: Row, col: int): seq[PgLine] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[PgLine](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in line array")
+ if e.len != 24:
+ raise newException(PgTypeError, "Invalid binary line element length: " & $e.len)
+ let o = off + e.off
+ result[i].a = decodeFloat64BE(row.data.buf, o)
+ result[i].b = decodeFloat64BE(row.data.buf, o + 8)
+ result[i].c = decodeFloat64BE(row.data.buf, o + 16)
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in line array")
+ let v = e.get.strip()
+ var inner = v
+ if inner.len >= 2 and inner[0] == '{' and inner[^1] == '}':
+ inner = inner[1 ..^ 2]
+ else:
+ raise newException(PgTypeError, "Invalid line: " & v)
+ let parts = inner.split(',')
+ if parts.len != 3:
+ raise newException(PgTypeError, "Invalid line: " & v)
+ result.add(
+ PgLine(a: parseFloat(parts[0]), b: parseFloat(parts[1]), c: parseFloat(parts[2]))
+ )
+
+proc getLsegArray*(row: Row, col: int): seq[PgLseg] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[PgLseg](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in lseg array")
+ result[i] = PgLseg(
+ p1: decodePointBinary(row.data.buf, off + e.off),
+ p2: decodePointBinary(row.data.buf, off + e.off + 16),
+ )
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in lseg array")
+ let v = e.get.strip()
+ var inner = v
+ if inner.len >= 2 and inner[0] == '[' and inner[^1] == ']':
+ inner = inner[1 ..^ 2]
+ let points = parsePointsText(inner)
+ if points.len != 2:
+ raise newException(PgTypeError, "Invalid lseg: " & v)
+ result.add(PgLseg(p1: points[0], p2: points[1]))
+
+proc getBoxArray*(row: Row, col: int): seq[PgBox] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[PgBox](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in box array")
+ result[i] = PgBox(
+ high: decodePointBinary(row.data.buf, off + e.off),
+ low: decodePointBinary(row.data.buf, off + e.off + 16),
+ )
+ return
+ # PostgreSQL uses ';' as array element delimiter for box type
+ let s = row.getStr(col)
+ if s.len < 2 or s[0] != '{' or s[^1] != '}':
+ raise newException(PgTypeError, "Invalid box array literal: " & s)
+ let inner = s[1 ..^ 2]
+ if inner.len == 0:
+ return
+ let parts = inner.split(';')
+ for p in parts:
+ let v = p.strip()
+ if v == "NULL":
+ raise newException(PgTypeError, "NULL element in box array")
+ let points = parsePointsText(v)
+ if points.len != 2:
+ raise newException(PgTypeError, "Invalid box: " & v)
+ result.add(PgBox(high: points[0], low: points[1]))
+
+proc getPathArray*(row: Row, col: int): seq[PgPath] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[PgPath](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in path array")
+ let b = row.data.buf
+ let o = off + e.off
+ result[i].closed = b[o] != 0
+ let npts = fromBE32(b.toOpenArray(o + 1, o + 4))
+ result[i].points = newSeq[PgPoint](npts)
+ for j in 0 ..< npts:
+ result[i].points[j] = decodePointBinary(b, o + 5 + j * 16)
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in path array")
+ let v = e.get.strip()
+ if v.len < 2:
+ raise newException(PgTypeError, "Invalid path: " & v)
+ let closed = v[0] == '('
+ let inner = v[1 ..^ 2]
+ result.add(PgPath(closed: closed, points: parsePointsText(inner)))
+
+proc getPolygonArray*(row: Row, col: int): seq[PgPolygon] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[PgPolygon](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in polygon array")
+ let b = row.data.buf
+ let o = off + e.off
+ let npts = fromBE32(b.toOpenArray(o, o + 3))
+ result[i].points = newSeq[PgPoint](npts)
+ for j in 0 ..< npts:
+ result[i].points[j] = decodePointBinary(b, o + 4 + j * 16)
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in polygon array")
+ let v = e.get.strip()
+ if v.len < 2 or v[0] != '(' or v[^1] != ')':
+ raise newException(PgTypeError, "Invalid polygon: " & v)
+ result.add(PgPolygon(points: parsePointsText(v[1 ..^ 2])))
+
+proc getCircleArray*(row: Row, col: int): seq[PgCircle] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[PgCircle](decoded.elements.len)
for i, e in decoded.elements:
if e.len == -1:
- raise newException(PgTypeError, "NULL element in float32 array")
- result[i] = cast[float32](cast[uint32](fromBE32(
- row.data.buf.toOpenArray(off + e.off, off + e.off + e.len - 1)
- )))
+ raise newException(PgTypeError, "NULL element in circle array")
+ if e.len != 24:
+ raise
+ newException(PgTypeError, "Invalid binary circle element length: " & $e.len)
+ result[i].center = decodePointBinary(row.data.buf, off + e.off)
+ result[i].radius = decodeFloat64BE(row.data.buf, off + e.off + 16)
return
let s = row.getStr(col)
let elems = parseTextArray(s)
for e in elems:
if e.isNone:
- raise newException(PgTypeError, "NULL element in float32 array")
- result.add(float32(parseFloat(e.get)))
+ raise newException(PgTypeError, "NULL element in circle array")
+ let v = e.get.strip()
+ if v.len < 2 or v[0] != '<' or v[^1] != '>':
+ raise newException(PgTypeError, "Invalid circle: " & v)
+ let inner = v[1 ..^ 2]
+ var depth = 0
+ var lastComma = -1
+ for j in 0 ..< inner.len:
+ if inner[j] == '(':
+ depth += 1
+ elif inner[j] == ')':
+ depth -= 1
+ elif inner[j] == ',' and depth == 0:
+ lastComma = j
+ if lastComma < 0:
+ raise newException(PgTypeError, "Invalid circle: " & v)
+ result.add(
+ PgCircle(
+ center: parsePointText(inner[0 ..< lastComma]),
+ radius: parseFloat(inner[lastComma + 1 ..^ 1]),
+ )
+ )
-proc getBoolArray*(row: Row, col: int): seq[bool] =
- ## Get a column value as a seq of bool. Handles binary array format.
+# --- Other array decoders ---
+
+proc getXmlArray*(row: Row, col: int): seq[PgXml] =
if row.isBinaryCol(col):
let (off, clen) = cellInfo(row, col)
if clen == -1:
raise newException(PgTypeError, "Column " & $col & " is NULL")
let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
- result = newSeq[bool](decoded.elements.len)
+ result = newSeq[PgXml](decoded.elements.len)
for i, e in decoded.elements:
if e.len == -1:
- raise newException(PgTypeError, "NULL element in bool array")
- result[i] = row.data.buf[off + e.off] == 1'u8
+ raise newException(PgTypeError, "NULL element in xml array")
+ var s = newString(e.len)
+ if e.len > 0:
+ copyMem(addr s[0], unsafeAddr row.data.buf[off + e.off], e.len)
+ result[i] = PgXml(s)
return
let s = row.getStr(col)
let elems = parseTextArray(s)
for e in elems:
if e.isNone:
- raise newException(PgTypeError, "NULL element in bool array")
- case e.get
- of "t", "true", "1":
- result.add(true)
- of "f", "false", "0":
- result.add(false)
- else:
- raise newException(PgTypeError, "Invalid boolean: " & e.get)
+ raise newException(PgTypeError, "NULL element in xml array")
+ result.add(PgXml(e.get))
-proc getStrArray*(row: Row, col: int): seq[string] =
- ## Get a column value as a seq of strings. Handles binary array format.
+proc getTsVectorArray*(row: Row, col: int): seq[PgTsVector] =
if row.isBinaryCol(col):
let (off, clen) = cellInfo(row, col)
if clen == -1:
raise newException(PgTypeError, "Column " & $col & " is NULL")
let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
- result = newSeq[string](decoded.elements.len)
+ result = newSeq[PgTsVector](decoded.elements.len)
for i, e in decoded.elements:
if e.len == -1:
- raise newException(PgTypeError, "NULL element in string array")
- result[i] = newString(e.len)
+ raise newException(PgTypeError, "NULL element in tsvector array")
+ var s = newString(e.len)
if e.len > 0:
- copyMem(addr result[i][0], unsafeAddr row.data.buf[off + e.off], e.len)
+ copyMem(addr s[0], unsafeAddr row.data.buf[off + e.off], e.len)
+ result[i] = PgTsVector(s)
return
let s = row.getStr(col)
let elems = parseTextArray(s)
for e in elems:
if e.isNone:
- raise newException(PgTypeError, "NULL element in string array")
- result.add(e.get)
+ raise newException(PgTypeError, "NULL element in tsvector array")
+ result.add(PgTsVector(e.get))
-proc getBitArray*(row: Row, col: int): seq[PgBit] =
- ## Get a column value as a seq of PgBit. Handles both text and binary format.
+proc getTsQueryArray*(row: Row, col: int): seq[PgTsQuery] =
if row.isBinaryCol(col):
let (off, clen) = cellInfo(row, col)
if clen == -1:
raise newException(PgTypeError, "Column " & $col & " is NULL")
let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
- result = newSeq[PgBit](decoded.elements.len)
+ result = newSeq[PgTsQuery](decoded.elements.len)
for i, e in decoded.elements:
if e.len == -1:
- raise newException(PgTypeError, "NULL element in bit array")
- if e.len < 4:
- raise newException(PgTypeError, "Invalid binary bit element: too short")
- let nbits = fromBE32(row.data.buf.toOpenArray(off + e.off, off + e.off + 3))
- let dataLen = e.len - 4
- var data = newSeq[byte](dataLen)
- for j in 0 ..< dataLen:
- data[j] = row.data.buf[off + e.off + 4 + j]
- result[i] = PgBit(nbits: nbits, data: data)
+ raise newException(PgTypeError, "NULL element in tsquery array")
+ var s = newString(e.len)
+ if e.len > 0:
+ copyMem(addr s[0], unsafeAddr row.data.buf[off + e.off], e.len)
+ result[i] = PgTsQuery(s)
return
let s = row.getStr(col)
let elems = parseTextArray(s)
for e in elems:
if e.isNone:
- raise newException(PgTypeError, "NULL element in bit array")
- result.add(parseBitString(e.get))
+ raise newException(PgTypeError, "NULL element in tsquery array")
+ result.add(PgTsQuery(e.get))
# Array Opt accessors (text format)
@@ -3102,6 +4059,30 @@ optAccessor(getFloat32Array, getFloat32ArrayOpt, seq[float32])
optAccessor(getBoolArray, getBoolArrayOpt, seq[bool])
optAccessor(getStrArray, getStrArrayOpt, seq[string])
optAccessor(getBitArray, getBitArrayOpt, seq[PgBit])
+optAccessor(getTimestampArray, getTimestampArrayOpt, seq[DateTime])
+optAccessor(getTimestampTzArray, getTimestampTzArrayOpt, seq[DateTime])
+optAccessor(getDateArray, getDateArrayOpt, seq[DateTime])
+optAccessor(getTimeArray, getTimeArrayOpt, seq[PgTime])
+optAccessor(getTimeTzArray, getTimeTzArrayOpt, seq[PgTimeTz])
+optAccessor(getIntervalArray, getIntervalArrayOpt, seq[PgInterval])
+optAccessor(getUuidArray, getUuidArrayOpt, seq[PgUuid])
+optAccessor(getInetArray, getInetArrayOpt, seq[PgInet])
+optAccessor(getCidrArray, getCidrArrayOpt, seq[PgCidr])
+optAccessor(getMacAddrArray, getMacAddrArrayOpt, seq[PgMacAddr])
+optAccessor(getMacAddr8Array, getMacAddr8ArrayOpt, seq[PgMacAddr8])
+optAccessor(getNumericArray, getNumericArrayOpt, seq[PgNumeric])
+optAccessor(getBytesArray, getBytesArrayOpt, seq[seq[byte]])
+optAccessor(getJsonArray, getJsonArrayOpt, seq[JsonNode])
+optAccessor(getPointArray, getPointArrayOpt, seq[PgPoint])
+optAccessor(getLineArray, getLineArrayOpt, seq[PgLine])
+optAccessor(getLsegArray, getLsegArrayOpt, seq[PgLseg])
+optAccessor(getBoxArray, getBoxArrayOpt, seq[PgBox])
+optAccessor(getPathArray, getPathArrayOpt, seq[PgPath])
+optAccessor(getPolygonArray, getPolygonArrayOpt, seq[PgPolygon])
+optAccessor(getCircleArray, getCircleArrayOpt, seq[PgCircle])
+optAccessor(getXmlArray, getXmlArrayOpt, seq[PgXml])
+optAccessor(getTsVectorArray, getTsVectorArrayOpt, seq[PgTsVector])
+optAccessor(getTsQueryArray, getTsQueryArrayOpt, seq[PgTsQuery])
# Coerce a binary PgParam to match the server-inferred type from a prepared
# statement. This handles the common case where e.g. int32.toPgParam is
@@ -4420,6 +5401,90 @@ proc toPgBinaryParam*(v: PgMultirange[DateTime]): PgParam =
oid: OidTsMultirange, format: 1, value: some(encodeMultirangeBinaryImpl(rangeData))
)
+# Multirange array encoders
+
+proc encodeMultirangeArrayText[T](v: seq[PgMultirange[T]]): string =
+ result = "{"
+ for i, x in v:
+ if i > 0:
+ result.add(',')
+ result.add('"')
+ let s = $x
+ for c in s:
+ if c == '"' or c == '\\':
+ result.add('\\')
+ result.add(c)
+ result.add('"')
+ result.add('}')
+
+proc toPgParam*(v: seq[PgMultirange[int32]]): PgParam =
+ PgParam(
+ oid: OidInt4MultirangeArray,
+ format: 0,
+ value: some(toBytes(encodeMultirangeArrayText(v))),
+ )
+
+proc toPgParam*(v: seq[PgMultirange[int64]]): PgParam =
+ PgParam(
+ oid: OidInt8MultirangeArray,
+ format: 0,
+ value: some(toBytes(encodeMultirangeArrayText(v))),
+ )
+
+proc toPgParam*(v: seq[PgMultirange[PgNumeric]]): PgParam =
+ PgParam(
+ oid: OidNumMultirangeArray,
+ format: 0,
+ value: some(toBytes(encodeMultirangeArrayText(v))),
+ )
+
+proc toPgTsMultirangeArrayParam*(v: seq[PgMultirange[DateTime]]): PgParam =
+ PgParam(
+ oid: OidTsMultirangeArray,
+ format: 0,
+ value: some(toBytes(encodeMultirangeArrayText(v))),
+ )
+
+proc toPgTsTzMultirangeArrayParam*(v: seq[PgMultirange[DateTime]]): PgParam =
+ PgParam(
+ oid: OidTsTzMultirangeArray,
+ format: 0,
+ value: some(toBytes(encodeMultirangeArrayText(v))),
+ )
+
+proc toPgDateMultirangeArrayParam*(v: seq[PgMultirange[DateTime]]): PgParam =
+ ## Encode date multirange array. DateTime values are formatted as date-only.
+ ## Cannot use the generic encodeMultirangeArrayText because DateTime's `$`
+ ## produces a timestamp format, but date ranges require "yyyy-MM-dd" only.
+ var s = "{"
+ for i, x in v:
+ if i > 0:
+ s.add(',')
+ s.add('"')
+ var mrStr = "{"
+ let ranges = seq[PgRange[DateTime]](x)
+ for j, r in ranges:
+ if j > 0:
+ mrStr.add(',')
+ if r.isEmpty:
+ mrStr.add("empty")
+ else:
+ mrStr.add(if r.hasLower and r.lower.inclusive: "[" else: "(")
+ if r.hasLower:
+ mrStr.add(r.lower.value.format("yyyy-MM-dd"))
+ mrStr.add(',')
+ if r.hasUpper:
+ mrStr.add(r.upper.value.format("yyyy-MM-dd"))
+ mrStr.add(if r.hasUpper and r.upper.inclusive: "]" else: ")")
+ mrStr.add('}')
+ for c in mrStr:
+ if c == '"' or c == '\\':
+ s.add('\\')
+ s.add(c)
+ s.add('"')
+ s.add('}')
+ PgParam(oid: OidDateMultirangeArray, format: 0, value: some(toBytes(s)))
+
# Multirange text format getters
proc getInt4Multirange*(row: Row, col: int): PgMultirange[int32] =
@@ -4570,6 +5635,238 @@ optAccessor(getTsMultirange, getTsMultirangeOpt, PgMultirange[DateTime])
optAccessor(getTsTzMultirange, getTsTzMultirangeOpt, PgMultirange[DateTime])
optAccessor(getDateMultirange, getDateMultirangeOpt, PgMultirange[DateTime])
+# Multirange array type support
+
+proc getInt4MultirangeArray*(row: Row, col: int): seq[PgMultirange[int32]] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[PgMultirange[int32]](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in multirange array")
+ let parts = decodeMultirangeBinaryRaw(
+ row.data.buf.toOpenArray(off + e.off, off + e.off + e.len - 1)
+ )
+ var ranges = newSeq[PgRange[int32]](parts.len)
+ for j, p in parts:
+ ranges[j] = decodeInt4RangeBinary(
+ row.data.buf.toOpenArray(off + e.off + p.off, off + e.off + p.off + p.len - 1)
+ )
+ result[i] = PgMultirange[int32](ranges)
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in multirange array")
+ result.add(
+ parseMultirangeText[int32](
+ e.get,
+ proc(x: string): int32 {.gcsafe, raises: [CatchableError].} =
+ int32(parseInt(x)),
+ )
+ )
+
+proc getInt8MultirangeArray*(row: Row, col: int): seq[PgMultirange[int64]] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[PgMultirange[int64]](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in multirange array")
+ let parts = decodeMultirangeBinaryRaw(
+ row.data.buf.toOpenArray(off + e.off, off + e.off + e.len - 1)
+ )
+ var ranges = newSeq[PgRange[int64]](parts.len)
+ for j, p in parts:
+ ranges[j] = decodeInt8RangeBinary(
+ row.data.buf.toOpenArray(off + e.off + p.off, off + e.off + p.off + p.len - 1)
+ )
+ result[i] = PgMultirange[int64](ranges)
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in multirange array")
+ result.add(
+ parseMultirangeText[int64](
+ e.get,
+ proc(x: string): int64 {.gcsafe, raises: [CatchableError].} =
+ parseBiggestInt(x),
+ )
+ )
+
+proc getNumMultirangeArray*(row: Row, col: int): seq[PgMultirange[PgNumeric]] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[PgMultirange[PgNumeric]](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in multirange array")
+ let parts = decodeMultirangeBinaryRaw(
+ row.data.buf.toOpenArray(off + e.off, off + e.off + e.len - 1)
+ )
+ var ranges = newSeq[PgRange[PgNumeric]](parts.len)
+ for j, p in parts:
+ ranges[j] = decodeNumRangeBinary(
+ row.data.buf.toOpenArray(off + e.off + p.off, off + e.off + p.off + p.len - 1)
+ )
+ result[i] = PgMultirange[PgNumeric](ranges)
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in multirange array")
+ result.add(
+ parseMultirangeText[PgNumeric](
+ e.get,
+ proc(x: string): PgNumeric {.gcsafe, raises: [CatchableError].} =
+ parsePgNumeric(x),
+ )
+ )
+
+proc getTsMultirangeArray*(row: Row, col: int): seq[PgMultirange[DateTime]] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[PgMultirange[DateTime]](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in multirange array")
+ let parts = decodeMultirangeBinaryRaw(
+ row.data.buf.toOpenArray(off + e.off, off + e.off + e.len - 1)
+ )
+ var ranges = newSeq[PgRange[DateTime]](parts.len)
+ for j, p in parts:
+ ranges[j] = decodeTsRangeBinary(
+ row.data.buf.toOpenArray(off + e.off + p.off, off + e.off + p.off + p.len - 1)
+ )
+ result[i] = PgMultirange[DateTime](ranges)
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in multirange array")
+ result.add(
+ parseMultirangeText[DateTime](
+ e.get,
+ proc(x: string): DateTime {.gcsafe, raises: [CatchableError].} =
+ const formats = ["yyyy-MM-dd HH:mm:ss'.'ffffff", "yyyy-MM-dd HH:mm:ss"]
+ for fmt in formats:
+ try:
+ return parse(x, fmt)
+ except TimeParseError, IndexDefect:
+ discard
+ raise newException(PgTypeError, "Invalid timestamp in multirange: " & x),
+ )
+ )
+
+proc getTsTzMultirangeArray*(row: Row, col: int): seq[PgMultirange[DateTime]] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[PgMultirange[DateTime]](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in multirange array")
+ let parts = decodeMultirangeBinaryRaw(
+ row.data.buf.toOpenArray(off + e.off, off + e.off + e.len - 1)
+ )
+ var ranges = newSeq[PgRange[DateTime]](parts.len)
+ for j, p in parts:
+ ranges[j] = decodeTsRangeBinary(
+ row.data.buf.toOpenArray(off + e.off + p.off, off + e.off + p.off + p.len - 1)
+ )
+ result[i] = PgMultirange[DateTime](ranges)
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in multirange array")
+ result.add(
+ parseMultirangeText[DateTime](
+ e.get,
+ proc(x: string): DateTime {.gcsafe, raises: [CatchableError].} =
+ const formats = [
+ "yyyy-MM-dd HH:mm:ss'.'ffffffzzz", "yyyy-MM-dd HH:mm:ss'.'ffffffzz",
+ "yyyy-MM-dd HH:mm:ss'.'ffffff", "yyyy-MM-dd HH:mm:sszzz",
+ "yyyy-MM-dd HH:mm:sszz", "yyyy-MM-dd HH:mm:ss",
+ ]
+ for fmt in formats:
+ try:
+ return parse(x, fmt)
+ except TimeParseError, IndexDefect:
+ discard
+ raise newException(PgTypeError, "Invalid timestamptz in multirange: " & x),
+ )
+ )
+
+proc getDateMultirangeArray*(row: Row, col: int): seq[PgMultirange[DateTime]] =
+ if row.isBinaryCol(col):
+ let (off, clen) = cellInfo(row, col)
+ if clen == -1:
+ raise newException(PgTypeError, "Column " & $col & " is NULL")
+ let decoded = decodeBinaryArray(row.data.buf.toOpenArray(off, off + clen - 1))
+ result = newSeq[PgMultirange[DateTime]](decoded.elements.len)
+ for i, e in decoded.elements:
+ if e.len == -1:
+ raise newException(PgTypeError, "NULL element in multirange array")
+ let parts = decodeMultirangeBinaryRaw(
+ row.data.buf.toOpenArray(off + e.off, off + e.off + e.len - 1)
+ )
+ var ranges = newSeq[PgRange[DateTime]](parts.len)
+ for j, p in parts:
+ ranges[j] = decodeDateRangeBinary(
+ row.data.buf.toOpenArray(off + e.off + p.off, off + e.off + p.off + p.len - 1)
+ )
+ result[i] = PgMultirange[DateTime](ranges)
+ return
+ let s = row.getStr(col)
+ let elems = parseTextArray(s)
+ for e in elems:
+ if e.isNone:
+ raise newException(PgTypeError, "NULL element in multirange array")
+ result.add(
+ parseMultirangeText[DateTime](
+ e.get,
+ proc(x: string): DateTime {.gcsafe, raises: [CatchableError].} =
+ try:
+ return parse(x, "yyyy-MM-dd")
+ except TimeParseError:
+ raise newException(PgTypeError, "Invalid date in multirange: " & x),
+ )
+ )
+
+optAccessor(getInt4MultirangeArray, getInt4MultirangeArrayOpt, seq[PgMultirange[int32]])
+optAccessor(getInt8MultirangeArray, getInt8MultirangeArrayOpt, seq[PgMultirange[int64]])
+optAccessor(
+ getNumMultirangeArray, getNumMultirangeArrayOpt, seq[PgMultirange[PgNumeric]]
+)
+optAccessor(getTsMultirangeArray, getTsMultirangeArrayOpt, seq[PgMultirange[DateTime]])
+optAccessor(
+ getTsTzMultirangeArray, getTsTzMultirangeArrayOpt, seq[PgMultirange[DateTime]]
+)
+optAccessor(
+ getDateMultirangeArray, getDateMultirangeArrayOpt, seq[PgMultirange[DateTime]]
+)
+
# Range array type support
#
# PostgreSQL range array types store arrays of range values.
@@ -4890,6 +6187,66 @@ proc get*(row: Row, col: int, T: typedesc[seq[string]]): seq[string] =
proc get*(row: Row, col: int, T: typedesc[seq[PgBit]]): seq[PgBit] =
row.getBitArray(col)
+proc get*(row: Row, col: int, T: typedesc[seq[PgTime]]): seq[PgTime] =
+ row.getTimeArray(col)
+
+proc get*(row: Row, col: int, T: typedesc[seq[PgTimeTz]]): seq[PgTimeTz] =
+ row.getTimeTzArray(col)
+
+proc get*(row: Row, col: int, T: typedesc[seq[PgInterval]]): seq[PgInterval] =
+ row.getIntervalArray(col)
+
+proc get*(row: Row, col: int, T: typedesc[seq[PgUuid]]): seq[PgUuid] =
+ row.getUuidArray(col)
+
+proc get*(row: Row, col: int, T: typedesc[seq[PgInet]]): seq[PgInet] =
+ row.getInetArray(col)
+
+proc get*(row: Row, col: int, T: typedesc[seq[PgCidr]]): seq[PgCidr] =
+ row.getCidrArray(col)
+
+proc get*(row: Row, col: int, T: typedesc[seq[PgMacAddr]]): seq[PgMacAddr] =
+ row.getMacAddrArray(col)
+
+proc get*(row: Row, col: int, T: typedesc[seq[PgMacAddr8]]): seq[PgMacAddr8] =
+ row.getMacAddr8Array(col)
+
+proc get*(row: Row, col: int, T: typedesc[seq[PgNumeric]]): seq[PgNumeric] =
+ row.getNumericArray(col)
+
+proc get*(row: Row, col: int, T: typedesc[seq[JsonNode]]): seq[JsonNode] =
+ row.getJsonArray(col)
+
+proc get*(row: Row, col: int, T: typedesc[seq[PgPoint]]): seq[PgPoint] =
+ row.getPointArray(col)
+
+proc get*(row: Row, col: int, T: typedesc[seq[PgLine]]): seq[PgLine] =
+ row.getLineArray(col)
+
+proc get*(row: Row, col: int, T: typedesc[seq[PgLseg]]): seq[PgLseg] =
+ row.getLsegArray(col)
+
+proc get*(row: Row, col: int, T: typedesc[seq[PgBox]]): seq[PgBox] =
+ row.getBoxArray(col)
+
+proc get*(row: Row, col: int, T: typedesc[seq[PgPath]]): seq[PgPath] =
+ row.getPathArray(col)
+
+proc get*(row: Row, col: int, T: typedesc[seq[PgPolygon]]): seq[PgPolygon] =
+ row.getPolygonArray(col)
+
+proc get*(row: Row, col: int, T: typedesc[seq[PgCircle]]): seq[PgCircle] =
+ row.getCircleArray(col)
+
+proc get*(row: Row, col: int, T: typedesc[seq[PgXml]]): seq[PgXml] =
+ row.getXmlArray(col)
+
+proc get*(row: Row, col: int, T: typedesc[seq[PgTsVector]]): seq[PgTsVector] =
+ row.getTsVectorArray(col)
+
+proc get*(row: Row, col: int, T: typedesc[seq[PgTsQuery]]): seq[PgTsQuery] =
+ row.getTsQueryArray(col)
+
# Range types (DateTime-based ranges excluded — see note above)
proc get*(row: Row, col: int, T: typedesc[PgRange[int32]]): PgRange[int32] =
@@ -4914,6 +6271,21 @@ proc get*(
): PgMultirange[PgNumeric] =
row.getNumMultirange(col)
+proc get*(
+ row: Row, col: int, T: typedesc[seq[PgMultirange[int32]]]
+): seq[PgMultirange[int32]] =
+ row.getInt4MultirangeArray(col)
+
+proc get*(
+ row: Row, col: int, T: typedesc[seq[PgMultirange[int64]]]
+): seq[PgMultirange[int64]] =
+ row.getInt8MultirangeArray(col)
+
+proc get*(
+ row: Row, col: int, T: typedesc[seq[PgMultirange[PgNumeric]]]
+): seq[PgMultirange[PgNumeric]] =
+ row.getNumMultirangeArray(col)
+
proc columnIndex*(fields: seq[FieldDescription], name: string): int =
## Find the index of a column by name. Raises PgTypeError if not found.
for i, f in fields:
@@ -5019,6 +6391,54 @@ nameAccessor(getFloat32ArrayOpt, Option[seq[float32]])
nameAccessor(getBoolArrayOpt, Option[seq[bool]])
nameAccessor(getStrArrayOpt, Option[seq[string]])
nameAccessor(getBitArrayOpt, Option[seq[PgBit]])
+nameAccessor(getTimestampArray, seq[DateTime])
+nameAccessor(getTimestampTzArray, seq[DateTime])
+nameAccessor(getDateArray, seq[DateTime])
+nameAccessor(getTimeArray, seq[PgTime])
+nameAccessor(getTimeTzArray, seq[PgTimeTz])
+nameAccessor(getIntervalArray, seq[PgInterval])
+nameAccessor(getUuidArray, seq[PgUuid])
+nameAccessor(getInetArray, seq[PgInet])
+nameAccessor(getCidrArray, seq[PgCidr])
+nameAccessor(getMacAddrArray, seq[PgMacAddr])
+nameAccessor(getMacAddr8Array, seq[PgMacAddr8])
+nameAccessor(getNumericArray, seq[PgNumeric])
+nameAccessor(getBytesArray, seq[seq[byte]])
+nameAccessor(getJsonArray, seq[JsonNode])
+nameAccessor(getPointArray, seq[PgPoint])
+nameAccessor(getLineArray, seq[PgLine])
+nameAccessor(getLsegArray, seq[PgLseg])
+nameAccessor(getBoxArray, seq[PgBox])
+nameAccessor(getPathArray, seq[PgPath])
+nameAccessor(getPolygonArray, seq[PgPolygon])
+nameAccessor(getCircleArray, seq[PgCircle])
+nameAccessor(getXmlArray, seq[PgXml])
+nameAccessor(getTsVectorArray, seq[PgTsVector])
+nameAccessor(getTsQueryArray, seq[PgTsQuery])
+nameAccessor(getTimestampArrayOpt, Option[seq[DateTime]])
+nameAccessor(getTimestampTzArrayOpt, Option[seq[DateTime]])
+nameAccessor(getDateArrayOpt, Option[seq[DateTime]])
+nameAccessor(getTimeArrayOpt, Option[seq[PgTime]])
+nameAccessor(getTimeTzArrayOpt, Option[seq[PgTimeTz]])
+nameAccessor(getIntervalArrayOpt, Option[seq[PgInterval]])
+nameAccessor(getUuidArrayOpt, Option[seq[PgUuid]])
+nameAccessor(getInetArrayOpt, Option[seq[PgInet]])
+nameAccessor(getCidrArrayOpt, Option[seq[PgCidr]])
+nameAccessor(getMacAddrArrayOpt, Option[seq[PgMacAddr]])
+nameAccessor(getMacAddr8ArrayOpt, Option[seq[PgMacAddr8]])
+nameAccessor(getNumericArrayOpt, Option[seq[PgNumeric]])
+nameAccessor(getBytesArrayOpt, Option[seq[seq[byte]]])
+nameAccessor(getJsonArrayOpt, Option[seq[JsonNode]])
+nameAccessor(getPointArrayOpt, Option[seq[PgPoint]])
+nameAccessor(getLineArrayOpt, Option[seq[PgLine]])
+nameAccessor(getLsegArrayOpt, Option[seq[PgLseg]])
+nameAccessor(getBoxArrayOpt, Option[seq[PgBox]])
+nameAccessor(getPathArrayOpt, Option[seq[PgPath]])
+nameAccessor(getPolygonArrayOpt, Option[seq[PgPolygon]])
+nameAccessor(getCircleArrayOpt, Option[seq[PgCircle]])
+nameAccessor(getXmlArrayOpt, Option[seq[PgXml]])
+nameAccessor(getTsVectorArrayOpt, Option[seq[PgTsVector]])
+nameAccessor(getTsQueryArrayOpt, Option[seq[PgTsQuery]])
nameAccessor(getInt4Range, PgRange[int32])
nameAccessor(getInt8Range, PgRange[int64])
nameAccessor(getNumRange, PgRange[PgNumeric])
@@ -5043,3 +6463,27 @@ nameAccessor(getNumMultirangeOpt, Option[PgMultirange[PgNumeric]])
nameAccessor(getTsMultirangeOpt, Option[PgMultirange[DateTime]])
nameAccessor(getTsTzMultirangeOpt, Option[PgMultirange[DateTime]])
nameAccessor(getDateMultirangeOpt, Option[PgMultirange[DateTime]])
+nameAccessor(getInt4RangeArray, seq[PgRange[int32]])
+nameAccessor(getInt8RangeArray, seq[PgRange[int64]])
+nameAccessor(getNumRangeArray, seq[PgRange[PgNumeric]])
+nameAccessor(getTsRangeArray, seq[PgRange[DateTime]])
+nameAccessor(getTsTzRangeArray, seq[PgRange[DateTime]])
+nameAccessor(getDateRangeArray, seq[PgRange[DateTime]])
+nameAccessor(getInt4RangeArrayOpt, Option[seq[PgRange[int32]]])
+nameAccessor(getInt8RangeArrayOpt, Option[seq[PgRange[int64]]])
+nameAccessor(getNumRangeArrayOpt, Option[seq[PgRange[PgNumeric]]])
+nameAccessor(getTsRangeArrayOpt, Option[seq[PgRange[DateTime]]])
+nameAccessor(getTsTzRangeArrayOpt, Option[seq[PgRange[DateTime]]])
+nameAccessor(getDateRangeArrayOpt, Option[seq[PgRange[DateTime]]])
+nameAccessor(getInt4MultirangeArray, seq[PgMultirange[int32]])
+nameAccessor(getInt8MultirangeArray, seq[PgMultirange[int64]])
+nameAccessor(getNumMultirangeArray, seq[PgMultirange[PgNumeric]])
+nameAccessor(getTsMultirangeArray, seq[PgMultirange[DateTime]])
+nameAccessor(getTsTzMultirangeArray, seq[PgMultirange[DateTime]])
+nameAccessor(getDateMultirangeArray, seq[PgMultirange[DateTime]])
+nameAccessor(getInt4MultirangeArrayOpt, Option[seq[PgMultirange[int32]]])
+nameAccessor(getInt8MultirangeArrayOpt, Option[seq[PgMultirange[int64]]])
+nameAccessor(getNumMultirangeArrayOpt, Option[seq[PgMultirange[PgNumeric]]])
+nameAccessor(getTsMultirangeArrayOpt, Option[seq[PgMultirange[DateTime]]])
+nameAccessor(getTsTzMultirangeArrayOpt, Option[seq[PgMultirange[DateTime]]])
+nameAccessor(getDateMultirangeArrayOpt, Option[seq[PgMultirange[DateTime]]])
diff --git a/tests/test_e2e.nim b/tests/test_e2e.nim
index 282f993..9269af7 100644
--- a/tests/test_e2e.nim
+++ b/tests/test_e2e.nim
@@ -1,7 +1,9 @@
import
- std/[unittest, options, strutils, tables, os, math, deques, sets, importutils, net]
+ std/[
+ unittest, options, strutils, tables, os, math, deques, sets, importutils, net, json
+ ]
from std/times import
- DateTime, dateTime, mMar, mJun, mJan, utc, year, month, monthday, hour, minute,
+ DateTime, dateTime, mMar, mJun, mJan, mDec, utc, year, month, monthday, hour, minute,
second, toTime, toUnix, nanosecond
import ../async_postgres/[async_backend, pg_protocol, pg_types, pg_replication]
@@ -7787,3 +7789,325 @@ suite "E2E: Multirange Types":
await conn.close()
waitFor t()
+
+suite "E2E: Temporal array types":
+ test "timestamp array roundtrip":
+ proc t() {.async.} =
+ let conn = await connect(plainConfig())
+ let dt1 = dateTime(2023, mJan, 15, 10, 30, 0, zone = utc())
+ let dt2 = dateTime(2024, mJun, 20, 14, 45, 30, zone = utc())
+ let res = await conn.query(
+ "SELECT $1::timestamp[]", @[toPgTimestampArrayParam(@[dt1, dt2])]
+ )
+ doAssert res.rows.len == 1
+ let arr = res.rows[0].getTimestampArray(0)
+ doAssert arr.len == 2
+ doAssert arr[0].year == 2023
+ doAssert arr[1].year == 2024
+ await conn.close()
+
+ waitFor t()
+
+ test "empty timestamp array":
+ proc t() {.async.} =
+ let conn = await connect(plainConfig())
+ let res = await conn.query(
+ "SELECT $1::timestamp[]", @[toPgTimestampArrayParam(newSeq[DateTime]())]
+ )
+ doAssert res.rows.len == 1
+ doAssert res.rows[0].getTimestampArray(0).len == 0
+ await conn.close()
+
+ waitFor t()
+
+ test "NULL timestamp array":
+ proc t() {.async.} =
+ let conn = await connect(plainConfig())
+ let res = await conn.query("SELECT NULL::timestamp[]")
+ doAssert res.rows.len == 1
+ doAssert res.rows[0].getTimestampArrayOpt(0).isNone
+ await conn.close()
+
+ waitFor t()
+
+ test "date array roundtrip":
+ proc t() {.async.} =
+ let conn = await connect(plainConfig())
+ let dt1 = dateTime(2023, mMar, 10, zone = utc())
+ let dt2 = dateTime(2024, mDec, 25, zone = utc())
+ let res =
+ await conn.query("SELECT $1::date[]", @[toPgDateArrayParam(@[dt1, dt2])])
+ doAssert res.rows.len == 1
+ let arr = res.rows[0].getDateArray(0)
+ doAssert arr.len == 2
+ doAssert arr[0].monthday == 10
+ doAssert arr[1].month == mDec
+ await conn.close()
+
+ waitFor t()
+
+ test "time array roundtrip":
+ proc t() {.async.} =
+ let conn = await connect(plainConfig())
+ let t1 = PgTime(hour: 10, minute: 30, second: 0, microsecond: 0)
+ let t2 = PgTime(hour: 23, minute: 59, second: 59, microsecond: 123456)
+ let res = await conn.query("SELECT $1::time[]", @[toPgParam(@[t1, t2])])
+ doAssert res.rows.len == 1
+ let arr = res.rows[0].getTimeArray(0)
+ doAssert arr.len == 2
+ doAssert arr[0] == t1
+ doAssert arr[1] == t2
+ await conn.close()
+
+ waitFor t()
+
+ test "timetz array roundtrip":
+ proc t() {.async.} =
+ let conn = await connect(plainConfig())
+ let t1 =
+ PgTimeTz(hour: 10, minute: 30, second: 0, microsecond: 0, utcOffset: 3600)
+ let t2 =
+ PgTimeTz(hour: 23, minute: 59, second: 59, microsecond: 0, utcOffset: -18000)
+ let res = await conn.query("SELECT $1::timetz[]", @[toPgParam(@[t1, t2])])
+ doAssert res.rows.len == 1
+ let arr = res.rows[0].getTimeTzArray(0)
+ doAssert arr.len == 2
+ doAssert arr[0] == t1
+ doAssert arr[1] == t2
+ await conn.close()
+
+ waitFor t()
+
+ test "interval array roundtrip":
+ proc t() {.async.} =
+ let conn = await connect(plainConfig())
+ let iv1 = PgInterval(months: 2, days: 3, microseconds: 3600000000)
+ let iv2 = PgInterval(months: 0, days: 0, microseconds: 1000000)
+ let res = await conn.query("SELECT $1::interval[]", @[toPgParam(@[iv1, iv2])])
+ doAssert res.rows.len == 1
+ let arr = res.rows[0].getIntervalArray(0)
+ doAssert arr.len == 2
+ doAssert arr[0] == iv1
+ doAssert arr[1] == iv2
+ await conn.close()
+
+ waitFor t()
+
+suite "E2E: Identifier / network array types":
+ test "uuid array roundtrip":
+ proc t() {.async.} =
+ let conn = await connect(plainConfig())
+ let u1 = PgUuid("550e8400-e29b-41d4-a716-446655440000")
+ let u2 = PgUuid("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
+ let res = await conn.query("SELECT $1::uuid[]", @[toPgParam(@[u1, u2])])
+ doAssert res.rows.len == 1
+ let arr = res.rows[0].getUuidArray(0)
+ doAssert arr.len == 2
+ doAssert arr[0] == u1
+ doAssert arr[1] == u2
+ await conn.close()
+
+ waitFor t()
+
+ test "inet array roundtrip":
+ proc t() {.async.} =
+ let conn = await connect(plainConfig())
+ let i1 = PgInet(address: parseIpAddress("192.168.1.1"), mask: 32)
+ let i2 = PgInet(address: parseIpAddress("10.0.0.0"), mask: 8)
+ let res = await conn.query("SELECT $1::inet[]", @[toPgParam(@[i1, i2])])
+ doAssert res.rows.len == 1
+ let arr = res.rows[0].getInetArray(0)
+ doAssert arr.len == 2
+ doAssert $arr[0].address == "192.168.1.1"
+ doAssert arr[1].mask == 8
+ await conn.close()
+
+ waitFor t()
+
+ test "cidr array roundtrip":
+ proc t() {.async.} =
+ let conn = await connect(plainConfig())
+ let c1 = PgCidr(address: parseIpAddress("192.168.1.0"), mask: 24)
+ let c2 = PgCidr(address: parseIpAddress("10.0.0.0"), mask: 8)
+ let res = await conn.query("SELECT $1::cidr[]", @[toPgParam(@[c1, c2])])
+ doAssert res.rows.len == 1
+ let arr = res.rows[0].getCidrArray(0)
+ doAssert arr.len == 2
+ doAssert arr[0].mask == 24
+ doAssert arr[1].mask == 8
+ await conn.close()
+
+ waitFor t()
+
+ test "macaddr array roundtrip":
+ proc t() {.async.} =
+ let conn = await connect(plainConfig())
+ let m1 = PgMacAddr("08:00:2b:01:02:03")
+ let m2 = PgMacAddr("aa:bb:cc:dd:ee:ff")
+ let res = await conn.query("SELECT $1::macaddr[]", @[toPgParam(@[m1, m2])])
+ doAssert res.rows.len == 1
+ let arr = res.rows[0].getMacAddrArray(0)
+ doAssert arr.len == 2
+ doAssert arr[0] == m1
+ doAssert arr[1] == m2
+ await conn.close()
+
+ waitFor t()
+
+suite "E2E: Numeric / binary / JSON array types":
+ test "numeric array roundtrip":
+ proc t() {.async.} =
+ let conn = await connect(plainConfig())
+ let n1 = parsePgNumeric("123.45")
+ let n2 = parsePgNumeric("0.001")
+ let res = await conn.query("SELECT $1::numeric[]", @[toPgParam(@[n1, n2])])
+ doAssert res.rows.len == 1
+ let arr = res.rows[0].getNumericArray(0)
+ doAssert arr.len == 2
+ doAssert $arr[0] == "123.45"
+ doAssert $arr[1] == "0.001"
+ await conn.close()
+
+ waitFor t()
+
+ test "bytea array roundtrip":
+ proc t() {.async.} =
+ let conn = await connect(plainConfig())
+ let b1 = @[1'u8, 2, 3]
+ let b2 = @[0xFF'u8, 0x00]
+ let res =
+ await conn.query("SELECT $1::bytea[]", @[toPgByteaArrayParam(@[b1, b2])])
+ doAssert res.rows.len == 1
+ let arr = res.rows[0].getBytesArray(0)
+ doAssert arr.len == 2
+ doAssert arr[0] == b1
+ doAssert arr[1] == b2
+ await conn.close()
+
+ waitFor t()
+
+ test "jsonb array roundtrip":
+ proc t() {.async.} =
+ let conn = await connect(plainConfig())
+ let j1 = %*{"key": "value"}
+ let j2 = %*[1, 2, 3]
+ let res = await conn.query("SELECT $1::jsonb[]", @[toPgParam(@[j1, j2])])
+ doAssert res.rows.len == 1
+ let arr = res.rows[0].getJsonArray(0)
+ doAssert arr.len == 2
+ doAssert arr[0]["key"].getStr == "value"
+ doAssert arr[1].len == 3
+ await conn.close()
+
+ waitFor t()
+
+suite "E2E: Geometric array types":
+ test "point array roundtrip":
+ proc t() {.async.} =
+ let conn = await connect(plainConfig())
+ let p1 = PgPoint(x: 1.0, y: 2.0)
+ let p2 = PgPoint(x: 3.5, y: 4.5)
+ let res = await conn.query("SELECT $1::point[]", @[toPgParam(@[p1, p2])])
+ doAssert res.rows.len == 1
+ let arr = res.rows[0].getPointArray(0)
+ doAssert arr.len == 2
+ doAssert arr[0] == p1
+ doAssert arr[1] == p2
+ await conn.close()
+
+ waitFor t()
+
+ test "circle array roundtrip":
+ proc t() {.async.} =
+ let conn = await connect(plainConfig())
+ let c1 = PgCircle(center: PgPoint(x: 1.0, y: 2.0), radius: 5.0)
+ let res = await conn.query("SELECT $1::circle[]", @[toPgParam(@[c1])])
+ doAssert res.rows.len == 1
+ let arr = res.rows[0].getCircleArray(0)
+ doAssert arr.len == 1
+ doAssert arr[0].center.x == 1.0
+ doAssert arr[0].radius == 5.0
+ await conn.close()
+
+ waitFor t()
+
+ test "box array roundtrip":
+ proc t() {.async.} =
+ let conn = await connect(plainConfig())
+ let b1 = PgBox(high: PgPoint(x: 3.0, y: 4.0), low: PgPoint(x: 1.0, y: 2.0))
+ let res = await conn.query("SELECT $1::box[]", @[toPgParam(@[b1])])
+ doAssert res.rows.len == 1
+ let arr = res.rows[0].getBoxArray(0)
+ doAssert arr.len == 1
+ doAssert arr[0].high.x == 3.0
+ await conn.close()
+
+ waitFor t()
+
+suite "E2E: Other array types":
+ test "xml array roundtrip":
+ proc t() {.async.} =
+ let conn = await connect(plainConfig())
+ let x1 = PgXml("")
+ let x2 = PgXml("hello")
+ let res = await conn.query("SELECT $1::xml[]", @[toPgParam(@[x1, x2])])
+ doAssert res.rows.len == 1
+ let arr = res.rows[0].getXmlArray(0)
+ doAssert arr.len == 2
+ doAssert string(arr[0]) == ""
+ doAssert string(arr[1]) == "hello"
+ await conn.close()
+
+ waitFor t()
+
+suite "E2E: Multirange array types":
+ test "int4multirange array roundtrip":
+ proc t() {.async.} =
+ let conn = await connect(plainConfig())
+ let mr1 = toMultirange(rangeOf(1'i32, 10'i32), rangeOf(20'i32, 30'i32))
+ let mr2 = toMultirange(rangeOf(100'i32, 200'i32))
+ let res =
+ await conn.query("SELECT $1::int4multirange[]", @[toPgParam(@[mr1, mr2])])
+ doAssert res.rows.len == 1
+ let arr = res.rows[0].getInt4MultirangeArray(0)
+ doAssert arr.len == 2
+ doAssert seq[PgRange[int32]](arr[0]).len == 2
+ doAssert seq[PgRange[int32]](arr[1]).len == 1
+ doAssert seq[PgRange[int32]](arr[0])[0].lower.value == 1'i32
+ await conn.close()
+
+ waitFor t()
+
+ test "int8multirange array roundtrip":
+ proc t() {.async.} =
+ let conn = await connect(plainConfig())
+ let mr1 = toMultirange(rangeOf(100'i64, 200'i64))
+ let res = await conn.query("SELECT $1::int8multirange[]", @[toPgParam(@[mr1])])
+ doAssert res.rows.len == 1
+ let arr = res.rows[0].getInt8MultirangeArray(0)
+ doAssert arr.len == 1
+ await conn.close()
+
+ waitFor t()
+
+ test "nummultirange array roundtrip":
+ proc t() {.async.} =
+ let conn = await connect(plainConfig())
+ let mr1 = toMultirange(rangeOf(parsePgNumeric("1.5"), parsePgNumeric("3.5")))
+ let res = await conn.query("SELECT $1::nummultirange[]", @[toPgParam(@[mr1])])
+ doAssert res.rows.len == 1
+ let arr = res.rows[0].getNumMultirangeArray(0)
+ doAssert arr.len == 1
+ await conn.close()
+
+ waitFor t()
+
+ test "NULL multirange array":
+ proc t() {.async.} =
+ let conn = await connect(plainConfig())
+ let res = await conn.query("SELECT NULL::int4multirange[]")
+ doAssert res.rows.len == 1
+ doAssert res.rows[0].getInt4MultirangeArrayOpt(0).isNone
+ await conn.close()
+
+ waitFor t()
diff --git a/tests/test_types.nim b/tests/test_types.nim
index 0afe2b2..0d32526 100644
--- a/tests/test_types.nim
+++ b/tests/test_types.nim
@@ -5109,3 +5109,382 @@ suite "Binary decoder validation":
]
expect PgTypeError:
discard decodeBinaryTsQuery(data)
+
+suite "Temporal array types":
+ test "toPgTimestampArrayParam roundtrip":
+ let dt1 = dateTime(2023, mJan, 15, 10, 30, 0, zone = utc())
+ let dt2 = dateTime(2024, mJun, 20, 14, 45, 30, zone = utc())
+ let p = toPgTimestampArrayParam(@[dt1, dt2])
+ check p.oid == OidTimestampArray
+ check p.format == 1'i16
+ let fields = @[mkField(OidTimestampArray, 1'i16)]
+ let row = mkRow(@[p.value], fields)
+ let arr = row.getTimestampArray(0)
+ check arr.len == 2
+ check arr[0].year == 2023
+ check arr[1].year == 2024
+
+ test "toPgTimestampArrayParam empty":
+ let p = toPgTimestampArrayParam(newSeq[DateTime]())
+ check p.oid == OidTimestampArray
+ let fields = @[mkField(OidTimestampArray, 1'i16)]
+ let row = mkRow(@[p.value], fields)
+ check row.getTimestampArray(0).len == 0
+
+ test "getTimestampArray text format":
+ let row: Row = @[some(toBytes("{\"2023-01-15 10:30:00\",\"2024-06-20 14:45:30\"}"))]
+ let arr = row.getTimestampArray(0)
+ check arr.len == 2
+ check arr[0].year == 2023
+ check arr[1].month == mJun
+
+ test "getTimestampArrayOpt none":
+ let fields = @[mkField(OidTimestampArray, 1'i16)]
+ let row = mkRow(@[none(seq[byte])], fields)
+ check row.getTimestampArrayOpt(0).isNone
+
+ test "toPgTimestampTzArrayParam roundtrip":
+ let dt1 = dateTime(2023, mJan, 15, 10, 30, 0, zone = utc())
+ let p = toPgTimestampTzArrayParam(@[dt1])
+ check p.oid == OidTimestampTzArray
+ let fields = @[mkField(OidTimestampTzArray, 1'i16)]
+ let row = mkRow(@[p.value], fields)
+ let arr = row.getTimestampTzArray(0)
+ check arr.len == 1
+ check arr[0].year == 2023
+
+ test "toPgDateArrayParam roundtrip":
+ let dt1 = dateTime(2023, mMar, 10, zone = utc())
+ let dt2 = dateTime(2024, mDec, 25, zone = utc())
+ let p = toPgDateArrayParam(@[dt1, dt2])
+ check p.oid == OidDateArray
+ let fields = @[mkField(OidDateArray, 1'i16)]
+ let row = mkRow(@[p.value], fields)
+ let arr = row.getDateArray(0)
+ check arr.len == 2
+ check arr[0].monthday == 10
+ check arr[1].month == mDec
+
+ test "getDateArray text format":
+ let row: Row = @[some(toBytes("{2023-03-10,2024-12-25}"))]
+ let arr = row.getDateArray(0)
+ check arr.len == 2
+ check arr[0].year == 2023
+
+ test "toPgParam seq[PgTime] roundtrip":
+ let t1 = PgTime(hour: 10, minute: 30, second: 0, microsecond: 0)
+ let t2 = PgTime(hour: 23, minute: 59, second: 59, microsecond: 123456)
+ let p = toPgParam(@[t1, t2])
+ check p.oid == OidTimeArray
+ let fields = @[mkField(OidTimeArray, 1'i16)]
+ let row = mkRow(@[p.value], fields)
+ let arr = row.getTimeArray(0)
+ check arr.len == 2
+ check arr[0] == t1
+ check arr[1] == t2
+
+ test "getTimeArray text format":
+ let row: Row = @[some(toBytes("{10:30:00,23:59:59.123456}"))]
+ let arr = row.getTimeArray(0)
+ check arr.len == 2
+ check arr[0].hour == 10
+ check arr[1].microsecond == 123456
+
+ test "toPgParam seq[PgTimeTz] roundtrip":
+ let t1 = PgTimeTz(hour: 10, minute: 30, second: 0, microsecond: 0, utcOffset: 3600)
+ let t2 =
+ PgTimeTz(hour: 23, minute: 59, second: 59, microsecond: 0, utcOffset: -18000)
+ let p = toPgParam(@[t1, t2])
+ check p.oid == OidTimeTzArray
+ let fields = @[mkField(OidTimeTzArray, 1'i16)]
+ let row = mkRow(@[p.value], fields)
+ let arr = row.getTimeTzArray(0)
+ check arr.len == 2
+ check arr[0] == t1
+ check arr[1] == t2
+
+ test "getTimeTzArray text format":
+ let row: Row = @[some(toBytes("{10:30:00+01,23:59:59-05}"))]
+ let arr = row.getTimeTzArray(0)
+ check arr.len == 2
+ check arr[0].utcOffset == 3600
+ check arr[1].utcOffset == -18000
+
+ test "toPgParam seq[PgInterval] roundtrip":
+ let iv1 = PgInterval(months: 2, days: 3, microseconds: 3600000000)
+ let iv2 = PgInterval(months: 0, days: 0, microseconds: 1000000)
+ let p = toPgParam(@[iv1, iv2])
+ check p.oid == OidIntervalArray
+ let fields = @[mkField(OidIntervalArray, 1'i16)]
+ let row = mkRow(@[p.value], fields)
+ let arr = row.getIntervalArray(0)
+ check arr.len == 2
+ check arr[0] == iv1
+ check arr[1] == iv2
+
+suite "Identifier / network array types":
+ test "toPgParam seq[PgUuid] roundtrip":
+ let u1 = PgUuid("550e8400-e29b-41d4-a716-446655440000")
+ let u2 = PgUuid("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
+ let p = toPgParam(@[u1, u2])
+ check p.oid == OidUuidArray
+ let fields = @[mkField(OidUuidArray, 1'i16)]
+ let row = mkRow(@[p.value], fields)
+ let arr = row.getUuidArray(0)
+ check arr.len == 2
+ check arr[0] == u1
+ check arr[1] == u2
+
+ test "getUuidArray text format":
+ let row: Row = @[
+ some(
+ toBytes(
+ "{550e8400-e29b-41d4-a716-446655440000,6ba7b810-9dad-11d1-80b4-00c04fd430c8}"
+ )
+ )
+ ]
+ let arr = row.getUuidArray(0)
+ check arr.len == 2
+ check arr[0] == PgUuid("550e8400-e29b-41d4-a716-446655440000")
+
+ test "getUuidArrayOpt none":
+ let fields = @[mkField(OidUuidArray, 1'i16)]
+ let row = mkRow(@[none(seq[byte])], fields)
+ check row.getUuidArrayOpt(0).isNone
+
+ test "toPgParam seq[PgInet] roundtrip":
+ let i1 = PgInet(address: parseIpAddress("192.168.1.1"), mask: 32)
+ let i2 = PgInet(address: parseIpAddress("10.0.0.0"), mask: 8)
+ let p = toPgParam(@[i1, i2])
+ check p.oid == OidInetArray
+ let fields = @[mkField(OidInetArray, 1'i16)]
+ let row = mkRow(@[p.value], fields)
+ let arr = row.getInetArray(0)
+ check arr.len == 2
+ check $arr[0].address == "192.168.1.1"
+ check arr[1].mask == 8
+
+ test "toPgParam seq[PgMacAddr] roundtrip":
+ let m1 = PgMacAddr("08:00:2b:01:02:03")
+ let m2 = PgMacAddr("aa:bb:cc:dd:ee:ff")
+ let p = toPgParam(@[m1, m2])
+ check p.oid == OidMacAddrArray
+ let fields = @[mkField(OidMacAddrArray, 1'i16)]
+ let row = mkRow(@[p.value], fields)
+ let arr = row.getMacAddrArray(0)
+ check arr.len == 2
+ check arr[0] == m1
+ check arr[1] == m2
+
+ test "getMacAddrArray text format":
+ let row: Row = @[some(toBytes("{08:00:2b:01:02:03,aa:bb:cc:dd:ee:ff}"))]
+ let arr = row.getMacAddrArray(0)
+ check arr.len == 2
+ check arr[0] == PgMacAddr("08:00:2b:01:02:03")
+
+ test "toPgParam seq[PgMacAddr8] roundtrip":
+ let m1 = PgMacAddr8("08:00:2b:01:02:03:04:05")
+ let p = toPgParam(@[m1])
+ check p.oid == OidMacAddr8Array
+ let fields = @[mkField(OidMacAddr8Array, 1'i16)]
+ let row = mkRow(@[p.value], fields)
+ let arr = row.getMacAddr8Array(0)
+ check arr.len == 1
+ check arr[0] == m1
+
+suite "Numeric / binary / JSON array types":
+ test "toPgParam seq[PgNumeric] roundtrip":
+ let n1 = parsePgNumeric("123.45")
+ let n2 = parsePgNumeric("0.001")
+ let p = toPgParam(@[n1, n2])
+ check p.oid == OidNumericArray
+ let fields = @[mkField(OidNumericArray, 1'i16)]
+ let row = mkRow(@[p.value], fields)
+ let arr = row.getNumericArray(0)
+ check arr.len == 2
+ check $arr[0] == "123.45"
+ check $arr[1] == "0.001"
+
+ test "getNumericArray text format":
+ let row: Row = @[some(toBytes("{123.45,0.001}"))]
+ let arr = row.getNumericArray(0)
+ check arr.len == 2
+ check $arr[0] == "123.45"
+
+ test "toPgByteaArrayParam roundtrip":
+ let b1 = @[1'u8, 2, 3]
+ let b2 = @[0xFF'u8, 0x00]
+ let p = toPgByteaArrayParam(@[b1, b2])
+ check p.oid == OidByteaArray
+ let fields = @[mkField(OidByteaArray, 1'i16)]
+ let row = mkRow(@[p.value], fields)
+ let arr = row.getBytesArray(0)
+ check arr.len == 2
+ check arr[0] == b1
+ check arr[1] == b2
+
+ test "toPgByteaArrayParam empty":
+ let p = toPgByteaArrayParam(newSeq[seq[byte]]())
+ check p.oid == OidByteaArray
+ let fields = @[mkField(OidByteaArray, 1'i16)]
+ let row = mkRow(@[p.value], fields)
+ check row.getBytesArray(0).len == 0
+
+ test "toPgParam seq[JsonNode] roundtrip":
+ let j1 = %*{"key": "value"}
+ let j2 = %*[1, 2, 3]
+ let p = toPgParam(@[j1, j2])
+ check p.oid == OidJsonbArray
+ let fields = @[mkField(OidJsonbArray, 1'i16)]
+ let row = mkRow(@[p.value], fields)
+ let arr = row.getJsonArray(0)
+ check arr.len == 2
+ check arr[0]["key"].getStr == "value"
+ check arr[1].len == 3
+
+ test "getJsonArray text format":
+ let row: Row = @[some(toBytes("""{"{\"a\":1}","{\"b\":2}"}"""))]
+ let arr = row.getJsonArray(0)
+ check arr.len == 2
+ check arr[0]["a"].getInt == 1
+
+suite "Geometric array types":
+ test "toPgParam seq[PgPoint] roundtrip":
+ let p1 = PgPoint(x: 1.0, y: 2.0)
+ let p2 = PgPoint(x: 3.0, y: 4.0)
+ let p = toPgParam(@[p1, p2])
+ check p.oid == OidPointArray
+ let fields = @[mkField(OidPointArray, 1'i16)]
+ let row = mkRow(@[p.value], fields)
+ let arr = row.getPointArray(0)
+ check arr.len == 2
+ check arr[0] == p1
+ check arr[1] == p2
+
+ test "getPointArray text format":
+ let row: Row = @[some(toBytes("{\"(1,2)\",\"(3,4)\"}"))]
+ let arr = row.getPointArray(0)
+ check arr.len == 2
+ check arr[0].x == 1.0
+
+ test "toPgParam seq[PgCircle] roundtrip":
+ let c1 = PgCircle(center: PgPoint(x: 1.0, y: 2.0), radius: 5.0)
+ let p = toPgParam(@[c1])
+ check p.oid == OidCircleArray
+ let fields = @[mkField(OidCircleArray, 1'i16)]
+ let row = mkRow(@[p.value], fields)
+ let arr = row.getCircleArray(0)
+ check arr.len == 1
+ check arr[0].center.x == 1.0
+ check arr[0].radius == 5.0
+
+ test "toPgParam seq[PgLseg] roundtrip":
+ let l1 = PgLseg(p1: PgPoint(x: 0.0, y: 0.0), p2: PgPoint(x: 1.0, y: 1.0))
+ let p = toPgParam(@[l1])
+ check p.oid == OidLsegArray
+ let fields = @[mkField(OidLsegArray, 1'i16)]
+ let row = mkRow(@[p.value], fields)
+ let arr = row.getLsegArray(0)
+ check arr.len == 1
+ check arr[0].p1.x == 0.0
+ check arr[0].p2.y == 1.0
+
+ test "toPgParam seq[PgBox] roundtrip":
+ let b1 = PgBox(high: PgPoint(x: 3.0, y: 4.0), low: PgPoint(x: 1.0, y: 2.0))
+ let p = toPgParam(@[b1])
+ check p.oid == OidBoxArray
+ let fields = @[mkField(OidBoxArray, 1'i16)]
+ let row = mkRow(@[p.value], fields)
+ let arr = row.getBoxArray(0)
+ check arr.len == 1
+ check arr[0].high.x == 3.0
+ check arr[0].low.y == 2.0
+
+ test "toPgParam seq[PgLine] roundtrip":
+ let l1 = PgLine(a: 1.0, b: 2.0, c: 3.0)
+ let p = toPgParam(@[l1])
+ check p.oid == OidLineArray
+ let fields = @[mkField(OidLineArray, 1'i16)]
+ let row = mkRow(@[p.value], fields)
+ let arr = row.getLineArray(0)
+ check arr.len == 1
+ check arr[0].a == 1.0
+ check arr[0].b == 2.0
+
+suite "Other array types":
+ test "toPgParam seq[PgXml] roundtrip":
+ let x1 = PgXml("")
+ let x2 = PgXml("hello")
+ let p = toPgParam(@[x1, x2])
+ check p.oid == OidXmlArray
+ let fields = @[mkField(OidXmlArray, 1'i16)]
+ let row = mkRow(@[p.value], fields)
+ let arr = row.getXmlArray(0)
+ check arr.len == 2
+ check string(arr[0]) == ""
+ check string(arr[1]) == "hello"
+
+ test "getXmlArray text format":
+ let row: Row = @[some(toBytes("{\"\",\"hello\"}"))]
+ let arr = row.getXmlArray(0)
+ check arr.len == 2
+ check string(arr[0]) == ""
+
+ test "toPgParam seq[PgTsVector] roundtrip":
+ let tv1 = PgTsVector("'hello':1 'world':2")
+ let p = toPgParam(@[tv1])
+ check p.oid == OidTsVectorArray
+ let fields = @[mkField(OidTsVectorArray, 1'i16)]
+ let row = mkRow(@[p.value], fields)
+ let arr = row.getTsVectorArray(0)
+ check arr.len == 1
+
+ test "toPgParam seq[PgTsQuery] roundtrip":
+ let tq1 = PgTsQuery("hello & world")
+ let p = toPgParam(@[tq1])
+ check p.oid == OidTsQueryArray
+ let fields = @[mkField(OidTsQueryArray, 1'i16)]
+ let row = mkRow(@[p.value], fields)
+ let arr = row.getTsQueryArray(0)
+ check arr.len == 1
+
+suite "Multirange array types":
+ test "toPgParam seq[PgMultirange[int32]] text roundtrip":
+ let mr1 = toMultirange(rangeOf(1'i32, 10'i32), rangeOf(20'i32, 30'i32))
+ let mr2 = toMultirange(rangeOf(100'i32, 200'i32))
+ let p = toPgParam(@[mr1, mr2])
+ check p.oid == OidInt4MultirangeArray
+ check p.format == 0'i16
+ # Text format roundtrip
+ let row: Row = @[p.value]
+ let arr = row.getInt4MultirangeArray(0)
+ check arr.len == 2
+ check seq[PgRange[int32]](arr[0]).len == 2
+ check seq[PgRange[int32]](arr[1]).len == 1
+
+ test "toPgParam seq[PgMultirange[int32]] empty":
+ let p = toPgParam(newSeq[PgMultirange[int32]]())
+ check p.oid == OidInt4MultirangeArray
+ let row: Row = @[p.value]
+ check row.getInt4MultirangeArray(0).len == 0
+
+ test "getInt4MultirangeArrayOpt none":
+ let fields = @[mkField(OidInt4MultirangeArray, 1'i16)]
+ let row = mkRow(@[none(seq[byte])], fields)
+ check row.getInt4MultirangeArrayOpt(0).isNone
+
+ test "toPgParam seq[PgMultirange[int64]] text roundtrip":
+ let mr1 = toMultirange(rangeOf(100'i64, 200'i64))
+ let p = toPgParam(@[mr1])
+ check p.oid == OidInt8MultirangeArray
+ let row: Row = @[p.value]
+ let arr = row.getInt8MultirangeArray(0)
+ check arr.len == 1
+
+ test "toPgParam seq[PgMultirange[PgNumeric]] text roundtrip":
+ let mr1 = toMultirange(rangeOf(parsePgNumeric("1.5"), parsePgNumeric("3.5")))
+ let p = toPgParam(@[mr1])
+ check p.oid == OidNumMultirangeArray
+ let row: Row = @[p.value]
+ let arr = row.getNumMultirangeArray(0)
+ check arr.len == 1