Skip to content

Commit

Permalink
Namespace Row column names
Browse files Browse the repository at this point in the history
A developer asks a SQLite statement, "What is the column name at this
index?"

It responds: "id."

"But SQLite," she asks, "I'm joining another table which also has a
column name 'id.' What is the table name at this index?"

It responds: "Undefined symbols for architecture."

The world crumbles under the developer's feet.

--

SQLite doesn't preserve column metadata unless compiled with
SQLITE_ENABLE_COLUMN_METADATA, and it isn't, by default, so we can't
rely on it. So for every column of a result set, we only have the column
name to work with.

We can't trick SQLite by being explicit: if we namespace a column
("SELECT table.column") doesn't preserve the namespace in the result
set, and neither does aliasing ("table.column AS unambiguous_column").

I guess we'll need to manage the logic ourselves:

 1. If a Query calls select with specific column names, honor the
    namespacing or lack thereof explicitly.

 2. If a Query calls select with a namespaced star, honor the
    namespacing while expanding the columns.

 3. If a Query joins another table and the select is the default, *,
    namespace all expanded columns.

 4. If a Query does not join another table, do not namespace expanded
    columns.

I think the third point may be problematic without proper error
handling, but otherwise think the interface is intuitive enough.

This commit removes the Statement.values dictionary, as it cannot return
all data in a result set with ambiguous column names.

Signed-off-by: Stephen Celis <stephen@stephencelis.com>
  • Loading branch information
stephencelis committed Nov 17, 2014
1 parent b1f7105 commit 7fa9a57
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 27 deletions.
14 changes: 14 additions & 0 deletions SQLite Common Tests/QueryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,20 @@ class QueryTests: XCTestCase {
ExpectExecutions(db, [SQL: 1]) { _ in for _ in middleManagers {} }
}

func test_namespacedColumnRowValueAccess() {
let aliceID = users.insert(email <- "alice@example.com")!
let bettyID = users.insert(email <- "betty@example.com", manager_id <- aliceID)!

let alice = users.first!
XCTAssertEqual(aliceID, alice[id])

let managers = db["users"].alias("managers")
let query = users.join(managers, on: managers[id] == users[manager_id])

let betty = query.first!
XCTAssertEqual(alice[email], betty[managers[email]])
}

func test_filter_compilesWhereClause() {
let query = users.filter(admin == true)

Expand Down
11 changes: 0 additions & 11 deletions SQLite Common Tests/StatementTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,17 +187,6 @@ class StatementTests: XCTestCase {
XCTAssertEqual("alice@example.com", row[1] as String)
}

func test_values_returnsDictionaryOfExpressionsToValues() {
InsertUser(db, "alice")
let stmt = db.prepare("SELECT id, \"email\" FROM users")
stmt.next()

let values = stmt.values!

XCTAssertEqual(1, values["id"] as Int)
XCTAssertEqual("alice@example.com", values["email"] as String)
}

}

func withBlob(block: Blob -> ()) {
Expand Down
47 changes: 42 additions & 5 deletions SQLite Common/Query.swift
Original file line number Diff line number Diff line change
Expand Up @@ -673,20 +673,57 @@ extension Query: SequenceType {

public typealias Generator = QueryGenerator

public func generate() -> Generator { return Generator(selectStatement) }
public func generate() -> Generator { return Generator(self) }

public var columnNames: [String] {
var columnNames = [String]()
for each in columns {
let pair = split(each.expression.SQL) { $0 == "." }
let (tableName, column) = (pair.count > 1 ? pair.first : nil, pair.last!)

func expandGlob(namespace: Bool) -> Query -> () {
return { table in
var names = self.database[table.tableName].selectStatement.columnNames
if namespace { names = names.map { "\(table.alias ?? table.tableName).\($0)" } }
columnNames.extend(names)
}
}

if column == "*" {
if let tableName = tableName {
expandGlob(true)(database[tableName])
continue
}
let tables = [self] + joins.map { $0.table }
tables.map(expandGlob(joins.count > 0))
continue
}

columnNames.append(each.expression.SQL)
}
return columnNames
}

}

// MARK: - GeneratorType
public struct QueryGenerator: GeneratorType {

private var statement: Statement
private let query: Query
private let statement: Statement

private init(_ statement: Statement) { self.statement = statement }
private init(_ query: Query) {
(self.query, self.statement) = (query, query.selectStatement)
}

public func next() -> Row? {
statement.next()
return statement.values.map { Row($0) }
if let row = statement.next() {
var values = [String: Binding?]()
let columnNames = query.columnNames
for idx in 0..<row.count { values[columnNames[idx]] = row[idx] }
return Row(values)
}
return nil
}

}
Expand Down
12 changes: 1 addition & 11 deletions SQLite Common/Statement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public final class Statement {

deinit { sqlite3_finalize(handle) }

private lazy var columnNames: [String] = { [unowned self] in
internal lazy var columnNames: [String] = { [unowned self] in
let count = sqlite3_column_count(self.handle)
return (0..<count).map { String.fromCString(sqlite3_column_name(self.handle, $0))! }
}()
Expand Down Expand Up @@ -239,16 +239,6 @@ extension Statement: GeneratorType {
return row
}

/// :returns: A dictionary of column name to row value.
public var values: [String: Binding?]? {
if let row = row {
var values = [String: Binding?]()
for idx in 0..<row.count { values[columnNames[idx]] = row[idx] }
return values
}
return nil
}

}

// MARK: - BooleanType
Expand Down

0 comments on commit 7fa9a57

Please sign in to comment.