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,357 @@
import Foundation
// MARK: - Dump
extension Database {
/// Prints the results of all statements in the provided SQL.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// // Prints
/// // 1|Arthur|500
/// // 2|Barbara|1000
/// try db.dumpSQL("SELECT * FROM player ORDER BY id")
/// }
/// ```
///
/// - Parameters:
/// - sql: The executed SQL.
/// - format: The output format.
/// - stream: A stream for text output, which directs output to the
/// console by default.
public func dumpSQL(
_ sql: SQL,
format: some DumpFormat = .debug(),
to stream: (any TextOutputStream)? = nil)
throws
{
var dumpStream = DumpStream(stream)
try _dumpSQL(sql, format: format, to: &dumpStream)
}
/// Prints the results of a request.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// // Prints
/// // 1|Arthur|500
/// // 2|Barbara|1000
/// try db.dumpRequest(Player.orderByPrimaryKey())
/// }
/// ```
///
/// - Parameters:
/// - request : The executed request.
/// - format: The output format.
/// - stream: A stream for text output, which directs output to the
/// console by default.
public func dumpRequest(
_ request: some FetchRequest,
format: some DumpFormat = .debug(),
to stream: (any TextOutputStream)? = nil)
throws
{
var dumpStream = DumpStream(stream)
try _dumpRequest(request, format: format, to: &dumpStream)
}
/// Prints the contents of the provided tables and views.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// // player
/// // 1|Arthur|500
/// // 2|Barbara|1000
/// //
/// // team
/// // 1|Red
/// // 2|Blue
/// try db.dumpTables(["player", "team"])
/// }
/// ```
///
/// - Parameters:
/// - tables: The table names.
/// - format: The output format.
/// - tableHeader: Options for printing table names.
/// - stableOrder: A boolean value that controls the ordering of
/// rows fetched from views. If false (the default), rows are
/// printed in the order specified by the view (which may be
/// undefined). It true, outputted rows are always printed in the
/// same stable order. The purpose of this stable order is to make
/// the output suitable for testing.
/// - stream: A stream for text output, which directs output to the
/// console by default.
public func dumpTables(
_ tables: [String],
format: some DumpFormat = .debug(),
tableHeader: DumpTableHeaderOptions = .automatic,
stableOrder: Bool = false,
to stream: (any TextOutputStream)? = nil)
throws
{
var dumpStream = DumpStream(stream)
try _dumpTables(
tables,
format: format,
tableHeader: tableHeader,
stableOrder: stableOrder,
to: &dumpStream)
}
/// Prints the contents of the database.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// try db.dumpContent()
/// }
/// ```
///
/// This prints the database schema as well as the content of all
/// tables. For example:
///
/// ```
/// sqlite_master
/// CREATE TABLE player (id INTEGER PRIMARY KEY, name TEXT, score INTEGER)
///
/// player
/// 1,'Arthur',500
/// 2,'Barbara',1000
/// ```
///
/// > Note: Internal SQLite and GRDB schema objects are not recorded
/// > (those with a name that starts with "sqlite_" or "grdb_").
/// >
/// > [Shadow tables](https://www.sqlite.org/vtab.html#xshadowname) are
/// > not recorded, starting SQLite 3.37+.
///
/// - Parameters:
/// - format: The output format.
/// - stream: A stream for text output, which directs output to the
/// console by default.
public func dumpContent(
format: some DumpFormat = .debug(),
to stream: (any TextOutputStream)? = nil)
throws
{
var dumpStream = DumpStream(stream)
try _dumpContent(format: format, to: &dumpStream)
}
/// Prints the schema of the database.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// try db.dumpSchema()
/// }
/// ```
///
/// This prints the database schema. For example:
///
/// ```
/// sqlite_master
/// CREATE TABLE player (id INTEGER PRIMARY KEY, name TEXT, score INTEGER)
/// ```
///
/// > Note: Internal SQLite and GRDB schema objects are not recorded
/// > (those with a name that starts with "sqlite_" or "grdb_").
/// >
/// > [Shadow tables](https://www.sqlite.org/vtab.html#xshadowname) are
/// > not recorded, starting SQLite 3.37+.
///
/// - Parameters:
/// - stream: A stream for text output, which directs output to the
/// console by default.
public func dumpSchema(
to stream: (any TextOutputStream)? = nil)
throws
{
var dumpStream = DumpStream(stream)
try _dumpSchema(to: &dumpStream)
}
}
// MARK: -
extension Database {
func _dumpStatements(
_ statements: some Cursor<Statement>,
format: some DumpFormat,
to stream: inout DumpStream)
throws
{
while let statement = try statements.next() {
var stepFormat = format
let cursor = try statement.makeCursor()
while try cursor.next() != nil {
try stepFormat.writeRow(self, statement: statement, to: &stream)
}
stepFormat.finalize(self, statement: statement, to: &stream)
}
}
func _dumpSQL(
_ sql: SQL,
format: some DumpFormat,
to stream: inout DumpStream)
throws
{
try _dumpStatements(allStatements(literal: sql), format: format, to: &stream)
}
func _dumpRequest(
_ request: some FetchRequest,
format: some DumpFormat,
to stream: inout DumpStream)
throws
{
let preparedRequest = try request.makePreparedRequest(self, forSingleResult: false)
try _dumpStatements(AnyCursor([preparedRequest.statement]), format: format, to: &stream)
if let supplementaryFetch = preparedRequest.supplementaryFetch {
let rows = try Row.fetchAll(self, request)
try withoutActuallyEscaping(
{ request, keyPath in
stream.write("\n")
stream.writeln(keyPath.joined(separator: "."))
try self._dumpRequest(request, format: format, to: &stream)
},
do: { willExecuteSupplementaryRequest in
try supplementaryFetch(self, rows, willExecuteSupplementaryRequest)
})
}
}
func _dumpTables(
_ tables: [String],
format: some DumpFormat,
tableHeader: DumpTableHeaderOptions,
stableOrder: Bool,
to stream: inout DumpStream)
throws
{
let header: Bool
switch tableHeader {
case .always: header = true
case .automatic: header = tables.count > 1
}
var first = true
for table in tables {
if first {
first = false
} else {
stream.write("\n")
}
if header {
stream.writeln(table)
}
if try tableExists(table) {
// Always sort tables by primary key
try _dumpRequest(Table(table).orderByPrimaryKey(), format: format, to: &stream)
} else if stableOrder {
// View with stable order
try _dumpRequest(Table(table).all().withStableOrder(), format: format, to: &stream)
} else {
// Use view ordering, if any (no guarantee of stable order).
try _dumpRequest(Table(table).all(), format: format, to: &stream)
}
}
}
func _dumpContent(
format: some DumpFormat,
to stream: inout DumpStream)
throws
{
try _dumpSchema(to: &stream)
stream.margin()
let tables = try String
.fetchAll(self, sql: """
SELECT name
FROM sqlite_master
WHERE type = 'table'
ORDER BY name COLLATE NOCASE
""")
.filter {
try !ignoresObject(named: $0)
}
try _dumpTables(tables, format: format, tableHeader: .always, stableOrder: true, to: &stream)
}
func _dumpSchema(
to stream: inout DumpStream)
throws
{
stream.writeln("sqlite_master")
let sqlRows = try Row.fetchAll(self, sql: """
SELECT sql || ';', name
FROM sqlite_master
WHERE sql IS NOT NULL
ORDER BY
tbl_name COLLATE NOCASE,
CASE type WHEN 'table' THEN 'a' WHEN 'index' THEN 'aa' ELSE type END,
name COLLATE NOCASE,
sql
""")
for row in sqlRows {
let name: String = row[1]
if try ignoresObject(named: name) {
continue
}
stream.writeln(row[0])
}
}
private func ignoresObject(named name: String) throws -> Bool {
if Database.isSQLiteInternalTable(name) { return true }
if Database.isGRDBInternalTable(name) { return true }
if try isShadowTable(name) { return true }
return false
}
private func isShadowTable(_ tableName: String) throws -> Bool {
#if GRDBCUSTOMSQLITE || GRDBCIPHER
// Maybe SQLCipher is too old: check actual version
if sqlite3_libversion_number() >= 3037000 {
guard let table = try table(tableName) else {
// Not a table
return false
}
return table.kind == .shadow
}
#else
if #available(iOS 15.4, macOS 12.4, tvOS 15.4, watchOS 8.5, *) { // SQLite 3.37+
guard let table = try table(tableName) else {
// Not a table
return false
}
return table.kind == .shadow
}
#endif
// Don't know
return false
}
}
/// Options for printing table names.
public enum DumpTableHeaderOptions: Sendable {
/// Table names are only printed when several tables are printed.
case automatic
/// Table names are always printed.
case always
}
@@ -0,0 +1,173 @@
extension DatabaseReader {
/// Prints the results of all statements in the provided SQL.
///
/// For example:
///
/// ```swift
/// // Prints
/// // 1|Arthur|500
/// // 2|Barbara|1000
/// try dbQueue.dumpSQL("SELECT * FROM player ORDER BY id")
/// ```
///
/// - Parameters:
/// - sql: The executed SQL.
/// - format: The output format.
/// - stream: A stream for text output, which directs output to the
/// console by default.
public func dumpSQL(
_ sql: SQL,
format: some DumpFormat = .debug(),
to stream: (any TextOutputStream)? = nil)
throws
{
try unsafeReentrantRead { db in
try db.dumpSQL(sql, format: format, to: stream)
}
}
/// Prints the results of a request.
///
/// For example:
///
/// ```swift
/// // Prints
/// // 1|Arthur|500
/// // 2|Barbara|1000
/// try dbQueue.dumpRequest(Player.orderByPrimaryKey())
/// ```
///
/// - Parameters:
/// - request : The executed request.
/// - format: The output format.
/// - stream: A stream for text output, which directs output to the
/// console by default.
public func dumpRequest(
_ request: some FetchRequest,
format: some DumpFormat = .debug(),
to stream: (any TextOutputStream)? = nil)
throws
{
try unsafeReentrantRead { db in
try db.dumpRequest(request, format: format, to: stream)
}
}
/// Prints the contents of the provided tables and views.
///
/// For example:
///
/// ```swift
/// // player
/// // 1|Arthur|500
/// // 2|Barbara|1000
/// //
/// // team
/// // 1|Red
/// // 2|Blue
/// try dbQueue.dumpTables(["player", "team"])
/// ```
///
/// - Parameters:
/// - tables: The table names.
/// - format: The output format.
/// - tableHeader: Options for printing table names.
/// - stableOrder: A boolean value that controls the ordering of
/// rows fetched from views. If false (the default), rows are
/// printed in the order specified by the view (which may be
/// undefined). It true, outputted rows are always printed in the
/// same stable order. The purpose of this stable order is to make
/// the output suitable for testing.
/// - stream: A stream for text output, which directs output to the
/// console by default.
public func dumpTables(
_ tables: [String],
format: some DumpFormat = .debug(),
tableHeader: DumpTableHeaderOptions = .automatic,
stableOrder: Bool = false,
to stream: (any TextOutputStream)? = nil)
throws
{
try unsafeReentrantRead { db in
try db.dumpTables(
tables,
format: format,
tableHeader: tableHeader,
stableOrder: stableOrder,
to: stream)
}
}
/// Prints the contents of the database.
///
/// For example:
///
/// ```swift
/// try dbQueue.dumpContent()
/// ```
///
/// This prints the database schema as well as the content of all
/// tables. For example:
///
/// ```
/// sqlite_master
/// CREATE TABLE player (id INTEGER PRIMARY KEY, name TEXT, score INTEGER)
///
/// player
/// 1,'Arthur',500
/// 2,'Barbara',1000
/// ```
///
/// > Note: Internal SQLite and GRDB schema objects are not recorded
/// > (those with a name that starts with "sqlite_" or "grdb_").
/// >
/// > [Shadow tables](https://www.sqlite.org/vtab.html#xshadowname) are
/// > not recorded, starting SQLite 3.37+.
///
/// - Parameters:
/// - format: The output format.
/// - stream: A stream for text output, which directs output to the
/// console by default.
public func dumpContent(
format: some DumpFormat = .debug(),
to stream: (any TextOutputStream)? = nil)
throws
{
try unsafeReentrantRead { db in
try db.dumpContent(format: format, to: stream)
}
}
/// Prints the schema of the database.
///
/// For example:
///
/// ```swift
/// try dbQueue.dumpSchema()
/// ```
///
/// This prints the database schema. For example:
///
/// ```
/// sqlite_master
/// CREATE TABLE player (id INTEGER PRIMARY KEY, name TEXT, score INTEGER)
/// ```
///
/// > Note: Internal SQLite and GRDB schema objects are not recorded
/// > (those with a name that starts with "sqlite_" or "grdb_").
/// >
/// > [Shadow tables](https://www.sqlite.org/vtab.html#xshadowname) are
/// > not recorded, starting SQLite 3.37+.
///
/// - Parameters:
/// - stream: A stream for text output, which directs output to the
/// console by default.
public func dumpSchema(
to stream: (any TextOutputStream)? = nil)
throws
{
try unsafeReentrantRead { db in
try db.dumpSchema(to: stream)
}
}
}
@@ -0,0 +1,112 @@
/// A type that prints database rows.
///
/// Types that conform to `DumpFormat` feed the printing methods such as
/// ``DatabaseReader/dumpContent(format:to:)`` and
/// ``Database/dumpSQL(_:format:to:)``.
///
/// Most built-in formats are inspired from the
/// [output formats of the SQLite command line tool](https://sqlite.org/cli.html#changing_output_formats).
///
/// ## Topics
///
/// ### Built-in Formats
///
/// - ``debug(header:separator:nullValue:)``
/// - ``json(encoder:)``
/// - ``line(nullValue:)``
/// - ``list(header:separator:nullValue:)``
/// - ``quote(header:separator:)``
///
/// ### Supporting Types
///
/// - ``DebugDumpFormat``
/// - ``JSONDumpFormat``
/// - ``LineDumpFormat``
/// - ``ListDumpFormat``
/// - ``QuoteDumpFormat``
/// - ``DumpStream``
///
/// ### Implementing a custom format
///
/// [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
///
/// - ``writeRow(_:statement:to:)``
/// - ``finalize(_:statement:to:)``
public protocol DumpFormat {
/// Writes a row from the given statement.
///
/// - Parameters:
/// - db: A connection to the database
/// - statement: The iterated statement
/// - stream: A stream for text output.
mutating func writeRow(
_ db: Database,
statement: Statement,
to stream: inout DumpStream) throws
/// All rows from the statement have been printed.
///
/// - Parameters:
/// - db: A connection to the database
/// - statement: The statement that was iterated.
/// - stream: A stream for text output.
mutating func finalize(
_ db: Database,
statement: Statement,
to stream: inout DumpStream)
}
/// A TextOutputStream that prints to standard output
struct StandardOutputStream: TextOutputStream {
func write(_ string: String) {
print(string, terminator: "")
}
}
/// A text output stream suited for printing database content.
///
/// [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
public struct DumpStream {
var base: any TextOutputStream
var needsMarginLine = false
init(_ base: (any TextOutputStream)?) {
self.base = base ?? StandardOutputStream()
}
/// Will write `"\n"` before the next non-empty string.
public mutating func margin() {
needsMarginLine = true
}
}
extension DumpStream: TextOutputStream {
public mutating func write(_ string: String) {
if needsMarginLine && !string.isEmpty {
needsMarginLine = false
if string.first != "\n" {
base.write("\n")
}
}
base.write(string)
}
}
extension TextOutputStream {
mutating func writeln(_ string: String) {
write(string)
write("\n")
}
}
extension String {
func leftPadding(toLength newLength: Int, withPad padString: String) -> String {
precondition(padString.count == 1)
if count < newLength {
return String(repeating: padString, count: newLength - count) + self
} else {
let startIndex = index(startIndex, offsetBy: count - newLength)
return String(self[startIndex...])
}
}
}
@@ -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)
}
}