Skip to content

Commit a3e257c

Browse files
MrMagetanner0101
authored andcommitted
Use a dictionary for Postgres row column lookups. (#22)
* Use a dictionary for Postgres row column lookups. * PR fixes. * Update the Linux tests.
1 parent d2d9e8c commit a3e257c

File tree

4 files changed

+300
-36
lines changed

4 files changed

+300
-36
lines changed

Sources/NIOPostgres/Data/PostgresRow.swift

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,70 @@ public struct PostgresRow: CustomStringConvertible {
22
final class LookupTable {
33
let rowDescription: PostgresMessage.RowDescription
44
let resultFormat: [PostgresFormatCode]
5-
5+
6+
struct Entry {
7+
let indexInRow: Int
8+
let field: PostgresMessage.RowDescription.Field
9+
}
10+
private var _columnNameToIndexLookupTable: [String: [Entry]]?
11+
var columnNameToIndexLookupTable: [String: [Entry]] {
12+
if let existing = _columnNameToIndexLookupTable {
13+
return existing
14+
}
15+
16+
var columnNameToIndexLookupTable: [String: [Entry]] = [:]
17+
for (fieldIndex, field) in rowDescription.fields.enumerated() {
18+
columnNameToIndexLookupTable[field.name, default: []].append(.init(indexInRow: fieldIndex, field: field))
19+
}
20+
self._columnNameToIndexLookupTable = columnNameToIndexLookupTable
21+
return columnNameToIndexLookupTable
22+
}
23+
624
init(
725
rowDescription: PostgresMessage.RowDescription,
826
resultFormat: [PostgresFormatCode]
927
) {
1028
self.rowDescription = rowDescription
1129
self.resultFormat = resultFormat
1230
}
13-
14-
func lookup(column: String, tableOID: UInt32) -> (Int, PostgresMessage.RowDescription.Field)? {
15-
for (i, field) in self.rowDescription.fields.enumerated() {
16-
if (tableOID == 0 || field.tableOID == tableOID) && field.name == column {
17-
return (i, field)
18-
}
31+
32+
func lookup(column: String, tableOID: UInt32) -> Entry? {
33+
guard let columnTable = columnNameToIndexLookupTable[column]
34+
else { return nil }
35+
36+
if tableOID == 0 {
37+
return columnTable.first
38+
} else {
39+
return columnTable.first { $0.field.tableOID == tableOID }
1940
}
20-
return nil
2141
}
2242
}
23-
43+
2444
let dataRow: PostgresMessage.DataRow
2545
let lookupTable: LookupTable
26-
46+
2747
public func column(_ column: String, tableOID: UInt32 = 0) -> PostgresData? {
28-
guard let (offset, field) = self.lookupTable.lookup(column: column, tableOID: tableOID) else {
48+
guard let entry = self.lookupTable.lookup(column: column, tableOID: tableOID) else {
2949
return nil
3050
}
3151
let formatCode: PostgresFormatCode
3252
switch self.lookupTable.resultFormat.count {
3353
case 1: formatCode = self.lookupTable.resultFormat[0]
34-
default: formatCode = field.formatCode
54+
default: formatCode = entry.field.formatCode
3555
}
3656
return PostgresData(
37-
type: field.dataType,
38-
typeModifier: field.dataTypeModifier,
57+
type: entry.field.dataType,
58+
typeModifier: entry.field.dataTypeModifier,
3959
formatCode: formatCode,
40-
value: self.dataRow.columns[offset].value
60+
value: self.dataRow.columns[entry.indexInRow].value
4161
)
4262
}
43-
63+
4464
public var description: String {
4565
var row: [String: PostgresData] = [:]
4666
for field in self.lookupTable.rowDescription.fields {
4767
#warning("TODO: reverse lookup table names for desc")
48-
row[field.name] = self.column(field.name, tableOID: field.tableOID)
68+
row[field.name + "(\(field.tableOID))"] = self.column(field.name, tableOID: field.tableOID)
4969
}
5070
return row.description
5171
}

Sources/NIOPostgres/Message/PostgresMessage+RowDescription.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ extension PostgresMessage {
6969

7070
/// See `CustomStringConvertible`.
7171
public var description: String {
72-
return self.name.description
72+
return self.name.description + "(\(tableOID))"
7373
}
7474
}
7575

Tests/NIOPostgresTests/NIOPostgresTests.swift

Lines changed: 256 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -277,46 +277,286 @@ final class NIOPostgresTests: XCTestCase {
277277
XCTAssertEqual(error.code, .invalid_password)
278278
}
279279
}
280-
281-
func testSelectPerformance() throws {
280+
281+
func testRangeSelectDecodePerformance() throws {
282282
// std deviation too high
283+
struct Series: Decodable {
284+
var num: Int
285+
}
286+
283287
let conn = try PostgresConnection.test(on: eventLoop).wait()
284288
defer { try! conn.close().wait() }
285289
measure {
286290
do {
287-
_ = try conn.query("SELECT * FROM pg_type").wait()
291+
for _ in 0..<50 {
292+
try conn.query("SELECT * FROM generate_series(1, 10000) num") { row in
293+
_ = row.column("num")?.int
294+
}.wait()
295+
}
288296
} catch {
289297
XCTFail("\(error)")
290298
}
291299
}
292300
}
293-
294-
func testRangeSelectPerformance() throws {
295-
// std deviation too high
301+
302+
func testColumnsInJoin() throws {
303+
let conn = try PostgresConnection.test(on: eventLoop).wait()
304+
defer { try! conn.close().wait() }
305+
306+
let dateInTable1 = Date(timeIntervalSince1970: 1234)
307+
let dateInTable2 = Date(timeIntervalSince1970: 5678)
308+
_ = try conn.simpleQuery("""
309+
CREATE TABLE table1 (
310+
"id" int8 NOT NULL,
311+
"table2_id" int8,
312+
"intValue" int8,
313+
"stringValue" text,
314+
"dateValue" timestamptz,
315+
PRIMARY KEY ("id")
316+
);
317+
""").wait()
318+
defer { _ = try! conn.simpleQuery("DROP TABLE \"table1\"").wait() }
319+
320+
_ = try conn.simpleQuery("""
321+
CREATE TABLE table2 (
322+
"id" int8 NOT NULL,
323+
"intValue" int8,
324+
"stringValue" text,
325+
"dateValue" timestamptz,
326+
PRIMARY KEY ("id")
327+
);
328+
""").wait()
329+
defer { _ = try! conn.simpleQuery("DROP TABLE \"table2\"").wait() }
330+
331+
_ = try conn.simpleQuery("INSERT INTO table1 VALUES (12, 34, 56, 'stringInTable1', to_timestamp(1234))").wait()
332+
_ = try conn.simpleQuery("INSERT INTO table2 VALUES (34, 78, 'stringInTable2', to_timestamp(5678))").wait()
333+
334+
let tableNameToOID = Dictionary(uniqueKeysWithValues: try conn.simpleQuery("SELECT relname, oid FROM pg_class WHERE relname in ('table1', 'table2')")
335+
.wait()
336+
.map { row -> (String, UInt32) in (row.column("relname")!.string!, row.column("oid")!.uint32!) })
337+
338+
let row = try conn.query("SELECT * FROM table1 INNER JOIN table2 ON table1.table2_id = table2.id").wait().first!
339+
XCTAssertEqual(12, row.column("id", tableOID: tableNameToOID["table1"]!)?.int)
340+
XCTAssertEqual(34, row.column("table2_id", tableOID: tableNameToOID["table1"]!)?.int)
341+
XCTAssertEqual(56, row.column("intValue", tableOID: tableNameToOID["table1"]!)?.int)
342+
XCTAssertEqual("stringInTable1", row.column("stringValue", tableOID: tableNameToOID["table1"]!)?.string)
343+
XCTAssertEqual(dateInTable1, row.column("dateValue", tableOID: tableNameToOID["table1"]!)?.date)
344+
XCTAssertEqual(34, row.column("id", tableOID: tableNameToOID["table2"]!)?.int)
345+
XCTAssertEqual(78, row.column("intValue", tableOID: tableNameToOID["table2"]!)?.int)
346+
XCTAssertEqual("stringInTable2", row.column("stringValue", tableOID: tableNameToOID["table2"]!)?.string, "stringInTable2")
347+
XCTAssertEqual(dateInTable2, row.column("dateValue", tableOID: tableNameToOID["table2"]!)?.date)
348+
}
349+
350+
private func prepareTableToMeasureSelectPerformance(
351+
rowCount: Int, batchSize: Int = 1_000, schema: String, fixtureData: [PostgresData],
352+
file: StaticString = #file, line: UInt = #line) throws {
353+
XCTAssertEqual(rowCount % batchSize, 0, "`rowCount` must be a multiple of `batchSize`", file: file, line: line)
296354
let conn = try PostgresConnection.test(on: eventLoop).wait()
297355
defer { try! conn.close().wait() }
356+
357+
_ = try conn.simpleQuery("""
358+
CREATE TABLE "measureSelectPerformance" (
359+
"id" int8 NOT NULL,
360+
\(schema)
361+
PRIMARY KEY ("id")
362+
);
363+
""").wait()
364+
365+
// Batch `batchSize` inserts into one for better insert performance.
366+
let totalArgumentsPerRow = fixtureData.count + 1
367+
let insertArgumentsPlaceholder = (0..<batchSize).map { indexInBatch in
368+
"("
369+
+ (0..<totalArgumentsPerRow).map { argumentIndex in "$\(indexInBatch * totalArgumentsPerRow + argumentIndex + 1)" }
370+
.joined(separator: ", ")
371+
+ ")"
372+
}.joined(separator: ", ")
373+
let insertQuery = "INSERT INTO \"measureSelectPerformance\" VALUES \(insertArgumentsPlaceholder)"
374+
var batchedFixtureData = Array(repeating: [PostgresData(int: 0)] + fixtureData, count: batchSize).flatMap { $0 }
375+
for batchIndex in 0..<(rowCount / batchSize) {
376+
for indexInBatch in 0..<batchSize {
377+
let rowIndex = batchIndex * batchSize + indexInBatch
378+
batchedFixtureData[indexInBatch * totalArgumentsPerRow] = PostgresData(int: rowIndex)
379+
}
380+
_ = try conn.query(insertQuery, batchedFixtureData).wait()
381+
}
382+
}
383+
384+
func testSelectTinyModel() throws {
385+
let conn = try PostgresConnection.test(on: eventLoop).wait()
386+
defer { try! conn.close().wait() }
387+
388+
let now = Date()
389+
let uuid = UUID()
390+
try prepareTableToMeasureSelectPerformance(
391+
rowCount: 300_000, batchSize: 5_000,
392+
schema:
393+
"""
394+
"int" int8,
395+
""",
396+
fixtureData: [PostgresData(int: 1234)])
397+
defer { _ = try! conn.simpleQuery("DROP TABLE \"measureSelectPerformance\"").wait() }
398+
298399
measure {
299400
do {
300-
_ = try conn.simpleQuery("SELECT * FROM generate_series(1, 10000) num").wait()
401+
try conn.query("SELECT * FROM \"measureSelectPerformance\"") { row in
402+
_ = row.column("int")?.int
403+
}.wait()
301404
} catch {
302405
XCTFail("\(error)")
303406
}
304407
}
305408
}
306-
307-
func testRangeSelectDecodePerformance() throws {
308-
// std deviation too high
309-
struct Series: Decodable {
310-
var num: Int
409+
410+
func testSelectMediumModel() throws {
411+
let conn = try PostgresConnection.test(on: eventLoop).wait()
412+
defer { try! conn.close().wait() }
413+
414+
let now = Date()
415+
let uuid = UUID()
416+
try prepareTableToMeasureSelectPerformance(
417+
rowCount: 300_000,
418+
schema:
419+
// TODO: Also add a `Double` and a `Data` field to this performance test.
420+
"""
421+
"string" text,
422+
"int" int8,
423+
"date" timestamptz,
424+
"uuid" uuid,
425+
""",
426+
fixtureData: [PostgresData(string: "foo"), PostgresData(int: 0),
427+
now.postgresData!, PostgresData(uuid: uuid)])
428+
defer { _ = try! conn.simpleQuery("DROP TABLE \"measureSelectPerformance\"").wait() }
429+
430+
measure {
431+
do {
432+
try conn.query("SELECT * FROM \"measureSelectPerformance\"") { row in
433+
_ = row.column("id")?.int
434+
_ = row.column("string")?.string
435+
_ = row.column("int")?.int
436+
_ = row.column("date")?.date
437+
_ = row.column("uuid")?.uuid
438+
}.wait()
439+
} catch {
440+
XCTFail("\(error)")
441+
}
311442
}
312-
443+
}
444+
445+
func testSelectLargeModel() throws {
313446
let conn = try PostgresConnection.test(on: eventLoop).wait()
314447
defer { try! conn.close().wait() }
448+
449+
let now = Date()
450+
let uuid = UUID()
451+
try prepareTableToMeasureSelectPerformance(
452+
rowCount: 100_000,
453+
schema:
454+
// TODO: Also add `Double` and `Data` fields to this performance test.
455+
"""
456+
"string1" text,
457+
"string2" text,
458+
"string3" text,
459+
"string4" text,
460+
"string5" text,
461+
"int1" int8,
462+
"int2" int8,
463+
"int3" int8,
464+
"int4" int8,
465+
"int5" int8,
466+
"date1" timestamptz,
467+
"date2" timestamptz,
468+
"date3" timestamptz,
469+
"date4" timestamptz,
470+
"date5" timestamptz,
471+
"uuid1" uuid,
472+
"uuid2" uuid,
473+
"uuid3" uuid,
474+
"uuid4" uuid,
475+
"uuid5" uuid,
476+
""",
477+
fixtureData: [PostgresData(string: "string1"), PostgresData(string: "string2"), PostgresData(string: "string3"), PostgresData(string: "string4"), PostgresData(string: "string5"),
478+
PostgresData(int: 1), PostgresData(int: 2), PostgresData(int: 3), PostgresData(int: 4), PostgresData(int: 5),
479+
now.postgresData!, now.postgresData!, now.postgresData!, now.postgresData!, now.postgresData!,
480+
PostgresData(uuid: uuid), PostgresData(uuid: uuid), PostgresData(uuid: uuid), PostgresData(uuid: uuid), PostgresData(uuid: uuid)])
481+
defer { _ = try! conn.simpleQuery("DROP TABLE \"measureSelectPerformance\"").wait() }
482+
483+
measure {
484+
do {
485+
try conn.query("SELECT * FROM \"measureSelectPerformance\"") { row in
486+
_ = row.column("id")?.int
487+
_ = row.column("string1")?.string
488+
_ = row.column("string2")?.string
489+
_ = row.column("string3")?.string
490+
_ = row.column("string4")?.string
491+
_ = row.column("string5")?.string
492+
_ = row.column("int1")?.int
493+
_ = row.column("int2")?.int
494+
_ = row.column("int3")?.int
495+
_ = row.column("int4")?.int
496+
_ = row.column("int5")?.int
497+
_ = row.column("date1")?.date
498+
_ = row.column("date2")?.date
499+
_ = row.column("date3")?.date
500+
_ = row.column("date4")?.date
501+
_ = row.column("date5")?.date
502+
_ = row.column("uuid1")?.uuid
503+
_ = row.column("uuid2")?.uuid
504+
_ = row.column("uuid3")?.uuid
505+
_ = row.column("uuid4")?.uuid
506+
_ = row.column("uuid5")?.uuid
507+
}.wait()
508+
} catch {
509+
XCTFail("\(error)")
510+
}
511+
}
512+
}
513+
514+
func testSelectLargeModelWithLongFieldNames() throws {
515+
let conn = try PostgresConnection.test(on: eventLoop).wait()
516+
defer { try! conn.close().wait() }
517+
518+
let fieldIndices = Array(1...20)
519+
let fieldNames = fieldIndices.map { "veryLongFieldNameVeryLongFieldName\($0)" }
520+
try prepareTableToMeasureSelectPerformance(
521+
rowCount: 50_000, batchSize: 200,
522+
schema: fieldNames.map { "\"\($0)\" int8" }.joined(separator: ", ") + ",",
523+
fixtureData: fieldIndices.map { PostgresData(int: $0) })
524+
defer { _ = try! conn.simpleQuery("DROP TABLE \"measureSelectPerformance\"").wait() }
525+
526+
measure {
527+
do {
528+
try conn.query("SELECT * FROM \"measureSelectPerformance\"") { row in
529+
_ = row.column("id")?.int
530+
for fieldName in fieldNames {
531+
_ = row.column(fieldName)?.int
532+
}
533+
}.wait()
534+
} catch {
535+
XCTFail("\(error)")
536+
}
537+
}
538+
}
539+
540+
func testSelectHugeModel() throws {
541+
let conn = try PostgresConnection.test(on: eventLoop).wait()
542+
defer { try! conn.close().wait() }
543+
544+
let fieldIndices = Array(1...100)
545+
let fieldNames = fieldIndices.map { "int\($0)" }
546+
try prepareTableToMeasureSelectPerformance(
547+
rowCount: 10_000, batchSize: 200,
548+
schema: fieldNames.map { "\"\($0)\" int8" }.joined(separator: ", ") + ",",
549+
fixtureData: fieldIndices.map { PostgresData(int: $0) })
550+
defer { _ = try! conn.simpleQuery("DROP TABLE \"measureSelectPerformance\"").wait() }
551+
315552
measure {
316553
do {
317-
try conn.query("SELECT * FROM generate_series(1, 10000) num") { row in
318-
_ = row.column("num")?.int
319-
}.wait()
554+
try conn.query("SELECT * FROM \"measureSelectPerformance\"") { row in
555+
_ = row.column("id")?.int
556+
for fieldName in fieldNames {
557+
_ = row.column(fieldName)?.int
558+
}
559+
}.wait()
320560
} catch {
321561
XCTFail("\(error)")
322562
}

0 commit comments

Comments
 (0)