From 4c62ecf4671a3b7b515f63dcb24a24c6e7895f05 Mon Sep 17 00:00:00 2001 From: Jan Likar Date: Fri, 26 May 2023 23:38:10 +0200 Subject: [PATCH] Implement server_default for SA columns CrateDB's SQLAlchemy dialect now handles the `server_default` when generating table DDL. Fix #454. --- CHANGES.txt | 1 + docs/sqlalchemy.rst | 4 +- src/crate/client/sqlalchemy/compiler.py | 5 +- .../sqlalchemy/tests/create_table_test.py | 52 +++++++++++++++++++ 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 0a7196f9..f8d0f54a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -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 ================= diff --git a/docs/sqlalchemy.rst b/docs/sqlalchemy.rst index b16f2abf..2c1a7471 100644 --- a/docs/sqlalchemy.rst +++ b/docs/sqlalchemy.rst @@ -206,6 +206,7 @@ system `: ... 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'] @@ -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:: diff --git a/src/crate/client/sqlalchemy/compiler.py b/src/crate/client/sqlalchemy/compiler.py index efa88d17..3ac6fa57 100644 --- a/src/crate/client/sqlalchemy/compiler.py +++ b/src/crate/client/sqlalchemy/compiler.py @@ -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) diff --git a/src/crate/client/sqlalchemy/tests/create_table_test.py b/src/crate/client/sqlalchemy/tests/create_table_test.py index f876655d..b7fb9b87 100644 --- a/src/crate/client/sqlalchemy/tests/create_table_test.py +++ b/src/crate/client/sqlalchemy/tests/create_table_test.py @@ -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'), ())