Skip to content

Commit

Permalink
Merge pull request #355 from groue/GRDB3-AutomaticTableName
Browse files Browse the repository at this point in the history
Automatic table name generation
  • Loading branch information
groue authored May 23, 2018
2 parents 5de7a52 + 2e477fe commit 5ccf878
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 32 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ It comes with new features, but also a few breaking changes, and a set of update
- Upgrade custom SQLite builds to [v3.23.0](http://www.sqlite.org/changes.html) (thanks to [@swiftlyfalling](https://github.com/swiftlyfalling/SQLiteLib)).
- Improve Row descriptions ([#331](https://github.com/groue/GRDB.swift/pull/331)).
- Request derivation protocols ([#329](https://github.com/groue/GRDB.swift/pull/329)).
- Preliminary Linux support for the main framework ([#354](https://github.com/groue/GRDB.swift/pull/354)).
- Automatic table name generation ([#355](https://github.com/groue/GRDB.swift/pull/355)).


### Breaking Changes
Expand All @@ -45,6 +47,7 @@ It comes with new features, but also a few breaking changes, and a set of update
- [Database Pools](README.md#database-pools): focus on the `read` and `write` methods.
- [Transactions and Savepoints](README.md#transactions-and-savepoints): the chapter has been rewritten in order to introduce transactions as a power-user feature.
- [ScopeAdapter](README.md#scopeadapter): do you use row adapters? If so, have a look.
- [TableRecord Protocol](README.md#tablerecord-protocol): updated for the new automatic generation of database table name.
- [Examples of Record Definitions](README.md#examples-of-record-definitions): this new chapter provides a handy reference of the three main ways to define record types (Codable, plain struct, Record subclass).
- [SQL Operators](README.md#sql-operators): the chapter introduces the new `joined(operator:)` method that lets you join a chain of expressions with `AND` or `OR` without nesting: `[cond1, cond2, ...].joined(operator: .and)`.
- [Custom Requests](README.md#custom-requests): the old `Request` and `TypedRequest` protocols have been replaced with `FetchRequest`. If you want to know more about custom requests, check this chapter.
Expand Down
46 changes: 45 additions & 1 deletion Documentation/GRDB2MigrationGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ GRDB 3 comes with new features, but also a few breaking changes, and a set of up
- [If You Use Database Queues]
- [If You Use Database Pools]
- [If You Use Database Snapshots]
- [If You Use Record Types]
- [If You Use Custom Requests]
- [If You Use RxGRDB]
- [Notable Documentation Updates]
Expand Down Expand Up @@ -85,9 +86,11 @@ GRDB 3 still accepts any database, but brings two schema recommendations:
}
```

- :bulb: Database table names should be singular, and camel-cased. Make them look like Swift identifiers: `place`, `country`, `postalAddress`.
- :bulb: Database table names should be singular, and camel-cased. Make them look like Swift identifiers: `place`, `country`, `postalAddress`, 'httpRequest'.

This will help you using the new [Associations] feature when you need it. Database table names that follow another naming convention are totally OK, but you will need to perform extra configuration.

This convention is applied by the default implementation of the `TableRecord.databaseTableName`: see [If You Use Record Types] below.

Since you are reading this guide, your application has already defined its database schema. You can migrate it in order to apply the new recommendations, if needed. Below is a sample code that uses [DatabaseMigrator], the recommended tool for managing your database schema:

Expand Down Expand Up @@ -349,6 +352,46 @@ let snapshot: DatabaseSnapshot = try dbPool.writeWithoutTransaction { db in
```


## If You Use Record Types

Record types that adopt the former TableMapping protocol, renamed TableRecord, used to declare their table name:

```swift
// GRDB 2
struct Place: TableMapping {
static let databaseTableName = "place"
}
print(Place.databaseTableName) // print "place"
```

With GRDB 3, the `databaseTableName` property gets a default implementation:

```swift
// GRDB 3
struct Place: TableRecord { }
print(Place.databaseTableName) // print "place"
```

That default name follows the [Database Schema Recommendations]: it is singular, camel-cased, and looks like a Swift identifier:

- Place: `place`
- Country: `country`
- PostalAddress: `postalAddress`
- HTTPRequest: `httpRequest`
- TOEFL: `toefl`

When you subclass the Record class, the Swift compiler won't let you profit from this default name: you have to keep on providing an explicit table name:

```swift
// GRDB 2 and GRDB 3
class Place: Record {
override var databaseTableName: String {
return "place"
}
}
```


## If You Use Custom Requests

[Custom requests] let you escape the limitations of the [query interface], when it can not generate the requests you need.
Expand Down Expand Up @@ -469,6 +512,7 @@ If you have time, you may dig deeper in GRDB 3 with those updated documentation
[If You Use Database Queues]: #if-you-use-database-queues
[If You Use Database Pools]: #if-you-use-database-pools
[If You Use Database Snapshots]: #if-you-use-database-snapshots
[If You Use Record Types]: #if-you-use-record-types
[If You Use Custom Requests]: #if-you-use-custom-requests
[If You Use RxGRDB]: #if-you-use-rxgrdb
[Notable Documentation Updates]: #notable-documentation-updates
Expand Down
34 changes: 34 additions & 0 deletions GRDB/Record/TableRecord.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,40 @@ public protocol TableRecord {
}

extension TableRecord {

/// The default name of the database table used to build requests.
///
/// - Player -> "player"
/// - Place -> "place"
/// - PostalAddress -> "postalAddress"
/// - HTTPRequest -> "httpRequest"
/// - TOEFL -> "toefl"
internal static var defaultDatabaseTableName: String {
let typeName = "\(Self.self)".replacingOccurrences(of: "(.)\\b.*$", with: "$1", options: [.regularExpression])
let initial = typeName.replacingOccurrences(of: "^([A-Z]+).*$", with: "$1", options: [.regularExpression])
switch initial.count {
case typeName.count:
return initial.lowercased()
case 0:
return typeName
case 1:
return initial.lowercased() + typeName.dropFirst()
default:
return initial.dropLast().lowercased() + typeName.dropFirst(initial.count - 1)
}
}

/// The default name of the database table used to build requests.
///
/// - Player -> "player"
/// - Place -> "place"
/// - PostalAddress -> "postalAddress"
/// - HTTPRequest -> "httpRequest"
/// - TOEFL -> "toefl"
public static var databaseTableName: String {
return defaultDatabaseTableName
}

/// Default value: `[AllColumns()]`.
public static var databaseSelection: [SQLSelectable] {
return [AllColumns()]
Expand Down
73 changes: 42 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2285,7 +2285,7 @@ See [fetching methods](#fetching-methods) for information about the `fetchCursor

## TableRecord Protocol

**Adopt the TableRecord protocol** on top of [FetchableRecord], and you are granted with the full [query interface](#the-query-interface).
**The TableRecord protocol** generates SQL for you. To use TableRecord, subclass the [Record](#record-class) class, or adopt it explicitly:

```swift
protocol TableRecord {
Expand All @@ -2294,17 +2294,44 @@ protocol TableRecord {
}
```

The `databaseTableName` type property is the name of a database table. `databaseSelection` is optional, and documented in the [Columns Selected by a Request](#columns-selected-by-a-request) chapter.

**To use TableRecord**, subclass the [Record](#record-class) class, or adopt it explicitly. For example:

```swift
extension Place : TableRecord {
static let databaseTableName = "place"
}
```
- The `databaseTableName` type property is the name of a database table. By default, it is derived from the type name:

```swift
struct Place: TableRecord { }
print(Place.databaseTableName) // prints "place"
```

For example:

- Place: `place`
- Country: `country`
- PostalAddress: `postalAddress`
- HTTPRequest: `httpRequest`
- TOEFL: `toefl`

You can still provide a custom table name:

```swift
struct Place: TableRecord {
static let databaseTableName = "location"
}
print(Place.databaseTableName) // prints "location"
```

Subclasses of the [Record](#record-class) class must always override their superclass's `databaseTableName` property:

```swift
class Place: Record {
override class var databaseTableName: String {
return "place"
}
}
print(Place.databaseTableName) // prints "place"
```

- The `databaseSelection` type property is optional, and documented in the [Columns Selected by a Request](#columns-selected-by-a-request) chapter.

Adopting types can be fetched without SQL, using the [query interface](#the-query-interface):
When a type adopts both TableRecord and [FetchableRecord](#fetchablerecord-protocol), it can be fetched using the [query interface](#the-query-interface):

```swift
// SELECT * FROM place WHERE name = 'Paris'
Expand Down Expand Up @@ -2523,9 +2550,7 @@ struct Player: Codable {
}

// Adopt Record protocols...
extension Player: FetchableRecord, PersistableRecord {
static let databaseTableName = "player"
}
extension Player: FetchableRecord, PersistableRecord { }

// ...and you can save and fetch players:
try dbQueue.write { db in
Expand All @@ -2539,8 +2564,6 @@ GRDB support for Codable works well with "flat" records, whose stored properties
```swift
// Can't take profit from Codable code generation:
struct Place: FetchableRecord, PersistableRecord, Codable {
static let databaseTableName = "place"

var title: String
var coordinate: CLLocationCoordinate2D // <- Not a simple value!
}
Expand Down Expand Up @@ -2570,9 +2593,7 @@ struct Place: Codable {
}

// Free database support!
extension Place: FetchableRecord, PersistableRecord {
static let databaseTableName = "place"
}
extension Place: FetchableRecord, PersistableRecord { }
```

GRDB ships with support for nested codable records, but this is a more complex topic. See [Joined Queries Support](#joined-queries-support) for more information.
Expand All @@ -2589,8 +2610,6 @@ struct Place: Codable {
}

extension Place: FetchableRecord, MutablePersistableRecord {
static let databaseTableName = "place"

mutating func didInsert(with rowID: Int64, for column: String?) {
// Update id after insertion
id = rowID
Expand Down Expand Up @@ -2994,10 +3013,7 @@ struct Place: Codable {
}

// SQL generation
extension Place: TableRecord {
/// The table name
static let databaseTableName = "place"
}
extension Place: TableRecord { }

// Fetching methods
extension Place: FetchableRecord { }
Expand Down Expand Up @@ -3028,9 +3044,6 @@ struct Place {

// SQL generation
extension Place: TableRecord {
/// The table name
static let databaseTableName = "place"

/// The table columns
enum Columns: String, ColumnExpression {
case id, title, favorite, latitude, longitude
Expand Down Expand Up @@ -3323,7 +3336,7 @@ try db.create(table: "example") { t in ... }
try db.create(table: "example", temporary: true, ifNotExists: true) { t in
```

> :bulb: **Tip**: database table names should be singular, and camel-cased. Make them look like Swift identifiers: `place`, `country`, `postalAddress`.
> :bulb: **Tip**: database table names should be singular, and camel-cased. Make them look like Swift identifiers: `place`, `country`, `postalAddress`, 'httpRequest'.
>
> This will help you using the new [Associations](Documentation/AssociationsBasics.md) feature when you need it. Database table names that follow another naming convention are totally OK, but you will need to perform extra configuration.

Expand Down Expand Up @@ -5320,12 +5333,10 @@ You can consume complex joined queries with Codable records as well. As a demons

```swift
struct Player: Decodable, FetchableRecord, TableRecord {
static let databaseTableName = "player"
var id: Int64
var name: String
}
struct Team: Decodable, FetchableRecord, TableRecord {
static let databaseTableName = "team"
var id: Int64
var name: String
var color: Color
Expand Down
87 changes: 87 additions & 0 deletions Tests/GRDBTests/TableRecordTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,95 @@ import XCTest
import GRDB
#endif

private struct RecordStruct: TableRecord { }
private struct CustomizedRecordStruct: TableRecord { static let databaseTableName = "CustomizedRecordStruct" }
private class RecordClass: TableRecord { }
private class RecordSubClass: RecordClass { }
private class CustomizedRecordClass: TableRecord { class var databaseTableName: String { return "CustomizedRecordClass" } }
private class CustomizedRecordSubClass: CustomizedRecordClass { override class var databaseTableName: String { return "CustomizedRecordSubClass" } }
private class CustomizedPlainRecord: Record { override class var databaseTableName: String { return "CustomizedPlainRecord" } }
private enum Namespace {
struct RecordStruct: TableRecord { }
struct CustomizedRecordStruct: TableRecord { static let databaseTableName = "CustomizedRecordStruct" }
class RecordClass: TableRecord { }
class RecordSubClass: RecordClass { }
class CustomizedRecordClass: TableRecord { class var databaseTableName: String { return "CustomizedRecordClass" } }
class CustomizedRecordSubClass: CustomizedRecordClass { override class var databaseTableName: String { return "CustomizedRecordSubClass" } }
class CustomizedPlainRecord: Record { override class var databaseTableName: String { return "CustomizedPlainRecord" } }
}
struct HTTPRequest: TableRecord { }
struct TOEFL: TableRecord { }

class TableRecordTests: GRDBTestCase {

func testDefaultDatabaseTableName() {
struct InnerRecordStruct: TableRecord { }
struct InnerCustomizedRecordStruct: TableRecord { static let databaseTableName = "InnerCustomizedRecordStruct" }
class InnerRecordClass: TableRecord { }
class InnerRecordSubClass: InnerRecordClass { }
class InnerCustomizedRecordClass: TableRecord { class var databaseTableName: String { return "InnerCustomizedRecordClass" } }
class InnerCustomizedRecordSubClass: InnerCustomizedRecordClass { override class var databaseTableName: String { return "InnerCustomizedRecordSubClass" } }
class InnerCustomizedPlainRecord: Record { override class var databaseTableName: String { return "InnerCustomizedPlainRecord" } }

XCTAssertEqual(RecordStruct.databaseTableName, "recordStruct")
XCTAssertEqual(CustomizedRecordStruct.databaseTableName, "CustomizedRecordStruct")
XCTAssertEqual(RecordClass.databaseTableName, "recordClass")
XCTAssertEqual(RecordSubClass.databaseTableName, "recordSubClass")
XCTAssertEqual(CustomizedRecordClass.databaseTableName, "CustomizedRecordClass")
XCTAssertEqual(CustomizedRecordSubClass.databaseTableName, "CustomizedRecordSubClass")
XCTAssertEqual(CustomizedPlainRecord.databaseTableName, "CustomizedPlainRecord")

XCTAssertEqual(Namespace.RecordStruct.databaseTableName, "recordStruct")
XCTAssertEqual(Namespace.CustomizedRecordStruct.databaseTableName, "CustomizedRecordStruct")
XCTAssertEqual(Namespace.RecordClass.databaseTableName, "recordClass")
XCTAssertEqual(Namespace.RecordSubClass.databaseTableName, "recordSubClass")
XCTAssertEqual(Namespace.CustomizedRecordClass.databaseTableName, "CustomizedRecordClass")
XCTAssertEqual(Namespace.CustomizedRecordSubClass.databaseTableName, "CustomizedRecordSubClass")
XCTAssertEqual(Namespace.CustomizedPlainRecord.databaseTableName, "CustomizedPlainRecord")

XCTAssertEqual(InnerRecordStruct.databaseTableName, "innerRecordStruct")
XCTAssertEqual(InnerCustomizedRecordStruct.databaseTableName, "InnerCustomizedRecordStruct")
XCTAssertEqual(InnerRecordClass.databaseTableName, "innerRecordClass")
XCTAssertEqual(InnerRecordSubClass.databaseTableName, "innerRecordSubClass")
XCTAssertEqual(InnerCustomizedRecordClass.databaseTableName, "InnerCustomizedRecordClass")
XCTAssertEqual(InnerCustomizedRecordSubClass.databaseTableName, "InnerCustomizedRecordSubClass")
XCTAssertEqual(InnerCustomizedPlainRecord.databaseTableName, "InnerCustomizedPlainRecord")

XCTAssertEqual(HTTPRequest.databaseTableName, "httpRequest")
XCTAssertEqual(TOEFL.databaseTableName, "toefl")

func tableName<T: TableRecord>(_ type: T.Type) -> String {
return T.databaseTableName
}

XCTAssertEqual(tableName(RecordStruct.self), "recordStruct")
XCTAssertEqual(tableName(CustomizedRecordStruct.self), "CustomizedRecordStruct")
XCTAssertEqual(tableName(RecordClass.self), "recordClass")
XCTAssertEqual(tableName(RecordSubClass.self), "recordSubClass")
XCTAssertEqual(tableName(CustomizedRecordClass.self), "CustomizedRecordClass")
XCTAssertEqual(tableName(CustomizedRecordSubClass.self), "CustomizedRecordSubClass")
XCTAssertEqual(tableName(CustomizedPlainRecord.self), "CustomizedPlainRecord")

XCTAssertEqual(tableName(Namespace.RecordStruct.self), "recordStruct")
XCTAssertEqual(tableName(Namespace.CustomizedRecordStruct.self), "CustomizedRecordStruct")
XCTAssertEqual(tableName(Namespace.RecordClass.self), "recordClass")
XCTAssertEqual(tableName(Namespace.RecordSubClass.self), "recordSubClass")
XCTAssertEqual(tableName(Namespace.CustomizedRecordClass.self), "CustomizedRecordClass")
XCTAssertEqual(tableName(Namespace.CustomizedRecordSubClass.self), "CustomizedRecordSubClass")
XCTAssertEqual(tableName(Namespace.CustomizedPlainRecord.self), "CustomizedPlainRecord")

XCTAssertEqual(tableName(InnerRecordStruct.self), "innerRecordStruct")
XCTAssertEqual(tableName(InnerCustomizedRecordStruct.self), "InnerCustomizedRecordStruct")
XCTAssertEqual(tableName(InnerRecordClass.self), "innerRecordClass")
XCTAssertEqual(tableName(InnerRecordSubClass.self), "innerRecordSubClass")
XCTAssertEqual(tableName(InnerCustomizedRecordClass.self), "InnerCustomizedRecordClass")
XCTAssertEqual(tableName(InnerCustomizedRecordSubClass.self), "InnerCustomizedRecordSubClass")
XCTAssertEqual(tableName(InnerCustomizedPlainRecord.self), "InnerCustomizedPlainRecord")

XCTAssertEqual(tableName(HTTPRequest.self), "httpRequest")
XCTAssertEqual(tableName(TOEFL.self), "toefl")
}

func testDefaultDatabaseSelection() throws {
struct Record: TableRecord {
static let databaseTableName = "t1"
Expand Down

0 comments on commit 5ccf878

Please sign in to comment.