Skip to content

Commit e92e804

Browse files
committed
added SUM aggregate function
1 parent 649485e commit e92e804

File tree

2 files changed

+170
-31
lines changed

2 files changed

+170
-31
lines changed

Sources/SQLiteORM/Storage.swift

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

616616
extension Storage {
617+
public func sum<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 SUM(\(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?
630+
repeat {
631+
resultCode = statement.step()
632+
switch resultCode {
633+
case self.apiProvider.SQLITE_ROW:
634+
let columnValue = statement.columnValue(columnIndex: 0)
635+
if !columnValue.isNull {
636+
res = Double(sqliteValue: columnValue)
637+
}
638+
case self.apiProvider.SQLITE_DONE:
639+
break
640+
default:
641+
let errorString = connectionRef.errorMessage
642+
throw Error.sqliteError(code: resultCode, text: errorString)
643+
}
644+
}while resultCode != self.apiProvider.SQLITE_DONE
645+
return res
646+
}
647+
617648
func minInternal<T, R>(_ columnKeyPath: PartialKeyPath<T>) throws -> R? where R: ConstructableFromSQLiteValue {
618649
guard let anyTable = self.tables.first(where: { $0.type == T.self }) else {
619650
throw Error.typeIsNotMapped

Tests/SQLiteORMTests/StorageAggregateFunctionsTests.swift

Lines changed: 139 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,143 @@ import XCTest
22
@testable import SQLiteORM
33

44
class StorageAggregateFunctionsTests: XCTestCase {
5-
struct AvgTest: Initializable {
6-
var value = Double(0)
7-
var unused = Double(0)
8-
}
9-
10-
struct StructWithNullable: Initializable {
11-
var value: Int?
12-
}
13-
145
struct Unknown {
156
var value = Double(0)
167
};
178

18-
var storage: Storage!
19-
var storageWithNullable: Storage!
20-
var apiProvider: SQLiteApiProviderMock!
21-
let filename = ""
22-
23-
override func setUpWithError() throws {
24-
self.apiProvider = .init()
25-
self.apiProvider.forwardsCalls = true
26-
self.storage = try Storage(filename: self.filename,
27-
apiProvider: self.apiProvider,
28-
tables: [Table<AvgTest>(name: "avg_test",
29-
columns: Column(name: "value", keyPath: \AvgTest.value))])
30-
self.storageWithNullable = try Storage(filename: self.filename,
31-
apiProvider: self.apiProvider,
32-
tables: [Table<StructWithNullable>(name: "max_test", columns: Column(name: "value", keyPath: \StructWithNullable.value))])
33-
}
34-
35-
override func tearDownWithError() throws {
36-
self.storage = nil
37-
self.apiProvider = nil
9+
func testSum() throws {
10+
try testCase(#function, routine: {
11+
struct SumTest {
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<SumTest>(name: "sum_test",
21+
columns:
22+
Column(name: "value", keyPath: \SumTest.value),
23+
Column(name: "null_value", keyPath: \SumTest.nullableValue))])
24+
try storage.syncSchema(preserve: false)
25+
try section("error", routine: {
26+
try section("error notMappedType", routine: {
27+
do {
28+
_ = try storage.sum(\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.sum(\SumTest.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?
50+
var result: Double?
51+
var expectedApiCalls = [SQLiteApiProviderMock.Call]()
52+
try section("not nullable field", routine: {
53+
try section("no rows", routine: {
54+
expectedResult = nil
55+
expectedApiCalls = [
56+
.init(id: 0, callType: .sqlite3PrepareV2(db, "SELECT SUM(value) FROM sum_test", -1, .ignore, nil)),
57+
.init(id: 1, callType: .sqlite3Step(.ignore)),
58+
.init(id: 2, callType: .sqlite3ColumnValue(.ignore, 0)),
59+
.init(id: 3, callType: .sqlite3ValueType(.ignore)),
60+
.init(id: 4, callType: .sqlite3Step(.ignore)),
61+
.init(id: 5, callType: .sqlite3Finalize(.ignore)),
62+
]
63+
})
64+
try section("1 row", routine: {
65+
try storage.replace(object: SumTest(value: 1))
66+
expectedResult = 1
67+
expectedApiCalls = [
68+
.init(id: 0, callType: .sqlite3PrepareV2(db, "SELECT SUM(value) FROM sum_test", -1, .ignore, nil)),
69+
.init(id: 1, callType: .sqlite3Step(.ignore)),
70+
.init(id: 2, callType: .sqlite3ColumnValue(.ignore, 0)),
71+
.init(id: 3, callType: .sqlite3ValueType(.ignore)),
72+
.init(id: 4, callType: .sqlite3ValueDouble(.ignore)),
73+
.init(id: 5, callType: .sqlite3Step(.ignore)),
74+
.init(id: 6, callType: .sqlite3Finalize(.ignore)),
75+
]
76+
})
77+
try section("2 rows", routine: {
78+
try storage.replace(object: SumTest(value: 2))
79+
try storage.replace(object: SumTest(value: 3))
80+
expectedResult = 5
81+
expectedApiCalls = [
82+
.init(id: 0, callType: .sqlite3PrepareV2(db, "SELECT SUM(value) FROM sum_test", -1, .ignore, nil)),
83+
.init(id: 1, callType: .sqlite3Step(.ignore)),
84+
.init(id: 2, callType: .sqlite3ColumnValue(.ignore, 0)),
85+
.init(id: 3, callType: .sqlite3ValueType(.ignore)),
86+
.init(id: 4, callType: .sqlite3ValueDouble(.ignore)),
87+
.init(id: 5, callType: .sqlite3Step(.ignore)),
88+
.init(id: 6, callType: .sqlite3Finalize(.ignore)),
89+
]
90+
})
91+
apiProvider.resetCalls()
92+
result = try storage.sum(\SumTest.value)
93+
XCTAssertEqual(result, expectedResult)
94+
XCTAssertEqual(apiProvider.calls, expectedApiCalls)
95+
})
96+
try section("nullable field", routine: {
97+
try section("no rows", routine: {
98+
expectedResult = nil
99+
expectedApiCalls = [
100+
.init(id: 0, callType: .sqlite3PrepareV2(db, "SELECT SUM(null_value) FROM sum_test", -1, .ignore, nil)),
101+
.init(id: 1, callType: .sqlite3Step(.ignore)),
102+
.init(id: 2, callType: .sqlite3ColumnValue(.ignore, 0)),
103+
.init(id: 3, callType: .sqlite3ValueType(.ignore)),
104+
.init(id: 4, callType: .sqlite3Step(.ignore)),
105+
.init(id: 5, callType: .sqlite3Finalize(.ignore)),
106+
]
107+
})
108+
try section("1 row", routine: {
109+
try storage.replace(object: SumTest(value: 0, nullableValue: 3))
110+
expectedResult = 3
111+
expectedApiCalls = [
112+
.init(id: 0, callType: .sqlite3PrepareV2(db, "SELECT SUM(null_value) FROM sum_test", -1, .ignore, nil)),
113+
.init(id: 1, callType: .sqlite3Step(.ignore)),
114+
.init(id: 2, callType: .sqlite3ColumnValue(.ignore, 0)),
115+
.init(id: 3, callType: .sqlite3ValueType(.ignore)),
116+
.init(id: 4, callType: .sqlite3ValueDouble(.ignore)),
117+
.init(id: 5, callType: .sqlite3Step(.ignore)),
118+
.init(id: 6, callType: .sqlite3Finalize(.ignore)),
119+
]
120+
})
121+
try section("2 rows", routine: {
122+
try storage.replace(object: SumTest(value: 0, nullableValue: 4))
123+
try storage.replace(object: SumTest(value: 0, nullableValue: 6))
124+
expectedResult = 10
125+
expectedApiCalls = [
126+
.init(id: 0, callType: .sqlite3PrepareV2(db, "SELECT SUM(null_value) FROM sum_test", -1, .ignore, nil)),
127+
.init(id: 1, callType: .sqlite3Step(.ignore)),
128+
.init(id: 2, callType: .sqlite3ColumnValue(.ignore, 0)),
129+
.init(id: 3, callType: .sqlite3ValueType(.ignore)),
130+
.init(id: 4, callType: .sqlite3ValueDouble(.ignore)),
131+
.init(id: 5, callType: .sqlite3Step(.ignore)),
132+
.init(id: 6, callType: .sqlite3Finalize(.ignore)),
133+
]
134+
})
135+
apiProvider.resetCalls()
136+
result = try storage.sum(\SumTest.nullableValue)
137+
XCTAssertEqual(result, expectedResult)
138+
XCTAssertEqual(apiProvider.calls, expectedApiCalls)
139+
})
140+
})
141+
})
38142
}
39143

40144
func testMin() throws {
@@ -56,7 +160,7 @@ class StorageAggregateFunctionsTests: XCTestCase {
56160
try section("error", routine: {
57161
try section("error notMappedType", routine: {
58162
do {
59-
_ = try storage.max(\Unknown.value)
163+
_ = try storage.min(\Unknown.value)
60164
XCTAssert(false)
61165
}catch SQLiteORM.Error.typeIsNotMapped{
62166
XCTAssert(true)
@@ -66,7 +170,7 @@ class StorageAggregateFunctionsTests: XCTestCase {
66170
})
67171
try section("error columnNotFound", routine: {
68172
do {
69-
_ = try storage.max(\MinTest.unknown)
173+
_ = try storage.min(\MinTest.unknown)
70174
XCTAssert(false)
71175
}catch SQLiteORM.Error.columnNotFound{
72176
XCTAssert(true)
@@ -576,6 +680,10 @@ class StorageAggregateFunctionsTests: XCTestCase {
576680
}
577681

578682
func testAvg() throws {
683+
struct AvgTest: Initializable {
684+
var value = Double(0)
685+
var unused = Double(0)
686+
}
579687
try testCase(#function) {
580688
let apiProvider = SQLiteApiProviderMock()
581689
apiProvider.forwardsCalls = true

0 commit comments

Comments
 (0)