Replies: 2 comments
-
|
I really appreciate these discussions, thank you for sharing your ideas! This is exactly what I wanted to provoke when publishing the v3 wishlist :)
I've experimented a bit with the approach of keeping the query builder and query executors / database classes separated (essentially having a standalone query builder library). I like the freedom that this unlocks, but it also feels like something that could become an inconvenience for most users. Today,
Fully agreed, I've added this to the todo list. FWIW, I also generally think that we should be moving into a more modular direction. But it's very hard to do without augmentations. With them, we could just generate code wherever a table is defined and introduce an augmentation on the table to start queries. Borrowing your first idea: Users.query().filter((row) => row.id.equals(3)).watch(database)Something like this could reduce the necessity for DAOs.
Yeah, parts of this mess have also grown out of additional indirections squeezed in to not break existing users. From a conceptional standpoint, the table class you write is the interface (providing getters for columns). Drift generates an implementation that:
But the fact that
The main problem with static table classes is that you can use the same table multiple times in one statement. E.g. you could have class Points extends Table {
static const id = Column<int>();
}
class Routes extends Table {
@References(Points.id)
static const start = Column<int>();
@References(Points.id)
static const end = Column<int>();
}Today, it is very neat that we can just write: final startPoint = alias(points, 'start');
final endPoint = alias(points, 'end');
final query = select(routes).join([
innerJoin(startPoint, startPoint.id.equalsExp(routes.start)),
innerJoin(endPoint, endPoint.id.equalsExp(routes.end)),
]);And then join Or is the idea that the generated table classes would provide typed getters for columns then, without inheriting from the definitions? That would make the table definitions kind of heavy for no reason. // You write an annotated data class, drift generates a table structure
@DriftTable()
@futureDataClassAugmentationMagic
final class Category {
final int id;
final String name;
}
@DriftTable()
@futureDataClassAugmentationMagic
final class TodoItem {
final int id;
@References(Category.$.id)
final int categoryId;
}But my preferred approach is to focus on breaking changes that are mostly internal to drift for now, and then unlock new generation styles as an opt-in builder options once augmentations or other language features enable them.
Also something that will magically work with augmentations. Until then, I recommend modular code generation which will make drift emit its own libraries with independent imports.
Yeah I stole the companion name from Kotlin, but I agree that it's not exactly an intuitive concept. Maybe it should have been called
I like that idea - it's kind of similar to what the manager is doing today for inserts (from an API standpoint to avoid explicit
Maybe it should be called
I agree with using records for data classes, but I'm not sure about using them to define tables. I intend to remove some mostly useless functionality from tables (mostly the verification in Dart, databases do that already) which should reduce their code size significantly. If we also replace the data class with a record, I suspect most of the code (excluding the manager code) will be made up of companions. It's a neat trick to avoid code generation altogether, but as you've also mentioned we can't get to named records without any codegen and then once we have it, I don't see an advantage over the current definitions. |
Beta Was this translation helpful? Give feedback.
-
|
Apologies for the late response - spent some time prototyping record overloading implementations, and in doing so don't believe they're as great as I thought they could be. During that I was also looking for an interface name for a certain family of objects and went for
Seems like a small price to pay for a clearer separation of concerns / type hierarchy, but I won't whip a dead horse.
My thoughts exactly. I like the idea of namespacing queries, but if that's all they're doing then DAO isn't really the right name. I might even argue that any operations that need to make multiple calls to the sqlite engine don't really belong in the database class itself, but rather should be in a separate layer on top - and preferably delegated to the user rather than in drift.
You're correct: to clarify, a static @DriftTable()
@futureDataClassAugmentationMagic
final class TodoItem {
final int id;
@References(Category.$.id)
final int categoryId;
}I don't know exactly how magic augmentations are going to be, but I'm under the impression that it's currently impossible to have compile-time constants of the form
Absolutely agree. What about foo.insert(requiredField1: x, requiredField2: y, updates: (u) => u
..optionalField = z
);
|
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
So having seen #3461 yesterday I started putting some ideas down... then realized it was too long for that issue and split it into multiple issues... then realized those issues were all kind of inter-related so have smooshed them all back together again... then I iterated a couple more times and now I've written a monster. I apologies in advance for the length.
I also apologise in advance if some of these come across a little blunt. Some are very opinionated - and I openly admit those opinions come from a place of limited experience with both code generation and running large/long term projects. Others are a little petty. Feel free to take what you want and ignore everything else. I'm happy to go into more detail on any that aren't sufficiently well fleshed out or contribute to their implementation if desired. Also happy to split things up again into separate issues for discussion if there's interest.
Here's hoping it's taken in the spirit it's intended :).
A data-independent query/statement construction layer
Drift treats queries very differently to tables and views. This makes a lot of sense since tables and views are persistent, while queries aren't. That said, I feel there's a missing layer between persistent entities (tables, view, triggers) and the procedural operations performed by DAOs and databases that are difficult or impossible to run in raw SQL.
Specifically, I think drift is missing a mechanism to construct static/parameterized sql statements that are independent of the records in the actual database through the dart API. I would like to see an interface similar to
Viewthat allows construction of queries and arbitrary sql statements based on schema information alone, either at compile time or dynamically. DAOs and databases would then connect these to executors and add an additional layer on top of this for procedural logic.I've experimented a little with the generated code from
.driftfiles and this is close to what I think should be possible from dart. Even then though I'd like to be able to generate this modularly without introducing a DAO.Taking an example from the docs:
Conceptually, this is just two sql statements being constructed in dart. I think it would be much cleaner / more managable to be able to do:
Here I use
opto mean a dart structure that represents an sql statement, andcompoundOpjust creates a single operation out of multiple sub-operations. Running anopwould just be a matter of forming the sql string and passing it to the executor. While I think the biggest appeal of this is separation of concerns / project maintainability/understandability, I feel there would have to be performance benefits too with only 1 FFI call.Parameter-free generated DAO classes
There comes a point in every project where things get big and messy and you want to rip things apart. Currently generated DAOs are parameterized by the database that will use them. This doesn't play nicely with modularity. If I have a database that uses a DAO, the DAO shouldn't need to know about the database. I believe this can be resolved by making a generated DAO also generate an interface class that gives access to the relevant
TableInfos and an associated manager (with corresponding abstract interface), and the database using it can implement that database.As an example, in the documentation we have:
I would rather it be used as
where
and then databases using the DAO will have generated classes:
Another step would be to make
$TodosDaoDatabasea concrete class and NOT make$MyDatabaseimplement it, but rather have$MyDatabaseconstruct a$TodosDaoDatabaseas a private member of it's tables. That wayMyDatabasedoesn't explicitly use a table used inTodosDaos it doesn't need to be part of$MyDatabase's public interface.Only 1 of Table / TableInfo
It's taken me a while to understand the difference between
TableandTableInfo. I'm still not entirely sure I understand the purpose of the distinction, but based on my current understanding I think they're highly counter-intuitive. ATableis aHasResultSet, which makes me think it has a result set - something I might be able to get with, I don't know, agetmethod maybe. Wrong.TableInfoon the other hand is aHasResultSetImplementation. If anything I would think aHasResultSetwould be a subclass ofHasResultSetImplementation- you need an implementation to get something right? Also wrong. That said,HasResultSetImplementationdoes have agetmethod that gets... what I would call a result set.Intuitively, I think of a
Tableas something with data in it, and if I had to guess,TableInfowould contain metadata about that table - e.g. column names and types. That's exactly the opposite of what they are.Tables represent schema definitions, whileTableInfocontains the schema info as well as methods to access the records stored in the database.Remove symbols for references
Symbols irk me. Having just argued to get rid of 1 of either
TableorTableInfo, I'm now going to argue in favor of keeping them both - albeit in different forms. Let's say we keepTableInfomuch that same. What if we defined our member tables like:These classes would never be instantiated - enforced by either a private constructor on
Tableor some other means - and serve purely to provide schema information and serialization/deserialization.Do we need to copy code?
Having to put in
exports just so generated code can work seems wrong.json_serializationhandles custom serialization and default values with annotations - is there places that approach can't be used in drift?Companions -> Builder
Ok, this is admittedly pretty small/very much personal preference, but as someone who's been playing around with dart on and off for a while, I'm familiar with builders since
built_valuedays. It's not nearly so popular now thanks tofreezed, but I still know what a builder does and why it's needed. Maybe it's popular in some circles I've never been a part of, but I've never heard of aCompanionwhile programming. It's not hard to read the documentation to understand it, but it's IMO unintuitive, and I don't think it looks great wrapping everything inValue. I'm not suggesting it would need to be built on top ofbuilt_value- though if that kills multiple birds with one stone all the better.If we consider the example from the docs:
I would much rather use
Here,
bwould be aUsersBuildersomething like the following:Consider renaming
SelectableAnother arguably petty one. I know naming is hard, and there's probably a very good reason this was originally used - but if I can't
selectaSelectablethen I would argue at least one of those is named incorrectly.An incomplete idea: records as data classes
If you've made it this far, I'll leave you with an idea that I'm a long way from done with yet, but could potentially radically reduce the amount of code generated.
Could records be used as the base data class, and other functionality be implemented as extensions on templated records similar to Future.wait? For the moment let's forget about elegant APIs and focus on type safety: then let's suppose we have something like the following:
This is supported with drift library extension methods:
We could then have
Obviously there's a lot of details that would need to be investigated further, but I believe most of the simple stuff could be handled like this in a type safe way without any project-specific code generation (generating
ColumnRecordNs would probably be wise - I nearly tore my hair out just writing theN=2case - but that would be part ofdrift, not for client libraries).Now I'll be the first to admit those
$1s and$2s are a lot less graceful than named fields. Unfortunately records with different named fields are different types, so we can't just extend over all possible record names like we can positional-only fields. My gut feeling is we could get essentially everything we want by generating code to handle record structuring (positional-only records -> named only records and back again) - i.e. have as much implementation on the flattened record implementations in thedriftlibrary and just transform to a named record interface at the API surface. I don't know much about dart internals but I feel those destructuring/restructuring operations would have to be pretty cheap, and I'm pretty sure each could be generated in 2 lines of code.Beta Was this translation helpful? Give feedback.
All reactions