--- /dev/null
+.. change::
+ :tags: changed, schema
+ :tickets: 5775
+
+ Altered the behavior of the :class:`_schema.Identity` construct such that
+ when applied to a :class:`_schema.Column`, it will automatically imply that
+ the value of :paramref:`_sql.Column.nullable` should default to ``False``,
+ in a similar manner as when the :paramref:`_sql.Column.primary_key`
+ parameter is set to ``True``. This matches the default behavior of all
+ supporting databases where ``IDENTITY`` implies ``NOT NULL``. The
+ PostgreSQL backend is the only one that supports adding ``NULL`` to an
+ ``IDENTITY`` column, which is here supported by passing a ``True`` value
+ for the :paramref:`_sql.Column.nullable` parameter at the same time.
+
.. sourcecode:: sql
CREATE TABLE mytable (
- id INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH 3) NOT NULL,
+ id INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH 3),
...,
PRIMARY KEY (id)
)
.. sourcecode:: sql
CREATE TABLE data (
- id INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH 42 CYCLE)
- NOT NULL,
+ id INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH 42 CYCLE),
data VARCHAR,
PRIMARY KEY (id)
)
Will generate on the backing database as::
CREATE TABLE t (
- id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
+ id INT GENERATED BY DEFAULT AS IDENTITY,
data VARCHAR,
PRIMARY KEY (id)
)
if column.identity is not None:
colspec += " " + self.process(column.identity)
- if not column.nullable:
+ if not column.nullable and not column.identity:
colspec += " NOT NULL"
+ elif column.nullable and column.identity:
+ colspec += " NULL"
return colspec
def visit_check_constraint(self, constraint):
if column.identity is not None:
colspec += " " + self.process(column.identity)
- if not column.nullable:
+ if not column.nullable and not column.identity:
colspec += " NOT NULL"
return colspec
""",
)
+NULL_UNSPECIFIED = util.symbol(
+ "NULL_UNSPECIFIED",
+ """Symbol indicating the "nullable" keyword was not passed to a Column.
+
+ Normally we would expect None to be acceptable for this but some backends
+ such as that of SQL Server place special signficance on a "nullability"
+ value of None.
+
+ """,
+)
+
def _get_table_key(name, schema):
if schema is None:
phrase to be added when generating DDL for the column. When
``True``, will normally generate nothing (in SQL this defaults to
"NULL"), except in some very specific backend-specific edge cases
- where "NULL" may render explicitly. Defaults to ``True`` unless
- :paramref:`_schema.Column.primary_key` is also ``True``,
- in which case it
- defaults to ``False``. This parameter is only used when issuing
- CREATE TABLE statements.
+ where "NULL" may render explicitly.
+ Defaults to ``True`` unless :paramref:`_schema.Column.primary_key`
+ is also ``True`` or the column specifies a :class:`_sql.Identity`,
+ in which case it defaults to ``False``.
+ This parameter is only used when issuing CREATE TABLE statements.
+
+ .. note::
+
+ When the column specifies a :class:`_sql.Identity` this
+ parameter is in general ignored by the DDL compiler. The
+ PostgreSQL database allows nullable identity column by
+ setting this parameter to ``True`` explicitly.
:param onupdate: A scalar, Python callable, or
:class:`~sqlalchemy.sql.expression.ClauseElement` representing a
super(Column, self).__init__(name, type_)
self.key = kwargs.pop("key", name)
- self.primary_key = kwargs.pop("primary_key", False)
- self.nullable = kwargs.pop("nullable", not self.primary_key)
+ self.primary_key = primary_key = kwargs.pop("primary_key", False)
+
+ self._user_defined_nullable = udn = kwargs.pop(
+ "nullable", NULL_UNSPECIFIED
+ )
+
+ if udn is not NULL_UNSPECIFIED:
+ self.nullable = udn
+ else:
+ self.nullable = not primary_key
+
self.default = kwargs.pop("default", None)
self.server_default = kwargs.pop("server_default", None)
self.server_onupdate = kwargs.pop("server_onupdate", None)
def _extra_kwargs(self, **kwargs):
self._validate_dialect_kwargs(kwargs)
- # @property
- # def quote(self):
- # return getattr(self.name, "quote", None)
-
def __str__(self):
if self.name is None:
return "(no name)"
if isinstance(type_, SchemaEventTarget):
type_ = type_.copy(**kw)
+ if self._user_defined_nullable is not NULL_UNSPECIFIED:
+ column_kwargs["nullable"] = self._user_defined_nullable
+
c = self._constructor(
name=self.name,
type_=type_,
key=self.key,
primary_key=self.primary_key,
- nullable=self.nullable,
unique=self.unique,
system=self.system,
# quote=self.quote, # disabled 2013-08-27 (commit 031ef080)
for c in self.columns:
c.primary_key = True
- c.nullable = False
+ if c._user_defined_nullable is NULL_UNSPECIFIED:
+ c.nullable = False
if table_pks:
self.columns.extend(table_pks)
"autoincrement=False"
)
self.column = parent
+
parent.identity = self
- # self.column.server_onupdate = self
- self.column.server_default = self
+ if parent._user_defined_nullable is NULL_UNSPECIFIED:
+ parent.nullable = False
+
+ parent.server_default = self
def _as_for_update(self, for_update):
return self
"(INCREMENT BY 7 START WITH 4))",
)
+ def test_column_identity_null(self):
+ # all other tests are in test_identity_column.py
+ m = MetaData()
+ t = Table(
+ "t",
+ m,
+ Column(
+ "y",
+ Integer,
+ Identity(always=True, start=4, increment=7),
+ nullable=True,
+ ),
+ )
+ self.assert_compile(
+ schema.CreateTable(t),
+ "CREATE TABLE t (y INTEGER GENERATED ALWAYS AS IDENTITY "
+ "(INCREMENT BY 7 START WITH 4) NULL)",
+ )
+
def test_index_extra_include_1(self):
metadata = MetaData()
tbl = Table(
CreateTable(t),
"CREATE TABLE foo_table ("
"foo INTEGER GENERATED ALWAYS AS IDENTITY (START "
- "WITH 3) NOT NULL, UNIQUE (foo))",
+ "WITH 3), UNIQUE (foo))",
)
def test_autoincrement_true(self):
self.assert_compile(
CreateTable(t),
"CREATE TABLE foo_table ("
- "foo INTEGER GENERATED ALWAYS AS IDENTITY (START WITH 3) NOT NULL"
+ "foo INTEGER GENERATED ALWAYS AS IDENTITY (START WITH 3)"
", PRIMARY KEY (foo))",
)
+ def test_nullable_kwarg(self):
+ t = Table(
+ "t",
+ MetaData(),
+ Column("a", Integer(), Identity(), nullable=False),
+ Column("b", Integer(), Identity(), nullable=True),
+ Column("c", Integer(), Identity()),
+ )
+
+ is_(t.c.a.nullable, False)
+ is_(t.c.b.nullable, True)
+ is_(t.c.c.nullable, False)
+
+ nullable = ""
+ if getattr(self, "__dialect__", None) != "default" and testing.against(
+ "postgresql"
+ ):
+ nullable = " NULL"
+
+ self.assert_compile(
+ CreateTable(t),
+ (
+ "CREATE TABLE t ("
+ "a INTEGER GENERATED BY DEFAULT AS IDENTITY, "
+ "b INTEGER GENERATED BY DEFAULT AS IDENTITY%s, "
+ "c INTEGER GENERATED BY DEFAULT AS IDENTITY"
+ ")"
+ )
+ % nullable,
+ )
+
class IdentityDDL(_IdentityDDLFixture, fixtures.TestBase):
# this uses the connection dialect
Column("foo", Integer(), Identity("always", start=3)),
)
self.assert_compile(
- CreateTable(t), "CREATE TABLE foo_table (foo INTEGER)"
+ CreateTable(t), "CREATE TABLE foo_table (foo INTEGER NOT NULL)"
)
assert not t2.c.x.nullable
assert not t1.c.x.nullable
+ def test_pk_can_be_nullable(self):
+ m = MetaData()
+
+ t1 = Table(
+ "t1",
+ m,
+ Column("x", Integer, nullable=True),
+ PrimaryKeyConstraint("x"),
+ )
+
+ t2 = Table(
+ "t2", m, Column("x", Integer, primary_key=True, nullable=True)
+ )
+
+ eq_(list(t1.primary_key), [t1.c.x])
+
+ eq_(list(t2.primary_key), [t2.c.x])
+
+ assert t1.c.x.primary_key
+ assert t2.c.x.primary_key
+
+ assert t2.c.x.nullable
+ assert t1.c.x.nullable
+
def test_must_exist(self):
with testing.expect_raises_message(
exc.InvalidRequestError, "Table 'foo' not defined"