Skip to content

Commit

Permalink
Improve foreign key support
Browse files Browse the repository at this point in the history
  • Loading branch information
kilnerm committed May 17, 2018
1 parent d48a2ad commit 2b81aba
Show file tree
Hide file tree
Showing 3 changed files with 223 additions and 70 deletions.
90 changes: 90 additions & 0 deletions Sources/SwiftKuery/ForeignKey.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
Copyright IBM Corporation 2016, 2017, 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import Foundation

struct ForeignKey: Hashable {
var keyColumns: [Column]
var refColumns: [Column]
var keyNames: [String]
var refNames: [String]

public init?(keys: [Column], refs: [Column]){
if !ForeignKey.validKey(keys, refs) {
return nil
}
keyColumns = keys
refColumns = refs
keyNames = keyColumns.map { "\($0._table._name).\($0.name)"}
refNames = refColumns.map { "\($0._table._name).\($0.name)"}
}

static func validKey(_ keys: [Column], _ refs: [Column]) -> Bool {
if keys.count == 0 || refs.count == 0 || keys.count != refs.count {
return false
}
else if !columnsBelongToSameTable(columns: keys) {
return false
}
else if !columnsBelongToSameTable(columns: refs) {
return false
}
return true
}

static func columnsBelongToSameTable(columns: [Column]) -> Bool {
let tableName = columns[0]._table._name
for column in columns {
if column.table._name != tableName {
return false
}
}
return true
}

public var hashValue: Int {
var hashvalue = "foreignKey".hashValue
for key in keyNames {
hashvalue = hashvalue ^ key.hashValue
}
for ref in refNames {
hashvalue = hashvalue ^ ref.hashValue
}
return hashvalue
}

static public func == (lhs: ForeignKey, rhs: ForeignKey) -> Bool {
// Foreign keys cannot span databases and we do not currently support temporary tables therefore checking key name and ref name sets match is sufficient to establish equality
if !(Set(lhs.keyNames) == Set(rhs.keyNames)) {
return false
}
if !(Set(lhs.refNames) == Set(rhs.refNames)) {
return false
}
return true
}

func describe(queryBuilder: QueryBuilder) -> String {
var append = ", FOREIGN KEY ("
append += keyColumns.map { Utils.packName($0.name, queryBuilder: queryBuilder) }.joined(separator: ", ")
append += ") REFERENCES "
append += Utils.packName(refColumns[0].table._name, queryBuilder: queryBuilder)
append += "("
append += refColumns.map { Utils.packName($0.name, queryBuilder: queryBuilder) }.joined(separator: ", ")
append += ")"
return append
}
}
128 changes: 65 additions & 63 deletions Sources/SwiftKuery/Table.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,17 @@ open class Table: Buildable {
public private (set) var alias: String?

private var primaryKey: [Column]?
private var foreignKeyColumns: [Column]?
private var foreignKeyReferences: [Column]?
private var syntaxError = ""


// An array of foreign keys for the table
private var foreignKeys: [ForeignKey] = []

/// The name of the table to be used inside a query, i.e., either its alias (if exists)
/// or its name.
public var nameInQuery: String {
return alias ?? _name
}

// MARK: Initializer
/// Initialize an instance of Table.
public required init() {
Expand Down Expand Up @@ -138,42 +139,51 @@ open class Table: Buildable {
}
return result
}

/// Function that returns the SQL CREATE TABLE query as a String for this TABLE.
/// - Returns: A String representation of the table create statement.
/// - Throws: QueryError.syntaxError if statement build fails.

/**
Function that returns the SQL CREATE TABLE statement for this table in string fromat.
### Usage Example: ###
In this example we define a simple table named exampleTable and then print its description
```swift
class ExampleTable: Table {
let tableName = "ExampleTable"
let name = Column("name", String.self)
}
let examples = ExampleTable()
let description = try examples.description(connection: getConnection(from: postgresPool))
print(description)
// Prints CREATE TABLE ExampleTable (name text)
```
/// - Returns: A String representation of the table create statement.
/// - Throws: QueryError.syntaxError if statement build fails.
*/
public func description(connection: Connection) throws -> String {
if syntaxError != "" {
throw QueryError.syntaxError(syntaxError)
}

let queryBuilder = connection.queryBuilder

var query = "CREATE TABLE " + Utils.packName(_name, queryBuilder: queryBuilder)
query += " ("

query += try columns.map { try $0.create(queryBuilder: queryBuilder) }.joined(separator: ", ")

if let primaryKey = primaryKey {
query += ", PRIMARY KEY ("
query += primaryKey.map { Utils.packName($0.name, queryBuilder: queryBuilder) }.joined(separator: ", ")
query += ")"
}

if let foreignKeyColumns = foreignKeyColumns, let foreignKeyReferences = foreignKeyReferences {
query += ", FOREIGN KEY ("
query += foreignKeyColumns.map { Utils.packName($0.name, queryBuilder: queryBuilder) }.joined(separator: ", ")
query += ") REFERENCES "
let referencedTableName = foreignKeyReferences[0].table._name
query += Utils.packName(referencedTableName, queryBuilder: queryBuilder) + "("
query += foreignKeyReferences.map { Utils.packName($0.name, queryBuilder: queryBuilder) }.joined(separator: ", ")
query += ")"

if !foreignKeys.isEmpty {
query += foreignKeys.map { $0.describe(queryBuilder: queryBuilder) }.joined(separator: "")
}

query += ")"
return query
}

// MARK: Create Alias
/**
Function to return a copy of the current `Table` instance with the given name as its alias.
Expand Down Expand Up @@ -317,12 +327,12 @@ open class Table: Buildable {
public func primaryKey(_ columns: Column...) -> Self {
return primaryKey(columns)
}

/**
Function to set a multiple `Column` instance, as a composite foreign key, in the `Table` instance referencing multiple column in another Table.
The function also validates the columns to ensure they belong to the table and do not conflict with the definition of a foreign key.
Function to set a multiple `Column` instance, as a composite foreign key, in the `Table` instance referencing multiple columns in another Table.
The function also validates the columns to ensure they belong to the table and do not conflict with the definition of an existing foreign key.
### Usage Example: ###
In this example, `Table` instances called personTable and employeeTable are created. A "personTable" foreign key is then set to be a composite of firstColumn and lastColumn, which reference firstName and surname in employeeTable.
In this example, `Table` instances called personTable and employeeTable are created. A composite primary key is created on "employeeTable". A "personTable" foreign key is then set to be a composite of firstColumn and lastColumn, which reference firstName and surname in employeeTable.
```swift
public class EmployeeTable: Table {
let tableName = "employeeTable"
Expand All @@ -331,76 +341,68 @@ open class Table: Buildable {
let monthlyPay = Column("monthlyPay", Int32.self)
}
public class PersonTable: Table {
let tableName = "personTable"
let firstName = Column("firstName", String.self, notNull: true)
let lastName = Column("lastName", String.self, notNull: true)
let dateOfBirth = Column("toDo_completed", String.self)
let tableName = "personTable"
let firstName = Column("firstName", String.self, notNull: true)
let lastName = Column("lastName", String.self, notNull: true)
let dateOfBirth = Column("toDo_completed", String.self)
}
var personTable = PersonTable()
var employeeTable = EmployeeTable()
employeeTable = employeeTable.primaryKey([employeeTable.firstname, employeeTable.surname])
personTable = personTable.foreignKey([personTable.firstName, personTable.lastName], references: [employeeTable.firstName, employeeTable.surname])
```
- Parameter columns: An Array of columns that constitute the foreign key.
- Parameter references: An Array of columns from the foreign table that are referenced by the foreign key.
- Returns: A new instance of `Table`.
*/
*/
public func foreignKey(_ columns: [Column], references: [Column]) -> Self {
if foreignKeyColumns != nil || foreignKeyReferences != nil {
syntaxError += "Conflicting definitions of foreign key. "
}
else if columns.count == 0 || references.count == 0 || columns.count != references.count {
guard let newKey = ForeignKey(keys: columns, refs: references) else {
syntaxError += "Invalid definition of foreign key. "
return self
}
else if !Table.columnsBelongTo(table: self, columns: columns) {
syntaxError += "Foreign key contains columns from another table. "
}
else if !Table.columnsBelongTo(table: references[0].table, columns: references) {
syntaxError += "Foreign key references columns from more than one table. "
}
else {
foreignKeyColumns = columns
foreignKeyReferences = references
if !foreignKeys.contains(newKey) {
foreignKeys.append(newKey)
}
return self
}

private static func columnsBelongTo(table: Table, columns: [Column]) -> Bool {
for column in columns {
if column.table._name != table._name {
return false
}
}
return true
}


/**
Function to set a single `Column` instance, as a foreign key, in the `Table` instance.
The function also validates the column to ensure it belongs to the table and does not conflict with the definition of a foreign key.
The function also validates the column to ensure it belongs to the table and does not conflict with the definition of an existing foreign key.
### Usage Example: ###
In this example, `Table` instances called personTable and employeeTable are created. A "personTable" foreign key is then set to be id, which reference identifier in employeeTable.
In this example, `Table` instances called personTable and employeeTable are created. A "personTable" foreign key is then set to be id, which references identifier in employeeTable.
```swift
public class EmployeeTable: Table {
let identifier = Column("identifier", Int32.self, notNull: true)
let monthlyPay = Column("monthlyPay", Int32.self)
let employeeBand = Column("employeeBand", String.self)
}
public class PersonTable: Table {
let tableName = "personTable"
let id = Column("id", Int32.self, notNull: true)
let firstName = Column("firstName", String.self, notNull: true)
let lastName = Column("lastName", String.self, notNull: true)
let tableName = "personTable"
let id = Column("id", Int32.self, notNull: true)
let firstName = Column("firstName", String.self, notNull: true)
let lastName = Column("lastName", String.self, notNull: true)
}
var personTable = PersonTable()
var employeeTable = EmployeeTable()
personTable = personTable.foreignKey(personTable.id, references: employeeTable.identifier)
```
- Parameter columns: A column that is the foreign key.
- Parameter references: A column in the foreign table the foreign key references.
- Returns: A new instance of `Table`.
*/
public func foreignKey(_ column: Column, references: Column) -> Self {
return foreignKey([column], references: [references])
}

private static func columnsBelongTo(table: Table, columns: [Column]) -> Bool {
for column in columns {
if column.table._name != table._name {
return false
}
}
return true
}
}
Loading

0 comments on commit 2b81aba

Please sign in to comment.