add swiftUI code

This commit is contained in:
zeus
2025-01-22 14:09:10 +08:00
parent 68e7b7347c
commit 8a99853829
2531 changed files with 486215 additions and 0 deletions
@@ -0,0 +1,153 @@
import Foundation
/// A format that prints one line per database row, suitable
/// for debugging.
///
/// This format may change in future releases. It is not suitable for
/// processing by other programs, or testing.
///
/// On each line, database values are separated by a separator (`|`
/// by default).
///
/// For example:
///
/// ```swift
/// // Arthur|500
/// // Barbara|1000
/// // Craig|200
/// try db.dumpRequest(Player.all(), format: .debug())
/// ```
public struct DebugDumpFormat: Sendable {
/// A boolean value indicating if column labels are printed as the first
/// line of output.
public var header: Bool
/// The separator between values.
public var separator: String
/// The string to print for NULL values.
public var nullValue: String
private var firstRow = true
/// Creates a `DebugDumpFormat`.
///
/// - Parameters:
/// - header: A boolean value indicating if column labels are printed
/// as the first line of output.
/// - separator: The separator between values.
/// - nullValue: The string to print for NULL values.
public init(
header: Bool = false,
separator: String = "|",
nullValue: String = "")
{
self.header = header
self.separator = separator
self.nullValue = nullValue
}
}
extension DebugDumpFormat: DumpFormat {
public mutating func writeRow(
_ db: Database,
statement: Statement,
to stream: inout DumpStream)
{
if firstRow {
firstRow = false
if header {
stream.writeln(statement.columnNames.joined(separator: separator))
}
}
let sqliteStatement = statement.sqliteStatement
var first = true
for index in 0..<sqlite3_column_count(sqliteStatement) {
// Don't log GRDB columns
let column = String(cString: sqlite3_column_name(sqliteStatement, index))
if column.starts(with: "grdb_") { continue }
if first {
first = false
} else {
stream.write(separator)
}
stream.write(formattedValue(db, in: sqliteStatement, at: index))
}
stream.write("\n")
}
public mutating func finalize(
_ db: Database,
statement: Statement,
to stream: inout DumpStream)
{
firstRow = true
}
private func formattedValue(_ db: Database, in sqliteStatement: SQLiteStatement, at index: CInt) -> String {
switch sqlite3_column_type(sqliteStatement, index) {
case SQLITE_NULL:
return nullValue
case SQLITE_INTEGER:
return Int64(sqliteStatement: sqliteStatement, index: index).description
case SQLITE_FLOAT:
return Double(sqliteStatement: sqliteStatement, index: index).description
case SQLITE_BLOB:
let data = Data(sqliteStatement: sqliteStatement, index: index)
if let string = String(data: data, encoding: .utf8) {
return string
} else if data.count == 16, let blob = sqlite3_column_blob(sqliteStatement, index) {
let uuid = UUID(uuid: blob.assumingMemoryBound(to: uuid_t.self).pointee)
return uuid.uuidString
} else {
return try! data.sqlExpression.quotedSQL(db)
}
case SQLITE_TEXT:
return String(sqliteStatement: sqliteStatement, index: index)
default:
return ""
}
}
}
extension DumpFormat where Self == DebugDumpFormat {
/// A format that prints one line per database row, suitable
/// for debugging.
///
/// This format may change in future releases. It is not suitable for
/// processing by other programs, or testing.
///
/// On each line, database values are separated by a separator (`|`
/// by default).
///
/// For example:
///
/// ```swift
/// // Arthur|500
/// // Barbara|1000
/// // Craig|200
/// try db.dumpRequest(Player.all(), format: .debug())
/// ```
///
/// - Parameters:
/// - header: A boolean value indicating if column labels are printed
/// as the first line of output.
/// - separator: The separator between values.
/// - nullValue: The string to print for NULL values.
public static func debug(
header: Bool = false,
separator: String = "|",
nullValue: String = "")
-> Self
{
DebugDumpFormat(header: header, separator: separator, nullValue: nullValue)
}
}
@@ -0,0 +1,204 @@
import Foundation
/// A format that prints database rows as a JSON array.
///
/// For example:
///
/// ```swift
/// // [{"name":"Arthur","score":500},
/// // {"name":"Barbara","score":1000}]
/// try db.dumpRequest(Player.all(), format: .json())
/// ```
///
/// For a pretty-printed output, customize the JSON encoder:
///
/// ```swift
/// // [
/// // {
/// // "name": "Arthur",
/// // "score": 500
/// // },
/// // {
/// // "name": "Barbara",
/// // "score": 1000
/// // }
/// // ]
/// let encoder = JSONDumpFormat.defaultEncoder
/// encoder.outputFormatting = .prettyPrinted
/// try db.dumpRequest(Player.all(), format: .json(encoder))
/// ```
public struct JSONDumpFormat: Sendable {
/// The default `JSONEncoder` for database values.
///
/// It is configured so that blob values (`Data`) are encoded in the
/// base64 format, and Non-conforming floats are encoded as "inf",
/// "-inf" and "nan".
///
/// It uses the output formatting option
/// `JSONEncoder.OutputFormatting.withoutEscapingSlashes` when available.
///
/// Modifying the returned encoder does not affect any encoder returned
/// by future calls to this method. It is always safe to use the
/// returned encoder as a starting point for additional customization.
public static var defaultEncoder: JSONEncoder {
// This encoder MUST NOT CHANGE, because some people rely on this format.
let encoder = JSONEncoder()
if #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) {
encoder.outputFormatting = .withoutEscapingSlashes
}
encoder.nonConformingFloatEncodingStrategy = .convertToString(
positiveInfinity: "inf",
negativeInfinity: "-inf",
nan: "nan")
encoder.dataEncodingStrategy = .base64
return encoder
}
/// The JSONEncoder that formats individual database values.
public var encoder: JSONEncoder
var firstRow = true
/// Creates a `JSONDumpFormat`.
///
/// - Parameter encoder: The JSONEncoder that formats individual
/// database values. If the outputFormatting` options contain
/// `.prettyPrinted`, the printed array has one value per line.
public init(encoder: JSONEncoder = JSONDumpFormat.defaultEncoder) {
self.encoder = encoder
}
}
extension JSONDumpFormat: DumpFormat {
public mutating func writeRow(
_ db: Database,
statement: Statement,
to stream: inout DumpStream)
throws {
if firstRow {
firstRow = false
stream.write("[")
if encoder.outputFormatting.contains(.prettyPrinted) {
stream.write("\n")
}
} else {
stream.write(",\n")
}
if encoder.outputFormatting.contains(.prettyPrinted) {
stream.write(" ")
}
stream.write("{")
let sqliteStatement = statement.sqliteStatement
var first = true
for index in 0..<sqlite3_column_count(sqliteStatement) {
// Don't log GRDB columns
let column = String(cString: sqlite3_column_name(sqliteStatement, index))
if column.starts(with: "grdb_") {
continue
}
if first {
first = false
} else {
stream.write(",")
}
if encoder.outputFormatting.contains(.prettyPrinted) {
stream.write("\n ")
}
try stream.write(formattedValue(column))
stream.write(":")
try stream.write(formattedValue(db, in: sqliteStatement, at: index))
}
if encoder.outputFormatting.contains(.prettyPrinted) {
stream.write("\n ")
}
stream.write("}")
}
public mutating func finalize(
_ db: Database,
statement: Statement,
to stream: inout DumpStream)
{
if firstRow {
if !statement.columnNames.isEmpty {
stream.writeln("[]")
}
} else {
if encoder.outputFormatting.contains(.prettyPrinted) {
stream.write("\n")
}
stream.writeln("]")
}
firstRow = true
}
private func formattedValue(_ db: Database, in sqliteStatement: SQLiteStatement, at index: CInt) throws -> String {
switch sqlite3_column_type(sqliteStatement, index) {
case SQLITE_NULL:
return "null"
case SQLITE_INTEGER:
return try formattedValue(Int64(sqliteStatement: sqliteStatement, index: index))
case SQLITE_FLOAT:
return try formattedValue(Double(sqliteStatement: sqliteStatement, index: index))
case SQLITE_BLOB:
return try formattedValue(Data(sqliteStatement: sqliteStatement, index: index))
case SQLITE_TEXT:
return try formattedValue(String(sqliteStatement: sqliteStatement, index: index))
default:
return ""
}
}
private func formattedValue(_ value: some Encodable) throws -> String {
let data = try encoder.encode(value)
guard let string = String(data: data, encoding: .utf8) else {
throw EncodingError.invalidValue(data, .init(codingPath: [], debugDescription: "Invalid JSON data"))
}
return string
}
}
extension DumpFormat where Self == JSONDumpFormat {
/// A format that prints database rows as a JSON array.
///
/// For example:
///
/// ```swift
/// // [{"name":"Arthur","score":500},
/// // {"name":"Barbara","score":1000}]
/// try db.dumpRequest(Player.all(), format: .json())
/// ```
///
/// For a pretty-printed output, customize the JSON encoder:
///
/// ```swift
/// // [
/// // {
/// // "name": "Arthur",
/// // "score": 500
/// // },
/// // {
/// // "name": "Barbara",
/// // "score": 1000
/// // }
/// // ]
/// let encoder = JSONDumpFormat.defaultEncoder
/// encoder.outputFormatting = .prettyPrinted
/// try db.dumpRequest(Player.all(), format: .json(encoder))
/// ```
///
/// - Parameter encoder: The JSONEncoder that formats individual
/// database values. If the outputFormatting` options contain
/// `.prettyPrinted`, the printed array has one value per line.
public static func json(encoder: JSONEncoder = JSONDumpFormat.defaultEncoder) -> Self {
JSONDumpFormat(encoder: encoder)
}
}
@@ -0,0 +1,129 @@
import Foundation
/// A format that prints one line per database value. All blob values
/// are interpreted as strings.
///
/// For example:
///
/// ```swift
/// // name = Arthur
/// // score = 500
/// //
/// // name = Barbara
/// // score = 1000
/// try db.dumpRequest(Player.all(), format: .line())
/// ```
public struct LineDumpFormat: Sendable {
/// The string to print for NULL values.
public var nullValue: String
var firstRow = true
/// Creates a `LineDumpFormat`.
///
/// - Parameters:
/// - nullValue: The string to print for NULL values.
public init(
nullValue: String = "")
{
self.nullValue = nullValue
}
}
extension LineDumpFormat: DumpFormat {
public mutating func writeRow(
_ db: Database,
statement: Statement,
to stream: inout DumpStream)
{
var lines: [(column: String, value: String)] = []
let sqliteStatement = statement.sqliteStatement
for index in 0..<sqlite3_column_count(sqliteStatement) {
// Don't log GRDB columns
let column = String(cString: sqlite3_column_name(sqliteStatement, index))
if column.starts(with: "grdb_") { continue }
lines.append((
column: column,
value: formattedValue(db, in: sqliteStatement, at: index)))
}
if lines.isEmpty { return }
if firstRow {
firstRow = false
} else {
stream.write("\n")
}
let columnWidth = lines.map(\.column.count).max()!
for line in lines {
stream.write(line.column.leftPadding(toLength: columnWidth, withPad: " "))
stream.write(" = ")
stream.writeln(line.value)
}
}
public mutating func finalize(
_ db: Database,
statement: Statement,
to stream: inout DumpStream)
{
if firstRow == false {
stream.margin()
}
firstRow = true
}
func formattedValue(
_ db: Database,
in sqliteStatement: SQLiteStatement,
at index: CInt)
-> String
{
switch sqlite3_column_type(sqliteStatement, index) {
case SQLITE_NULL:
return nullValue
case SQLITE_INTEGER:
return Int64(sqliteStatement: sqliteStatement, index: index).description
case SQLITE_FLOAT:
return Double(sqliteStatement: sqliteStatement, index: index).description
case SQLITE_BLOB, SQLITE_TEXT:
return String(sqliteStatement: sqliteStatement, index: index)
default:
return ""
}
}
}
extension DumpFormat where Self == LineDumpFormat {
/// A format that prints one line per database value. All blob values
/// are interpreted as strings.
///
/// On each line, database values are separated by a separator (`|`
/// by default). Blob values are interpreted as UTF8 strings.
///
/// For example:
///
/// ```swift
/// // name = Arthur
/// // score = 500
/// //
/// // name = Barbara
/// // score = 1000
/// try db.dumpRequest(Player.all(), format: .line())
/// ```
///
/// - Parameters:
/// - nullValue: The string to print for NULL values.
public static func line(
nullValue: String = "")
-> Self
{
LineDumpFormat(nullValue: nullValue)
}
}
@@ -0,0 +1,141 @@
import Foundation
/// A format that prints one line per database row. All blob values
/// are interpreted as strings.
///
/// On each line, database values are separated by a separator (`|`
/// by default). Blob values are interpreted as UTF8 strings.
///
/// For example:
///
/// ```swift
/// // Arthur|500
/// // Barbara|1000
/// // Craig|200
/// try db.dumpRequest(Player.all(), format: .list())
/// ```
public struct ListDumpFormat: Sendable {
/// A boolean value indicating if column labels are printed as the first
/// line of output.
public var header: Bool
/// The separator between values.
public var separator: String
/// The string to print for NULL values.
public var nullValue: String
private var firstRow = true
/// Creates a `ListDumpFormat`.
///
/// - Parameters:
/// - header: A boolean value indicating if column labels are printed
/// as the first line of output.
/// - separator: The separator between values.
/// - nullValue: The string to print for NULL values.
public init(
header: Bool = false,
separator: String = "|",
nullValue: String = "")
{
self.header = header
self.separator = separator
self.nullValue = nullValue
}
}
extension ListDumpFormat: DumpFormat {
public mutating func writeRow(
_ db: Database,
statement: Statement,
to stream: inout DumpStream)
{
if firstRow {
firstRow = false
if header {
stream.writeln(statement.columnNames.joined(separator: separator))
}
}
let sqliteStatement = statement.sqliteStatement
var first = true
for index in 0..<sqlite3_column_count(sqliteStatement) {
// Don't log GRDB columns
let column = String(cString: sqlite3_column_name(sqliteStatement, index))
if column.starts(with: "grdb_") { continue }
if first {
first = false
} else {
stream.write(separator)
}
stream.write(formattedValue(db, in: sqliteStatement, at: index))
}
stream.write("\n")
}
public mutating func finalize(
_ db: Database,
statement: Statement,
to stream: inout DumpStream)
{
firstRow = true
}
func formattedValue(
_ db: Database,
in sqliteStatement: SQLiteStatement,
at index: CInt)
-> String
{
switch sqlite3_column_type(sqliteStatement, index) {
case SQLITE_NULL:
return nullValue
case SQLITE_INTEGER:
return Int64(sqliteStatement: sqliteStatement, index: index).description
case SQLITE_FLOAT:
return Double(sqliteStatement: sqliteStatement, index: index).description
case SQLITE_BLOB, SQLITE_TEXT:
return String(sqliteStatement: sqliteStatement, index: index)
default:
return ""
}
}
}
extension DumpFormat where Self == ListDumpFormat {
/// A format that prints one line per database row. All blob values
/// are interpreted as strings.
///
/// On each line, database values are separated by a separator (`|`
/// by default). Blob values are interpreted as UTF8 strings.
///
/// For example:
///
/// ```swift
/// // Arthur|500
/// // Barbara|1000
/// // Craig|200
/// try db.dumpRequest(Player.all(), format: .list())
/// ```
///
/// - Parameters:
/// - header: A boolean value indicating if column labels are printed
/// as the first line of output.
/// - separator: The separator between values.
/// - nullValue: The string to print for NULL values.
public static func list(
header: Bool = false,
separator: String = "|",
nullValue: String = "")
-> Self
{
ListDumpFormat(header: header, separator: separator, nullValue: nullValue)
}
}
@@ -0,0 +1,101 @@
/// A format that prints one line per database row, formatting values
/// as SQL literals.
///
/// For example:
///
/// ```swift
/// // 'Arthur',500
/// // 'Barbara',1000
/// // 'Craig',200
/// try db.dumpRequest(Player.all(), format: .quote())
/// ```
public struct QuoteDumpFormat: Sendable {
/// A boolean value indicating if column labels are printed as the first
/// line of output.
public var header: Bool
/// The separator between values.
public var separator: String
var firstRow = true
/// Creates a `QuoteDumpFormat`.
///
/// - Parameters:
/// - header: A boolean value indicating if column labels are printed
/// as the first line of output.
/// - separator: The separator between values.
public init(
header: Bool = false,
separator: String = ",")
{
self.header = header
self.separator = separator
}
}
extension QuoteDumpFormat: DumpFormat {
public mutating func writeRow(
_ db: Database,
statement: Statement,
to stream: inout DumpStream)
{
if firstRow {
firstRow = false
if header {
stream.writeln(statement.columnNames
.map { try! $0.sqlExpression.quotedSQL(db) }
.joined(separator: separator))
}
}
let sqliteStatement = statement.sqliteStatement
var first = true
for index in 0..<sqlite3_column_count(sqliteStatement) {
// Don't log GRDB columns
let column = String(cString: sqlite3_column_name(sqliteStatement, index))
if column.starts(with: "grdb_") { continue }
if first {
first = false
} else {
stream.write(separator)
}
let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: index)
try! stream.write(dbValue.sqlExpression.quotedSQL(db))
}
stream.write("\n")
}
public mutating func finalize(
_ db: Database,
statement: Statement,
to stream: inout DumpStream)
{
firstRow = true
}
}
extension DumpFormat where Self == QuoteDumpFormat {
/// A format that prints one line per database row, formatting values
/// as SQL literals.
///
/// For example:
///
/// ```swift
/// // 'Arthur',500
/// // 'Barbara',1000
/// // 'Craig',200
/// try db.dumpRequest(Player.all(), format: .quote())
/// ```
///
/// - Parameters:
/// - header: A boolean value indicating if column labels are printed
/// as the first line of output.
/// - separator: The separator between values.
public static func quote(header: Bool = false, separator: String = ",") -> Self {
QuoteDumpFormat(header: header, separator: separator)
}
}