Skip to content

Commit

Permalink
Implement server_default for SA columns
Browse files Browse the repository at this point in the history
CrateDB's SQLAlchemy dialect now handles the `server_default` when generating
table DDL.

Fix #454.
  • Loading branch information
JanLikar authored and amotl committed Jun 9, 2023
1 parent 7b86801 commit 4c62ecf
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Unreleased
SQLAlchemy 2.0 by adding the new ``insert_returning`` and ``update_returning`` flags
in the CrateDB dialect.

- SQLAlchemy DDL: Allow setting ``server_default`` on columns to enable server-generated defaults.

2023/03/30 0.31.0
=================
Expand Down
4 changes: 3 additions & 1 deletion docs/sqlalchemy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ system <sa:orm_declarative_mapping>`:
... name_ft = sa.Column(sa.String)
... quote_ft = sa.Column(sa.String)
... even_more_details = sa.Column(sa.String, crate_columnstore=False)
... created_at = sa.Column(sa.DateTime, server_default=sa.func.now())
...
... __mapper_args__ = {
... 'exclude_properties': ['name_ft', 'quote_ft']
Expand All @@ -221,13 +222,14 @@ In this example, we:
- Use standard SQLAlchemy types for the ``id``, ``name``, and ``quote`` columns
- Use ``nullable=False`` to define a ``NOT NULL`` constraint
- Disable indexing of the ``name`` column using ``crate_index=False``
- Disable the columnstore of the ``even_more_details`` column using ``crate_columnstore=False``
- Define a computed column ``name_normalized`` (based on ``name``) that
translates into a generated column
- Use the `Object`_ extension type for the ``details`` column
- Use the `ObjectArray`_ extension type for the ``more_details`` column
- Set up the ``name_ft`` and ``quote_ft`` fulltext indexes, but exclude them from
the mapping (so SQLAlchemy doesn't try to update them as if they were columns)
- Disable the columnstore of the ``even_more_details`` column using ``crate_columnstore=False``
- Add a ``created_at`` column whose default value is set by CrateDB's ``now()`` function.

.. TIP::

Expand Down
5 changes: 4 additions & 1 deletion src/crate/client/sqlalchemy/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@ class CrateDDLCompiler(compiler.DDLCompiler):
def get_column_specification(self, column, **kwargs):
colspec = self.preparer.format_column(column) + " " + \
self.dialect.type_compiler.process(column.type)
# TODO: once supported add default here

default = self.get_column_default_string(column)
if default is not None:
colspec += " DEFAULT " + default

if column.computed is not None:
colspec += " " + self.process(column.computed)
Expand Down
52 changes: 52 additions & 0 deletions src/crate/client/sqlalchemy/tests/create_table_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,3 +259,55 @@ class DummyTable(self.Base):

with self.assertRaises(sa.exc.CompileError):
self.Base.metadata.create_all(bind=self.engine)

def test_column_server_default_text_func(self):
class DummyTable(self.Base):
__tablename__ = 't'
pk = sa.Column(sa.String, primary_key=True)
a = sa.Column(sa.DateTime, server_default=sa.text("now()"))

self.Base.metadata.create_all(bind=self.engine)
fake_cursor.execute.assert_called_with(
('\nCREATE TABLE t (\n\t'
'pk STRING NOT NULL, \n\t'
'a TIMESTAMP DEFAULT now(), \n\t'
'PRIMARY KEY (pk)\n)\n\n'), ())

def test_column_server_default_string(self):
class DummyTable(self.Base):
__tablename__ = 't'
pk = sa.Column(sa.String, primary_key=True)
a = sa.Column(sa.String, server_default="Zaphod")

self.Base.metadata.create_all(bind=self.engine)
fake_cursor.execute.assert_called_with(
('\nCREATE TABLE t (\n\t'
'pk STRING NOT NULL, \n\t'
'a STRING DEFAULT \'Zaphod\', \n\t'
'PRIMARY KEY (pk)\n)\n\n'), ())

def test_column_server_default_func(self):
class DummyTable(self.Base):
__tablename__ = 't'
pk = sa.Column(sa.String, primary_key=True)
a = sa.Column(sa.DateTime, server_default=sa.func.now())

self.Base.metadata.create_all(bind=self.engine)
fake_cursor.execute.assert_called_with(
('\nCREATE TABLE t (\n\t'
'pk STRING NOT NULL, \n\t'
'a TIMESTAMP DEFAULT now(), \n\t'
'PRIMARY KEY (pk)\n)\n\n'), ())

def test_column_server_default_text_constant(self):
class DummyTable(self.Base):
__tablename__ = 't'
pk = sa.Column(sa.String, primary_key=True)
answer = sa.Column(sa.Integer, server_default=sa.text("42"))

self.Base.metadata.create_all(bind=self.engine)
fake_cursor.execute.assert_called_with(
('\nCREATE TABLE t (\n\t'
'pk STRING NOT NULL, \n\t'
'answer INT DEFAULT 42, \n\t'
'PRIMARY KEY (pk)\n)\n\n'), ())

0 comments on commit 4c62ecf

Please sign in to comment.