Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve foreign key support #133

Merged
merged 2 commits into from
May 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions Sources/SwiftKuery/ForeignKey.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
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, Buildable {
var keyColumns: [Column]
var refColumns: [Column]
var keyNames: [String]
var refNames: [String]

public init?(keys: [Column], refs: [Column],_ tableName: String, _ errorString: inout String) {
if !ForeignKey.validKey(keys, refs, tableName, &errorString) {
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],_ tableName: String, _ errorString: inout String) -> Bool {
if keys.count == 0 || refs.count == 0 || keys.count != refs.count {
errorString = "Invalid definition of foreign key. "
return false
}
else if !columnsBelongToSameTable(columns: keys, tableName: tableName) {
errorString = "Foreign key contains columns from another table. "
return false
}
else if !columnsBelongToSameTable(columns: refs, tableName: refs[0]._table._name) {
errorString = "Foreign key references columns from more than one table. "
return false
}
return true
}

static func columnsBelongToSameTable(columns: [Column], tableName: String) -> Bool {
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 build(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
}
}
131 changes: 67 additions & 64 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.build(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,69 @@ 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. "
var errorString: String = ""
guard let newKey = ForeignKey(keys: columns, refs: references, self._name, &errorString) else {
syntaxError += errorString
return self
}
else if columns.count == 0 || references.count == 0 || columns.count != references.count {
syntaxError += "Invalid definition of foreign key. "
}
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