Add unit tests
This commit is contained in:
@@ -3,20 +3,20 @@ import Foundation
|
||||
import DataLiteCore
|
||||
|
||||
struct ConnectionKeyTests {
|
||||
@Test func testPassphrase() {
|
||||
@Test func passphrase() {
|
||||
let key = Connection.Key.passphrase("secret123")
|
||||
#expect(key.keyValue == "secret123")
|
||||
#expect(key.length == 9)
|
||||
}
|
||||
|
||||
@Test func testRawKey() {
|
||||
@Test func rawKey() {
|
||||
let keyData = Data([0x01, 0xAB, 0xCD, 0xEF])
|
||||
let key = Connection.Key.rawKey(keyData)
|
||||
#expect(key.keyValue == "X'01ABCDEF'")
|
||||
#expect(key.length == 11)
|
||||
}
|
||||
|
||||
@Test func testRawKeyLengthConsistency() {
|
||||
@Test func rawKeyLengthConsistency() {
|
||||
let rawBytes = Data(repeating: 0x00, count: 32)
|
||||
let key = Connection.Key.rawKey(rawBytes)
|
||||
let hexPart = key.keyValue.dropFirst(2).dropLast()
|
||||
|
||||
@@ -2,18 +2,18 @@ import Testing
|
||||
@testable import DataLiteCore
|
||||
|
||||
struct ConnectionLocationTests {
|
||||
@Test func testFileLocationPath() {
|
||||
@Test func fileLocationPath() {
|
||||
let filePath = "/path/to/database.db"
|
||||
let location = Connection.Location.file(path: filePath)
|
||||
#expect(location.path == filePath)
|
||||
}
|
||||
|
||||
@Test func testInMemoryLocationPath() {
|
||||
@Test func inMemoryLocationPath() {
|
||||
let inMemoryLocation = Connection.Location.inMemory
|
||||
#expect(inMemoryLocation.path == ":memory:")
|
||||
}
|
||||
|
||||
@Test func testTemporaryLocationPath() {
|
||||
@Test func temporaryLocationPath() {
|
||||
let temporaryLocation = Connection.Location.temporary
|
||||
#expect(temporaryLocation.path == "")
|
||||
}
|
||||
|
||||
@@ -3,40 +3,40 @@ import DataLiteC
|
||||
import DataLiteCore
|
||||
|
||||
struct ConnectionOptionsTests {
|
||||
@Test func testReadOnlyOption() {
|
||||
@Test func readOnlyOption() {
|
||||
let options: Connection.Options = [.readonly]
|
||||
#expect(options.contains(.readonly))
|
||||
}
|
||||
|
||||
@Test func testReadWriteOption() {
|
||||
@Test func readWriteOption() {
|
||||
let options: Connection.Options = [.readwrite]
|
||||
#expect(options.contains(.readwrite))
|
||||
}
|
||||
|
||||
@Test func testCreateOption() {
|
||||
@Test func createOption() {
|
||||
let options: Connection.Options = [.create]
|
||||
#expect(options.contains(.create))
|
||||
}
|
||||
|
||||
@Test func testMultipleOptions() {
|
||||
@Test func multipleOptions() {
|
||||
let options: Connection.Options = [.readwrite, .create, .memory]
|
||||
#expect(options.contains(.readwrite))
|
||||
#expect(options.contains(.create))
|
||||
#expect(options.contains(.memory))
|
||||
}
|
||||
|
||||
@Test func testNoFollowOption() {
|
||||
@Test func noFollowOption() {
|
||||
let options: Connection.Options = [.nofollow]
|
||||
#expect(options.contains(.nofollow))
|
||||
}
|
||||
|
||||
@Test func testAllOptions() {
|
||||
@Test func allOptions() {
|
||||
let options: Connection.Options = [
|
||||
.readonly, .readwrite, .create, .uri, .memory,
|
||||
.nomutex, .fullmutex, .sharedcache,
|
||||
.privatecache, .exrescode, .nofollow
|
||||
]
|
||||
|
||||
|
||||
#expect(options.contains(.readonly))
|
||||
#expect(options.contains(.readwrite))
|
||||
#expect(options.contains(.create))
|
||||
@@ -50,7 +50,7 @@ struct ConnectionOptionsTests {
|
||||
#expect(options.contains(.nofollow))
|
||||
}
|
||||
|
||||
@Test func testOptionsRawValue() {
|
||||
@Test func optionsRawValue() {
|
||||
let options: Connection.Options = [.readwrite, .create]
|
||||
let expectedRawValue = Int32(SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE)
|
||||
#expect(options.rawValue == expectedRawValue)
|
||||
|
||||
@@ -1,73 +1,71 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
import Foundation
|
||||
import DataLiteC
|
||||
import DataLiteCore
|
||||
|
||||
@testable import DataLiteCore
|
||||
|
||||
struct ConnectionTests {
|
||||
@Test func testIsAutocommitInitially() throws {
|
||||
let connection = try Connection(
|
||||
location: .inMemory,
|
||||
options: [.create, .readwrite]
|
||||
)
|
||||
#expect(connection.isAutocommit == true)
|
||||
@Test(arguments: [
|
||||
Connection.Location.inMemory,
|
||||
Connection.Location.temporary
|
||||
])
|
||||
func initLocation(_ location: Connection.Location) throws {
|
||||
let _ = try Connection(location: location, options: [.create, .readwrite])
|
||||
}
|
||||
|
||||
@Test func testIsAutocommitDuringTransaction() throws {
|
||||
let connection = try Connection(
|
||||
location: .inMemory,
|
||||
options: [.create, .readwrite]
|
||||
)
|
||||
try connection.beginTransaction()
|
||||
#expect(connection.isAutocommit == false)
|
||||
@Test func initPath() throws {
|
||||
let dir = FileManager.default.temporaryDirectory
|
||||
let file = UUID().uuidString
|
||||
let path = dir.appending(component: file).path
|
||||
defer { try? FileManager.default.removeItem(atPath: path) }
|
||||
let _ = try Connection(path: path, options: [.create, .readwrite])
|
||||
}
|
||||
|
||||
@Test func testIsAutocommitAfterCommit() throws {
|
||||
let connection = try Connection(
|
||||
location: .inMemory,
|
||||
options: [.create, .readwrite]
|
||||
@Test func initPathFail() {
|
||||
#expect(
|
||||
throws: SQLiteError(
|
||||
code: SQLITE_CANTOPEN,
|
||||
message: "unable to open database file"
|
||||
),
|
||||
performing: {
|
||||
try Connection(
|
||||
path: "/invalid-path/",
|
||||
options: [.create, .readwrite]
|
||||
)
|
||||
}
|
||||
)
|
||||
try connection.beginTransaction()
|
||||
try connection.commitTransaction()
|
||||
#expect(connection.isAutocommit == true)
|
||||
}
|
||||
|
||||
@Test func testIsAutocommitAfterRollback() throws {
|
||||
@Test func isAutocommit() throws {
|
||||
let connection = try Connection(
|
||||
location: .inMemory,
|
||||
options: [.create, .readwrite]
|
||||
)
|
||||
try connection.beginTransaction()
|
||||
try connection.rollbackTransaction()
|
||||
#expect(connection.isAutocommit == true)
|
||||
#expect(connection.isAutocommit)
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
(Connection.Options.readwrite, false),
|
||||
(Connection.Options.readonly, true)
|
||||
])
|
||||
func testIsReadonly(
|
||||
_ opt: Connection.Options,
|
||||
_ isReadonly: Bool
|
||||
func isReadonly(
|
||||
_ options: Connection.Options,
|
||||
_ expected: Bool
|
||||
) throws {
|
||||
let url = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(UUID().uuidString)
|
||||
.appendingPathExtension("sqlite")
|
||||
defer { try? FileManager.default.removeItem(at: url) }
|
||||
let _ = try Connection(
|
||||
location: .file(path: url.path),
|
||||
options: [.create, .readwrite]
|
||||
)
|
||||
let connection = try Connection(
|
||||
location: .file(path: url.path),
|
||||
options: [opt]
|
||||
)
|
||||
#expect(connection.isReadonly == isReadonly)
|
||||
let dir = FileManager.default.temporaryDirectory
|
||||
let file = UUID().uuidString
|
||||
let path = dir.appending(component: file).path
|
||||
defer { try? FileManager.default.removeItem(atPath: path) }
|
||||
|
||||
let _ = try Connection(path: path, options: [.create, .readwrite])
|
||||
let connection = try Connection(path: path, options: options)
|
||||
|
||||
#expect(connection.isReadonly == expected)
|
||||
}
|
||||
|
||||
@Test func testBusyTimeout() throws {
|
||||
let connection = try Connection(
|
||||
location: .inMemory,
|
||||
options: [.create, .readwrite]
|
||||
location: .inMemory, options: [.create, .readwrite]
|
||||
)
|
||||
connection.busyTimeout = 5000
|
||||
#expect(try connection.get(pragma: .busyTimeout) == 5000)
|
||||
@@ -76,50 +74,9 @@ struct ConnectionTests {
|
||||
#expect(connection.busyTimeout == 1000)
|
||||
}
|
||||
|
||||
@Test func testBusyTimeoutSQLiteBusy() throws {
|
||||
let url = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(UUID().uuidString)
|
||||
.appendingPathExtension("sqlite")
|
||||
defer { try? FileManager.default.removeItem(at: url) }
|
||||
|
||||
let oneConn = try Connection(
|
||||
location: .file(path: url.path),
|
||||
options: [.create, .readwrite, .fullmutex]
|
||||
)
|
||||
let twoConn = try Connection(
|
||||
location: .file(path: url.path),
|
||||
options: [.create, .readwrite, .fullmutex]
|
||||
)
|
||||
|
||||
try oneConn.execute(sql: """
|
||||
CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT);
|
||||
""")
|
||||
|
||||
try oneConn.beginTransaction()
|
||||
try oneConn.execute(sql: """
|
||||
INSERT INTO test (value) VALUES ('first');
|
||||
""")
|
||||
|
||||
#expect(
|
||||
throws: SQLiteError(
|
||||
code: SQLITE_BUSY,
|
||||
message: "database is locked"
|
||||
),
|
||||
performing: {
|
||||
twoConn.busyTimeout = 0
|
||||
try twoConn.execute(sql: """
|
||||
INSERT INTO test (value) VALUES ('second');
|
||||
""")
|
||||
}
|
||||
)
|
||||
|
||||
try oneConn.rollbackTransaction()
|
||||
}
|
||||
|
||||
@Test func testApplicationID() throws {
|
||||
let connection = try Connection(
|
||||
location: .inMemory,
|
||||
options: [.create, .readwrite]
|
||||
location: .inMemory, options: [.create, .readwrite]
|
||||
)
|
||||
|
||||
#expect(connection.applicationID == 0)
|
||||
@@ -133,8 +90,7 @@ struct ConnectionTests {
|
||||
|
||||
@Test func testForeignKeys() throws {
|
||||
let connection = try Connection(
|
||||
location: .inMemory,
|
||||
options: [.create, .readwrite]
|
||||
location: .inMemory, options: [.create, .readwrite]
|
||||
)
|
||||
|
||||
#expect(connection.foreignKeys == false)
|
||||
@@ -147,15 +103,12 @@ struct ConnectionTests {
|
||||
}
|
||||
|
||||
@Test func testJournalMode() throws {
|
||||
let url = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(UUID().uuidString)
|
||||
.appendingPathExtension("sqlite")
|
||||
defer { try? FileManager.default.removeItem(at: url) }
|
||||
let dir = FileManager.default.temporaryDirectory
|
||||
let file = UUID().uuidString
|
||||
let path = dir.appending(component: file).path
|
||||
defer { try? FileManager.default.removeItem(atPath: path) }
|
||||
|
||||
let connection = try Connection(
|
||||
location: .file(path: url.path),
|
||||
options: [.create, .readwrite]
|
||||
)
|
||||
let connection = try Connection(path: path, options: [.create, .readwrite])
|
||||
|
||||
connection.journalMode = .delete
|
||||
#expect(try connection.get(pragma: .journalMode) == JournalMode.delete)
|
||||
@@ -166,8 +119,7 @@ struct ConnectionTests {
|
||||
|
||||
@Test func testSynchronous() throws {
|
||||
let connection = try Connection(
|
||||
location: .inMemory,
|
||||
options: [.create, .readwrite]
|
||||
location: .inMemory, options: [.create, .readwrite]
|
||||
)
|
||||
|
||||
connection.synchronous = .normal
|
||||
@@ -179,8 +131,7 @@ struct ConnectionTests {
|
||||
|
||||
@Test func testUserVersion() throws {
|
||||
let connection = try Connection(
|
||||
location: .inMemory,
|
||||
options: [.create, .readwrite]
|
||||
location: .inMemory, options: [.create, .readwrite]
|
||||
)
|
||||
|
||||
connection.userVersion = 42
|
||||
@@ -190,88 +141,265 @@ struct ConnectionTests {
|
||||
#expect(connection.userVersion == 13)
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
(TestScalarFunc.self, TestScalarFunc.name),
|
||||
(TestAggregateFunc.self, TestAggregateFunc.name)
|
||||
] as [(Function.Type, String)])
|
||||
func testAddFunction(
|
||||
_ function: Function.Type,
|
||||
_ name: String
|
||||
) throws {
|
||||
let connection = try Connection(
|
||||
location: .inMemory,
|
||||
options: [.create, .readwrite]
|
||||
)
|
||||
try connection.execute(sql: """
|
||||
CREATE TABLE items (value INTEGER);
|
||||
INSERT INTO items (value) VALUES (1), (2), (NULL), (3);
|
||||
""")
|
||||
try connection.add(function: function)
|
||||
try connection.execute(sql: "SELECT \(name)(value) FROM items")
|
||||
@Test(arguments: ["main", nil])
|
||||
func applyKeyEncrypt(_ name: String?) throws {
|
||||
let dir = FileManager.default.temporaryDirectory
|
||||
let file = UUID().uuidString
|
||||
let path = dir.appending(component: file).path
|
||||
defer { try? FileManager.default.removeItem(atPath: path) }
|
||||
|
||||
do {
|
||||
let connection = try Connection(path: path, options: [.create, .readwrite])
|
||||
try connection.apply(.passphrase("test"), name: name)
|
||||
try connection.execute(sql: "CREATE TABLE t (id INT PRIMARY KEY)")
|
||||
}
|
||||
|
||||
do {
|
||||
var connection: OpaquePointer!
|
||||
sqlite3_open_v2(path, &connection, SQLITE_OPEN_READONLY, nil)
|
||||
let status = sqlite3_exec(
|
||||
connection, "SELECT count(*) FROM sqlite_master", nil, nil, nil
|
||||
)
|
||||
#expect(status == SQLITE_NOTADB)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
(TestScalarFunc.self, TestScalarFunc.name),
|
||||
(TestAggregateFunc.self, TestAggregateFunc.name)
|
||||
] as [(Function.Type, String)])
|
||||
func testRemoveFunction(
|
||||
_ function: Function.Type,
|
||||
_ name: String
|
||||
) throws {
|
||||
let connection = try Connection(
|
||||
location: .inMemory,
|
||||
options: [.create, .readwrite]
|
||||
)
|
||||
try connection.execute(sql: """
|
||||
CREATE TABLE items (value INTEGER);
|
||||
INSERT INTO items (value) VALUES (1), (2), (NULL), (3);
|
||||
""")
|
||||
try connection.add(function: function)
|
||||
try connection.remove(function: function)
|
||||
#expect(
|
||||
throws: SQLiteError(
|
||||
code: SQLITE_ERROR,
|
||||
message: "no such function: \(name)"
|
||||
),
|
||||
performing: {
|
||||
try connection.execute(sql: """
|
||||
SELECT \(name)(value) FROM items
|
||||
""")
|
||||
@Test(arguments: ["main", nil])
|
||||
func applyKeyDecrypt(_ name: String?) throws {
|
||||
let dir = FileManager.default.temporaryDirectory
|
||||
let file = UUID().uuidString
|
||||
let path = dir.appending(component: file).path
|
||||
defer { try? FileManager.default.removeItem(atPath: path) }
|
||||
|
||||
do {
|
||||
var connection: OpaquePointer!
|
||||
let options = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE
|
||||
sqlite3_open_v2(path, &connection, options, nil)
|
||||
if let name {
|
||||
sqlite3_key_v2(connection, name, "test", Int32("test".utf8.count))
|
||||
} else {
|
||||
sqlite3_key(connection, "test", Int32("test".utf8.count))
|
||||
}
|
||||
sqlite3_exec(
|
||||
connection, "CREATE TABLE t (id INT PRIMARY KEY)", nil, nil, nil
|
||||
)
|
||||
sqlite3_close_v2(connection)
|
||||
}
|
||||
|
||||
do {
|
||||
let connection = try Connection(path: path, options: [.readwrite])
|
||||
try connection.apply(.passphrase("test"), name: name)
|
||||
try connection.execute(sql: "SELECT count(*) FROM sqlite_master")
|
||||
}
|
||||
}
|
||||
|
||||
@Test(arguments: ["main", nil])
|
||||
func applyKeyInvalid(_ name: String?) throws {
|
||||
let connection = try Connection(
|
||||
location: .inMemory, options: [.create, .readwrite]
|
||||
)
|
||||
#expect(
|
||||
throws: SQLiteError(code: SQLITE_MISUSE, message: ""),
|
||||
performing: { try connection.apply(.passphrase(""), name: name) }
|
||||
)
|
||||
}
|
||||
|
||||
@Test(arguments: ["main", nil])
|
||||
func rekey(_ name: String?) throws {
|
||||
let dir = FileManager.default.temporaryDirectory
|
||||
let file = UUID().uuidString
|
||||
let path = dir.appending(component: file).path
|
||||
defer { try? FileManager.default.removeItem(atPath: path) }
|
||||
|
||||
do {
|
||||
let connection = try Connection(path: path, options: [.create, .readwrite])
|
||||
try connection.apply(.passphrase("old-test"), name: name)
|
||||
try connection.execute(sql: "CREATE TABLE t (id INT PRIMARY KEY)")
|
||||
}
|
||||
|
||||
do {
|
||||
let connection = try Connection(path: path, options: [.create, .readwrite])
|
||||
try connection.apply(.passphrase("old-test"), name: name)
|
||||
try connection.rekey(.passphrase("new-test"), name: name)
|
||||
}
|
||||
|
||||
do {
|
||||
let connection = try Connection(path: path, options: [.readwrite])
|
||||
try connection.apply(.passphrase("new-test"), name: name)
|
||||
try connection.execute(sql: "SELECT count(*) FROM sqlite_master")
|
||||
}
|
||||
}
|
||||
|
||||
@Test(arguments: ["main", nil])
|
||||
func rekeyInvalid(_ name: String?) throws {
|
||||
let connection = try Connection(
|
||||
location: .inMemory, options: [.create, .readwrite]
|
||||
)
|
||||
try connection.apply(.passphrase("test"), name: name)
|
||||
try connection.execute(sql: "CREATE TABLE t (id INT PRIMARY KEY)")
|
||||
|
||||
#expect(
|
||||
throws: SQLiteError(code: SQLITE_ERROR, message: ""),
|
||||
performing: { try connection.rekey(.passphrase(""), name: name) }
|
||||
)
|
||||
}
|
||||
|
||||
@Test func addDelegate() throws {
|
||||
let connection = try Connection(
|
||||
location: .inMemory, options: [.create, .readwrite]
|
||||
)
|
||||
try connection.execute(sql: "CREATE TABLE t (id INT PRIMARY KEY)")
|
||||
|
||||
let delegate = ConnectionDelegate()
|
||||
connection.add(delegate: delegate)
|
||||
|
||||
try connection.execute(sql: "INSERT INTO t (id) VALUES (1)")
|
||||
#expect(delegate.didUpdate)
|
||||
#expect(delegate.willCommit)
|
||||
#expect(delegate.didRollback == false)
|
||||
|
||||
delegate.reset()
|
||||
delegate.error = SQLiteError(code: -1, message: "")
|
||||
|
||||
try? connection.execute(sql: "INSERT INTO t (id) VALUES (2)")
|
||||
#expect(delegate.didUpdate)
|
||||
#expect(delegate.willCommit)
|
||||
#expect(delegate.didRollback)
|
||||
|
||||
delegate.reset()
|
||||
connection.remove(delegate: delegate)
|
||||
|
||||
try connection.execute(sql: "INSERT INTO t (id) VALUES (3)")
|
||||
#expect(delegate.didUpdate == false)
|
||||
#expect(delegate.willCommit == false)
|
||||
#expect(delegate.didRollback == false)
|
||||
}
|
||||
|
||||
@Test func addTraceDelegate() throws {
|
||||
let connection = try Connection(
|
||||
location: .inMemory, options: [.create, .readwrite]
|
||||
)
|
||||
try connection.execute(sql: "CREATE TABLE t (id INT PRIMARY KEY)")
|
||||
|
||||
let delegate = ConnectionTraceDelegate()
|
||||
connection.add(trace: delegate)
|
||||
|
||||
try connection.execute(sql: "INSERT INTO t (id) VALUES (:id)")
|
||||
#expect(delegate.expandedSQL == "INSERT INTO t (id) VALUES (NULL)")
|
||||
#expect(delegate.unexpandedSQL == "INSERT INTO t (id) VALUES (:id)")
|
||||
|
||||
delegate.reset()
|
||||
connection.remove(trace: delegate)
|
||||
|
||||
try connection.execute(sql: "INSERT INTO t (id) VALUES (:id)")
|
||||
#expect(delegate.expandedSQL == nil)
|
||||
#expect(delegate.unexpandedSQL == nil)
|
||||
}
|
||||
|
||||
@Test func addFunction() throws {
|
||||
let connection = try Connection(
|
||||
location: .inMemory, options: [.create, .readwrite]
|
||||
)
|
||||
|
||||
try connection.add(function: TestFunction.self)
|
||||
#expect(TestFunction.isInstalled)
|
||||
|
||||
try connection.remove(function: TestFunction.self)
|
||||
#expect(TestFunction.isInstalled == false)
|
||||
}
|
||||
|
||||
@Test func beginTransaction() throws {
|
||||
let connection = try Connection(
|
||||
location: .inMemory, options: [.create, .readwrite]
|
||||
)
|
||||
#expect(connection.isAutocommit)
|
||||
|
||||
try connection.beginTransaction()
|
||||
#expect(connection.isAutocommit == false)
|
||||
}
|
||||
|
||||
@Test func commitTransaction() throws {
|
||||
let connection = try Connection(
|
||||
location: .inMemory, options: [.create, .readwrite]
|
||||
)
|
||||
#expect(connection.isAutocommit)
|
||||
|
||||
try connection.beginTransaction()
|
||||
try connection.commitTransaction()
|
||||
#expect(connection.isAutocommit)
|
||||
}
|
||||
|
||||
@Test func rollbackTransaction() throws {
|
||||
let connection = try Connection(
|
||||
location: .inMemory, options: [.create, .readwrite]
|
||||
)
|
||||
#expect(connection.isAutocommit)
|
||||
|
||||
try connection.beginTransaction()
|
||||
try connection.rollbackTransaction()
|
||||
#expect(connection.isAutocommit)
|
||||
}
|
||||
}
|
||||
|
||||
private extension ConnectionTests {
|
||||
final class TestScalarFunc: Function.Scalar {
|
||||
override class var argc: Int32 { 1 }
|
||||
override class var name: String { "TO_STR" }
|
||||
override class var options: Options {
|
||||
[.deterministic, .innocuous]
|
||||
final class ConnectionDelegate: DataLiteCore.ConnectionDelegate {
|
||||
var error: Error?
|
||||
|
||||
var didUpdate = false
|
||||
var willCommit = false
|
||||
var didRollback = false
|
||||
|
||||
func reset() {
|
||||
didUpdate = false
|
||||
willCommit = false
|
||||
didRollback = false
|
||||
}
|
||||
|
||||
override class func invoke(args: any ArgumentsProtocol) throws -> SQLiteRepresentable? {
|
||||
args[0].description
|
||||
func connection(
|
||||
_ connection: any ConnectionProtocol,
|
||||
didUpdate action: SQLiteAction
|
||||
) {
|
||||
didUpdate = true
|
||||
}
|
||||
|
||||
func connectionWillCommit(_ connection: any ConnectionProtocol) throws {
|
||||
willCommit = true
|
||||
if let error { throw error }
|
||||
}
|
||||
|
||||
func connectionDidRollback(_ connection: any ConnectionProtocol) {
|
||||
didRollback = true
|
||||
}
|
||||
}
|
||||
|
||||
final class TestAggregateFunc: Function.Aggregate {
|
||||
override class var argc: Int32 { 1 }
|
||||
override class var name: String { "MY_COUNT" }
|
||||
override class var options: Options {
|
||||
[.deterministic, .innocuous]
|
||||
final class ConnectionTraceDelegate: DataLiteCore.ConnectionTraceDelegate {
|
||||
var expandedSQL: String?
|
||||
var unexpandedSQL: String?
|
||||
|
||||
func reset() {
|
||||
expandedSQL = nil
|
||||
unexpandedSQL = nil
|
||||
}
|
||||
|
||||
private var count: Int = 0
|
||||
func connection(_ connection: any ConnectionProtocol, trace sql: Trace) {
|
||||
expandedSQL = sql.expandedSQL
|
||||
unexpandedSQL = sql.unexpandedSQL
|
||||
}
|
||||
}
|
||||
|
||||
final class TestFunction: DataLiteCore.Function {
|
||||
nonisolated(unsafe) static var isInstalled = false
|
||||
|
||||
override func step(args: any ArgumentsProtocol) throws {
|
||||
if args[0] != .null {
|
||||
count += 1
|
||||
}
|
||||
override class func install(
|
||||
db connection: OpaquePointer
|
||||
) throws(SQLiteError) {
|
||||
isInstalled = true
|
||||
}
|
||||
|
||||
override func finalize() throws -> SQLiteRepresentable? {
|
||||
count
|
||||
override class func uninstall(
|
||||
db connection: OpaquePointer
|
||||
) throws(SQLiteError) {
|
||||
isInstalled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,20 +3,20 @@ import DataLiteC
|
||||
import DataLiteCore
|
||||
|
||||
struct FunctionOptionsTests {
|
||||
@Test func testSingleOption() {
|
||||
@Test func singleOption() {
|
||||
#expect(Function.Options.deterministic.rawValue == SQLITE_DETERMINISTIC)
|
||||
#expect(Function.Options.directonly.rawValue == SQLITE_DIRECTONLY)
|
||||
#expect(Function.Options.innocuous.rawValue == SQLITE_INNOCUOUS)
|
||||
}
|
||||
|
||||
@Test func testMultipleOptions() {
|
||||
@Test func multipleOptions() {
|
||||
let options: Function.Options = [.deterministic, .directonly]
|
||||
#expect(options.contains(.deterministic))
|
||||
#expect(options.contains(.directonly))
|
||||
#expect(options.contains(.innocuous) == false)
|
||||
}
|
||||
|
||||
@Test func testEqualityAndHashability() {
|
||||
@Test func equalityAndHashability() {
|
||||
let options1: Function.Options = [.deterministic, .innocuous]
|
||||
let options2: Function.Options = [.deterministic, .innocuous]
|
||||
#expect(options1 == options2)
|
||||
@@ -26,14 +26,14 @@ struct FunctionOptionsTests {
|
||||
#expect(hash1 == hash2)
|
||||
}
|
||||
|
||||
@Test func testEmptyOptions() {
|
||||
@Test func emptyOptions() {
|
||||
let options = Function.Options(rawValue: 0)
|
||||
#expect(options.contains(.deterministic) == false)
|
||||
#expect(options.contains(.directonly) == false)
|
||||
#expect(options.contains(.innocuous) == false)
|
||||
}
|
||||
|
||||
@Test func testRawValueInitialization() {
|
||||
@Test func rawValueInitialization() {
|
||||
let rawValue: Int32 = SQLITE_DETERMINISTIC | SQLITE_INNOCUOUS
|
||||
let options = Function.Options(rawValue: rawValue)
|
||||
|
||||
@@ -42,7 +42,7 @@ struct FunctionOptionsTests {
|
||||
#expect(options.contains(.directonly) == false)
|
||||
}
|
||||
|
||||
@Test func testAddingAndRemovingOptions() {
|
||||
@Test func addingAndRemovingOptions() {
|
||||
var options: Function.Options = []
|
||||
|
||||
options.insert(.deterministic)
|
||||
@@ -55,4 +55,3 @@ struct FunctionOptionsTests {
|
||||
#expect(options.contains(.deterministic) == false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
82
Tests/DataLiteCoreTests/Classes/Function+RegexpTests.swift
Normal file
82
Tests/DataLiteCoreTests/Classes/Function+RegexpTests.swift
Normal file
@@ -0,0 +1,82 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
import DataLiteCore
|
||||
|
||||
struct FunctionRegexpTests {
|
||||
@Test func metadata() {
|
||||
#expect(Regexp.argc == 2)
|
||||
#expect(Regexp.name == "REGEXP")
|
||||
#expect(Regexp.options == [.deterministic, .innocuous])
|
||||
}
|
||||
|
||||
@Test func invalidArguments() {
|
||||
let arguments: Arguments = [.int(1), .text("value")]
|
||||
#expect(
|
||||
performing: {
|
||||
try Regexp.invoke(args: arguments)
|
||||
},
|
||||
throws: {
|
||||
switch $0 {
|
||||
case Regexp.Error.invalidArguments:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Test func invalidPattern() {
|
||||
let arguments: Arguments = [.text("("), .text("value")]
|
||||
#expect(
|
||||
performing: {
|
||||
try Regexp.invoke(args: arguments)
|
||||
},
|
||||
throws: {
|
||||
switch $0 {
|
||||
case Regexp.Error.regexError:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Test func matchesPattern() throws {
|
||||
let arguments: Arguments = [.text("foo.*"), .text("foobar")]
|
||||
#expect(try Regexp.invoke(args: arguments) as? Bool == true)
|
||||
}
|
||||
|
||||
@Test func doesNotMatchPattern() throws {
|
||||
let arguments: Arguments = [.text("bar.*"), .text("foobar")]
|
||||
#expect(try Regexp.invoke(args: arguments) as? Bool == false)
|
||||
}
|
||||
}
|
||||
|
||||
private extension FunctionRegexpTests {
|
||||
typealias Regexp = Function.Regexp
|
||||
|
||||
struct Arguments: ArgumentsProtocol, ExpressibleByArrayLiteral {
|
||||
private let values: [SQLiteValue]
|
||||
|
||||
var startIndex: Int { values.startIndex }
|
||||
var endIndex: Int { values.endIndex }
|
||||
|
||||
init(_ values: [SQLiteValue]) {
|
||||
self.values = values
|
||||
}
|
||||
|
||||
init(arrayLiteral elements: SQLiteValue...) {
|
||||
self.values = elements
|
||||
}
|
||||
|
||||
subscript(index: Int) -> SQLiteValue {
|
||||
values[index]
|
||||
}
|
||||
|
||||
func index(after i: Int) -> Int {
|
||||
values.index(after: i)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,15 @@ import DataLiteC
|
||||
import DataLiteCore
|
||||
|
||||
struct StatementOptionsTests {
|
||||
@Test func testPersistentOptions() {
|
||||
@Test func persistentOptions() {
|
||||
#expect(Statement.Options.persistent.rawValue == UInt32(SQLITE_PREPARE_PERSISTENT))
|
||||
}
|
||||
|
||||
@Test func testNoVtabOptions() {
|
||||
@Test func noVtabOptions() {
|
||||
#expect(Statement.Options.noVtab.rawValue == UInt32(SQLITE_PREPARE_NO_VTAB))
|
||||
}
|
||||
|
||||
@Test func testCombineOptions() {
|
||||
@Test func combineOptions() {
|
||||
let options: Statement.Options = [.persistent, .noVtab]
|
||||
let expected = UInt32(SQLITE_PREPARE_PERSISTENT | SQLITE_PREPARE_NO_VTAB)
|
||||
#expect(options.contains(.persistent))
|
||||
@@ -20,19 +20,19 @@ struct StatementOptionsTests {
|
||||
#expect(options.rawValue == expected)
|
||||
}
|
||||
|
||||
@Test func testInitWithUInt32RawValue() {
|
||||
@Test func initWithUInt32RawValue() {
|
||||
let raw = UInt32(SQLITE_PREPARE_PERSISTENT)
|
||||
let options = Statement.Options(rawValue: raw)
|
||||
#expect(options == .persistent)
|
||||
}
|
||||
|
||||
@Test func testInitWithInt32RawValue() {
|
||||
@Test func initWithInt32RawValue() {
|
||||
let raw = Int32(SQLITE_PREPARE_NO_VTAB)
|
||||
let options = Statement.Options(rawValue: raw)
|
||||
#expect(options == .noVtab)
|
||||
}
|
||||
|
||||
@Test func testEmptySetRawValueIsZero() {
|
||||
@Test func emptySetRawValueIsZero() {
|
||||
let empty: Statement.Options = []
|
||||
#expect(empty.rawValue == 0)
|
||||
#expect(!empty.contains(.persistent))
|
||||
|
||||
@@ -30,7 +30,7 @@ final class StatementTests {
|
||||
sqlite3_close_v2(connection)
|
||||
}
|
||||
|
||||
@Test func testInitWithError() throws {
|
||||
@Test func initWithError() throws {
|
||||
#expect(
|
||||
throws: SQLiteError(
|
||||
code: SQLITE_ERROR,
|
||||
@@ -46,19 +46,22 @@ final class StatementTests {
|
||||
)
|
||||
}
|
||||
|
||||
@Test func testParameterCount() throws {
|
||||
let sql = "SELECT * FROM t WHERE id = ? AND s = ?"
|
||||
@Test func sqlString() throws {
|
||||
let sql = "SELECT * FROM t WHERE id = ?"
|
||||
let stmt = try Statement(db: connection, sql: sql, options: [])
|
||||
#expect(stmt.parameterCount() == 2)
|
||||
#expect(stmt.sql == sql)
|
||||
}
|
||||
|
||||
@Test func testZeroParameterCount() throws {
|
||||
let sql = "SELECT * FROM t"
|
||||
@Test(arguments: [
|
||||
("SELECT * FROM t WHERE id = ? AND s = ?", 2),
|
||||
("SELECT * FROM t WHERE id = 1 AND s = ''", 0)
|
||||
])
|
||||
func parameterCount(_ sql: String, _ expanded: Int32) throws {
|
||||
let stmt = try Statement(db: connection, sql: sql, options: [])
|
||||
#expect(stmt.parameterCount() == 0)
|
||||
#expect(stmt.parameterCount() == expanded)
|
||||
}
|
||||
|
||||
@Test func testParameterIndexByName() throws {
|
||||
@Test func parameterIndexByName() throws {
|
||||
let sql = "SELECT * FROM t WHERE id = :id AND s = :s"
|
||||
let stmt = try Statement(db: connection, sql: sql, options: [])
|
||||
#expect(stmt.parameterIndexBy(":id") == 1)
|
||||
@@ -66,7 +69,7 @@ final class StatementTests {
|
||||
#expect(stmt.parameterIndexBy(":invalid") == 0)
|
||||
}
|
||||
|
||||
@Test func testParameterNameByIndex() throws {
|
||||
@Test func parameterNameByIndex() throws {
|
||||
let sql = "SELECT * FROM t WHERE id = :id AND s = :s"
|
||||
let stmt = try Statement(db: connection, sql: sql, options: [])
|
||||
#expect(stmt.parameterNameBy(1) == ":id")
|
||||
@@ -74,20 +77,36 @@ final class StatementTests {
|
||||
#expect(stmt.parameterNameBy(3) == nil)
|
||||
}
|
||||
|
||||
@Test func testBindValueAtIndex() throws {
|
||||
let sql = "SELECT * FROM t where id = ?"
|
||||
@Test func bindValueAtIndex() throws {
|
||||
let sql = "SELECT * FROM t WHERE id = ?"
|
||||
|
||||
let stmt = try Statement(db: connection, sql: sql, options: [])
|
||||
#expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = NULL")
|
||||
|
||||
try stmt.bind(.int(42), at: 1)
|
||||
#expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = 42")
|
||||
|
||||
try stmt.bind(.real(42), at: 1)
|
||||
#expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = 42.0")
|
||||
|
||||
try stmt.bind(.text("42"), at: 1)
|
||||
#expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = '42'")
|
||||
|
||||
try stmt.bind(.blob(Data([0x42])), at: 1)
|
||||
#expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = x'42'")
|
||||
|
||||
try stmt.bind(.null, at: 1)
|
||||
#expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = NULL")
|
||||
|
||||
try stmt.bind(TestValue(value: 42), at: 1)
|
||||
#expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = 42")
|
||||
|
||||
try stmt.bind(TestValue?.none, at: 1)
|
||||
#expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = NULL")
|
||||
}
|
||||
|
||||
@Test func testErrorBindValueAtIndex() throws {
|
||||
let sql = "SELECT * FROM t where id = ?"
|
||||
@Test func errorBindValueAtIndex() throws {
|
||||
let sql = "SELECT * FROM t WHERE id = ?"
|
||||
let stmt = try Statement(db: connection, sql: sql, options: [])
|
||||
#expect(
|
||||
throws: SQLiteError(
|
||||
@@ -100,20 +119,36 @@ final class StatementTests {
|
||||
)
|
||||
}
|
||||
|
||||
@Test func testBindValueByName() throws {
|
||||
let sql = "SELECT * FROM t where id = :id"
|
||||
@Test func bindValueByName() throws {
|
||||
let sql = "SELECT * FROM t WHERE id = :id"
|
||||
|
||||
let stmt = try Statement(db: connection, sql: sql, options: [])
|
||||
#expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = NULL")
|
||||
|
||||
try stmt.bind(.int(42), by: ":id")
|
||||
#expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = 42")
|
||||
|
||||
try stmt.bind(.real(42), by: ":id")
|
||||
#expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = 42.0")
|
||||
|
||||
try stmt.bind(.text("42"), by: ":id")
|
||||
#expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = '42'")
|
||||
|
||||
try stmt.bind(.blob(Data([0x42])), by: ":id")
|
||||
#expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = x'42'")
|
||||
|
||||
try stmt.bind(.null, by: ":id")
|
||||
#expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = NULL")
|
||||
|
||||
try stmt.bind(TestValue(value: 42), by: ":id")
|
||||
#expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = 42")
|
||||
|
||||
try stmt.bind(TestValue?.none, by: ":id")
|
||||
#expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = NULL")
|
||||
}
|
||||
|
||||
@Test func testErrorBindValueByName() throws {
|
||||
let sql = "SELECT * FROM t where id = :id"
|
||||
@Test func errorBindValueByName() throws {
|
||||
let sql = "SELECT * FROM t WHERE id = :id"
|
||||
let stmt = try Statement(db: connection, sql: sql, options: [])
|
||||
#expect(
|
||||
throws: SQLiteError(
|
||||
@@ -126,14 +161,52 @@ final class StatementTests {
|
||||
)
|
||||
}
|
||||
|
||||
@Test func testStepOneRow() throws {
|
||||
let sql = "SELECT 1 where 1"
|
||||
@Test func bindRow() throws {
|
||||
let row: SQLiteRow = ["id": .int(42), "name": .text("Alice")]
|
||||
let sql = "SELECT * FROM t WHERE id = :id AND s = :name"
|
||||
|
||||
let stmt = try Statement(db: connection, sql: sql, options: [])
|
||||
#expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = NULL AND s = NULL")
|
||||
|
||||
try stmt.bind(row)
|
||||
#expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = 42 AND s = 'Alice'")
|
||||
}
|
||||
|
||||
@Test func errorBindRow() throws {
|
||||
let row: SQLiteRow = ["name": .text("Alice")]
|
||||
let stmt = try Statement(
|
||||
db: connection, sql: "SELECT * FROM t", options: []
|
||||
)
|
||||
#expect(
|
||||
throws: SQLiteError(
|
||||
code: SQLITE_RANGE,
|
||||
message: "column index out of range"
|
||||
),
|
||||
performing: {
|
||||
try stmt.bind(row)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Test func clearBindings() throws {
|
||||
let sql = "SELECT * FROM t WHERE id = :id"
|
||||
let stmt = try Statement(db: connection, sql: sql, options: [])
|
||||
|
||||
try stmt.bind(.int(42), at: 1)
|
||||
#expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = 42")
|
||||
|
||||
try stmt.clearBindings()
|
||||
#expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = NULL")
|
||||
}
|
||||
|
||||
@Test func stepOneRow() throws {
|
||||
let sql = "SELECT 1 WHERE 1"
|
||||
let stmt = try Statement(db: connection, sql: sql, options: [])
|
||||
#expect(try stmt.step())
|
||||
#expect(try stmt.step() == false)
|
||||
}
|
||||
|
||||
@Test func testStepMultipleRows() throws {
|
||||
@Test func stepMultipleRows() throws {
|
||||
sqlite3_exec(connection, "INSERT INTO t(n) VALUES (1),(2),(3)", nil, nil, nil)
|
||||
let sql = "SELECT id FROM t ORDER BY id"
|
||||
let stmt = try Statement(db: connection, sql: sql, options: [])
|
||||
@@ -143,13 +216,13 @@ final class StatementTests {
|
||||
#expect(try stmt.step() == false)
|
||||
}
|
||||
|
||||
@Test func testStepNoRows() throws {
|
||||
@Test func stepNoRows() throws {
|
||||
let sql = "SELECT 1 WHERE 0"
|
||||
let stmt = try Statement(db: connection, sql: sql, options: [])
|
||||
#expect(try stmt.step() == false)
|
||||
}
|
||||
|
||||
@Test func testStepWithError() throws {
|
||||
@Test func stepWithError() throws {
|
||||
sqlite3_exec(connection, "INSERT INTO t(id, n) VALUES (1, 10)", nil, nil, nil)
|
||||
let sql = "INSERT INTO t(id, n) VALUES (?, ?)"
|
||||
let stmt = try Statement(db: connection, sql: sql, options: [])
|
||||
@@ -166,13 +239,52 @@ final class StatementTests {
|
||||
)
|
||||
}
|
||||
|
||||
@Test func testColumnCount() throws {
|
||||
@Test func executeRows() throws {
|
||||
let rows: [SQLiteRow] = [
|
||||
[
|
||||
"id": .int(1),
|
||||
"n": .int(42),
|
||||
"r": .real(3.14),
|
||||
"s": .text("Test"),
|
||||
"b": .blob(Data([0x42]))
|
||||
],
|
||||
[
|
||||
"id": .int(2),
|
||||
"n": .null,
|
||||
"r": .null,
|
||||
"s": .null,
|
||||
"b": .null
|
||||
]
|
||||
]
|
||||
let sql = "INSERT INTO t(id, n, r, s, b) VALUES (:id, :n, :r, :s, :b)"
|
||||
try Statement(db: connection, sql: sql, options: []).execute(rows)
|
||||
|
||||
let stmt = try Statement(db: connection, sql: "SELECT * FROM t", options: [])
|
||||
|
||||
#expect(try stmt.step())
|
||||
#expect(stmt.currentRow() == rows[0])
|
||||
|
||||
#expect(try stmt.step())
|
||||
#expect(stmt.currentRow() == rows[1])
|
||||
|
||||
#expect(try stmt.step() == false)
|
||||
}
|
||||
|
||||
@Test func executeEmptyRows() throws {
|
||||
let sql = "INSERT INTO t(id, n, r, s, b) VALUES (:id, :n, :r, :s, :b)"
|
||||
try Statement(db: connection, sql: sql, options: []).execute([])
|
||||
|
||||
let stmt = try Statement(db: connection, sql: "SELECT * FROM t", options: [])
|
||||
#expect(try stmt.step() == false)
|
||||
}
|
||||
|
||||
@Test func columnCount() throws {
|
||||
let sql = "SELECT * FROM t"
|
||||
let stmt = try Statement(db: connection, sql: sql, options: [])
|
||||
#expect(stmt.columnCount() == 5)
|
||||
}
|
||||
|
||||
@Test func testColumnName() throws {
|
||||
@Test func columnName() throws {
|
||||
let sql = "SELECT * FROM t"
|
||||
let stmt = try Statement(db: connection, sql: sql, options: [])
|
||||
#expect(stmt.columnName(at: 0) == "id")
|
||||
@@ -180,10 +292,13 @@ final class StatementTests {
|
||||
#expect(stmt.columnName(at: 2) == "r")
|
||||
#expect(stmt.columnName(at: 3) == "s")
|
||||
#expect(stmt.columnName(at: 4) == "b")
|
||||
#expect(stmt.columnName(at: 5) == nil)
|
||||
}
|
||||
|
||||
@Test func testColumnValueAtIndex() throws {
|
||||
sqlite3_exec(connection, """
|
||||
@Test func columnValueAtIndex() throws {
|
||||
sqlite3_exec(
|
||||
connection,
|
||||
"""
|
||||
INSERT INTO t (id, n, r, s, b)
|
||||
VALUES (10, 42, 3.5, 'hello', x'DEADBEEF')
|
||||
""", nil, nil, nil
|
||||
@@ -201,8 +316,10 @@ final class StatementTests {
|
||||
#expect(stmt.columnValue(at: 4) == .blob(Data([0xDE, 0xAD, 0xBE, 0xEF])))
|
||||
}
|
||||
|
||||
@Test func testColumnNullValueAtIndex() throws {
|
||||
sqlite3_exec(connection, """
|
||||
@Test func columnNullValueAtIndex() throws {
|
||||
sqlite3_exec(
|
||||
connection,
|
||||
"""
|
||||
INSERT INTO t (id) VALUES (10)
|
||||
""", nil, nil, nil
|
||||
)
|
||||
@@ -215,6 +332,30 @@ final class StatementTests {
|
||||
#expect(stmt.columnValue(at: 1) == .null)
|
||||
#expect(stmt.columnValue(at: 1) == TestValue?.none)
|
||||
}
|
||||
|
||||
@Test func currentRow() throws {
|
||||
sqlite3_exec(
|
||||
connection,
|
||||
"""
|
||||
INSERT INTO t (id, n, r, s, b)
|
||||
VALUES (10, 42, 3.5, 'hello', x'DEADBEEF')
|
||||
""", nil, nil, nil
|
||||
)
|
||||
|
||||
let row: SQLiteRow = [
|
||||
"id": .int(10),
|
||||
"n": .int(42),
|
||||
"r": .real(3.5),
|
||||
"s": .text("hello"),
|
||||
"b": .blob(Data([0xDE, 0xAD, 0xBE, 0xEF]))
|
||||
]
|
||||
|
||||
let sql = "SELECT * FROM t WHERE id = 10"
|
||||
let stmt = try Statement(db: connection, sql: sql, options: [])
|
||||
|
||||
#expect(try stmt.step())
|
||||
#expect(stmt.currentRow() == row)
|
||||
}
|
||||
}
|
||||
|
||||
private extension StatementTests {
|
||||
|
||||
Reference in New Issue
Block a user