Skip to content

Commit d4adb75

Browse files
authored
Merge pull request #16 from DanCardin/dc/view-diff
fix: Simplify view definition options.
2 parents 17b7a9b + 6f43871 commit d4adb75

38 files changed

Lines changed: 696 additions & 472 deletions

File tree

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545
${{ runner.os }}-poetry-
4646
4747
- name: Install dependencies
48-
run: poetry install
48+
run: poetry install -E parse
4949

5050
- name: Install specific sqlalchemy version
5151
run: |

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class Foo(Base):
6161
id = Column(types.Integer(), primary_key=True)
6262

6363

64-
@view()
64+
@view(Base)
6565
class HighFoo:
6666
__tablename__ = "high_foo"
6767
__view__ = select(Foo.__table__).where(Foo.__table__.c.id >= 10)

docs/source/views.md

Lines changed: 23 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,6 @@ This strategy allows one to organize their views alongside the models/tables tho
5151
views happen to be referencing, without requiring the view be importable at MetaData/model base
5252
definition time.
5353

54-
### Option 1
55-
5654
```python
5755
from sqlalchemy import Column, types, select
5856
from sqlalchemy.ext.declarative import declarative_base
@@ -67,95 +65,43 @@ class Foo(Base):
6765
id = Column(types.Integer, primary_key=True)
6866

6967

70-
@view()
71-
class Bar1(Base):
68+
@view(Base)
69+
class Bar:
7270
__tablename__ = 'bar'
7371
__view__ = select(Foo.__table__).where(Foo.__table__.id > 10)
7472

7573
id = Column(types.Integer, primary_key=True)
7674
```
7775

78-
The primary difference between Options 1 and 2 in the above example is of how the
79-
resulting classes are seen by SQLAlchemy/Alembic natively.
80-
81-
In the case of `Bar1`, SQLAlchemy/Alembic actually think that class is a normal table.
82-
Therefore querying the view looks identical to a real table: `session.query(Bar1).all()`
76+
The protocol this class is following provides an interface that is intentionally similar to the one
77+
given by a normal sqlalchemy model. From the perspective of code, your `Bar` class will be usable
78+
by SQLAlchemy in the same way as a normal table, i.e. `session.query(Bar1).all()`.
8379

84-
For alembic, this means that alembic thinks you defined a table and will attempt to
85-
autogenerate it (while this library will also notice it and attempt to autogenerate
86-
a conflicting view.
80+
Alternatively, if you dont **care** about being able to programmatically make use of the model-like
81+
ORM interface, you can omit the model-style declaration of columns. That at least allows you to
82+
avoid duplicating the list of columns unnecessarily.
8783

88-
In order to use this option, we suggest you use one or both of some utility functions provided
89-
under the `sqlalchemy_declarative_extensions.alembic`: [ignore_view_tables](sqlalchemy_declarative_extensions.alembic.ignore_view_tables)
90-
and [compose_include_object_callbacks](sqlalchemy_declarative_extensions.alembic.compose_include_object_callbacks).
84+
Finally, you can directly call `register_view` to imperitively register a normal [View](sqlalchemy_declarative_extensions.View)
85+
object, if the class interface doesn't float your boat.
9186

92-
Somewhere in your Alembic `env.py`, you will have a block which looks like this:
87+
## Materialized views
9388

94-
```python
95-
with connectable.connect() as connection:
96-
context.configure(
97-
connection=connection,
98-
target_metadata=target_metadata,
99-
...
100-
)
101-
```
89+
Materialized views can be created by adding the `materialized=True` kwarg to the `@view` decorator,
90+
or else by supplying the same kwarg directly to the `View` constructor.
10291

103-
The above call to `configure` accepts an `include_object`, which tells alembic to include or ignore
104-
all detected objects.
92+
Note that in order to refresh materialized views concurrently, the Postgres requires the view to
93+
have a unique constraint. The constraint can be applied in the same way that it would be on a
94+
normal table (i.e. `__table_args__`):
10595

10696
```python
107-
from sqlalchemy_declarative_extensions.alembic import ignore_view_tables
108-
...
109-
context.configure(..., include_object=ignore_view_tables)
110-
```
111-
112-
If you happen to already be using `include_object` to perform filtering, we provide an additional
113-
utility to more easily compose our version with your own. Although you can certainly manually call
114-
`ignore_view_tables` directly, yourself.
115-
116-
```python
117-
from sqlalchemy_declarative_extensions.alembic import ignore_view_tables, compose_include_object_callbacks
118-
...
119-
def my_include_object(object, *_):
120-
if object.name != 'foo':
121-
return True
122-
return False
123-
124-
context.configure(..., include_object=compose_include_object_callbacks(my_include_object, ignore_view_tables))
125-
```
126-
127-
## Option 2
128-
129-
```python
130-
from sqlalchemy import Column, types, select
131-
from sqlalchemy.ext.declarative import declarative_base
132-
from sqlalchemy_declarative_extensions import view
133-
134-
Base = declarative_base()
135-
136-
137-
class Foo(Base):
138-
__tablename__ = 'foo'
139-
140-
id = Column(types.Integer, primary_key=True)
141-
142-
143-
@view(Base) # or `@view(Base.metadata)`
144-
class Bar2:
97+
@view(Base, materialized=True)
98+
class Bar:
14599
__tablename__ = 'bar'
146100
__view__ = select(Foo.__table__).where(Foo.__table__.id > 10)
101+
__table_args__ = (Index('uq_bar', 'id', unique=True))
147102
```
148103

149-
By contrast, with Option 2, your class is not subclassing `Base`, therefore it's
150-
not registered as a real table by SQLAlchemy or Alembic. There's no additional
151-
work required to get them to ignore the table, because it's not one.
152-
153-
Unfortunately, that means you cannot **invisibly** treat it as though it's a normal model,
154-
largely because it doesn't have the columns enumerated out in the same way.
155-
156-
However we can provide some basic support for treating it as a table as far as the ORM is concerned.
157-
For example, you can still `session.query(Bar2).all()` directly.
158-
159-
However, in most cases views primarily benefit non-code consumers of the database, because there's
160-
no practical difference between querying a literal view, versus executing the underlying query
161-
of that view, through something like `session.execute(Bar2.__view__)`.
104+
There is a caveat (at least currently), that the `UniqueConstraint` convenience class provided by
105+
SQLAlchemy does not appear to function in the same way as `Index` in a way that makes it incompatible
106+
with the mechanisms used by this library at the time of writing. As such, we ignore `__table_args__`
107+
constraints which are not `Index`.

0 commit comments

Comments
 (0)