sqldelight
sqldelight copied to clipboard
Feature request: possibility to change visibility of generated models
We have a multimodule project with SqlDelight in different modules, it would be perfect if we were able to hide delight models from the world (for instance if models were marked as internal) and stop polluting client code with generated models from Delight.
Instead of adding more options, it might be an improvement to consider all SELECT *
-type models as db-level implementation details that should be hidden from library clients. Often, you'll create a table and one or more columns will be used purely for query parameters (i.e. the WHERE
clause), sorting, and/or grouping results. But, they won't be exposed in the model used to show the list of results. There's usually a subset of columns that you actually care about copying out of SQLite for your result list.
In our project, we now almost entirely use custom mappers and custom models that represent the "view" models that we want to expose to clients (i.e. the subset of columns), and we now have a custom gradle task that runs after SQLDelight generation to internal
all of the SQLDelight-generated interfaces.
With that in mind, SQLDelight could generate these "view" models as public with the addition of AS
at the end of the select:
customFetch:
SELECT col1, col2 FROM table AS TableRowModel WHERE col3 > :param
That would generate a query and a public interface
called TableRowModel
(with a mapper).
In this way, SQLDelight models that are just the column-wise representation of your table are internal
by default, and you can selectively expose views of your data (without CREATE VIEW
, which has migration overhead) to your library clients.
IMHO, the fact that SqlDelight doesn't provide this (and a few others like aliasing table names) out of the box encourages anti-patterns and makes it suitable only for small let's-drop-everything-into-the-same-module kind of projects or quick mocks. Generated files aren't domain models but DTO's, and they should be treated as such. Exposing a DTO to the client sort of violates architecture rules 😕
You can achieve encapsulation of these types today with existing modularization mechanisms that hide dependencies as implementation details. The hyperbole is otherwise not helpful.
@JakeWharton which "modularization mechanisms" are you referring to? I honestly tried a few and always end up exposing implementation details one way or another. Any help or advice would be much appreciated.
The implementation
and api
configurations of Gradle. A db module can be an implementation
dependency of a store module which entirely encapsulates SQL interaction behind a high-level interface with semantic operations (e.g., insert user, query user, update user) that execute one or more SQL statements. Consumers of the store module will then be entirely unaware of the SQL DB and its types.
Isn't that architecting and modularizing around a library? I mean, don't take me wrong, I'm all in for having a dedicated database module, but I'm not entirely sure if the module architecture should be outlined around a library or if the library should be flexible enough to accommodate different styles of modularization.
Ive seen plenty of projects having only domain, data and presentation modules. Being able to mark generated interfaces internal
would be very beneficial in these scenarios instead of adding a fourth module.
SQL Delight is not a library though. If it were you would be able to just declare an implementation
dependency on it and be done (like you can with an ORM). It's a code generator and we have no idea how you are going to use it.
The main problem with exposing something to control visibility is not really having an internal
vs. public
switch, it's more the precedent that it sets. What do we do when someone asks for some tables to be internal
and some to be public
? What about query visibility? Visibility reaches into many parts of the system and it's not only challenging to figure out where to draw the line, but its interaction with other features may become non-trivial. As far as features go, since you can already fully achieve what you're after with a separate module, unless there's some killer reason to add this feature it moves very far down the priority list.
@JakeWharton I started taking that approach recently, and there is some friction to using it. For example, I would like the database and queries to be internal, but not the models. That way I can continue to use the generated models, but hide the more complex "store" functionality behind an interface.
If I hide everything behind an implementation dependency, I now have to define my own implementation of each model interface (which gets cumbersome), or not use the models at all (which is also cumbersome).
I think it would be helpful to have 2 flags in the gradle config; one for making models internal, and the other for making database/queries internal. Although I suppose it would get tricky if models were internal but queries weren't :thinking:
one thing I've been thinking about recently is how this might relate to this issue: https://github.com/cashapp/sqldelight/issues/1531
One thing we might be able to do is split sqldelights codegen into three tasks: generate-interface, generate-models, generate-implementation
that would allow you to specify a different output directory for the three, so you could potentially have a setup like
db
--api
--impl
--models
it would enable doing what you're talking about @eygraber by depending only on the db:models module, or db:api module. Curious if that would help resolve some of these issues?
That looks like it would work for my use case. Would that complicate (or become complicated due to) https://github.com/cashapp/sqldelight/issues/1455?
uhhh hmmmmm maybe. it might simplify some things, since the only thing that needs to generate for all dependencies is impl
, the api
and models
modules would both only have generated code from its source .sq files (and not dependency schemas)
@AlecStrong your suggestion should solve for my use case, assuming the api & models output can each be generated or output to different Gradle modules: https://github.com/cashapp/sqldelight/issues/1333#issuecomment-642213490
I'd then allow the models to be usable in all app modules, but the api would only be available to the repository/store layer.
Hello, while I agree that we can achieve something similar by adding one more Gradle module, this puts a burden on the developers as they are now facing perhaps the hardest thing in software development: naming, that is, naming these two modules, without making the project significantly harder to understand.
Adding an option into the Gradle plugin (on a per database basis, targeting the type com.squareup.sqldelight.gradle.SqlDelightDatabase
) to make everything be of internal
visibility would make it easier to control the API surface and hide the implementation details to prevent misuse or technology lock-in.
If someone is ready to make the contribution, would the PR get accepted once naming and stability is alright? (Not personally volunteering because my OSS contributions backlog is way too long at the moment).
Another use case for specifying visibility for generated classes/interfaces is when you would like to use SQLDelight in Android/multiplatform library. Because of the lack of official support to build a library from multimodule projects (see) we can't extract database implementation to different module. If we would like to use it in the library, we end up with public interface of it polluted with generated types.
Another use case for specifying visibility for generated classes/interfaces is when you would like to use SQLDelight in Android/multiplatform library. Because of the lack of official support to build a library from multimodule projects (see) we can't extract database implementation to different module. If we would like to use it in the library, we end up with public interface of it polluted with generated types.
we have the same issue, worst of all iOS is seeing our custom ModelName and generated ModelName_, which is confusing iOS devs 😄
Hello, Another use case I've just stumble upon since generated classes are public by default is that you can't use internal enum or custom type in your table. It's a minor issue since you could store it as a TEXT only and do the conversion manually, but I wanted to point it out.