Files
UUVPN/iOS-SwiftUI-Code/GRDB.swift-6.29.3/Tests/GRDBTests/JoinSupportTests.swift
T
2025-01-28 12:28:03 +08:00

619 lines
24 KiB
Swift

import XCTest
import GRDB
let expectedSQL = """
SELECT
"t1".*,
"t2Left".*,
"t2Right".*,
"t3"."t1id", "t3"."name",
COUNT(DISTINCT t5.id) AS t5count
FROM t1
LEFT JOIN t2 t2Left ON t2Left.t1id = t1.id AND t2Left.name = 'left'
LEFT JOIN t2 t2Right ON t2Right.t1id = t1.id AND t2Right.name = 'right'
LEFT JOIN t3 ON t3.t1id = t1.id
LEFT JOIN t4 ON t4.t1id = t1.id
LEFT JOIN t5 ON t5.t3id = t3.t1id OR t5.t4id = t4.t1id
GROUP BY t1.id
ORDER BY t1.id
"""
let testedLiteral: SQL = """
SELECT
\(columnsOf: T1.self),
\(columnsOf: T2.self, tableAlias: "t2Left"),
\(columnsOf: T2.self, tableAlias: "t2Right"),
\(columnsOf: T3.self),
COUNT(DISTINCT t5.id) AS t5count
FROM t1
LEFT JOIN t2 t2Left ON t2Left.t1id = t1.id AND t2Left.name = 'left'
LEFT JOIN t2 t2Right ON t2Right.t1id = t1.id AND t2Right.name = 'right'
LEFT JOIN t3 ON t3.t1id = t1.id
LEFT JOIN t4 ON t4.t1id = t1.id
LEFT JOIN t5 ON t5.t3id = t3.t1id OR t5.t4id = t4.t1id
GROUP BY t1.id
ORDER BY t1.id
"""
private struct T1: Codable, FetchableRecord, TableRecord {
static let databaseTableName = "t1"
var id: Int64
var name: String
}
private struct T2: Codable, FetchableRecord, TableRecord {
static let databaseTableName = "t2"
var id: Int64
var t1id: Int64
var name: String
}
private struct T3: Codable, FetchableRecord, TableRecord {
static let databaseTableName = "t3"
static let databaseSelection: [any SQLSelectable] = [Column("t1id"), Column("name")]
var t1id: Int64
var name: String
}
private struct T4: Codable, FetchableRecord, TableRecord {
static let databaseTableName = "t4"
var t1id: Int64
var name: String
}
private struct T5: Codable, FetchableRecord, TableRecord {
static let databaseTableName = "t5"
var id: Int64
var t3id: Int64?
var t4id: Int64?
var name: String
}
private struct FlatModel: FetchableRecord {
private enum Scopes {
static let t1 = "t1"
static let t2Left = "t2Left"
static let t2Right = "t2Right"
static let t3 = "t3"
static let suffix = "suffix"
}
var t1: T1
var t2Left: T2?
var t2Right: T2?
var t3: T3?
var t5count: Int
init(row: Row) {
self.t1 = row[Scopes.t1]
self.t2Left = row[Scopes.t2Left]
self.t2Right = row[Scopes.t2Right]
self.t3 = row[Scopes.t3]
self.t5count = row.scopes[Scopes.suffix]!["t5count"]
}
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
static func modernAll() -> some FetchRequest<FlatModel> {
all()
}
static func all() -> AdaptedFetchRequest<SQLRequest<FlatModel>> {
SQLRequest<FlatModel>(literal: testedLiteral).adapted { db in
let adapters = try splittingRowAdapters(columnCounts: [
T1.numberOfSelectedColumns(db),
T2.numberOfSelectedColumns(db),
T2.numberOfSelectedColumns(db),
T3.numberOfSelectedColumns(db)])
return ScopeAdapter([
Scopes.t1: adapters[0],
Scopes.t2Left: adapters[1],
Scopes.t2Right: adapters[2],
Scopes.t3: adapters[3],
Scopes.suffix: adapters[4]])
}
}
static func hierarchicalAll() -> AdaptedFetchRequest<SQLRequest<FlatModel>> {
SQLRequest<FlatModel>(literal: testedLiteral).adapted { db in
let adapters = try splittingRowAdapters(columnCounts: [
T1.numberOfSelectedColumns(db),
T2.numberOfSelectedColumns(db),
T2.numberOfSelectedColumns(db),
T3.numberOfSelectedColumns(db)])
return ScopeAdapter([
Scopes.t1: adapters[0],
"t2": ScopeAdapter(base: EmptyRowAdapter(), scopes: [
Scopes.t2Left: adapters[1],
Scopes.t2Right: adapters[2]]),
Scopes.t3: adapters[3],
Scopes.suffix: adapters[4]])
}
}
}
private struct CodableFlatModel: FetchableRecord, Codable {
var t1: T1
var t2Left: T2?
var t2Right: T2?
var t3: T3?
var t5count: Int
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
static func modernAll() -> some FetchRequest<CodableFlatModel> {
all()
}
static func all() -> AdaptedFetchRequest<SQLRequest<CodableFlatModel>> {
SQLRequest<CodableFlatModel>(literal: testedLiteral).adapted { db in
let adapters = try splittingRowAdapters(columnCounts: [
T1.numberOfSelectedColumns(db),
T2.numberOfSelectedColumns(db),
T2.numberOfSelectedColumns(db),
T3.numberOfSelectedColumns(db)])
return ScopeAdapter([
CodingKeys.t1.stringValue: adapters[0],
CodingKeys.t2Left.stringValue: adapters[1],
CodingKeys.t2Right.stringValue: adapters[2],
CodingKeys.t3.stringValue: adapters[3]])
}
}
static func hierarchicalAll() -> AdaptedFetchRequest<SQLRequest<CodableFlatModel>> {
SQLRequest<CodableFlatModel>(literal: testedLiteral).adapted { db in
let adapters = try splittingRowAdapters(columnCounts: [
T1.numberOfSelectedColumns(db),
T2.numberOfSelectedColumns(db),
T2.numberOfSelectedColumns(db),
T3.numberOfSelectedColumns(db)])
return ScopeAdapter([
CodingKeys.t1.stringValue: adapters[0],
"t2": ScopeAdapter(base: EmptyRowAdapter(), scopes: [
CodingKeys.t2Left.stringValue: adapters[1],
CodingKeys.t2Right.stringValue: adapters[2]]),
CodingKeys.t3.stringValue: adapters[3]])
}
}
}
private struct CodableNestedModel: FetchableRecord, Codable {
struct T2Pair: Codable {
var left: T2?
var right: T2?
}
var t1: T1
var optionalT2Pair: T2Pair?
var t2Pair: T2Pair
var t3: T3?
var t5count: Int
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
static func modernAll() -> some FetchRequest<CodableNestedModel> {
all()
}
static func all() -> AdaptedFetchRequest<SQLRequest<CodableNestedModel>> {
SQLRequest<CodableNestedModel>(literal: testedLiteral).adapted { db in
let adapters = try splittingRowAdapters(columnCounts: [
T1.numberOfSelectedColumns(db),
T2.numberOfSelectedColumns(db),
T2.numberOfSelectedColumns(db),
T3.numberOfSelectedColumns(db)])
return ScopeAdapter([
CodingKeys.t1.stringValue: adapters[0],
CodingKeys.optionalT2Pair.stringValue: ScopeAdapter(base: EmptyRowAdapter(), scopes: [ // EmptyRowAdapter base so that optionalT2Pair only instantiates when its scopes contain non-null values
"left": adapters[1],
"right": adapters[2]]),
CodingKeys.t2Pair.stringValue: ScopeAdapter([
"left": adapters[1],
"right": adapters[2]]),
CodingKeys.t3.stringValue: adapters[3]])
}
}
}
class JoinSupportTests: GRDBTestCase {
override func setup(_ dbWriter: some DatabaseWriter) throws {
try dbWriter.write { db in
try db.create(table: "t1") { t in
t.primaryKey("id", .integer)
t.column("name", .text).notNull()
}
try db.create(table: "t2") { t in
t.primaryKey("id", .integer)
t.column("t1id", .integer).notNull().references("t1", onDelete: .cascade)
t.column("name", .text).notNull()
t.uniqueKey(["t1id", "name"])
}
try db.create(table: "t3") { t in
t.primaryKey("t1id", .integer).references("t1", onDelete: .cascade)
t.column("name", .text).notNull()
t.column("ignored", .integer)
}
try db.create(table: "t4") { t in
t.primaryKey("t1id", .integer).references("t1", onDelete: .cascade)
t.column("name", .text).notNull()
}
try db.create(table: "t5") { t in
t.primaryKey("id", .integer)
t.column("t3id", .integer).references("t3", onDelete: .cascade)
t.column("t4id", .integer).references("t4", onDelete: .cascade)
t.column("name", .text).notNull()
t.check(sql: "(t3id IS NOT NULL) + (t4id IS NOT NULL) = 1")
}
// Sample data
try db.execute(sql: """
INSERT INTO t1 (id, name) VALUES (1, 'A1');
INSERT INTO t1 (id, name) VALUES (2, 'A2');
INSERT INTO t1 (id, name) VALUES (3, 'A3');
INSERT INTO t2 (id, t1id, name) VALUES (1, 1, 'left');
INSERT INTO t2 (id, t1id, name) VALUES (2, 1, 'right');
INSERT INTO t2 (id, t1id, name) VALUES (3, 2, 'left');
INSERT INTO t3 (t1id, name) VALUES (1, 'A3');
INSERT INTO t4 (t1id, name) VALUES (1, 'A4');
INSERT INTO t4 (t1id, name) VALUES (2, 'B4');
INSERT INTO t5 (id, t3id, t4id, name) VALUES (1, 1, NULL, 'A5');
INSERT INTO t5 (id, t3id, t4id, name) VALUES (2, 1, NULL, 'B5');
INSERT INTO t5 (id, t3id, t4id, name) VALUES (3, NULL, 1, 'C5');
INSERT INTO t5 (id, t3id, t4id, name) VALUES (4, NULL, 1, 'D5');
INSERT INTO t5 (id, t3id, t4id, name) VALUES (5, NULL, 1, 'E5');
INSERT INTO t5 (id, t3id, t4id, name) VALUES (6, NULL, 2, 'F5');
INSERT INTO t5 (id, t3id, t4id, name) VALUES (7, NULL, 2, 'G5');
""")
}
}
func testNumberOfSelectedColumns() throws {
struct T: TableRecord { }
let dbQueue = try makeDatabaseQueue()
try dbQueue.inTransaction { db in
try db.execute(sql: "CREATE TABLE t (a)")
try XCTAssertEqual(T.numberOfSelectedColumns(db), 1)
return .rollback
}
try dbQueue.inTransaction { db in
try db.execute(sql: "CREATE TABLE t (a, b)")
try XCTAssertEqual(T.numberOfSelectedColumns(db), 2)
return .rollback
}
}
func testNumberOfSelectedColumnsIncludeGeneratedColumns() throws {
#if !GRDBCUSTOMSQLITE
throw XCTSkip("Generated columns are not available")
#else
struct T: TableRecord { }
let dbQueue = try makeDatabaseQueue()
try dbQueue.inTransaction { db in
try db.execute(sql: "CREATE TABLE t (a, b ALWAYS GENERATED AS (a) VIRTUAL)")
try XCTAssertEqual(T.numberOfSelectedColumns(db), 2)
return .rollback
}
try dbQueue.inTransaction { db in
try db.execute(sql: "CREATE TABLE t (a, b ALWAYS GENERATED AS (a) STORED)")
try XCTAssertEqual(T.numberOfSelectedColumns(db), 2)
return .rollback
}
#endif
}
func testSampleData() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let rows = try SQLRequest<Row>(literal: testedLiteral).fetchAll(db)
XCTAssertEqual(rows.count, 3)
XCTAssertEqual(rows[0], [
// t1.*
"id": 1, "name": "A1",
// t2Left.*
"id": 1, "t1id": 1, "name": "left",
// t2Right.*
"id": 2, "t1id": 1, "name": "right",
// t3.*
"t1id": 1, "name": "A3",
// t5count
"t5count": 5])
XCTAssertEqual(rows[1], [
// t1.*
"id": 2, "name": "A2",
// t2Left.*
"id": 3, "t1id": 2, "name": "left",
// t2Right.*
"id": nil, "t1id": nil, "name": nil,
// t3.*
"t1id": nil, "name": nil,
// t5count
"t5count": 2])
XCTAssertEqual(rows[2], [
// t1.*
"id": 3, "name": "A3",
// t2Left.*
"id": nil, "t1id": nil, "name": nil,
// t2Right.*
"id": nil, "t1id": nil, "name": nil,
// t3.*
"t1id": nil, "name": nil,
// t5count
"t5count": 0])
}
}
func testTestedSQL() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let (testedSQL, _) = try testedLiteral.build(db)
XCTAssertEqual(testedSQL, expectedSQL)
}
}
func testSplittingRowAdapters() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let request = SQLRequest<Row>(literal: testedLiteral).adapted { db in
let adapters = try splittingRowAdapters(columnCounts: [
T1.numberOfSelectedColumns(db),
T2.numberOfSelectedColumns(db),
T2.numberOfSelectedColumns(db),
T3.numberOfSelectedColumns(db)])
XCTAssertEqual(adapters.count, 5)
return ScopeAdapter([
"t1": adapters[0],
"t2Left": adapters[1],
"t2Right": adapters[2],
"t3": adapters[3],
"suffix": adapters[4]])
}
let rows = try request.fetchAll(db)
XCTAssertEqual(rows.count, 3)
XCTAssertEqual(rows[0].unscoped, [
// t1.*
"id": 1, "name": "A1",
// t2Left.*
"id": 1, "t1id": 1, "name": "left",
// t2Right.*
"id": 2, "t1id": 1, "name": "right",
// t3.*
"t1id": 1, "name": "A3",
// t5count
"t5count": 5])
XCTAssertEqual(rows[0].scopes["t1"]!, ["id": 1, "name": "A1"])
XCTAssertEqual(rows[0].scopes["t2Left"]!, ["id": 1, "t1id": 1, "name": "left"])
XCTAssertEqual(rows[0].scopes["t2Right"]!, ["id": 2, "t1id": 1, "name": "right"])
XCTAssertEqual(rows[0].scopes["t3"]!, ["t1id": 1, "name": "A3"])
XCTAssertEqual(rows[0].scopes["suffix"]!, ["t5count": 5])
XCTAssertEqual(rows[1].unscoped, [
// t1.*
"id": 2, "name": "A2",
// t2Left.*
"id": 3, "t1id": 2, "name": "left",
// t2Right.*
"id": nil, "t1id": nil, "name": nil,
// t3.*
"t1id": nil, "name": nil,
// t5count
"t5count": 2])
XCTAssertEqual(rows[1].scopes["t1"]!, ["id": 2, "name": "A2"])
XCTAssertEqual(rows[1].scopes["t2Left"]!, ["id": 3, "t1id": 2, "name": "left"])
XCTAssertEqual(rows[1].scopes["t2Right"]!, ["id": nil, "t1id": nil, "name": nil])
XCTAssertEqual(rows[1].scopes["t3"]!, ["t1id": nil, "name": nil])
XCTAssertEqual(rows[1].scopes["suffix"]!, ["t5count": 2])
XCTAssertEqual(rows[2].unscoped, [
// t1.*
"id": 3, "name": "A3",
// t2Left.*
"id": nil, "t1id": nil, "name": nil,
// t2Right.*
"id": nil, "t1id": nil, "name": nil,
// t3.*
"t1id": nil, "name": nil,
// t5count
"t5count": 0])
XCTAssertEqual(rows[2].scopes["t1"]!, ["id": 3, "name": "A3"])
XCTAssertEqual(rows[2].scopes["t2Left"]!, ["id": nil, "t1id": nil, "name": nil])
XCTAssertEqual(rows[2].scopes["t2Right"]!, ["id": nil, "t1id": nil, "name": nil])
XCTAssertEqual(rows[2].scopes["t3"]!, ["t1id": nil, "name": nil])
XCTAssertEqual(rows[2].scopes["suffix"]!, ["t5count": 0])
}
}
func testFlatModel() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let models = try FlatModel.all().fetchAll(db)
XCTAssertEqual(models.count, 3)
XCTAssertEqual(models[0].t1.id, 1)
XCTAssertEqual(models[0].t1.name, "A1")
XCTAssertEqual(models[0].t2Left!.id, 1)
XCTAssertEqual(models[0].t2Left!.t1id, 1)
XCTAssertEqual(models[0].t2Left!.name, "left")
XCTAssertEqual(models[0].t2Right!.id, 2)
XCTAssertEqual(models[0].t2Right!.t1id, 1)
XCTAssertEqual(models[0].t2Right!.name, "right")
XCTAssertEqual(models[0].t3!.t1id, 1)
XCTAssertEqual(models[0].t3!.name, "A3")
XCTAssertEqual(models[0].t5count, 5)
XCTAssertEqual(models[1].t1.id, 2)
XCTAssertEqual(models[1].t1.name, "A2")
XCTAssertEqual(models[1].t2Left!.id, 3)
XCTAssertEqual(models[1].t2Left!.t1id, 2)
XCTAssertEqual(models[1].t2Left!.name, "left")
XCTAssertNil(models[1].t2Right)
XCTAssertNil(models[1].t3)
XCTAssertEqual(models[1].t5count, 2)
XCTAssertEqual(models[2].t1.id, 3)
XCTAssertEqual(models[2].t1.name, "A3")
XCTAssertNil(models[2].t2Left)
XCTAssertNil(models[2].t2Right)
XCTAssertNil(models[2].t3)
XCTAssertEqual(models[2].t5count, 0)
}
}
func testFlatModelFromHierarchicalRequest() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let models = try FlatModel.hierarchicalAll().fetchAll(db)
XCTAssertEqual(models.count, 3)
XCTAssertEqual(models[0].t1.id, 1)
XCTAssertEqual(models[0].t1.name, "A1")
XCTAssertEqual(models[0].t2Left!.id, 1)
XCTAssertEqual(models[0].t2Left!.t1id, 1)
XCTAssertEqual(models[0].t2Left!.name, "left")
XCTAssertEqual(models[0].t2Right!.id, 2)
XCTAssertEqual(models[0].t2Right!.t1id, 1)
XCTAssertEqual(models[0].t2Right!.name, "right")
XCTAssertEqual(models[0].t3!.t1id, 1)
XCTAssertEqual(models[0].t3!.name, "A3")
XCTAssertEqual(models[0].t5count, 5)
XCTAssertEqual(models[1].t1.id, 2)
XCTAssertEqual(models[1].t1.name, "A2")
XCTAssertEqual(models[1].t2Left!.id, 3)
XCTAssertEqual(models[1].t2Left!.t1id, 2)
XCTAssertEqual(models[1].t2Left!.name, "left")
XCTAssertNil(models[1].t2Right)
XCTAssertNil(models[1].t3)
XCTAssertEqual(models[1].t5count, 2)
XCTAssertEqual(models[2].t1.id, 3)
XCTAssertEqual(models[2].t1.name, "A3")
XCTAssertNil(models[2].t2Left)
XCTAssertNil(models[2].t2Right)
XCTAssertNil(models[2].t3)
XCTAssertEqual(models[2].t5count, 0)
}
}
func testCodableFlatModel() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let models = try CodableFlatModel.all().fetchAll(db)
XCTAssertEqual(models.count, 3)
XCTAssertEqual(models[0].t1.id, 1)
XCTAssertEqual(models[0].t1.name, "A1")
XCTAssertEqual(models[0].t2Left!.id, 1)
XCTAssertEqual(models[0].t2Left!.t1id, 1)
XCTAssertEqual(models[0].t2Left!.name, "left")
XCTAssertEqual(models[0].t2Right!.id, 2)
XCTAssertEqual(models[0].t2Right!.t1id, 1)
XCTAssertEqual(models[0].t2Right!.name, "right")
XCTAssertEqual(models[0].t3!.t1id, 1)
XCTAssertEqual(models[0].t3!.name, "A3")
XCTAssertEqual(models[0].t5count, 5)
XCTAssertEqual(models[1].t1.id, 2)
XCTAssertEqual(models[1].t1.name, "A2")
XCTAssertEqual(models[1].t2Left!.id, 3)
XCTAssertEqual(models[1].t2Left!.t1id, 2)
XCTAssertEqual(models[1].t2Left!.name, "left")
XCTAssertNil(models[1].t2Right)
XCTAssertNil(models[1].t3)
XCTAssertEqual(models[1].t5count, 2)
XCTAssertEqual(models[2].t1.id, 3)
XCTAssertEqual(models[2].t1.name, "A3")
XCTAssertNil(models[2].t2Left)
XCTAssertNil(models[2].t2Right)
XCTAssertNil(models[2].t3)
XCTAssertEqual(models[2].t5count, 0)
}
}
func testCodableFlatModelFromHierarchicalRequest() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let models = try CodableFlatModel.hierarchicalAll().fetchAll(db)
XCTAssertEqual(models.count, 3)
XCTAssertEqual(models[0].t1.id, 1)
XCTAssertEqual(models[0].t1.name, "A1")
XCTAssertEqual(models[0].t2Left!.id, 1)
XCTAssertEqual(models[0].t2Left!.t1id, 1)
XCTAssertEqual(models[0].t2Left!.name, "left")
XCTAssertEqual(models[0].t2Right!.id, 2)
XCTAssertEqual(models[0].t2Right!.t1id, 1)
XCTAssertEqual(models[0].t2Right!.name, "right")
XCTAssertEqual(models[0].t3!.t1id, 1)
XCTAssertEqual(models[0].t3!.name, "A3")
XCTAssertEqual(models[0].t5count, 5)
XCTAssertEqual(models[1].t1.id, 2)
XCTAssertEqual(models[1].t1.name, "A2")
XCTAssertEqual(models[1].t2Left!.id, 3)
XCTAssertEqual(models[1].t2Left!.t1id, 2)
XCTAssertEqual(models[1].t2Left!.name, "left")
XCTAssertNil(models[1].t2Right)
XCTAssertNil(models[1].t3)
XCTAssertEqual(models[1].t5count, 2)
XCTAssertEqual(models[2].t1.id, 3)
XCTAssertEqual(models[2].t1.name, "A3")
XCTAssertNil(models[2].t2Left)
XCTAssertNil(models[2].t2Right)
XCTAssertNil(models[2].t3)
XCTAssertEqual(models[2].t5count, 0)
}
}
func testCodableNestedModel() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let models = try CodableNestedModel.all().fetchAll(db)
XCTAssertEqual(models.count, 3)
XCTAssertEqual(models[0].t1.id, 1)
XCTAssertEqual(models[0].t1.name, "A1")
XCTAssertEqual(models[0].optionalT2Pair!.left!.id, 1)
XCTAssertEqual(models[0].optionalT2Pair!.left!.t1id, 1)
XCTAssertEqual(models[0].optionalT2Pair!.left!.name, "left")
XCTAssertEqual(models[0].optionalT2Pair!.right!.id, 2)
XCTAssertEqual(models[0].optionalT2Pair!.right!.t1id, 1)
XCTAssertEqual(models[0].optionalT2Pair!.right!.name, "right")
XCTAssertEqual(models[0].t2Pair.left!.id, 1)
XCTAssertEqual(models[0].t2Pair.left!.t1id, 1)
XCTAssertEqual(models[0].t2Pair.left!.name, "left")
XCTAssertEqual(models[0].t2Pair.right!.id, 2)
XCTAssertEqual(models[0].t2Pair.right!.t1id, 1)
XCTAssertEqual(models[0].t2Pair.right!.name, "right")
XCTAssertEqual(models[0].t3!.t1id, 1)
XCTAssertEqual(models[0].t3!.name, "A3")
XCTAssertEqual(models[0].t5count, 5)
XCTAssertEqual(models[1].t1.id, 2)
XCTAssertEqual(models[1].t1.name, "A2")
XCTAssertEqual(models[1].optionalT2Pair!.left!.id, 3)
XCTAssertEqual(models[1].optionalT2Pair!.left!.t1id, 2)
XCTAssertEqual(models[1].optionalT2Pair!.left!.name, "left")
XCTAssertEqual(models[1].t2Pair.left!.id, 3)
XCTAssertEqual(models[1].t2Pair.left!.t1id, 2)
XCTAssertEqual(models[1].t2Pair.left!.name, "left")
XCTAssertNil(models[1].optionalT2Pair!.right)
XCTAssertNil(models[1].t2Pair.right)
XCTAssertNil(models[1].t3)
XCTAssertEqual(models[1].t5count, 2)
XCTAssertEqual(models[2].t1.id, 3)
XCTAssertEqual(models[2].t1.name, "A3")
XCTAssertNil(models[2].optionalT2Pair)
XCTAssertNil(models[2].t2Pair.left)
XCTAssertNil(models[2].t2Pair.right)
XCTAssertNil(models[2].t3)
XCTAssertEqual(models[2].t5count, 0)
}
}
}