Skip to content

Commit

Permalink
Merge branch 'development'
Browse files Browse the repository at this point in the history
  • Loading branch information
groue committed Oct 29, 2023
2 parents 8fa4342 + 6bd1e6b commit f1f8e84
Show file tree
Hide file tree
Showing 19 changed files with 659 additions and 21 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Expand Up @@ -7,6 +7,7 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:

#### 6.x Releases

- `6.21.x` Releases - [6.21.0](#6210)
- `6.20.x` Releases - [6.20.0](#6200) - [6.20.1](#6201) - [6.20.2](#6202)
- `6.19.x` Releases - [6.19.0](#6190)
- `6.18.x` Releases - [6.18.0](#6180)
Expand Down Expand Up @@ -118,6 +119,13 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:

---

## 6.21.0

Released October 29, 2023

- **New**: [#1448](https://github.com/groue/GRDB.swift/pull/1448) by [@groue](https://github.com/groue): Add support for stable ordering and dump of views
- **New**: [#1449](https://github.com/groue/GRDB.swift/pull/1449) by [@groue](https://github.com/groue): Backport temporary copies from GRDBSnapshotTesting

## 6.20.2

Released October 15, 2023 • [diff](https://github.com/groue/GRDB.swift/compare/v6.20.1...v6.20.2)
Expand Down
2 changes: 1 addition & 1 deletion GRDB.swift.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'GRDB.swift'
s.version = '6.20.2'
s.version = '6.21.0'

s.license = { :type => 'MIT', :file => 'LICENSE' }
s.summary = 'A toolkit for SQLite databases, with a focus on application development.'
Expand Down
8 changes: 8 additions & 0 deletions GRDB.xcodeproj/project.pbxproj
Expand Up @@ -75,6 +75,8 @@
562393601DEE06D300A6B01F /* CursorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5623935F1DEE06D300A6B01F /* CursorTests.swift */; };
562393691DEE0CD200A6B01F /* FlattenCursorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 562393681DEE0CD200A6B01F /* FlattenCursorTests.swift */; };
562393721DEE104400A6B01F /* MapCursorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 562393711DEE104400A6B01F /* MapCursorTests.swift */; };
5623B6142AED39A600436239 /* DatabaseQueueInMemoryCopyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5623B6122AED39A600436239 /* DatabaseQueueInMemoryCopyTests.swift */; };
5623B6152AED39A600436239 /* DatabaseQueueTemporaryCopyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5623B6132AED39A600436239 /* DatabaseQueueTemporaryCopyTests.swift */; };
56256ED025D1ACD0008C2BDD /* Table.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56256ECF25D1ACD0008C2BDD /* Table.swift */; };
56256ED925D1B316008C2BDD /* ForeignKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56256ED825D1B316008C2BDD /* ForeignKey.swift */; };
562756431E963AAC0035B653 /* DatabaseWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 562756421E963AAC0035B653 /* DatabaseWriterTests.swift */; };
Expand Down Expand Up @@ -489,6 +491,8 @@
5623935F1DEE06D300A6B01F /* CursorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CursorTests.swift; sourceTree = "<group>"; };
562393681DEE0CD200A6B01F /* FlattenCursorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlattenCursorTests.swift; sourceTree = "<group>"; };
562393711DEE104400A6B01F /* MapCursorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapCursorTests.swift; sourceTree = "<group>"; };
5623B6122AED39A600436239 /* DatabaseQueueInMemoryCopyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueInMemoryCopyTests.swift; sourceTree = "<group>"; };
5623B6132AED39A600436239 /* DatabaseQueueTemporaryCopyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueTemporaryCopyTests.swift; sourceTree = "<group>"; };
5623E0901B4AFACC00B20B7F /* GRDBTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GRDBTestCase.swift; sourceTree = "<group>"; };
56256ECF25D1ACD0008C2BDD /* Table.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Table.swift; sourceTree = "<group>"; };
56256ED825D1B316008C2BDD /* ForeignKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForeignKey.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1125,8 +1129,10 @@
5687359E1CEDE16C009B9116 /* Betty.jpeg */,
5672DE581CDB72520022BA81 /* DatabaseQueueBackupTests.swift */,
563363BC1C93FD5E000BE133 /* DatabaseQueueConcurrencyTests.swift */,
5623B6122AED39A600436239 /* DatabaseQueueInMemoryCopyTests.swift */,
56A238141B9C74A90082EB20 /* DatabaseQueueInMemoryTests.swift */,
567156151CB142AA007DC145 /* DatabaseQueueReadOnlyTests.swift */,
5623B6132AED39A600436239 /* DatabaseQueueTemporaryCopyTests.swift */,
569178451CED9B6000E179EA /* DatabaseQueueTests.swift */,
);
name = DatabaseQueue;
Expand Down Expand Up @@ -2006,6 +2012,7 @@
56419C6B24A519A2004967E1 /* Support.swift in Sources */,
56D496871D81316E008276D7 /* DatabaseTimestampTests.swift in Sources */,
5615B26A222AFE8F00061C1C /* AssociationHasOneThroughRowScopeTests.swift in Sources */,
5623B6152AED39A600436239 /* DatabaseQueueTemporaryCopyTests.swift in Sources */,
561CFA9C2376EC86000C8BAA /* AssociationHasManyOrderingTests.swift in Sources */,
56176C5A1EACCCC7000F3F2B /* FTS5PatternTests.swift in Sources */,
56D496581D81304E008276D7 /* FoundationDateTests.swift in Sources */,
Expand Down Expand Up @@ -2080,6 +2087,7 @@
56D496791D81309E008276D7 /* RecordWithColumnNameManglingTests.swift in Sources */,
56D4966C1D81309E008276D7 /* RecordMinimalPrimaryKeyRowIDTests.swift in Sources */,
564CE5BE21B8FFA300652B19 /* DatabaseRegionObservationTests.swift in Sources */,
5623B6142AED39A600436239 /* DatabaseQueueInMemoryCopyTests.swift in Sources */,
56AFEF372996B9DC00CA1E51 /* TransactionDateTests.swift in Sources */,
564F9C1E1F069B4E00877A00 /* DatabaseAggregateTests.swift in Sources */,
D263F40A26C613090038B07F /* DatabaseColumnEncodingStrategyTests.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion GRDB/Core/Database.swift
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
93 changes: 93 additions & 0 deletions GRDB/Core/DatabaseQueue.swift
Expand Up @@ -429,3 +429,96 @@ extension DatabaseQueue: DatabaseWriter {
writer.async(updates)
}
}

// MARK: - Temp Copy

extension DatabaseQueue {
/// Returns a connection to an in-memory copy of the database at `path`.
///
/// Changes performed on the returned connection do not impact the
/// original database at `path`.
///
/// The database memory is released when the returned connection
/// is deallocated.
///
/// For example:
///
/// ```swift
/// let path = "/path/to/database.sqlite"
/// let dbQueue = try DatabaseQueue.inMemoryCopy(fromPath: path)
/// ```
public static func inMemoryCopy(
fromPath path: String,
configuration: Configuration = Configuration())
throws -> DatabaseQueue
{
var sourceConfig = configuration
sourceConfig.readonly = true
let source = try DatabaseQueue(path: path, configuration: sourceConfig)

var copyConfig = configuration
copyConfig.readonly = false
let result = try DatabaseQueue(configuration: copyConfig)

try source.backup(to: result)

if configuration.readonly {
// Result was not opened read-only so that we could perform the
// copy. And SQLITE_OPEN_READONLY has no effect on in-memory
// databases anyway.
//
// So let's simulate read-only with PRAGMA query_only.
try result.inDatabase { db in
try db.beginReadOnly()
}
}

return result
}

/// Returns a connection to a private, temporary, on-disk copy of the
/// database at `path`.
///
/// Changes performed on the returned connection do not impact the
/// original database at `path`.
///
/// The on-disk copy will be automatically deleted from disk as soon as
/// the returned connection is closed or deallocated.
///
/// For example:
///
/// ```swift
/// let path = "/path/to/database.sqlite"
/// let dbQueue = try DatabaseQueue.temporaryCopy(fromPath: path)
/// ```
public static func temporaryCopy(
fromPath path: String,
configuration: Configuration = Configuration())
throws -> DatabaseQueue
{
var sourceConfig = configuration
sourceConfig.readonly = true
let source = try DatabaseQueue(path: path, configuration: sourceConfig)

// <https://www.sqlite.org/c3ref/open.html>
// > If the filename is an empty string, then a private, temporary
// > on-disk database will be created. This private database will be
// > automatically deleted as soon as the database connection
// > is closed.
var copyConfig = configuration
copyConfig.readonly = false
let result = try DatabaseQueue(path: "", configuration: copyConfig)

try source.backup(to: result)

if configuration.readonly {
// Result was not opened read-only so that we could perform the
// copy. So let's simulate read-only with PRAGMA query_only.
try result.inDatabase { db in
try db.beginReadOnly()
}
}

return result
}
}
2 changes: 1 addition & 1 deletion GRDB/Core/DatabaseReader.swift
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
2 changes: 2 additions & 0 deletions GRDB/Documentation.docc/Extension/DatabaseQueue.md
Expand Up @@ -88,6 +88,8 @@ A `DatabaseQueue` needs your application to follow rules in order to deliver its

- ``init(named:configuration:)``
- ``init(path:configuration:)``
- ``inMemoryCopy(fromPath:configuration:)``
- ``temporaryCopy(fromPath:configuration:)``

### Accessing the Database

Expand Down
34 changes: 29 additions & 5 deletions GRDB/Dump/Database+Dump.swift
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
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
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

0 comments on commit f1f8e84

Please sign in to comment.