sql

iarenaza 2025-02-07T17:49:34.408189Z

We have a library (https://github.com/gethop-dev/rbac.next) that implements role based access control (RBAC) using some database tables. The library uses next.jdbc to manage and query those tables as needed. The applications using this library only need to make sure that the tables needed by the library are created with the right properties (there is example template for Postgresql). From that point forward, the application shouldn't deal with those tables at all, and should only use the API offered by the library. Because the database connection(s) are managed by the application (and not the library), all the library public API functions require an argument that can be use to connect to the database (and execute queries). That means that the library can get a "JDBC URL" (as a string), a "db-spec" hash-map, a DataSource, a Connection, a Transaction or the "wrapped" variants of some of them. In some cases, the library needs to insertions in two (or more) tables. In the past we managed that by using with-transaction, because we were naïve and because all of the applications that used the library didn't use "wrapped connection" variants , or didn't call those functions from within an transaction already started on the application side. But, as you might have already guessed, that is no longer the case :-) Now we have an application that sometimes calls the library with no transactions going on, and sometimes calls the library from within an (application initiated) transaction. And in both cases, with a "wrapped connection" variant for the connection argument. There is an additional requirement (that we'd like to keep, as much as possible, not to break our API backwards compatibility): the library doesn't use exceptions as a way to convey failures related to "database query executions". We catch those exceptions internally and return the relevant values instead. Given the above scenario and set of constraints, we've come up to the conclusion that savepoints are a possible way out for us. Our reasoning is that, if there is no ongoing transaction started on the application (caller) side, we use with-transaction+options with the passed in connection parameter (be it a "JDBC URL", a "db-spec" hash-map, a DataSource, or a Connection), do our stuff, commit or rollback as needed, and return whatever is required in each case. On the other hand, if there is an ongoing transaction by the time the library is called (we check that with jdbc/active-tx?, yeah, we've read the caveat about that not reflecting any other operations on a Connection, performed via JDBC interop directly.), we cannot use with-transaction+options with the passed in connection parameter, because that would mean "nesting" the transactions. And the docs clearly state how bad that is, in different ways, depending of what the type of the connection parameter is. We could theoretically do our library things using the passed in connection parameter, thus as part of the (outer transaction). And if our library things don't pan out, rollback the (outer) transaction at that point ourselves. But we'd rather not rollback the rest of the things the caller did before calling the library, because the library doesn't have any clue about whether those things should be rolled back or not. So here's where savepoints enter the picture. And our request for help too :-) We plan to use savepoints when an (outer) transaction is already ongoing, to do a "controlled rollback" for just the thinks we did in our library, leaving the rest of the things the caller did before calling the library untouched. But in order to create (and optionally rollback) savepoints, we need direct Java interop on the Connection object. And the library can sometimes get the "wrapped" version of that. And that wrapped version can sometimes be a wrapper around a pool connection (the particular app that triggered the bug we are trying to fix is using HikariCP, but we have another one that uses a plain JDBC URL, wrapped with some options). So our question is, is there a next.jdbc officially blessed way to get the Connection instance from anything that the caller throws at us? We've tried several approaches, but we've failed to find a way that works in all cases, without relying on implementation details that are not officially documented (.e.g, directly checking if the passed in connection parameter "satisfies" next.jdbc.default-options.DefaultOptions, and getting the :connectable key) Thanks a lot in advance!

seancorfield 2025-02-07T22:23:35.162369Z

I'm traveling so I can't look at this in detail, but I'll point out that DefaultOptions isn't the only thing that might wrap a connectable. With the latest version of next.jdbc, you can safe "nest" with-transaction if you are passed anything other than a Connection (or wrapped connection), because it will use a different Connection to any calling code -- but if the caller passes you a (potentially wrapped) Connection you should probably shouldn't even try to use save points, because you've no idea what the caller may have set the Connection's state to and it just isn't safe for you to reuse that for your own state management. My advice would be to change your documented API to note that users should not pass an existing connection (or wrapped connection) -- but use a datasource, db-spec, or URL instead.

seancorfield 2025-02-07T22:24:03.530759Z

(at least for the parts of the API that may do inserts)

seancorfield 2025-02-07T22:24:30.140929Z

Dividing the API into readonly (immutable) and read/write (mutable) is probably good practice anyway.