Remix.run Logo
jmull 3 days ago

The abstractions you build your code on top of can be very costly, and a lot of that cost is due later.

You need to carefully weight the costs of abstractions you adopt. That goes double (or triple or quadruple) for data access, which is typically the central point of an app.

When sqlite is involved, I'm really doubting an ORM is worth it in the great majority of cases.

That's because sqlite is robust, mature, full-featured, very well documented, has demonstrated long-term viability, has a commitment to backwards compatibility and support, etc. I think it makes the most sense to implement an application-level data-access layer and have it use sqlite as directly as possible.

What sqlite doesn't do that an app needs is dealing with Swift types. I.e., the grunt work of the sqlite3_bind_* and sqlite3_column_* calls to transform Swift values to and from sqlite3 values. (And to a lesser extent, various Swift-isms for quality of life.) But you can have that without the rather more intrusive footprint -- and therefore much higher cost -- of an ORM.

wahnfrieden 3 days ago | parent | next [-]

How else will you support CloudKit with SQLite? Personally CloudKit is a requirement as my customers value having their personal data inside their own Apple account rather than storing it on my servers. I also rely on CloudKit to avoid the cost and on-call burden of operating a realtime sync service.

Re: ORM, the SwiftData-like ORM part of SQLiteData is actually completely optional. You can write SQL statements directly via #sql: https://swiftpackageindex.com/pointfreeco/swift-structured-q... You can wrap usage of this in your own method so that you can swap out SQLiteData in the future without lock-in.

Re: Swift types, you may roll your own mapping, but performance is not trivial. Here is a benchmark from SQLiteData:

Orders.fetchAll setup rampup duration

   SQLite (generated by Enlighter 1.4.10) 0        0.144    7.183

   Lighter (1.4.10)                       0        0.164    8.059

   SQLiteData (1.0.0)                     0        0.172    8.511

   GRDB (7.4.1, manual decoding)          0        0.376    18.819

   SQLite.swift (0.15.3, manual decoding) 0        0.564    27.994

   SQLite.swift (0.15.3, Codable)         0        0.863    43.261

   GRDB (7.4.1, Codable)                  0.002    1.07     53.326
I found Enlighter and Lighter more intrusive to adopt, and the other open source solutions far slower (too slow for my needs, where my users have hundreds of thousands or millions of rows within the iOS apps)
stephencelis 3 days ago | parent | prev [-]

Are you suggesting SQLiteData is an ORM, or SwiftData? SwiftData (and CoreData) are certainly ORMs, but SQLiteData is not. It's simply a collection of tools on top of SQLite that provide similar functionality to SwiftData, but you always have direct access to SQLite.

The things you say that SQLite doesn't do is exactly what SQLiteData provides (Swift-friendly bindings for encoding and decoding data from SQLite), and more. There's a footprint, as there is with any library, but there is no ORM level of abstraction here.

jmull 3 days ago | parent [-]

Well, it has things like @Table and wants you to write SQL using Swift syntax. It's good that it lets you bypass this, but it encourages using the wrappers so I think that's going to be most of the usage. Also, if you're going to mostly bypass, then why incur the costs of adopting? The main bypass I saw was #sql, but while you can write SQL, it's a substantially different API than sqlite's C API to do the same.

One problem with data access wrappers is that what's both above it and below it have strong app concerns. The developer needs to understand and control what's below it, so an intermediate abstraction gets in the way. That is, in addition to understanding the lower-level, they also have to understand the intermediate abstraction, and how it maps to the lower level. So it's really best if the API surface is minimal.

What I would want is an API where the core is something like:

bindParameters(pStmt, anEncodableThing) readRowColumns(pStmt, ADecodableThing.Type) -> ADecodableThing

where pStmt would be as minimal a wrapper around a sqlite statement as is feasible (maybe even a pointer to a sqlite3_stmt directly?). There might be a minimal (non-existent?) wrapper around the sqlite3 connection too. (The sqlite pointer should be a public member of any wrapper, so you can call any sqlite3_ functions you want).

(I'd want some convenience methods too, that combine bindParameters and readRowColumns with preparing a statement and stepping through it, like sqlite's exec.)

Now, I know this doesn't address CloudKit sync at all, but I think a similarity minimal type-focused binding is best there too. It has nowhere near the 5-star API that sqlite has so there's a better argument for wrapping it, but sync tends to quickly accumulate app-level concerns when it comes to the exact details of sync conflicts, so you might as well keep that at app level. I think there maybe a set of composable convenience methods might do it, to handle the pain points without hiding the details you'll need access to.

Anyway, my point is NOT that sqlitedata is bad, it's that people should be really careful about taking on the costs that it has, and consider whether it will ultimately cause more problems than it solves. Meanwhile, sqlite is world-class -- you want to wrap it as little as possible.

wahnfrieden 3 days ago | parent | next [-]

World-class doesn’t help me if there is no off the shelf or easily made solution to CloudKit sync. Which hasn’t existed until SQLiteData.

(Previously I rolled my own sync with RealmSwift but that’s now dead.)

Without that, my only alternatives are SwiftData or CoreData, with severe downsides.

3 days ago | parent | prev [-]
[deleted]