Skip to content

Commit

Permalink
Merge pull request #1511 from groue/dev/schema-dump
Browse files Browse the repository at this point in the history
Database schema dump
  • Loading branch information
groue authored Mar 17, 2024
2 parents 9b95dd5 + f274e46 commit 1464940
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 22 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:

- **New**: [#1503](https://github.com/groue/GRDB.swift/pull/1503) by [@simba909](https://github.com/simba909): Conform Database.ColumnType to Sendable
- **New**: [#1510](https://github.com/groue/GRDB.swift/pull/1510) by [@groue](https://github.com/groue): Add Sendable conformances and unavailabilities
- **New**: [#1511](https://github.com/groue/GRDB.swift/pull/1511) by [@groue](https://github.com/groue): Database schema dump
- **Fixed**: [#1508](https://github.com/groue/GRDB.swift/pull/1508) by [@groue](https://github.com/groue): Fix ValueObservation mishandling of database schema modification

## 6.25.0
Expand Down
1 change: 1 addition & 0 deletions GRDB/Core/Database.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ let SQLITE_TRANSIENT = unsafeBitCast(OpaquePointer(bitPattern: -1), to: sqlite3_
///
/// - ``dumpContent(format:to:)``
/// - ``dumpRequest(_:format:to:)``
/// - ``dumpSchema(to:)``
/// - ``dumpSQL(_:format:to:)``
/// - ``dumpTables(_:format:tableHeader:stableOrder:to:)``
/// - ``DumpFormat``
Expand Down
1 change: 1 addition & 0 deletions GRDB/Core/DatabaseReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import Dispatch
///
/// - ``dumpContent(format:to:)``
/// - ``dumpRequest(_:format:to:)``
/// - ``dumpSchema(to:)``
/// - ``dumpSQL(_:format:to:)``
/// - ``dumpTables(_:format:tableHeader:stableOrder:to:)``
/// - ``DumpFormat``
Expand Down
76 changes: 58 additions & 18 deletions GRDB/Dump/Database+Dump.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ extension Database {
/// // Prints
/// // 1|Arthur|500
/// // 2|Barbara|1000
/// db.dumpSQL("SELECT * FROM player ORDER BY id")
/// try db.dumpSQL("SELECT * FROM player ORDER BY id")
/// }
/// ```
///
Expand Down Expand Up @@ -40,7 +40,7 @@ extension Database {
/// // Prints
/// // 1|Arthur|500
/// // 2|Barbara|1000
/// db.dumpRequest(Player.orderByPrimaryKey())
/// try db.dumpRequest(Player.orderByPrimaryKey())
/// }
/// ```
///
Expand Down Expand Up @@ -72,7 +72,7 @@ extension Database {
/// // team
/// // 1|Red
/// // 2|Blue
/// db.dumpTables(["player", "team"])
/// try db.dumpTables(["player", "team"])
/// }
/// ```
///
Expand Down Expand Up @@ -111,7 +111,7 @@ extension Database {
///
/// ```swift
/// try dbQueue.read { db in
/// db.dumpContent()
/// try db.dumpContent()
/// }
/// ```
///
Expand Down Expand Up @@ -145,6 +145,40 @@ extension Database {
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: -
Expand Down Expand Up @@ -241,6 +275,26 @@ extension Database {
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: """
Expand All @@ -260,20 +314,6 @@ extension Database {
}
stream.writeln(row[0])
}

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)
}
if tables.isEmpty { return }
stream.write("\n")
try _dumpTables(tables, format: format, tableHeader: .always, stableOrder: true, to: &stream)
}

private func ignoresObject(named name: String) throws -> Bool {
Expand Down
41 changes: 37 additions & 4 deletions GRDB/Dump/DatabaseReader+dump.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ extension DatabaseReader {
/// // Prints
/// // 1|Arthur|500
/// // 2|Barbara|1000
/// dbQueue.dumpSQL("SELECT * FROM player ORDER BY id")
/// try dbQueue.dumpSQL("SELECT * FROM player ORDER BY id")
/// ```
///
/// - Parameters:
Expand All @@ -34,7 +34,7 @@ extension DatabaseReader {
/// // Prints
/// // 1|Arthur|500
/// // 2|Barbara|1000
/// dbQueue.dumpRequest(Player.orderByPrimaryKey())
/// try dbQueue.dumpRequest(Player.orderByPrimaryKey())
/// ```
///
/// - Parameters:
Expand Down Expand Up @@ -65,7 +65,7 @@ extension DatabaseReader {
/// // team
/// // 1|Red
/// // 2|Blue
/// dbQueue.dumpTables(["player", "team"])
/// try dbQueue.dumpTables(["player", "team"])
/// ```
///
/// - Parameters:
Expand Down Expand Up @@ -103,7 +103,7 @@ extension DatabaseReader {
/// For example:
///
/// ```swift
/// dbQueue.dumpContent()
/// try dbQueue.dumpContent()
/// ```
///
/// This prints the database schema as well as the content of all
Expand Down Expand Up @@ -137,4 +137,37 @@ extension DatabaseReader {
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)
}
}
}
158 changes: 158 additions & 0 deletions Tests/GRDBTests/DatabaseDumpTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,164 @@ final class DatabaseDumpTests: GRDBTestCase {
}
}

// MARK: - Database schema dump

func test_dumpSchema() throws {
try makeRugbyDatabase().read { db in
let stream = TestStream()
try db.dumpSchema(to: stream)
XCTAssertEqual(stream.output, """
sqlite_master
CREATE TABLE "player" ("id" INTEGER PRIMARY KEY AUTOINCREMENT, "teamId" TEXT REFERENCES "team"("id"), "name" TEXT NOT NULL);
CREATE INDEX "player_on_teamId" ON "player"("teamId");
CREATE TABLE "team" ("id" TEXT PRIMARY KEY NOT NULL, "name" TEXT NOT NULL, "color" TEXT NOT NULL);
""")
}
}

func test_dumpSchema_empty_database() throws {
try makeDatabaseQueue().read { db in
let stream = TestStream()
try db.dumpSchema(to: stream)
XCTAssertEqual(stream.output, """
sqlite_master
""")
}
}

func test_dumpSchema_empty_tables() throws {
try makeDatabaseQueue().write { db in
try db.execute(literal: """
CREATE TABLE blue(name);
CREATE TABLE red(name);
CREATE TABLE yellow(name);
INSERT INTO red VALUES ('vermillon')
""")
let stream = TestStream()
try db.dumpSchema(to: stream)
XCTAssertEqual(stream.output, """
sqlite_master
CREATE TABLE blue(name);
CREATE TABLE red(name);
CREATE TABLE yellow(name);
""")
}
}

func test_dumpSchema_sqlite_master_ordering() throws {
try makeDatabaseQueue().write { db in
try db.execute(literal: """
CREATE TABLE blue(name);
CREATE TABLE RED(name);
CREATE TABLE yellow(name);
CREATE INDEX index_blue1 ON blue(name);
CREATE INDEX INDEX_blue2 ON blue(name);
CREATE INDEX indexRed1 ON RED(name);
CREATE INDEX INDEXRed2 ON RED(name);
CREATE VIEW colors1 AS SELECT name FROM blue;
CREATE VIEW COLORS2 AS SELECT name FROM blue UNION SELECT name FROM yellow;
CREATE TRIGGER update_blue UPDATE OF name ON blue
BEGIN
DELETE FROM RED;
END;
CREATE TRIGGER update_RED UPDATE OF name ON RED
BEGIN
DELETE FROM yellow;
END;
""")
let stream = TestStream()
try db.dumpSchema(to: stream)
XCTAssertEqual(stream.output, """
sqlite_master
CREATE TABLE blue(name);
CREATE INDEX index_blue1 ON blue(name);
CREATE INDEX INDEX_blue2 ON blue(name);
CREATE TRIGGER update_blue UPDATE OF name ON blue
BEGIN
DELETE FROM RED;
END;
CREATE VIEW colors1 AS SELECT name FROM blue;
CREATE VIEW COLORS2 AS SELECT name FROM blue UNION SELECT name FROM yellow;
CREATE TABLE RED(name);
CREATE INDEX indexRed1 ON RED(name);
CREATE INDEX INDEXRed2 ON RED(name);
CREATE TRIGGER update_RED UPDATE OF name ON RED
BEGIN
DELETE FROM yellow;
END;
CREATE TABLE yellow(name);
""")
}
}

func test_dumpSchema_ignores_shadow_tables() throws {
guard sqlite3_libversion_number() >= 3037000 else {
throw XCTSkip("Can't detect shadow tables")
}

try makeDatabaseQueue().write { db in
try db.create(table: "document") { t in
t.autoIncrementedPrimaryKey("id")
t.column("body")
}

try db.execute(sql: "INSERT INTO document VALUES (1, 'Hello world!')")

try db.create(virtualTable: "document_ft", using: FTS4()) { t in
t.synchronize(withTable: "document")
t.column("body")
}

let stream = TestStream()
try db.dumpSchema(to: stream)
print(stream.output)
XCTAssertEqual(stream.output, """
sqlite_master
CREATE TABLE "document" ("id" INTEGER PRIMARY KEY AUTOINCREMENT, "body");
CREATE TRIGGER "__document_ft_ai" AFTER INSERT ON "document" BEGIN
INSERT INTO "document_ft"("docid", "body") VALUES(new."id", new."body");
END;
CREATE TRIGGER "__document_ft_au" AFTER UPDATE ON "document" BEGIN
INSERT INTO "document_ft"("docid", "body") VALUES(new."id", new."body");
END;
CREATE TRIGGER "__document_ft_bd" BEFORE DELETE ON "document" BEGIN
DELETE FROM "document_ft" WHERE docid=old."id";
END;
CREATE TRIGGER "__document_ft_bu" BEFORE UPDATE ON "document" BEGIN
DELETE FROM "document_ft" WHERE docid=old."id";
END;
CREATE VIRTUAL TABLE "document_ft" USING fts4(body, content="document");
""")
}
}

func test_dumpSchema_ignores_GRDB_internal_tables() throws {
let dbQueue = try makeDatabaseQueue()
var migrator = DatabaseMigrator()
migrator.registerMigration("v1") { db in
try db.create(table: "player") { t in
t.autoIncrementedPrimaryKey("id")
}
}
try migrator.migrate(dbQueue)

try dbQueue.read { db in
let stream = TestStream()
try db.dumpSchema(to: stream)
print(stream.output)
XCTAssertEqual(stream.output, """
sqlite_master
CREATE TABLE "player" ("id" INTEGER PRIMARY KEY AUTOINCREMENT);
""")
}
}

// MARK: - Database content dump

func test_dumpContent() throws {
Expand Down

0 comments on commit 1464940

Please sign in to comment.