marcoarment / blackbird Goto Github PK
View Code? Open in Web Editor NEWLicense: MIT License
License: MIT License
I just create a new app and follow the tutorial in README,
After specifying a db path, the db was successfully created, but all the write(to: db) are not actually written to the db, but in memory. If the app is killed and restarted, no data will be left.
iOS 16.2, Simulator
iOS 16.3, 12 mini
This example does not work.
struct MyView: View {
// title will be of type: String?
@BlackbirdColumnObserver(\MyModel.$title, primaryKey: 123) var title
var body: some View {
Text(title ?? "Loading…")
}
}
The error I am getting is:
Value of optional type 'String?' must be unwrapped to a value of type 'String'
Weirdly this seems to work by using a !:
struct MyView: View {
// title will be of type: String?
@BlackbirdColumnObserver(\MyModel.$title, primaryKey: 123) var title
var body: some View {
Text((title ?? "Loading…")!)
}
}
I think this happens when the column is an optional already:
@BlackbirdColumn public var title: String?
This results in:
@BlackbirdColumnObserver(\MyModel.$title, primaryKey: 123) var title : String??
Related to: 2f15d7f
I attempted to create a custom column type (UUID) and implemented BlackbirdColumnWrappable
and BlackbirdStorableAsText
as follows:
extension UUID: BlackbirdColumnWrappable, BlackbirdStorableAsText {
public static func fromValue(_ value: Blackbird.Value) -> UUID? {
guard case let Blackbird.Value.text(stringValue) = value else {
return nil
}
return UUID(uuidString: stringValue)
}
public func unifiedRepresentation() -> String {
self.uuidString
}
public static func from(unifiedRepresentation: String) -> UUID {
return UUID(uuidString: unifiedRepresentation)!
}
}
I also added an init(from:)
decoder to my model as show in the comments of BlackbirdSchema.swift.
However, my model does not pass the schema validation because instead of using the BlackbirdDefaultsDecoder
, which supplies a valid default for UUID, it uses the BlackbirdSQLiteDecoder
which bypasses the if statement in my init(from:)
to test for the BlackbirdDefaultsDecoder
and provide a valid default.
Should the decoder at the following line be BlackbirdDefaultsDecoder
to ensure the testRow has the right default for the column?
https://github.com/marcoarment/Blackbird/blob/2f15d7fae9f3821b43b7a945af38273fcb955157/Sources/Blackbird/BlackbirdModel.swift#L826C1-L835C10
Or am I just on the wrong path and this is not a feature Blackbird is supporting?
TL;DR I had a hard time debugging a crash that was occurring while initializing the schema for my database. It looked like a crash related to a string enum not having an empty string case, but it was actually caused by having the same (unrelated) column listed in both indexes
and uniqueIndexes
for the affected model.
This is clearly a client mistake, however I think this could be validated in a way that makes it easier to spot, to prevent others from having to debug this on their own.
I had a model where I accidentally had indexes
and uniqueIndexes
repeat the same column, such as the example below:
struct Post: BlackbirdModel {
static var primaryKey: [BlackbirdColumnKeyPath] = [ \.$guid ]
static var indexes: [[BlackbirdColumnKeyPath]] = [
[ \.$guid ],
[ \.$title ],
]
static var uniqueIndexes: [[BlackbirdColumnKeyPath]] = [
[ \.$guid ],
]
enum Format: String, BlackbirdStringEnum {
case markdown
case html
}
@BlackbirdColumn var guid: String
@BlackbirdColumn var title: String
@BlackbirdColumn var format: Format
}
When running the app, as soon as the model was touched (causing Blackbird to initialize the schema) I'd get a force unwrap crash here:
In the example above, the crash would be caused by attempting to initialize a BlackbirdStringEnum
with an empty string as its rawValue
, but since there's no such case in that enum, this resulted in a crash.
I tracked it down to here:
Blackbird/Sources/Blackbird/BlackbirdSchema.swift
Lines 315 to 318 in 1f485e1
When a column is mistakenly repeated in indexes
and uniqueIndexes
, currentIndexes != targetIndexes
will always be true
, initiating the chain of events that lead to that crash.
I'm not familiar with the whole code base enough to send a PR right now, but wanted to document this here in case anyone else runs into this issue.
@marcoarment FYI, you don't have your username and/or email configured to match your github user when you contribute code to this repo so your contributions don't show up associated with your github user account.
See here: https://github.com/marcoarment/Blackbird/commits/main
In the above screenshot, the commit from Jan 17th is done through the GitHub interface and is correctly associated, while the commits on Jan 23rd are done locally and then pushed, but GitHub isn't associating them with @marcoarment user account.
This can be configured just for this repo locally on the mac you contribute / push to this repo from.
I think it is just: git config user.name "marcoarment"
(on the command line within the repo folder)
It might also require configuring the email address to match as well, but I am not certain.
The idea is something equivalent to CoreData's @FetchRequest.
As I understand it, the thing you need to do is use https://www.sqlite.org/c3ref/update_hook.html to implement https://developer.apple.com/documentation/swiftui/dynamicproperty
GRDBQuery does this, and supposedly is db-wrapper-agnostic, so maybe people can already use GRDBQuery with Blackbird...
The Readme has this code:
struct RootView: View {
// The database that all child views will automatically use
var database = try! Blackbird.Database.inMemoryDatabase()
...
But I think that has the potential issue of the database being recreated if/when RootView is recreated, which SwiftUI can do at whim.
I think the @StateObject decorator is supposed to be used:
struct RootView: View {
// The database that all child views will automatically use
@StateObject
var database = try! Blackbird.Database.inMemoryDatabase()
...
I notice that your actual example SwiftUI code doesn't have this issue because it stores the database in an App object rather than in a view.
Because BlackbirdModel
extends Decodable
it's possible to construct an empty instance of the correct type in order to interrogate it. I've created a package to do just that at https://github.com/jjrscott/EmptyInitializer but it's easy to roll your own, especially as you've already done it once (BlackbirdSQLiteDecoder
).
let mirror = try Mirror(reflecting: EmptyInitializer.initialize(type: Post.self))
This doesn't solve all the issues however as property wrappers don't always do what one would expect when interacting with Decodable
. Hopefully people comment on Property wrapper initialisation issues to either show what I did wrong or propose a fix.
I hacked up my toy image board viewer app to use Blackbird, and compared it to a version that uses GRDB.
Blackbird branch: https://github.com/jackpal/HD/commits/Blackbird
GRDB branch: https://github.com/jackpal/HD/commits/main
The main UX difference between the two versions is that the Blackbird-based list views start out blank, while the GRDB-based list views start out filled in.
I think this is due to GRDB's value change publisher having a default "immediate" mode that fetches the value immediately upon creation.
It makes a big difference in the perceived speed of the app.
So I would like to request similar Blackbird functionality.
FWIW the Blackbird version of my app was less code and ceremony than the GRDB version. Good job!
But, the GRDB version does more. It uses foreign keys with cascading deletes to update the local database when posts and threads are deleted on the service. I haven't tried implementing similar functionality in the Blackbird version yet.
Any plan for support CocoaPods?
When following the SwiftUI example from the README and initializing the database via @StateObject var database = try! Blackbird.Database.inMemoryDatabase()
, I get an error message in Xcode stating the following:
Generic struct 'StateObject' requires that 'Blackbird.Database' conform to 'ObservableObject'
Any ideas? I assume I'm just missing something obvious here, but would love some insight! This is using Xcode 15, for what it's worth.
Really happy with how this is designed and would love to use it if I can get past the initialization hurdle. More than happy to provide whatever info you need, but hopefully I'm just doing something dumb.
Consider this model object:
struct Meal: BlackbirdModel, Identifiable {
@BlackbirdColumn var id: String
@BlackbirdColumn var name: String
@BlackbirdColumn var thumbnailUrl: URL
enum CodingKeys: String, CodingKey {
case id = "idMeal"
case name = "strMeal"
case thumbnailUrl = "strMealThumb"
}
}
For the sake of decoding a network resource with questionable naming conventions, I have introduced a CodingKeys
to map their names to my own. Fine.
Blackbird creates a table with names of id
, name
, and thumbnailUrl
. That seems good to me. But, when I try to save a record I get the following fatal error message:
2023-04-22 15:27:10.868259-0700 MyApp[49584:3615565] Blackbird/BlackbirdModel.swift:829: Fatal error: Table "Meal" definition defaults do not decode to model Meal: missingValue("idMeal")
Note, the database has the column name
, but validateSchema(database:core:)
is looking for strMeal
(the name from CodingKeys
). It looks like the logic it used for creating the column names (using the actual property names) is different than what validateSchema(database:core:)
used. It strikes me that we should either use CodingKeys
everywhere, or not at all. (I'd vote for the latter.)
Thoughts?
I am not sure if this was intended but the following README.md example for monitoring row-level changes does not work anymore since the access level of these properties got changed to internal
in 4b822c0.
let listener = Post.changePublisher(in: db).sink { change in
print("Post IDs changed: \(change.primaryKeys ?? "all")")
print(" Columns changed: \(change.columnNames ?? "all")")
}
Currently SPM isn't supported as the tags are prefixed with v
.
Please could you support SPM compatible tags by changing your tag from v0.5
to 0.5
?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.