Skip to content

Commit cf02fd5

Browse files
committed
Add support for RETURNING in SQLAlchemy dialect
This will enable the usage of `FetchedValue()` as server side default for primary keys in the declarative model. ```python import random from sqlalchemy import create_engine, inspect, func, Column, String, DateTime from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import FetchedValue engine = create_engine( "crate://", connect_args={ "username": "crate", }, ) Session = sessionmaker(bind=engine) Base = declarative_base(bind=engine) class Log(Base): __tablename__ = "logs" __table_args__ = {"schema": "doc"} __mapper_args__ = {"exclude_properties": ["id"]} id = Column("_id", String, server_default=FetchedValue(), primary_key=True) ts = Column(DateTime, server_default=func.current_timestamp()) level = Column(String) message = Column(String) if __name__ == "__main__": o = Log(level="info", message="Hello World") session = Session() session.add(o) session.commit() print(f"{o.id=}, {o.ts=}, {o.level=}, {o.message=}") ```
1 parent 1f3189e commit cf02fd5

File tree

4 files changed

+55
-3
lines changed

4 files changed

+55
-3
lines changed

CHANGES.txt

+6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ Changes for crate
55
Unreleased
66
==========
77

8+
- Added support for the ``RETURNING`` clause to the SQLAlchemy dialect. This
9+
requires CrateDB 4.2 or greater. In case you use any server side generated
10+
columns in your primary key constraint with earlier CrateDB versions, you can
11+
turn this feature off by passing ``implicit_returning=False`` in the
12+
``create_engine()`` call.
13+
814
- Added support for ``geo_point`` and ``geo_json`` types to the SQLAlchemy
915
dialect.
1016

docs/sqlalchemy.rst

+41-3
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,45 @@ In this example, we:
198198
The SQLAlchemy documentation has more information about `working with
199199
tables`_.
200200

201+
``_id`` as primary key
202+
......................
203+
204+
As with version 4.2 CrateDB supports the ``RETURNING`` clause, which makes it
205+
possible to use the ``_id`` column as fetched value for the ``PRIMARY KEY``
206+
constraint, since the SQLAlchemy ORM always **requires** a primary key.
207+
208+
A table schema like this
209+
210+
.. code-block:: sql
211+
212+
CREATE TABLE "doc"."logs" (
213+
"ts" TIMESTAMP WITH TIME ZONE,
214+
"level" TEXT,
215+
"message" TEXT
216+
)
217+
218+
would translate into the following declarative model::
219+
220+
>>> from sqlalchemy.schema import FetchedValue
221+
222+
>>> class Log(Base):
223+
...
224+
... __tablename__ = 'logs'
225+
... __mapper_args__ = {
226+
... 'exclude_properties': ['id']
227+
... }
228+
...
229+
... id = sa.Column("_id", sa.String, server_default=FetchedValue(), primary_key=True)
230+
... ts = sa.Column(sa.DateTime, server_default=sa.func.current_timestamp())
231+
... level = sa.Column(sa.String)
232+
... message = sa.Column(sa.String)
233+
234+
>>> log = Log(level="info", message="Hello World")
235+
>>> session.add(log)
236+
>>> session.commit()
237+
>>> log.id
238+
...
239+
201240
.. _using-extension-types:
202241

203242
Extension types
@@ -336,11 +375,9 @@ You can then set the values of the ``Geopoint`` and ``Geoshape`` columns::
336375
>>> session.add(tokyo)
337376
>>> session.commit()
338377

339-
340378
Querying
341379
========
342380

343-
344381
When the ``commit`` method is called, two ``INSERT`` statements are sent to
345382
CrateDB. However, the newly inserted rows aren't immediately available for
346383
querying because the table index is only updated periodically (one second, by
@@ -544,7 +581,7 @@ The score is made available via the ``_score`` column, which is a virtual
544581
column, meaning that it doesn't exist on the source table, and in most cases,
545582
should not be included in your :ref:`table definition <table-definition>`.
546583

547-
You can select ``_score`` as part of a query, like this:
584+
You can select ``_score`` as part of a query, like this::
548585

549586
>>> session.query(Character.name, '_score') \
550587
... .filter(match(Character.quote_ft, 'space')) \
@@ -557,6 +594,7 @@ table definition But notice that we select the associated score by passing in
557594
the virtual column name as a string (``_score``) instead of using a defined
558595
column on the ``Character`` class.
559596

597+
560598
.. _SQLAlchemy: http://www.sqlalchemy.org/
561599
.. _Object-Relational Mapping: https://en.wikipedia.org/wiki/Object-relational_mapping
562600
.. _dialect: http://docs.sqlalchemy.org/en/latest/dialects/

src/crate/client/sqlalchemy/compiler.py

+7
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,13 @@ def visit_any(self, element, **kw):
177177
self.process(element.right, **kw)
178178
)
179179

180+
def returning_clause(self, stmt, returning_cols):
181+
columns = [
182+
self._label_select_column(None, c, True, False, {})
183+
for c in sa.sql.expression._select_iterables(returning_cols)
184+
]
185+
return "RETURNING " + ", ".join(columns)
186+
180187
def visit_insert(self, insert_stmt, asfrom=False, **kw):
181188
"""
182189
used to compile <sql.expression.Insert> expressions.

src/crate/client/sqlalchemy/dialect.py

+1
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ class CrateDialect(default.DefaultDialect):
165165
type_compiler = CrateTypeCompiler
166166
supports_native_boolean = True
167167
colspecs = colspecs
168+
implicit_returning = True
168169

169170
def __init__(self, *args, **kwargs):
170171
super(CrateDialect, self).__init__(*args, **kwargs)

0 commit comments

Comments
 (0)