Skip to content

Commit 9eb29eb

Browse files
committed
added TOTAL aggregate function
1 parent e92e804 commit 9eb29eb

File tree

2 files changed

+153
-0
lines changed

2 files changed

+153
-0
lines changed

Sources/SQLiteORM/Storage.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,34 @@ extension Storage {
614614
}
615615

616616
extension Storage {
617+
public func total<T, R>(_ columnKeyPath: KeyPath<T, R>) throws -> Double {
618+
guard let anyTable = self.tables.first(where: { $0.type == T.self }) else {
619+
throw Error.typeIsNotMapped
620+
}
621+
let table = anyTable as! Table<T>
622+
guard let column = table.columns.first(where: { $0.keyPath == columnKeyPath }) else {
623+
throw Error.columnNotFound
624+
}
625+
let sql = "SELECT TOTAL(\(column.name)) FROM \(table.name)"
626+
let connectionRef = try ConnectionRef(connection: self.connection)
627+
let statement = try connectionRef.prepare(sql: sql)
628+
var resultCode = Int32(0)
629+
var res: Double = 0
630+
repeat {
631+
resultCode = statement.step()
632+
switch resultCode {
633+
case self.apiProvider.SQLITE_ROW:
634+
res = statement.columnDouble(index: 0)
635+
case self.apiProvider.SQLITE_DONE:
636+
break
637+
default:
638+
let errorString = connectionRef.errorMessage
639+
throw Error.sqliteError(code: resultCode, text: errorString)
640+
}
641+
}while resultCode != self.apiProvider.SQLITE_DONE
642+
return res
643+
}
644+
617645
public func sum<T, R>(_ columnKeyPath: KeyPath<T, R>) throws -> Double? {
618646
guard let anyTable = self.tables.first(where: { $0.type == T.self }) else {
619647
throw Error.typeIsNotMapped

Tests/SQLiteORMTests/StorageAggregateFunctionsTests.swift

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,131 @@ class StorageAggregateFunctionsTests: XCTestCase {
66
var value = Double(0)
77
};
88

9+
func testTotal() throws {
10+
try testCase(#function, routine: {
11+
struct TotalTest {
12+
var value: Int = 0
13+
var nullableValue: Int? = 0
14+
var unknown = 0
15+
}
16+
let apiProvider = SQLiteApiProviderMock()
17+
apiProvider.forwardsCalls = true
18+
let storage = try Storage(filename: "",
19+
apiProvider: apiProvider,
20+
tables: [Table<TotalTest>(name: "total_test",
21+
columns:
22+
Column(name: "value", keyPath: \TotalTest.value),
23+
Column(name: "null_value", keyPath: \TotalTest.nullableValue))])
24+
try storage.syncSchema(preserve: false)
25+
try section("error", routine: {
26+
try section("error notMappedType", routine: {
27+
do {
28+
_ = try storage.total(\Unknown.value)
29+
XCTAssert(false)
30+
}catch SQLiteORM.Error.typeIsNotMapped{
31+
XCTAssert(true)
32+
}catch{
33+
XCTAssert(false)
34+
}
35+
})
36+
try section("error columnNotFound", routine: {
37+
do {
38+
_ = try storage.total(\TotalTest.unknown)
39+
XCTAssert(false)
40+
}catch SQLiteORM.Error.columnNotFound{
41+
XCTAssert(true)
42+
}catch{
43+
XCTAssert(false)
44+
}
45+
})
46+
})
47+
try section("no error", routine: {
48+
let db = storage.connection.dbMaybe!
49+
var expectedResult: Double = 0
50+
var result: Double = -1
51+
var expectedApiCalls = [SQLiteApiProviderMock.Call]()
52+
try section("not nullable field", routine: {
53+
try section("no rows", routine: {
54+
expectedResult = 0
55+
expectedApiCalls = [
56+
.init(id: 0, callType: .sqlite3PrepareV2(db, "SELECT TOTAL(value) FROM total_test", -1, .ignore, nil)),
57+
.init(id: 1, callType: .sqlite3Step(.ignore)),
58+
.init(id: 2, callType: .sqlite3ColumnDouble(.ignore, 0)),
59+
.init(id: 3, callType: .sqlite3Step(.ignore)),
60+
.init(id: 4, callType: .sqlite3Finalize(.ignore)),
61+
]
62+
})
63+
try section("1 row", routine: {
64+
try storage.replace(object: TotalTest(value: 1))
65+
expectedResult = 1
66+
expectedApiCalls = [
67+
.init(id: 0, callType: .sqlite3PrepareV2(db, "SELECT TOTAL(value) FROM total_test", -1, .ignore, nil)),
68+
.init(id: 1, callType: .sqlite3Step(.ignore)),
69+
.init(id: 2, callType: .sqlite3ColumnDouble(.ignore, 0)),
70+
.init(id: 3, callType: .sqlite3Step(.ignore)),
71+
.init(id: 4, callType: .sqlite3Finalize(.ignore)),
72+
]
73+
})
74+
try section("2 rows", routine: {
75+
try storage.replace(object: TotalTest(value: 2))
76+
try storage.replace(object: TotalTest(value: 3))
77+
expectedResult = 5
78+
expectedApiCalls = [
79+
.init(id: 0, callType: .sqlite3PrepareV2(db, "SELECT TOTAL(value) FROM total_test", -1, .ignore, nil)),
80+
.init(id: 1, callType: .sqlite3Step(.ignore)),
81+
.init(id: 2, callType: .sqlite3ColumnDouble(.ignore, 0)),
82+
.init(id: 3, callType: .sqlite3Step(.ignore)),
83+
.init(id: 4, callType: .sqlite3Finalize(.ignore)),
84+
]
85+
})
86+
apiProvider.resetCalls()
87+
result = try storage.total(\TotalTest.value)
88+
XCTAssertEqual(result, expectedResult)
89+
XCTAssertEqual(apiProvider.calls, expectedApiCalls)
90+
})
91+
try section("nullable field", routine: {
92+
try section("no rows", routine: {
93+
expectedResult = 0
94+
expectedApiCalls = [
95+
.init(id: 0, callType: .sqlite3PrepareV2(db, "SELECT TOTAL(null_value) FROM total_test", -1, .ignore, nil)),
96+
.init(id: 1, callType: .sqlite3Step(.ignore)),
97+
.init(id: 2, callType: .sqlite3ColumnDouble(.ignore, 0)),
98+
.init(id: 3, callType: .sqlite3Step(.ignore)),
99+
.init(id: 4, callType: .sqlite3Finalize(.ignore)),
100+
]
101+
})
102+
try section("1 row", routine: {
103+
try storage.replace(object: TotalTest(value: 0, nullableValue: 3))
104+
expectedResult = 3
105+
expectedApiCalls = [
106+
.init(id: 0, callType: .sqlite3PrepareV2(db, "SELECT TOTAL(null_value) FROM total_test", -1, .ignore, nil)),
107+
.init(id: 1, callType: .sqlite3Step(.ignore)),
108+
.init(id: 2, callType: .sqlite3ColumnDouble(.ignore, 0)),
109+
.init(id: 3, callType: .sqlite3Step(.ignore)),
110+
.init(id: 4, callType: .sqlite3Finalize(.ignore)),
111+
]
112+
})
113+
try section("2 rows", routine: {
114+
try storage.replace(object: TotalTest(value: 0, nullableValue: 4))
115+
try storage.replace(object: TotalTest(value: 0, nullableValue: 6))
116+
expectedResult = 10
117+
expectedApiCalls = [
118+
.init(id: 0, callType: .sqlite3PrepareV2(db, "SELECT TOTAL(null_value) FROM total_test", -1, .ignore, nil)),
119+
.init(id: 1, callType: .sqlite3Step(.ignore)),
120+
.init(id: 2, callType: .sqlite3ColumnDouble(.ignore, 0)),
121+
.init(id: 3, callType: .sqlite3Step(.ignore)),
122+
.init(id: 4, callType: .sqlite3Finalize(.ignore)),
123+
]
124+
})
125+
apiProvider.resetCalls()
126+
result = try storage.total(\TotalTest.nullableValue)
127+
XCTAssertEqual(result, expectedResult)
128+
XCTAssertEqual(apiProvider.calls, expectedApiCalls)
129+
})
130+
})
131+
})
132+
}
133+
9134
func testSum() throws {
10135
try testCase(#function, routine: {
11136
struct SumTest {

0 commit comments

Comments
 (0)