Skip to content

Commit fa93d76

Browse files
authored
Fixes an issue where empty values were treated as null values and vica versa (#144)
1 parent cdb18d1 commit fa93d76

File tree

11 files changed

+74
-20
lines changed

11 files changed

+74
-20
lines changed

Sources/PostgresNIO/New/Data/Array+PSQLCodable.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ extension Array: PSQLEncodable where Element: PSQLArrayElement {
9191
buffer.writeInteger(1, as: Int32.self)
9292

9393
try self.forEach { element in
94-
try element._encode(into: &buffer, context: context)
94+
try element.encodeRaw(into: &buffer, context: context)
9595
}
9696
}
9797
}

Sources/PostgresNIO/New/Data/Optional+PSQLCodable.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,15 @@ extension Optional: PSQLEncodable where Wrapped: PSQLEncodable {
1919
}
2020

2121
func encode(into byteBuffer: inout ByteBuffer, context: PSQLEncodingContext) throws {
22+
preconditionFailure("Should never be hit, since `encodeRaw` is implemented.")
23+
}
24+
25+
func encodeRaw(into byteBuffer: inout ByteBuffer, context: PSQLEncodingContext) throws {
2226
switch self {
2327
case .none:
24-
return
28+
byteBuffer.writeInteger(-1, as: Int32.self)
2529
case .some(let value):
26-
try value.encode(into: &byteBuffer, context: context)
30+
try value.encodeRaw(into: &byteBuffer, context: context)
2731
}
2832
}
2933
}

Sources/PostgresNIO/New/Messages/Bind.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ extension PSQLFrontendMessage {
2828
let context = PSQLEncodingContext(jsonEncoder: jsonEncoder)
2929

3030
try self.parameters.forEach { parameter in
31-
try parameter._encode(into: &buffer, context: context)
31+
try parameter.encodeRaw(into: &buffer, context: context)
3232
}
3333

3434
// The number of result-column format codes that follow (denoted R below). This can be

Sources/PostgresNIO/New/Messages/DataRow.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ extension PSQLBackendMessage {
1717
try PSQLBackendMessage.ensureAtLeastNBytesRemaining(2, in: buffer)
1818
let bufferLength = Int(buffer.readInteger(as: Int32.self)!)
1919

20-
guard bufferLength > 0 else {
20+
guard bufferLength >= 0 else {
2121
result.append(nil)
2222
continue
2323
}

Sources/PostgresNIO/New/PSQLCodable.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@ protocol PSQLEncodable {
33
/// identifies the data type that we will encode into `byteBuffer` in `encode`
44
var psqlType: PSQLDataType { get }
55

6-
/// encoding the entity into the `byteBuffer` in postgres binary format
6+
/// Encode the entity into the `byteBuffer` in Postgres binary format, without setting
7+
/// the byte count. This method is called from the default `encodeRaw` implementation.
78
func encode(into byteBuffer: inout ByteBuffer, context: PSQLEncodingContext) throws
9+
10+
/// Encode the entity into the `byteBuffer` in Postgres binary format including its
11+
/// leading byte count. This method has a default implementation and may be overriden
12+
/// only for special cases, like `Optional`s.
13+
func encodeRaw(into byteBuffer: inout ByteBuffer, context: PSQLEncodingContext) throws
814
}
915

1016
/// A type that can decode itself from a postgres wire binary representation.
@@ -18,7 +24,7 @@ protocol PSQLDecodable {
1824
protocol PSQLCodable: PSQLEncodable, PSQLDecodable {}
1925

2026
extension PSQLEncodable {
21-
func _encode(into buffer: inout ByteBuffer, context: PSQLEncodingContext) throws {
27+
func encodeRaw(into buffer: inout ByteBuffer, context: PSQLEncodingContext) throws {
2228
// The length of the parameter value, in bytes (this count does not include
2329
// itself). Can be zero.
2430
let lengthIndex = buffer.writerIndex

Sources/PostgresNIO/Postgres+PSQLCompat.swift

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,19 @@ extension PostgresData: PSQLEncodable {
3030
PSQLDataType(Int32(self.type.rawValue))
3131
}
3232

33-
// encoding
3433
func encode(into byteBuffer: inout ByteBuffer, context: PSQLEncodingContext) throws {
35-
guard var selfBuffer = self.value else {
36-
return
34+
preconditionFailure("Should never be hit, since `encodeRaw` is implemented.")
35+
}
36+
37+
// encoding
38+
func encodeRaw(into byteBuffer: inout ByteBuffer, context: PSQLEncodingContext) throws {
39+
switch self.value {
40+
case .none:
41+
byteBuffer.writeInteger(-1, as: Int32.self)
42+
case .some(var input):
43+
byteBuffer.writeInteger(Int32(input.readableBytes))
44+
byteBuffer.writeBuffer(&input)
3745
}
38-
byteBuffer.writeBuffer(&selfBuffer)
3946
}
4047
}
4148

Tests/PostgresNIOTests/New/Data/Optional+PSQLCodableTests.swift

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ class Optional_PSQLCodableTests: XCTestCase {
3737

3838
var buffer = ByteBuffer()
3939
XCTAssertEqual(encodable.psqlType, .uuid)
40-
XCTAssertNoThrow(try encodable.encode(into: &buffer, context: .forTests()))
41-
XCTAssertEqual(buffer.readableBytes, 16)
42-
40+
XCTAssertNoThrow(try encodable.encodeRaw(into: &buffer, context: .forTests()))
41+
XCTAssertEqual(buffer.readableBytes, 20)
42+
XCTAssertEqual(buffer.readInteger(as: Int32.self), 16)
4343
let data = PSQLData(bytes: buffer, dataType: .uuid)
4444

4545
var result: UUID?
@@ -53,8 +53,9 @@ class Optional_PSQLCodableTests: XCTestCase {
5353

5454
var buffer = ByteBuffer()
5555
XCTAssertEqual(encodable.psqlType, .null)
56-
XCTAssertNoThrow(try encodable.encode(into: &buffer, context: .forTests()))
57-
XCTAssertEqual(buffer.readableBytes, 0)
56+
XCTAssertNoThrow(try encodable.encodeRaw(into: &buffer, context: .forTests()))
57+
XCTAssertEqual(buffer.readableBytes, 4)
58+
XCTAssertEqual(buffer.readInteger(as: Int32.self), -1)
5859

5960
let data = PSQLData(bytes: nil, dataType: .uuid)
6061

Tests/PostgresNIOTests/New/Extensions/ConnectionAction+TestUtils.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ extension ConnectionStateMachine.ConnectionAction: Equatable {
4545
let encodingContext = PSQLEncodingContext(jsonEncoder: JSONEncoder())
4646

4747
do {
48-
try lhs._encode(into: &lhsbuffer, context: encodingContext)
49-
try rhs._encode(into: &rhsbuffer, context: encodingContext)
48+
try lhs.encodeRaw(into: &lhsbuffer, context: encodingContext)
49+
try rhs.encodeRaw(into: &rhsbuffer, context: encodingContext)
5050
} catch {
5151
return false
5252
}

Tests/PostgresNIOTests/New/Messages/DataRowTests.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,23 @@ import XCTest
66
class DataRowTests: XCTestCase {
77
func testDecode() {
88
let buffer = ByteBuffer.backendMessage(id: .dataRow) { buffer in
9-
buffer.writeInteger(2, as: Int16.self)
9+
// the data row has 3 columns
10+
buffer.writeInteger(3, as: Int16.self)
11+
12+
// this is a null value
1013
buffer.writeInteger(-1, as: Int32.self)
14+
15+
// this is an empty value. for example a empty string
16+
buffer.writeInteger(0, as: Int32.self)
17+
18+
// this is a column with ten bytes
1119
buffer.writeInteger(10, as: Int32.self)
1220
buffer.writeBytes([UInt8](repeating: 5, count: 10))
1321
}
1422

1523
let expectedColumns: [ByteBuffer?] = [
1624
nil,
25+
ByteBuffer(),
1726
ByteBuffer(bytes: [UInt8](repeating: 5, count: 10))
1827
]
1928

Tests/PostgresNIOTests/PostgresNIOTests.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,33 @@ final class PostgresNIOTests: XCTestCase {
560560
]).wait())
561561
XCTAssertEqual(rows?.first?.column("array")?.array(of: Int.self), [])
562562
}
563+
564+
// https://github.com/vapor/postgres-nio/issues/143
565+
func testEmptyStringFromNonNullColumn() {
566+
var conn: PostgresConnection?
567+
XCTAssertNoThrow(conn = try PostgresConnection.test(on: eventLoop).wait())
568+
defer { XCTAssertNoThrow( try conn?.close().wait() ) }
569+
570+
XCTAssertNoThrow(_ = try conn?.simpleQuery(#"DROP TABLE IF EXISTS "non_null_empty_strings""#).wait())
571+
XCTAssertNoThrow(_ = try conn?.simpleQuery("""
572+
CREATE TABLE non_null_empty_strings (
573+
"id" SERIAL,
574+
"nonNullString" text NOT NULL,
575+
PRIMARY KEY ("id")
576+
);
577+
""").wait())
578+
defer { XCTAssertNoThrow(_ = try conn?.simpleQuery(#"DROP TABLE "non_null_empty_strings""#).wait()) }
579+
580+
XCTAssertNoThrow(_ = try conn?.simpleQuery("""
581+
INSERT INTO non_null_empty_strings ("nonNullString") VALUES ('')
582+
""").wait())
583+
584+
var rows: [PostgresRow]?
585+
XCTAssertNoThrow(rows = try conn?.simpleQuery(#"SELECT * FROM "non_null_empty_strings""#).wait())
586+
XCTAssertEqual(rows?.count, 1)
587+
XCTAssertEqual(rows?.first?.column("nonNullString")?.string, "") // <--- this fails
588+
}
589+
563590

564591
func testBoolSerialize() {
565592
var conn: PostgresConnection?

Tests/PostgresNIOTests/Utilities.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ extension PostgresConnection {
1717
}
1818
}
1919

20-
static func test(on eventLoop: EventLoop, logLevel: Logger.Level = .trace) -> EventLoopFuture<PostgresConnection> {
20+
static func test(on eventLoop: EventLoop, logLevel: Logger.Level = .info) -> EventLoopFuture<PostgresConnection> {
2121
return testUnauthenticated(on: eventLoop, logLevel: logLevel).flatMap { conn in
2222
return conn.authenticate(
2323
username: env("POSTGRES_USER") ?? "vapor_username",

0 commit comments

Comments
 (0)