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, 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 }