groue / GRDB.swift

A toolkit for SQLite databases, with a focus on application development
MIT License
6.88k stars 708 forks source link

Encoding Arrays into JSON Column #429

Closed wildthink closed 6 years ago

wildthink commented 6 years ago

The documentation indicates/implies that a JSON compatible structure (e.g. Dictionary or Array) will automatically encoded/decoded into a JSON Column

What did you do?

I tried adding a var tags: [String] property to the Player struct definition in the DemoiOSApp. I also added a tags column to the db create and also to the CodingKeys of Player.

What did you expect to happen?

I expected the app to function and to be able to see the tags in the sqlite database.

What happened instead?

I observed a fatal error at

   func unkeyedContainer() throws -> UnkeyedDecodingContainer {
        fatalError("unkeyed decoding from database row is not supported")
    }

Environment

Standard GRDB v3.3.1 as configured in the Example application. Xcode Version 10.0 (10A255) Swift 4.3 macOS 10.14

Demo Project

I modified the DemoiSOapp

groue commented 6 years ago

@wildthink it definitively should work. Thanks for reporting this issue!

groue commented 6 years ago

@wildthink, I did exactly what you said:

I tried adding a var tags: [String] property to the Player struct definition in the DemoiOSApp. I also added a tags column to the db create and also to the CodingKeys of Player.

And GRDB would happily insert and update JSON arrays. The demo app just works.

So I can't really reproduce your issue.

groue commented 6 years ago

Please give more details about your issue, so that it can be addressed.

wildthink commented 6 years ago

Is there a special sqlite data type required? I tried .text as well.

diff --git a/DemoApps/GRDBDemoiOS/GRDBDemoiOS/AppDatabase.swift b/DemoApps/GRDBDemoiOS/GRDBDemoiOS/AppDatabase.swift
index dd39c279..8f9aa08d 100644
--- a/DemoApps/GRDBDemoiOS/GRDBDemoiOS/AppDatabase.swift
+++ b/DemoApps/GRDBDemoiOS/GRDBDemoiOS/AppDatabase.swift
@@ -37,6 +37,7 @@ struct AppDatabase {
                 t.column("name", .text).notNull().collate(.localizedCaseInsensitiveCompare)

                 t.column("score", .integer).notNull()
+                t.column("tags")
             }
         }

diff --git a/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Player.swift b/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Player.swift
index 06838bb0..7283fc42 100644
--- a/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Player.swift
+++ b/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Player.swift
@@ -4,6 +4,14 @@ struct Player {
     var id: Int64? // Auto-incremented database ids are Int64
     var name: String
     var score: Int
+    var tags: [String] = []
+
+    init (id: Int64?, name: String, score: Int, tags: [String] = ["a", "b"]) {
+        self.id = id
+        self.name = name
+        self.score = score
+        self.tags = tags
+    }
 }

 // MARK: - Persistence
@@ -15,7 +23,7 @@ extension Player: Codable, FetchableRecord, MutablePersistableRecord {
     // See https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types
     // for more information about CodingKeys.
     private enum CodingKeys: String, CodingKey, ColumnExpression {
-        case id, name, score
+        case id, name, score, tags
     }
groue commented 6 years ago

@wildthink. Please provide the stack trace of the crash.

wildthink commented 6 years ago

Thread 1 Queue : com.apple.main-thread (serial)

0 0x000000010c0b8ab0 in _swift_runtime_on_report ()

1 0x000000010c10a1eb in _swift_stdlib_reportFatalErrorInFile ()

2 0x000000010be10fb1 in closure #1 in closure #1 in closure #1 in assertionFailure(:_:file:line:flags:) ()

3 0x000000010c0987d2 in partial apply for closure #1 in closure #1 in closure #1 in assertionFailure(:_:file:line:flags:) ()

4 0x000000010c09aa89 in closure #1 in closure #1 in closure #1 in assertionFailure(:_:file:line:flags:)partial apply ()

5 0x000000010be1070a in specialized StaticString.withUTF8Buffer(_:) ()

6 0x000000010c0986e3 in partial apply for closure #1 in assertionFailure(:_:file:line:flags:) ()

7 0x000000010be1070a in specialized StaticString.withUTF8Buffer(_:) ()

8 0x000000010bfdc1b8 in specialized assertionFailure(:_:file:line:flags:) ()

9 0x000000010be0ff99 in assertionFailure(:_:file:line:flags:) ()

10 0x000000010aad9723 in RowDecoder.unkeyedContainer() at /Users/jason/Contracts/wildthink/frameworks/GRDB.swift/GRDB/Record/FetchableRecord+Decodable.swift:28

11 0x000000010aadf16d in protocol witness for Decoder.unkeyedContainer() in conformance RowDecoder ()

12 0x000000010aadf120 in protocol witness for Decoder.unkeyedContainer() in conformance RowDecoder ()

13 0x000000010be353b9 in Array.init(from:) ()

14 0x000000010be35566 in protocol witness for Decodable.init(from:) in conformance [A] ()

15 0x000000010c07db27 in dispatch thunk of Decodable.init(from:) ()

16 0x000000010aadce54 in RowDecoder.KeyedContainer.decode(_:fromRow:codingPath:) [inlined] at /Users/jason/Contracts/wildthink/frameworks/GRDB.swift/GRDB/Record/FetchableRecord+Decodable.swift:202

17 0x000000010aadce3d in RowDecoder.KeyedContainer.decode(_:forKey:) at /Users/jason/Contracts/wildthink/frameworks/GRDB.swift/GRDB/Record/FetchableRecord+Decodable.swift:170

18 0x000000010aadef7d in protocol witness for KeyedDecodingContainerProtocol.decode(_:forKey:) in conformance RowDecoder.KeyedContainer ()

19 0x000000010aadeb98 in protocol witness for KeyedDecodingContainerProtocol.decode(_:forKey:) in conformance RowDecoder.KeyedContainer ()

20 0x000000010be2e009 in KeyedDecodingContainerBox.decode(:forKey:) ()

21 0x000000010a6f3e20 in specialized KeyedDecodingContainer.decode(_:forKey:) [inlined] ()

22 0x000000010a6f3dba in specialized Player.init(from:) ()

23 0x000000010a6f2a15 in Player.init(from:) [inlined] ()

24 0x000000010a6f2a10 in protocol witness for Decodable.init(from:) in conformance Player ()

25 0x000000010c07db27 in dispatch thunk of Decodable.init(from:) ()

26 0x000000010aad9551 in FetchableRecord<>.init(row:) at /Users/jason/Contracts/wildthink/frameworks/GRDB.swift/GRDB/Record/FetchableRecord+Decodable.swift:11

27 0x000000010a6f2aec in protocol witness for FetchableRecord.init(row:) in conformance Player ()

28 0x000000010ab694c0 in Item.record.getter at /Users/jason/Contracts/wildthink/frameworks/GRDB.swift/GRDB/Record/FetchedRecordsController.swift:1021

29 0x000000010ab6fa67 in FetchedRecordsController.record(at:) at /Users/jason/Contracts/wildthink/frameworks/GRDB.swift/GRDB/Record/FetchedRecordsController.swift:861

30 0x000000010a6f72d1 in PlayersViewController.configure(_:at:) at /Users/jason/Contracts/wildthink/frameworks/GRDB.swift/DemoApps/GRDBDemoiOS/GRDBDemoiOS/PlayersViewController.swift:145

31 0x000000010a6f77a8 in PlayersViewController.tableView(_:cellForRowAt:) [inlined] at /Users/jason/Contracts/wildthink/frameworks/GRDB.swift/DemoApps/GRDBDemoiOS/GRDBDemoiOS/PlayersViewController.swift:132

32 0x000000010a6f778f in @objc PlayersViewController.tableView(_:cellForRowAt:) at /Users/jason/Contracts/wildthink/frameworks/GRDB.swift/DemoApps/GRDBDemoiOS/:130

33 0x000000010f81f4ee in -[UITableView _createPreparedCellForGlobalRow:withIndexPath:willDisplay:] ()

34 0x000000010f81fa7b in -[UITableView _createPreparedCellForGlobalRow:willDisplay:] ()

35 0x000000010f7e6b62 in -[UITableView _updateVisibleCellsNow:isRecursive:] ()

36 0x000000010f80781a in -[UITableView layoutSubviews] ()

37 0x000000010f9c5015 in -[UIView(CALayerDelegate) layoutSublayersOfLayer:] ()

38 0x00000001112e9d3d in -[CALayer layoutSublayers] ()

39 0x00000001112eebf7 in CA::Layer::layout_if_needed(CA::Transaction*) ()

40 0x0000000111267aa6 in CA::Context::commit_transaction(CA::Transaction*) ()

41 0x000000011129ec2a in CA::Transaction::commit() ()

42 0x000000010f2c6d4c in __34-[UIApplication _firstCommitBlock]_block_invoke_2 ()

43 0x000000010c8bba3c in CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK ()

44 0x000000010c8bb1f0 in __CFRunLoopDoBlocks ()

45 0x000000010c8b5a64 in __CFRunLoopRun ()

46 0x000000010c8b5221 in CFRunLoopRunSpecific ()

47 0x00000001150001dd in GSEventRunModal ()

48 0x000000010f2ac115 in UIApplicationMain ()

groue commented 6 years ago

@wildthink I'm still unable to reproduce your crash. I cloned the GRDB repo in its latest version, opened the GRDB.xcworkspace, modified files in the GRDBDemo target according to your comment, run the app, which did not exhibit any problem.

There sure is a fatalError("unkeyed decoding from database row is not supported") line in GRDB. But it is not supposed to be reached under normal operations.

I'm not saying there is no issue. I'm just saying I'm unable to exhibit any. Would you please provide a reproducible way to exhibit your crash? Thanks.

groue commented 6 years ago

No news is good news: happy GRDB, @wildthink! Just reopen the issue if you need more support.

sameer4 commented 5 years ago

@groue can you provide a link for example of storing arrays?? didn't find any in Demo App

i have array [Int] in an object of Record to store but in

override func encode(to container: inout PersistenceContainer) i'm getting error Cannot assign value of type '[Int]' to type 'DatabaseValueConvertible?'

XCode 10.2 Swift 5 GRDB.swift (3.7.0)

sameer4 commented 5 years ago
class TodoStates: Record  {

var id : String = ""
var title : String = ""
var color : String = ""
var access : Int = 0
var workareas : [Int] = []
var isclosestate : Int = 0

override class var databaseTableName: String {
    return "TodoStates"
}

enum CodingKeys: String, CodingKey, ColumnExpression {
    case id = "id"
    case title = "title"
    case color = "color"
    case access = "access"
    case workareas = "workareas"
    case isclosestate = "isclosestate"
}

override func encode(to container: inout PersistenceContainer) {
    container[CodingKeys.id] = id
    container[CodingKeys.title] = title
    container[CodingKeys.color] = color
    container[CodingKeys.access] = access
    container[CodingKeys.workareas] = workareas -> Error
    container[CodingKeys.isclosestate] = isclosestate
}

required init(json: JSON) {
    id = json["id"].stringValue
    title = json["title"].stringValue
    color = json["color"].stringValue
    access = json["access"].intValue
    workareas = json["workareas"].arrayValue.map { $0.intValue}
    isclosestate = json["isclosestate"].intValue

    super.init()
}

required init(row: Row) {
    id = row[CodingKeys.id]
    title = row[CodingKeys.title]
    color = row[CodingKeys.color]
    access = row[CodingKeys.access]
    workareas = row[CodingKeys.workareas]  -> Error
    isclosestate = row[CodingKeys.isclosestate]

    super.init(row: row)
}
}
groue commented 5 years ago

Hello @sameer4,

Automatic JSON encoding of arrays and other complex types are reserved to Codable records (search this term in the main README).

When you perform manual encoding (with encode(to:), as in your sample code, you have to perform your own JSON encoding into String or Data. See the documentation of the standard JSONEncoder for further information.

sameer4 commented 5 years ago

if i remove encode(to:), app crashes with

Thread 2: Fatal error: AppIcons: invalid empty persistence container

groue commented 5 years ago

If you remove encode(to:), your record type inherits the encode(to:) method from its Record superclass, which does fill anything in the container. Consequence: GRDB does not know which value should be stored in database columns, and it rightfully complains.

I understand that this is not easy to guess.

Here is my advice: if you use Codable Records, don't subclass the Record class. It just doesn't provide any useful service any more, and, on the contrary, creates the inheritance problems you are experiencing.

Don't rush, and have another read of the documentation of record types if necessary.