Setting column as None does not explicitly add the NULL to insert statement

Issue #3794 duplicate
Konsta Vesterinen created an issue

The following code illustrates this problem:

import sqlalchemy as sa
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base


engine = sa.create_engine(
    'postgres://postgres@localhost/test'
)
engine.echo = True
Base = declarative_base()
Session = sessionmaker(bind=engine)
session = Session()


class Question(Base):
    __tablename__ = 'question'
    id = sa.Column(sa.Integer, primary_key=True)

    weight = sa.Column(sa.Integer, default=1)


Base.metadata.create_all(bind=session.bind)


q = Question(weight=None)
session.add(q)
session.commit()

assert q.weight is None

The problem also exists by changing the default=1 to server_default='1'. Also in this case the explicit weight=NULL is not added to INSERT statement.

Comments (5)

  1. Mike Bayer repo owner

    Here's the reason we don't do that:

    cursor.execute("""
        create table x (
            a integer,
            b integer not null default 5
        )
    """)
    
    cursor.execute("insert into x (a, b) values (?, ?)", (1, None))
    
    #!
    
    Traceback (most recent call last):
      File "test.py", line 13, in <module>
        cursor.execute("insert into x (a, b) values (?, ?)", (1, None))
    sqlite3.IntegrityError: NOT NULL constraint failed: x.b
    

    Changing this in place would break the (IMO, probably very large) number of applications that don't want NULL in there but have situations where None is being explicitly set. Some will have a NOT NULL constraint that will fail, others won't and will just have silent failure of their default generator.

    Turning off the attribute in place:

    diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py
    index 0b029f4..1abfa8a 100644
    --- a/lib/sqlalchemy/orm/persistence.py
    +++ b/lib/sqlalchemy/orm/persistence.py
    @@ -386,7 +386,7 @@ def _collect_insert_commands(
             for propkey in set(propkey_to_col).intersection(state_dict):
                 value = state_dict[propkey]
                 col = propkey_to_col[propkey]
    -            if value is None and propkey not in eval_none and not render_nulls:
    +            if False and value is None and propkey not in eval_none and not render_nulls:
                     continue
                 elif not bulk and isinstance(value, sql.ClauseElement):
                     value_params[col.key] = value
    

    leads to these failures, I haven't evaluated which of these are "this is just what we expect" vs. "this is a thing that can no longer work":

    #!
    
    
    =================================== FAILURES ===================================
    __________________ DefaultsTest.test_attributes_with_defaults __________________
    Traceback (most recent call last):
      File "/home/classic/dev/sqlalchemy/test/orm/test_composites.py", line 461, in test_attributes_with_defaults
        eq_(f1.foob, FBComposite(2, 5, 15, None))
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/testing/assertions.py", line 214, in eq_
        assert a == b, msg or "%r != %r" % (a, b)
    AssertionError: FBComposite(None, 5, None, None) != FBComposite(2, 5, 15, None)
    ____________________ DefaultsTest.test_set_composite_values ____________________
    Traceback (most recent call last):
      File "/home/classic/dev/sqlalchemy/test/orm/test_composites.py", line 477, in test_set_composite_values
        eq_(f1.foob, FBComposite(2, 5, 15, None))
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/testing/assertions.py", line 214, in eq_
        assert a == b, msg or "%r != %r" % (a, b)
    AssertionError: FBComposite(None, 5, None, None) != FBComposite(2, 5, 15, None)
    __________________ RequirementsTest.test_comparison_overrides __________________
    Traceback (most recent call last):
      File "/home/classic/dev/sqlalchemy/test/orm/test_mapper.py", line 3001, in test_comparison_overrides
        s.flush()
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/orm/session.py", line 2065, in flush
        self._flush(objects)
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/orm/session.py", line 2183, in _flush
        transaction.rollback(_capture_exception=True)
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/util/langhelpers.py", line 60, in __exit__
        compat.reraise(exc_type, exc_value, exc_tb)
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/orm/session.py", line 2147, in _flush
        flush_context.execute()
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/orm/unitofwork.py", line 386, in execute
        rec.execute(self)
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/orm/unitofwork.py", line 500, in execute
        self.dependency_processor.process_saves(uow, states)
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/orm/dependency.py", line 1092, in process_saves
        secondary_update, secondary_delete)
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/orm/dependency.py", line 1136, in _run_crud
        connection.execute(statement, secondary_insert)
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/engine/base.py", line 945, in execute
        return meth(self, multiparams, params)
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/sql/elements.py", line 263, in _execute_on_connection
        return connection._execute_clauseelement(self, multiparams, params)
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/engine/base.py", line 1053, in _execute_clauseelement
        compiled_sql, distilled_params
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/engine/base.py", line 1189, in _execute_context
        context)
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/engine/base.py", line 1384, in _handle_dbapi_exception
        exc_info
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/util/compat.py", line 202, in raise_from_cause
        reraise(type(exception), exception, tb=exc_tb, cause=cause)
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/engine/base.py", line 1159, in _execute_context
        context)
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/engine/default.py", line 459, in do_executemany
        cursor.executemany(statement, parameters)
    IntegrityError: (sqlite3.IntegrityError) NOT NULL constraint failed: ht4.ht1_id [SQL: u'INSERT INTO ht4 (ht1_id, ht3_id) VALUES (?, ?)'] [parameters: ((None, None), (None, None))]
    _______ TransientExceptionTesst_sqlite_pysqlite.test_transient_exception _______
    Traceback (most recent call last):
      File "/home/classic/dev/sqlalchemy/test/orm/test_naturalpks.py", line 491, in test_transient_exception
        sess.flush()
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/orm/session.py", line 2065, in flush
        self._flush(objects)
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/orm/session.py", line 2183, in _flush
        transaction.rollback(_capture_exception=True)
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/util/langhelpers.py", line 60, in __exit__
        compat.reraise(exc_type, exc_value, exc_tb)
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/orm/session.py", line 2153, in _flush
        flush_context.finalize_flush_changes()
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/orm/unitofwork.py", line 408, in finalize_flush_changes
        self.session._register_newly_persistent(other)
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/orm/session.py", line 1535, in _register_newly_persistent
        % state_str(state)
    FlushError: Instance <User at 0x7f5bf19365d0> has a NULL identity key.  If this is an auto-generated value, check that the database table allows generation of new primary key values, and that the mapped Column object is configured to expect these generated values.  Ensure also that this flush() is not occurring at an inappropriate time, such aswithin a load() event.
    ___________ NullEvaluatingTest.test_no_evalnull_default_bulk_insert ____________
    Traceback (most recent call last):
      File "/home/classic/dev/sqlalchemy/test/orm/test_unitofworkv2.py", line 2564, in test_no_evalnull_default_bulk_insert
        "no_eval_null_default", 'default_val'
      File "/home/classic/dev/sqlalchemy/test/orm/test_unitofworkv2.py", line 2474, in _test_bulk_insert
        self._assert_col(attr, expected)
      File "/home/classic/dev/sqlalchemy/test/orm/test_unitofworkv2.py", line 2452, in _assert_col
        obj = s.query(col).filter(col == value).one()
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/orm/query.py", line 2760, in one
        raise orm_exc.NoResultFound("No row was found for one()")
    NoResultFound: No row was found for one()
    ______________ NullEvaluatingTest.test_no_evalnull_default_insert ______________
    Traceback (most recent call last):
      File "/home/classic/dev/sqlalchemy/test/orm/test_unitofworkv2.py", line 2559, in test_no_evalnull_default_insert
        "no_eval_null_default", 'default_val'
      File "/home/classic/dev/sqlalchemy/test/orm/test_unitofworkv2.py", line 2463, in _test_insert
        self._assert_col(attr, expected)
      File "/home/classic/dev/sqlalchemy/test/orm/test_unitofworkv2.py", line 2452, in _assert_col
        obj = s.query(col).filter(col == value).one()
      File "/home/classic/dev/sqlalchemy/test/../lib/sqlalchemy/orm/query.py", line 2760, in one
        raise orm_exc.NoResultFound("No row was found for one()")
    NoResultFound: No row was found for one()
    =========================== short test summary info ============================
    FAIL test/orm/test_composites.py::DefaultsTest::()::test_attributes_with_defaults
    FAIL test/orm/test_composites.py::DefaultsTest::()::test_set_composite_values
    FAIL test/orm/test_mapper.py::RequirementsTest::()::test_comparison_overrides
    FAIL test/orm/test_naturalpks.py::TransientExceptionTesst_sqlite_pysqlite::()::test_transient_exception
    FAIL test/orm/test_unitofworkv2.py::NullEvaluatingTest::()::test_no_evalnull_default_bulk_insert
    FAIL test/orm/test_unitofworkv2.py::NullEvaluatingTest::()::test_no_evalnull_default_insert
    

    you will note there's a flag in there called "render_nulls". This is a flag I added recently to help the use case of "bulk inserts", where a large set of inserts need to have the same parameters in order for them to be batched. This flag isn't used by the normal flush() process right now, however. At best there would be some path to enabling this for a flush in some case.

    There is a workaround for this, which is to set your value to "sa.null()" instead. This s an explicit NULL and you'll see NULL rendered in the INSERT (it isn't passed as a parameter however).

  2. Mike Bayer repo owner

    You know I knew we handled this and I thought the "render_nulls" was where we did this, but that led me to forget we have another setting too that is at the column level which IMO this is the best we can do:

    import sqlalchemy as sa
    from sqlalchemy.orm import sessionmaker
    from sqlalchemy.ext.declarative import declarative_base
    
    
    engine = sa.create_engine(
        'postgres://postgres@localhost/test'
    )
    engine.echo = True
    Base = declarative_base()
    Session = sessionmaker(bind=engine)
    session = Session()
    
    
    class Question(Base):
        __tablename__ = 'question'
        id = sa.Column(sa.Integer, primary_key=True)
    
        weight = sa.Column(sa.Integer().evaluates_none(), default=1)
    
    
    Base.metadata.create_all(bind=session.bind)
    
    
    q = Question(weight=None)
    session.add(q)
    session.commit()
    
    assert q.weight is None
    
  3. Konsta Vesterinen reporter

    Thanks for the quick response and explaining this thoroughly. evaluates_none() solves this for me.

  4. Log in to comment