Skip to content

Commit

Permalink
Add support for identity columns
Browse files Browse the repository at this point in the history
  • Loading branch information
kasium committed Feb 19, 2024
1 parent ecf902e commit 3f4198e
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 35 deletions.
42 changes: 16 additions & 26 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,22 @@ insert/upsert/merge implementation is available in SQLAlchemy (see https://githu
statement upsert(stuff).values(id=1, data="some").filter_by(id=1)
conn.execute(statement)
Identity
~~~~~~~~
Identity columns are fully supported but not reflection of those.

Auto-increment
~~~~~~~~~~~~~~
SAP HANA only supports auto-increment with identity columns, therefore an identity will be rendered
if needed. This means that the the following constructs are equivalent:

* ``Column('some', Integer, autoincrement=True)``
* ``Column('some', Integer, Identity, autoincrement=True)``
* ``Column('some', Integer, Identity, autoincrement=True)``

Note, that for ``autoincrement=True`` a post-execute statement execution is needed to fetch the
inserted identity value which might affect performance.

Alembic
-------
The sqlalchemy-hana dialect also contains a dialect for ``alembic``.
Expand Down Expand Up @@ -349,32 +365,6 @@ error and raise a more specific exception if possible.
# if you reach this line, either the wrapped error of DBAPIError was not a hdbcli error
# of no more specific exception was found
Cookbook
--------

IDENTITY Feature
~~~~~~~~~~~~~~~~
SAP HANA also comes with an option to have an ``IDENTITY`` column which can also be used to create
new primary key values for integer-based primary key columns.
Built-in support for rendering of ``IDENTITY`` is not available yet, however the following compilation
hook may be used to make use of
the IDENTITY feature.

.. code-block:: python
from sqlalchemy.schema import CreateColumn
from sqlalchemy.ext.compiler import compiles
@compiles(CreateColumn, 'hana')
def use_identity(element, compiler, **kw):
text = compiler.visit_create_column(element, **kw)
text = text.replace('NOT NULL', 'NOT NULL GENERATED BY DEFAULT AS IDENTITY')
return text
t = Table('t', meta, Column('id', Integer, primary_key=True), Column('data', String))
t.create(engine)
Development Setup
-----------------
We recommend the usage of ``pyenv`` to install a proper 3.11 python version for development.
Expand Down
19 changes: 17 additions & 2 deletions sqlalchemy_hana/dialect.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import sqlalchemy
from sqlalchemy import (
Computed,
Identity,
Integer,
PrimaryKeyConstraint,
Sequence,
Expand All @@ -20,6 +21,7 @@
util,
)
from sqlalchemy.engine import Connection, default, reflection
from sqlalchemy.schema import CreateColumn
from sqlalchemy.sql import Select, compiler, sqltypes
from sqlalchemy.sql.ddl import _DropView as BaseDropView
from sqlalchemy.sql.elements import (
Expand Down Expand Up @@ -436,12 +438,25 @@ def visit_drop_view(self, drop: DropView | BaseDropView, **kw: Any) -> str:
return f"DROP VIEW {self.preparer.format_table(drop.element)}"
return f"DROP VIEW {drop.name}"

def visit_create_column(
self, create: CreateColumn, first_pk: bool = False, **kw: Any
) -> str:
if create.element.autoincrement is True and not create.element.identity:
create.element.identity = Identity()
return super().visit_create_column(create, first_pk, **kw)


class HANAExecutionContext(default.DefaultExecutionContext):
def fire_sequence(self, seq: Sequence, type_: Integer) -> int:
seq = self.identifier_preparer.format_sequence(seq)
return self._execute_scalar(f"SELECT {seq}.NEXTVAL FROM DUMMY", type_)

def get_lastrowid(self) -> int:
self.cursor.execute("SELECT CURRENT_IDENTITY_VALUE () FROM DUMMY")
res = self.cursor.fetchone()
assert res, "No lastrowid available"
return res[0]


class HANAInspector(reflection.Inspector):
dialect: HANAHDBCLIDialect
Expand Down Expand Up @@ -475,14 +490,14 @@ class HANAHDBCLIDialect(default.DefaultDialect):

div_is_floordiv = False
implicit_returning = False
postfetch_lastrowid = False
postfetch_lastrowid = True
requires_name_normalize = True
returns_native_bytes = False
supports_comments = True
supports_default_values = False
supports_empty_insert = False
supports_for_update_of = True
supports_identity_columns = False
supports_identity_columns = True
supports_is_distinct_from = True
supports_native_boolean = True
supports_native_decimal = True
Expand Down
15 changes: 9 additions & 6 deletions test/requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ def two_phase_transactions(self) -> compound:

@property
def autoincrement_without_sequence(self) -> compound:
# Not supported yet
return exclusions.closed()
return exclusions.open()

@property
def isolation_level(self) -> compound:
Expand Down Expand Up @@ -302,16 +301,20 @@ def computed_columns_default_persisted(self) -> compound:
def computed_columns_reflect_persisted(self) -> compound:
return exclusions.open()

# identity columns are not supported yet

@property
def identity_columns(self) -> compound:
return exclusions.closed()
return exclusions.open()

@property
def identity_columns_alter(self) -> compound:
# SAP HANA does not return the semantics of an identity column
# Therefore, no diff can be calculated
return exclusions.closed()

@property
def identity_columns_standard(self) -> compound:
return exclusions.open()

@property
def autoincrement_insert(self) -> compound:
return exclusions.closed()
return exclusions.open()
25 changes: 25 additions & 0 deletions test/test_select.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""SQL select tests."""

from __future__ import annotations

from sqlalchemy import Integer, String
from sqlalchemy.testing.assertions import eq_
from sqlalchemy.testing.fixtures import TablesTest
from sqlalchemy.testing.schema import Column, Table


class SelectTest(TablesTest):

@classmethod
def define_tables(cls, metadata):
Table(
"tbl",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("desc", String(100)),
)

def test_autoincrement(self, connection):
res = connection.execute(self.tables.tbl.insert(), {"desc": "row"})
res = connection.execute(self.tables.tbl.select()).first()
eq_(res, (1, "row"))
28 changes: 27 additions & 1 deletion test/test_sqlalchemy_dialect_suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
inspect,
testing,
)
from sqlalchemy.testing.assertions import eq_
from sqlalchemy.exc import DBAPIError
from sqlalchemy.testing.assertions import assert_raises, eq_
from sqlalchemy.testing.provision import temp_table_keyword_args
from sqlalchemy.testing.schema import Column, Table
from sqlalchemy.testing.suite import * # noqa: F401, F403
Expand All @@ -26,6 +27,12 @@
from sqlalchemy.testing.suite.test_reflection import (
ComponentReflectionTestExtra as _ComponentReflectionTestExtra,
)
from sqlalchemy.testing.suite.test_reflection import (
IdentityReflectionTest as _IdentityReflectionTest,
)
from sqlalchemy.testing.suite.test_select import (
IdentityColumnTest as _IdentityColumnTest,
)

# Import dialect test suite provided by SQLAlchemy into SQLAlchemy-HANA test collection.
# Please don't add other tests in this file. Only adjust or overview SQLAlchemy tests
Expand Down Expand Up @@ -134,3 +141,22 @@ def test_select_recursive_round_trip(self, connection):

def test_insert_from_select_round_trip(self, connection):
pytest.skip("Insert CTEs are not supported by SAP HANA")


class IdentityReflectionTest(_IdentityReflectionTest):
def test_reflect_identity(self, connection):
pytest.skip("Identity column reflection is not supported")

def test_reflect_identity_schema(self, connection):
pytest.skip("Identity column reflection is not supported")


class IdentityColumnTest(_IdentityColumnTest):
def test_insert_always_error(self, connection):
def fn():
connection.execute(
self.tables.tbl_a.insert(),
[{"id": 200, "desc": "a"}],
)

assert_raises((DBAPIError,), fn)

0 comments on commit 3f4198e

Please sign in to comment.