Skip to content

Commit 649485e

Browse files
committed
added MIN aggregate function
1 parent 0511be0 commit 649485e

File tree

3 files changed

+176
-0
lines changed

3 files changed

+176
-0
lines changed

Sources/SQLiteORM/AnyTable.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ public class AnyTable: NSObject {
66
let name: String
77
let columns: [AnyColumn]
88

9+
/// Constructor that must be called from subclass constructor.
910
public init(name: String, columns: AnyColumn...) {
1011
self.name = name
1112
self.columns = columns
1213
super.init()
1314
}
1415

16+
/// Getter for type mapped to this table. Got to be overridden inside subclass
1517
var type: Any.Type {
1618
return Void.self
1719
}

Sources/SQLiteORM/Storage.swift

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

616616
extension Storage {
617+
func minInternal<T, R>(_ columnKeyPath: PartialKeyPath<T>) throws -> R? where R: ConstructableFromSQLiteValue {
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 MIN(\(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: R?
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 = R(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+
648+
public func min<T, F>(_ columnKeyPath: KeyPath<T, Optional<F>>) throws -> F? where F: ConstructableFromSQLiteValue {
649+
return try self.minInternal(columnKeyPath)
650+
}
651+
652+
public func min<T, F>(_ columnKeyPath: KeyPath<T, F>) throws -> F? where F: ConstructableFromSQLiteValue {
653+
return try self.minInternal(columnKeyPath)
654+
}
655+
617656
func maxInternal<T, R>(_ columnKeyPath: PartialKeyPath<T>) throws -> R? where R: ConstructableFromSQLiteValue {
618657
guard let anyTable = self.tables.first(where: { $0.type == T.self }) else {
619658
throw Error.typeIsNotMapped

Tests/SQLiteORMTests/StorageAggregateFunctionsTests.swift

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,141 @@ class StorageAggregateFunctionsTests: XCTestCase {
3737
self.apiProvider = nil
3838
}
3939

40+
func testMin() throws {
41+
try testCase(#function, routine: {
42+
struct MinTest {
43+
var value: Int = 0
44+
var nullableValue: Int? = 0
45+
var unknown = 0
46+
}
47+
let apiProvider = SQLiteApiProviderMock()
48+
apiProvider.forwardsCalls = true
49+
let storage = try Storage(filename: "",
50+
apiProvider: apiProvider,
51+
tables: [Table<MinTest>(name: "min_test",
52+
columns:
53+
Column(name: "value", keyPath: \MinTest.value),
54+
Column(name: "null_value", keyPath: \MinTest.nullableValue))])
55+
try storage.syncSchema(preserve: false)
56+
try section("error", routine: {
57+
try section("error notMappedType", routine: {
58+
do {
59+
_ = try storage.max(\Unknown.value)
60+
XCTAssert(false)
61+
}catch SQLiteORM.Error.typeIsNotMapped{
62+
XCTAssert(true)
63+
}catch{
64+
XCTAssert(false)
65+
}
66+
})
67+
try section("error columnNotFound", routine: {
68+
do {
69+
_ = try storage.max(\MinTest.unknown)
70+
XCTAssert(false)
71+
}catch SQLiteORM.Error.columnNotFound{
72+
XCTAssert(true)
73+
}catch{
74+
XCTAssert(false)
75+
}
76+
})
77+
})
78+
try section("no error", routine: {
79+
let db = storage.connection.dbMaybe!
80+
var expectedResult: Int?
81+
var result: Int?
82+
var expectedApiCalls = [SQLiteApiProviderMock.Call]()
83+
try section("not nullable field", routine: {
84+
try section("no rows", routine: {
85+
expectedResult = nil
86+
expectedApiCalls = [
87+
.init(id: 0, callType: .sqlite3PrepareV2(db, "SELECT MIN(value) FROM min_test", -1, .ignore, nil)),
88+
.init(id: 1, callType: .sqlite3Step(.ignore)),
89+
.init(id: 2, callType: .sqlite3ColumnValue(.ignore, 0)),
90+
.init(id: 3, callType: .sqlite3ValueType(.ignore)),
91+
.init(id: 4, callType: .sqlite3Step(.ignore)),
92+
.init(id: 5, callType: .sqlite3Finalize(.ignore)),
93+
]
94+
})
95+
try section("1 row", routine: {
96+
try storage.replace(object: MinTest(value: 10))
97+
expectedResult = 10
98+
expectedApiCalls = [
99+
.init(id: 0, callType: .sqlite3PrepareV2(db, "SELECT MIN(value) FROM min_test", -1, .ignore, nil)),
100+
.init(id: 1, callType: .sqlite3Step(.ignore)),
101+
.init(id: 2, callType: .sqlite3ColumnValue(.ignore, 0)),
102+
.init(id: 3, callType: .sqlite3ValueType(.ignore)),
103+
.init(id: 4, callType: .sqlite3ValueInt(.ignore)),
104+
.init(id: 5, callType: .sqlite3Step(.ignore)),
105+
.init(id: 6, callType: .sqlite3Finalize(.ignore)),
106+
]
107+
})
108+
try section("2 rows", routine: {
109+
try storage.replace(object: MinTest(value: 4))
110+
try storage.replace(object: MinTest(value: 6))
111+
expectedResult = 4
112+
expectedApiCalls = [
113+
.init(id: 0, callType: .sqlite3PrepareV2(db, "SELECT MIN(value) FROM min_test", -1, .ignore, nil)),
114+
.init(id: 1, callType: .sqlite3Step(.ignore)),
115+
.init(id: 2, callType: .sqlite3ColumnValue(.ignore, 0)),
116+
.init(id: 3, callType: .sqlite3ValueType(.ignore)),
117+
.init(id: 4, callType: .sqlite3ValueInt(.ignore)),
118+
.init(id: 5, callType: .sqlite3Step(.ignore)),
119+
.init(id: 6, callType: .sqlite3Finalize(.ignore)),
120+
]
121+
})
122+
apiProvider.resetCalls()
123+
result = try storage.min(\MinTest.value)
124+
XCTAssertEqual(result, expectedResult)
125+
XCTAssertEqual(apiProvider.calls, expectedApiCalls)
126+
})
127+
try section("nullable field", routine: {
128+
try section("no rows", routine: {
129+
expectedResult = nil
130+
expectedApiCalls = [
131+
.init(id: 0, callType: .sqlite3PrepareV2(db, "SELECT MIN(null_value) FROM min_test", -1, .ignore, nil)),
132+
.init(id: 1, callType: .sqlite3Step(.ignore)),
133+
.init(id: 2, callType: .sqlite3ColumnValue(.ignore, 0)),
134+
.init(id: 3, callType: .sqlite3ValueType(.ignore)),
135+
.init(id: 4, callType: .sqlite3Step(.ignore)),
136+
.init(id: 5, callType: .sqlite3Finalize(.ignore)),
137+
]
138+
})
139+
try section("1 row", routine: {
140+
try storage.replace(object: MinTest(value: 0, nullableValue: 10))
141+
expectedResult = 10
142+
expectedApiCalls = [
143+
.init(id: 0, callType: .sqlite3PrepareV2(db, "SELECT MIN(null_value) FROM min_test", -1, .ignore, nil)),
144+
.init(id: 1, callType: .sqlite3Step(.ignore)),
145+
.init(id: 2, callType: .sqlite3ColumnValue(.ignore, 0)),
146+
.init(id: 3, callType: .sqlite3ValueType(.ignore)),
147+
.init(id: 4, callType: .sqlite3ValueInt(.ignore)),
148+
.init(id: 5, callType: .sqlite3Step(.ignore)),
149+
.init(id: 6, callType: .sqlite3Finalize(.ignore)),
150+
]
151+
})
152+
try section("2 rows", routine: {
153+
try storage.replace(object: MinTest(value: 0, nullableValue: 4))
154+
try storage.replace(object: MinTest(value: 0, nullableValue: 6))
155+
expectedResult = 4
156+
expectedApiCalls = [
157+
.init(id: 0, callType: .sqlite3PrepareV2(db, "SELECT MIN(null_value) FROM min_test", -1, .ignore, nil)),
158+
.init(id: 1, callType: .sqlite3Step(.ignore)),
159+
.init(id: 2, callType: .sqlite3ColumnValue(.ignore, 0)),
160+
.init(id: 3, callType: .sqlite3ValueType(.ignore)),
161+
.init(id: 4, callType: .sqlite3ValueInt(.ignore)),
162+
.init(id: 5, callType: .sqlite3Step(.ignore)),
163+
.init(id: 6, callType: .sqlite3Finalize(.ignore)),
164+
]
165+
})
166+
apiProvider.resetCalls()
167+
result = try storage.min(\MinTest.nullableValue)
168+
XCTAssertEqual(result, expectedResult)
169+
XCTAssertEqual(apiProvider.calls, expectedApiCalls)
170+
})
171+
})
172+
})
173+
}
174+
40175
func testMax() throws {
41176
try testCase(#function, routine: {
42177
struct MaxTest {

0 commit comments

Comments
 (0)