JSONB supported as of GRDB 7 #1656
Replies: 7 comments
-
Thank you @Jason-Abbott for raising the topic of JSONB. I'll probably ask a few questions in the next few days :-) |
Beta Was this translation helpful? Give feedback.
-
As an oldie who has been writing SQL for three decades 😄, let me plug SQLiteFlow (no affiliation) as a fast and focused tool with JSONB support making it easy to still see the content of these BLOBs. In fact, this is a nicer view of the JSON than before. Here I updated the |
Beta Was this translation helpful? Give feedback.
-
I have worked a bit today on streamlining the JSONB encoding to avoid the need for string conversions and custom One hang-up happens here in I did have some private methods that were glossing over these gaps. I think GRDB will need some signal to determine the Knowing the target column type would be a “magical” signal but may not always be feasible or performant. I’ll play around with some options here ... func encode<T: Encodable>(_ value: T) throws {
do {
try value.encode(to: self)
if requiresJSON {
// Here we handle empty arrays and dictionaries.
throw JSONRequiredError()
}
} catch is JSONRequiredError {
let jsonData = try jsonEncoder.encode(value)
// Store JSON String in the database for easier debugging and
// database inspection. Thanks to SQLite weak typing, we won’t
// have any trouble decoding this string into data when we
// eventually perform JSON decoding.
// TODO: possible optimization: avoid this conversion to string,
// and store raw data bytes as an SQLite string
let jsonString = String(decoding: jsonData, as: UTF8.self)
try jsonString.encode(to: self)
}
} |
Beta Was this translation helpful? Give feedback.
-
Edit: This isn’t actually valid. See the following comment. I made a quick solution that I’ll probably run with for a bit while I focus on other things. I updated the public import GRDB
import OSLog
private let log = Logger.current
public protocol JSONBConvertible: Codable, DatabaseValueConvertible, Sendable {}
public extension JSONBConvertible {
var databaseValue: DatabaseValue {
let encoder = DatabaseValue.databaseJSONEncoder()
do {
let data = try encoder.encode(self)
return data.databaseValue
} catch {
log.error("Failed to JSONEncode \(Self.Type.self)")
return DatabaseValue.null
}
}
static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? {
if case let .blob(data) = dbValue.storage {
let decoder = DatabaseValue.databaseJSONDecoder()
do {
return try decoder.decode(self.self, from: data)
} catch {
log.error("Failed to JSONDecode \(Self.Type.self)")
return nil
}
} else {
return nil
}
}
} |
Beta Was this translation helpful? Give feedback.
-
More learnings. It turns out most of the above was working thanks to a bug in SQLite that allows most things JSONish to function as The test database conversion I did with The options to make that happen are (in a table where INSERT INTO jsonb_test (value) VALUES (jsonb('{"key1": 1, "key2": "two"}'))
INSERT INTO jsonb_test (value) VALUES (jsonb(x'7b226b657931223a20312c20226b657932223a202274776f227d')) This is a little tricky to accomplish through GRDB helper functions since an attempt to insert the column value jsonb('{"key1": 1, "key2": "two"}') is emitted as (note extra quotes) 'jsonb(''{"key1": 1, "key2": “two"}'')' which isn’t valid. Obviously manual SQL will do the trick but I’ve been poking around for something more idiomatic. This isn’t my top concern so commentary on the topic is likely to remain sporadic 🙂 |
Beta Was this translation helpful? Give feedback.
-
Last update for the day: Rather than try to automate something on the binding end of things, it works fine if the statement takes the form INSERT INTO table (column) VALUES (jsonb(?)) But once made proper SELECT json(column) FROM table which converts SQLite proprietary At this point, the effort seems to exceed the benefit. In my own case, CloudKit is also hitting these tables so I would need to be careful to I am going to sleep on it but my inclination is to revert the changes I made, stick with I was enthusiastic for a little speed bump and size savings with |
Beta Was this translation helpful? Give feedback.
-
Last monologue update 🙂 The other way to make So I did that. Looking at an existing Rust version of the same, and a Swift binary encoder for a different format, gave me a good start. I am glad to share code if there’s interest (no signs of that yet 🙂) but since its currently tuned and tested for only my exact Having looked at the Swift source for JSONEncoder and Decoder as part of this project, I think a direct binary encoder like this offers a lot of simplification. |
Beta Was this translation helpful? Give feedback.
-
Important caveat: this requires the GRDB custom build process since it depends on SQLite 3.45.0 or higher.
The benefits of
BLOB
rather thanTEXT
-backed JSON, according to the SQLite documentation, are "that JSONB is both slightly smaller (by between 5% and 10% in most cases) and can be processed in less than half the number of CPU cycles.”I was already using a custom build for other reasons so when I saw @groue had updated SQLiteLib as part of GRDB 7, it was easy to start using JSONB.
First verifying with manual SQLite changes
I first vetted the change with manual updates to a test SQLite database. The steps I took were
<column>_new
variant of typeBLOB
corresponding to the old<column>
withTEXT
JSONUPDATE <table> SET <column>_new = jsonb(<column>)
From there, I confirmed the many views and triggers I’ve defined (some of which iterate through the JSON) work without alteration. Indeed, all the previous SQLite JSON functions are documented to work seamlessly whether the JSON is
BLOB
orTEXT
.The only
JSONB
-specific command I used was jsonb() for the conversion. I see no need for it in regular data access.Code changes
Those manual SQLite changes were only for testing. The live database is actually built by GRDB, of course. I mention them just to paint a fuller picture of the difference.
In code, all I needed to do was change column types from
.text
to.blob
. The types I persist as JSON already conform toDatabaseValueConvertible
so that was it!Minor notes
I had SQL check constraints on the JSON columns that I had to update from
json_valid(<column>) = 1
tojson_valid(<column>, 4) = 1
. The second parameter indicates JSONB.As I write this up, I’m a little worried I’ve overlooked some private extension in my code that made this easy. I don’t think so but if anything comes up, I’ll elaborate here!
Beta Was this translation helpful? Give feedback.
All reactions