Skip to content

Commit

Permalink
Merge pull request #1448 from groue/dev/dump-views
Browse files Browse the repository at this point in the history
Add support for stable ordering and dump of views.
  • Loading branch information
groue authored Oct 28, 2023
2 parents 8fa4342 + ba320a0 commit aaad020
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 18 deletions.
2 changes: 1 addition & 1 deletion GRDB/Core/Database.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ let SQLITE_TRANSIENT = unsafeBitCast(OpaquePointer(bitPattern: -1), to: sqlite3_
/// - ``dumpContent(format:to:)``
/// - ``dumpRequest(_:format:to:)``
/// - ``dumpSQL(_:format:to:)``
/// - ``dumpTables(_:format:tableHeader:to:)``
/// - ``dumpTables(_:format:tableHeader:stableOrder:to:)``
/// - ``DumpFormat``
/// - ``DumpTableHeaderOptions``
///
Expand Down
2 changes: 1 addition & 1 deletion GRDB/Core/DatabaseReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import Dispatch
/// - ``dumpContent(format:to:)``
/// - ``dumpRequest(_:format:to:)``
/// - ``dumpSQL(_:format:to:)``
/// - ``dumpTables(_:format:tableHeader:to:)``
/// - ``dumpTables(_:format:tableHeader:stableOrder:to:)``
/// - ``DumpFormat``
/// - ``DumpTableHeaderOptions``
///
Expand Down
34 changes: 29 additions & 5 deletions GRDB/Dump/Database+Dump.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ extension Database {
try _dumpRequest(request, format: format, to: &dumpStream)
}

/// Prints the contents of the provided tables.
/// Prints the contents of the provided tables and views.
///
/// For example:
///
Expand All @@ -80,17 +80,29 @@ extension Database {
/// - 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, to: &dumpStream)
try _dumpTables(
tables,
format: format,
tableHeader: tableHeader,
stableOrder: stableOrder,
to: &dumpStream)
}

/// Prints the contents of the database.
Expand Down Expand Up @@ -186,7 +198,8 @@ extension Database {
func _dumpTables(
_ tables: [String],
format: some DumpFormat,
tableHeader: DumpTableHeaderOptions = .automatic,
tableHeader: DumpTableHeaderOptions,
stableOrder: Bool,
to stream: inout DumpStream)
throws
{
Expand All @@ -203,10 +216,21 @@ extension Database {
} else {
stream.write("\n")
}

if header {
stream.writeln(table)
}
try _dumpRequest(Table(table).orderByPrimaryKey(), format: format, to: &stream)

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

Expand Down Expand Up @@ -246,7 +270,7 @@ extension Database {
}
if tables.isEmpty { return }
stream.write("\n")
try _dumpTables(tables, format: format, tableHeader: .always, to: &stream)
try _dumpTables(tables, format: format, tableHeader: .always, stableOrder: true, to: &stream)
}
}

Expand Down
16 changes: 14 additions & 2 deletions GRDB/Dump/DatabaseReader+dump.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ extension DatabaseReader {
}
}

/// Prints the contents of the provided tables.
/// Prints the contents of the provided tables and views.
///
/// For example:
///
Expand All @@ -72,17 +72,29 @@ extension DatabaseReader {
/// - 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, to: stream)
try db.dumpTables(
tables,
format: format,
tableHeader: tableHeader,
stableOrder: stableOrder,
to: stream)
}
}

Expand Down
19 changes: 13 additions & 6 deletions GRDB/QueryInterface/SQL/SQLRelation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -283,13 +283,20 @@ extension SQLRelation: Refinable {
}

func withStableOrder() -> Self {
with {
// Order by primary key. Don't order by rowid because those are
// not stable: rowids can change after a vacuum.
$0.ordering = $0.ordering.appending(Ordering(orderings: { db in
try db.primaryKey(source.tableName).columns.map { SQLExpression.column($0).sqlOrdering }
with { relation in
relation.ordering = relation.ordering.appending(Ordering(orderings: { [relation] db in
if try db.tableExists(source.tableName) {
// Order by primary key. Don't order by rowid because those are
// not stable: rowids can change after a vacuum.
return try db.primaryKey(source.tableName).columns.map { SQLExpression.column($0).sqlOrdering }
} else {
// Support for views: create a stable order from all columns:
// ORDER BY 1, 2, 3, ...
let columnCount = try SQLQueryGenerator(relation: relation).columnCount(db)
return (1...columnCount).map { SQL(sql: $0.description).sqlOrdering }
}
}))
$0.children = children.mapValues { child in
relation.children = children.mapValues { child in
child.with {
$0.relation = $0.relation.withStableOrder()
}
Expand Down
62 changes: 61 additions & 1 deletion Tests/GRDBTests/DatabaseDumpTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1072,7 +1072,7 @@ final class DatabaseDumpTests: GRDBTestCase {
}
}

func test_dumpTables_single() throws {
func test_dumpTables_single_table() throws {
try makeRugbyDatabase().read { db in
do {
// Default format
Expand All @@ -1099,6 +1099,66 @@ final class DatabaseDumpTests: GRDBTestCase {
}
}

func test_dumpTables_single_view() throws {
try makeRugbyDatabase().write { db in
try db.create(view: "playerName", as: Player
.orderByPrimaryKey()
.select(Column("name")))

do {
// Default order: use the view ordering
do {
// Default format
let stream = TestStream()
try db.dumpTables(["playerName"], to: stream)
XCTAssertEqual(stream.output, """
Antoine Dupond
Owen Farrell
Gwendal Roué
""")
}
do {
// Custom format
let stream = TestStream()
try db.dumpTables(["playerName"], format: .json(), to: stream)
XCTAssertEqual(stream.output, """
[{"name":"Antoine Dupond"},
{"name":"Owen Farrell"},
{"name":"Gwendal Roué"}]
""")
}
}

do {
// Stable order
do {
// Default format
let stream = TestStream()
try db.dumpTables(["playerName"], stableOrder: true, to: stream)
XCTAssertEqual(stream.output, """
Antoine Dupond
Gwendal Roué
Owen Farrell
""")
}
do {
// Custom format
let stream = TestStream()
try db.dumpTables(["playerName"], format: .json(), stableOrder: true, to: stream)
XCTAssertEqual(stream.output, """
[{"name":"Antoine Dupond"},
{"name":"Gwendal Roué"},
{"name":"Owen Farrell"}]
""")
}
}
}
}

func test_dumpTables_multiple() throws {
try makeRugbyDatabase().read { db in
do {
Expand Down
64 changes: 62 additions & 2 deletions Tests/GRDBTests/DerivableRequestTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,13 @@ class DerivableRequestTests: GRDBTestCase {
let dbQueue = try makeDatabaseQueue()
try libraryMigrator.migrate(dbQueue)
try dbQueue.inDatabase { db in
// ... for two requests (1)
try db.create(view: "authorView", as: Author.select(
AllColumns(),
[Column("firstName"), Column("lastName")]
.joined(operator: .concat)
.forKey("fullName")))

// ... for one table
sqlQueries.removeAll()
let authorNames = try Author.all()
.orderByFullName()
Expand Down Expand Up @@ -208,6 +214,14 @@ class DerivableRequestTests: GRDBTestCase {
SELECT * FROM "author"
""")

sqlQueries.removeAll()
_ /* stableOrderAuthors */ = try Author.all()
.withStableOrder()
.fetchAll(db)
XCTAssertEqual(lastSQLQuery, """
SELECT * FROM "author" ORDER BY "id"
""")

sqlQueries.removeAll()
_ /* stableOrderAuthors */ = try Author.all()
.orderByFullName()
Expand All @@ -217,7 +231,53 @@ class DerivableRequestTests: GRDBTestCase {
SELECT * FROM "author" ORDER BY "lastName" COLLATE swiftLocalizedCaseInsensitiveCompare, "firstName" COLLATE swiftLocalizedCaseInsensitiveCompare, "id"
""")

// ... for two requests (2)
// ... for one view
sqlQueries.removeAll()
_ /* authorViewNames */ = try Table("authorView").all()
.order(Column("fullName"))
.fetchAll(db)
XCTAssertEqual(lastSQLQuery, """
SELECT * FROM "authorView" \
ORDER BY "fullName"
""")

sqlQueries.removeAll()
_ /* reversedAuthorViewNames */ = try Table("authorView").all()
.order(Column("fullName"))
.reversed()
.fetchAll(db)
XCTAssertEqual(lastSQLQuery, """
SELECT * FROM "authorView" \
ORDER BY "fullName" DESC
""")

sqlQueries.removeAll()
_ /* unorderedAuthorViews */ = try Table("authorView").all()
.order(Column("fullName"))
.unordered()
.fetchAll(db)
XCTAssertEqual(lastSQLQuery, """
SELECT * FROM "authorView"
""")

sqlQueries.removeAll()
_ /* stableOrderAuthorViews */ = try Table("authorView").all()
.withStableOrder()
.fetchAll(db)
XCTAssertEqual(lastSQLQuery, """
SELECT * FROM "authorView" ORDER BY 1, 2, 3, 4, 5
""")

sqlQueries.removeAll()
_ /* stableOrderAuthorViews */ = try Table("authorView").all()
.order(Column("fullName"))
.withStableOrder()
.fetchAll(db)
XCTAssertEqual(lastSQLQuery, """
SELECT * FROM "authorView" ORDER BY "fullName", 1, 2, 3, 4, 5
""")

// ... for two tables (2)
sqlQueries.removeAll()
let bookTitles = try Book
.joining(required: Book.author.orderByFullName())
Expand Down

0 comments on commit aaad020

Please sign in to comment.