stephencelis / SQLite.swift

A type-safe, Swift-language layer over SQLite3.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Fatal error: 'try!' expression unexpectedly raised an error: Unexpected null value for column `"to"`

stefansaasen opened this issue · comments

The following error occurs using the schema reader (specifically columnDefinitions(table:)) for a particular table (see below for the tables/foreign key definition that triggers this):

SQLite/Query.swift:1216: Fatal error: 'try!' expression unexpectedly raised an error: Unexpected null value for column `"to"`

This is due to the fact that the foreignKeys function in the SchemaReader assumes that the to colum returned by running PRAGMA foreign_key_list("table name") contains a valid primary key column and is not null:

func foreignKeys(table: String) throws -> [ColumnDefinition.ForeignKey] {
try connection.prepareRowIterator("PRAGMA foreign_key_list(\(table.quote()))")
.map { row in
ColumnDefinition.ForeignKey(
table: row[ForeignKeyListTable.tableColumn],
column: row[ForeignKeyListTable.fromColumn],
primaryKey: row[ForeignKeyListTable.toColumn],
onUpdate: row[ForeignKeyListTable.onUpdateColumn] == TableBuilder.Dependency.noAction.rawValue
? nil : row[ForeignKeyListTable.onUpdateColumn],
onDelete: row[ForeignKeyListTable.onDeleteColumn] == TableBuilder.Dependency.noAction.rawValue
? nil : row[ForeignKeyListTable.onDeleteColumn]
)
}
}

The error occurs in line 100: row[ForeignKeyListTable.toColumn].

That is not necessarily true though.

How to reproduce

Minimal example (see below for the necessary Package.swift definition):

import SQLite

do {
    let db = try Connection()

    let sql = """
CREATE TABLE artist(
  artistid    INTEGER PRIMARY KEY,
  artistname  TEXT
);
CREATE TABLE track(
  trackid     INTEGER,
  trackname   TEXT,
  trackartist INTEGER REFERENCES artist
);
CREATE INDEX trackindex ON track(trackartist);
"""

    try db.execute(sql)

    let schemaInfoArtist = try db.schema.columnDefinitions(table: "track")
    print("schemaInfoArtist: \(schemaInfoArtist)")

} catch {
    print("error: \(error)")
}

The table definition is taken from the last example on https://www.sqlite.org/foreignkeys.html#fk_indexes (Section 3 "Required and Suggested Database Indexes").

The result of the PRAGMA query is the following table:

sqlite> PRAGMA foreign_key_list('track');
id|seq|table|from|to|on_update|on_delete|match
0|0|artist|trackartist||NO ACTION|NO ACTION|NONE

Here the to column is empty.

The SQLite source for the foreign key contains the following bits (see https://github.com/sqlite/sqlite/blob/master/src/sqliteInt.h#L2500):

struct FKey {
  ...
  struct sColMap {      /* Mapping of columns in pFrom to columns in zTo */
   ....
    char *zCol;           /* Name of column in zTo.  If NULL use PRIMARY KEY */
  } aCol[1];           
};

So it seems valid to omit the primary key column and SQLite will automatically use the primary key column of the referenced table. On the flip side that means, to can in fact be null. In this example, the referenced column would be artist. artistid (the primary key of the artist table).

Build Information

SQLite.swift version: 0.14.1 (also tested on master at 1b1eba0)
Xcode version: 14.3
macOS: 13.3.1

SQLite.swift is integrated via the SPM:

import PackageDescription

let package = Package(
    name: "to-column-null",
    dependencies: [
        .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.14.1")
    ],
    targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .executableTarget(
            name: "to-column-null",
            dependencies: [
                .product(name: "SQLite", package: "SQLite.swift")
            ],
            path: "Sources"),
    ]
)

Thanks for reporting this. Can you take a look at #1210 to see if this fixes your problem?

Thanks for looking into this! That does in fact solve the problem and matches the information returned by e.g. PRAGMA foreign_key_list('track');.

Should this be documented somehow? E.g. as the caller I need to work out what the primary key of the referenced table is (depending on the use case).

There's a code comment // when null, use primary key, maybe surface this?

Yes, good point, I'll add some documentation to the ForeignKey struct.