I think the first thing to do is to try to start using our own definitions of the unit relationships (i.e. the objects in daf/butler/core/dataUnits.py) in the preflight code, so you're not relying on the SQLAlchemy foreign key relationships, as we may just not be able to make those as expressive as what we define ourselves.
I'm a bit ambivalent about whether it's a good idea to then use SQLAlchemy to write the query itself - that might make things more portable as long as the Registry is implemented on top of SQLAlchemy, but means we can't use this (shared) component at all if the Registry isn't implemented on top of SQLAlchemy, and while we want to use SQLAlchemy to make it easier to implement Registries, we don't want to make it part of any interface. Of course, using it in the implementation of a shared component isn't quite the same as making it part of an interface - we could always reimplement the shared component when if/when we have a non-SQLAlchemy Registry. In any case, I'm happy to follow your judgement on that question for now, but I wanted to make sure you understood the situation.
Anyhow, I'm hoping we may already have information in our own DataUnit classes that could be used t say that whenever Visit appears, PhysicalFilter should as well. That might make PhysicalFilter appear when it isn't needed, but that's "only" a performance problem, and I hope it may not be a meaningful one. I'm pretty sure our DataUnit classes should report that there is a foreign-key-like relationship between AbstractFilter and PhysicalFilter, even if there isn't one in the actual SQL.
I just had a quick look at the options for implementing joins with "join tables" and it turns out there is no trivial way to do that. For non-join tables I join those using foreign key definition in SQLAlchemy (SA) schema. It is even possible in trivial cases to use SA join() method which does everything automagically, but our case is not that trivial so I have to generate all those joins myself just looking at Table foreign key constraints.
With Join tables situation is problematic because those are not actually tables but views and views do not have foreign key constraints. What is also problematic is that DataUnitRegistry does not store foreign key info in DataUnitJoin instances. I'll probably have to extend that class to include that otherwise there is no place where to get this info from.