Skip to content

Commit 4dfc236

Browse files
committed
alembic support
1 parent 607a1ad commit 4dfc236

File tree

8 files changed

+192
-18
lines changed

8 files changed

+192
-18
lines changed

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ flake8
33
pytest
44
black
55
twine
6+
alembic

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = sqlalchemy-iris
3-
version = 0.9.1
3+
version = 0.10.0
44
description = InterSystems IRIS for SQLAlchemy
55
long_description = file: README.md
66
url = https://github.com/caretdev/sqlalchemy-iris

sqlalchemy_iris/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
from . import base
44
from . import iris
55

6+
try:
7+
import alembic
8+
except ImportError:
9+
pass
10+
else:
11+
from .alembic import IRISImpl
12+
613
from .base import BIGINT
714
from .base import BIT
815
from .base import DATE

sqlalchemy_iris/alembic.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import logging
2+
3+
from sqlalchemy.ext.compiler import compiles
4+
from alembic.ddl import DefaultImpl
5+
from alembic.ddl.base import ColumnNullable
6+
from alembic.ddl.base import ColumnType
7+
from alembic.ddl.base import ColumnName
8+
from alembic.ddl.base import Column
9+
from alembic.ddl.base import alter_table
10+
from alembic.ddl.base import alter_column
11+
from alembic.ddl.base import format_type
12+
from alembic.ddl.base import format_column_name
13+
14+
from .base import IRISDDLCompiler
15+
16+
log = logging.getLogger(__name__)
17+
18+
19+
class IRISImpl(DefaultImpl):
20+
__dialect__ = "iris"
21+
22+
type_synonyms = DefaultImpl.type_synonyms + (
23+
{"BLOB", "LONGVARBINARY"},
24+
{"DOUBLE", "FLOAT"},
25+
{"DATETIME", "TIMESTAMP"},
26+
)
27+
28+
def compare_type(self, inspector_column: Column, metadata_column: Column) -> bool:
29+
# Don't change type of IDENTITY column
30+
if (
31+
metadata_column.primary_key
32+
and metadata_column is metadata_column.table._autoincrement_column
33+
):
34+
return False
35+
36+
return super().compare_type(inspector_column, metadata_column)
37+
38+
def compare_server_default(
39+
self,
40+
inspector_column: Column,
41+
metadata_column: Column,
42+
rendered_metadata_default,
43+
rendered_inspector_default,
44+
):
45+
# don't do defaults for IDENTITY columns
46+
if (
47+
metadata_column.primary_key
48+
and metadata_column is metadata_column.table._autoincrement_column
49+
):
50+
return False
51+
52+
return super().compare_server_default(
53+
inspector_column,
54+
metadata_column,
55+
rendered_metadata_default,
56+
rendered_inspector_default,
57+
)
58+
59+
def correct_for_autogen_constraints(
60+
self,
61+
conn_unique_constraints,
62+
conn_indexes,
63+
metadata_unique_constraints,
64+
metadata_indexes,
65+
):
66+
67+
doubled_constraints = {
68+
index
69+
for index in conn_indexes
70+
if index.info.get("duplicates_constraint")
71+
}
72+
73+
for ix in doubled_constraints:
74+
conn_indexes.remove(ix)
75+
76+
# if not sqla_compat.sqla_2:
77+
# self._skip_functional_indexes(metadata_indexes, conn_indexes)
78+
79+
@compiles(ColumnNullable, "iris")
80+
def visit_column_nullable(
81+
element: ColumnNullable, compiler: IRISDDLCompiler, **kw
82+
) -> str:
83+
return "%s %s %s" % (
84+
alter_table(compiler, element.table_name, element.schema),
85+
alter_column(compiler, element.column_name),
86+
"NULL" if element.nullable else "NOT NULL",
87+
)
88+
89+
90+
@compiles(ColumnType, "iris")
91+
def visit_column_type(element: ColumnType, compiler: IRISDDLCompiler, **kw) -> str:
92+
return "%s %s %s" % (
93+
alter_table(compiler, element.table_name, element.schema),
94+
alter_column(compiler, element.column_name),
95+
"%s" % format_type(compiler, element.type_),
96+
)
97+
98+
99+
@compiles(ColumnName, "iris")
100+
def visit_rename_column(element: ColumnName, compiler: IRISDDLCompiler, **kw) -> str:
101+
return "%s %s RENAME %s" % (
102+
alter_table(compiler, element.table_name, element.schema),
103+
alter_column(compiler, element.column_name),
104+
format_column_name(compiler, element.newname),
105+
)

sqlalchemy_iris/base.py

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,10 @@ def unique_constraints(cls):
6363
def check_constraints(cls):
6464
return []
6565

66+
6667
from sqlalchemy.types import BIGINT
6768
from sqlalchemy.types import VARCHAR
69+
from sqlalchemy.types import CHAR
6870
from sqlalchemy.types import INTEGER
6971
from sqlalchemy.types import DATE
7072
from sqlalchemy.types import TIMESTAMP
@@ -87,6 +89,7 @@ def check_constraints(cls):
8789
from .types import IRISDate
8890
from .types import IRISDateTime
8991

92+
9093
ischema_names = {
9194
"BIGINT": BIGINT,
9295
"BIT": BIT,
@@ -634,7 +637,7 @@ def visit_computed_column(self, generated, **kwargs):
634637
text = self.sql_compiler.process(
635638
generated.sqltext, include_table=True, literal_binds=True
636639
)
637-
text = re.sub(r"(?<!')(\b[^\d]\w+\b)", r"{\g<1>}", text)
640+
text = re.sub(r"(?<!')(\b[^\W\d]+\w+\b)", r"{\g<1>}", text)
638641
# text = text.replace("'", '"')
639642
text = "COMPUTECODE {Set {*} = %s}" % (text,)
640643
if generated.persisted is False:
@@ -722,6 +725,12 @@ def visit_BIT(self, type_, **kw):
722725
def visit_TEXT(self, type_, **kw):
723726
return "VARCHAR(65535)"
724727

728+
def visit_LONGVARBINARY(self, type_, **kw):
729+
return "LONGVARBINARY"
730+
731+
def visit_DOUBLE(self, type_, **kw):
732+
return "DOUBLE"
733+
725734

726735
class IRISIdentifierPreparer(sql.compiler.IdentifierPreparer):
727736
"""Install IRIS specific reserved words."""
@@ -1428,22 +1437,20 @@ def get_multi_foreign_keys(
14281437
)
14291438
)
14301439

1431-
rs = connection.execute(s)
1440+
rs = connection.execution_options(future_result=True).execute(s)
14321441

14331442
fkeys = util.defaultdict(dict)
14341443

1435-
for row in rs:
1436-
(
1437-
table_name,
1438-
rfknm,
1439-
scol,
1440-
rschema,
1441-
rtbl,
1442-
rcol,
1443-
_, # match rule
1444-
fkuprule,
1445-
fkdelrule,
1446-
) = row
1444+
for row in rs.mappings():
1445+
table_name = row[key_constraints.c.table_name]
1446+
rfknm = row[key_constraints.c.constraint_name]
1447+
scol = row[key_constraints.c.column_name]
1448+
rschema = row[key_constraints_ref.c.table_schema]
1449+
rtbl = row[key_constraints_ref.c.table_name]
1450+
rcol = row[key_constraints_ref.c.column_name]
1451+
_ = row[ref_constraints.c.match_option]
1452+
fkuprule = row[ref_constraints.c.update_rule]
1453+
fkdelrule = row[ref_constraints.c.delete_rule]
14471454

14481455
table_fkey = fkeys[(schema, table_name)]
14491456

@@ -1540,7 +1547,10 @@ def get_multi_columns(
15401547
).outerjoin(
15411548
property,
15421549
sql.and_(
1543-
property.c.SqlFieldName == columns.c.column_name,
1550+
sql.or_(
1551+
property.c.Name == columns.c.column_name,
1552+
property.c.SqlFieldName == columns.c.column_name,
1553+
),
15441554
property.c.parent
15451555
== sql.select(tables.c.classname)
15461556
.where(
@@ -1593,6 +1603,9 @@ def get_multi_columns(
15931603
if coltype is None:
15941604
util.warn("Did not recognize type '%s' of column '%s'" % (type_, name))
15951605
coltype = sqltypes.NULLTYPE
1606+
elif coltype is VARCHAR and charlen == 1:
1607+
# VARCHAR(1) as CHAR
1608+
coltype = CHAR
15961609
else:
15971610
if issubclass(coltype, sqltypes.Numeric):
15981611
kwargs["precision"] = int(numericprec)
@@ -1602,6 +1615,10 @@ def get_multi_columns(
16021615

16031616
coltype = coltype(**kwargs)
16041617

1618+
default = "" if default == "$c(0)" else default
1619+
if default and default.startswith('"'):
1620+
default = "'%s'" % (default[1:-1].replace("'", "''"),)
1621+
16051622
cdict = {
16061623
"name": name,
16071624
"type": coltype,

sqlalchemy_iris/information_schema.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def process_result_value(self, value, dialect):
7676
"PropertyDefinition",
7777
ischema,
7878
Column("parent", String),
79+
Column("Name", String),
7980
Column("SqlFieldName", String),
8081
Column("SqlComputeCode", String),
8182
Column("SqlComputed", Boolean),

sqlalchemy_iris/requirements.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
from sqlalchemy.testing.requirements import SuiteRequirements
22

3+
try:
4+
from alembic.testing.requirements import SuiteRequirements as AlembicRequirements
5+
except: # noqa
6+
from sqlalchemy.testing.requirements import Requirements as BaseRequirements
7+
8+
class AlembicRequirements(BaseRequirements):
9+
pass
10+
11+
312
from sqlalchemy.testing import exclusions
413

514

6-
class Requirements(SuiteRequirements):
15+
class Requirements(SuiteRequirements, AlembicRequirements):
716
@property
817
def array_type(self):
918
return exclusions.closed()
@@ -121,7 +130,6 @@ def foreign_key_constraint_option_reflection_onupdate(self):
121130
def fk_constraint_option_reflection_onupdate_restrict(self):
122131
return exclusions.closed()
123132

124-
125133
@property
126134
def precision_numerics_many_significant_digits(self):
127135
"""target backend supports values with many digits on both sides,
@@ -239,3 +247,13 @@ def unique_index_reflect_as_unique_constraints(self):
239247
"""Target database reflects unique indexes as unique constrains."""
240248

241249
return exclusions.open()
250+
251+
# alembic
252+
253+
@property
254+
def fk_onupdate_restrict(self):
255+
return exclusions.closed()
256+
257+
@property
258+
def fk_ondelete_restrict(self):
259+
return exclusions.closed()

tests/test_alembic.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
try:
2+
import alembic # noqa
3+
except: # noqa
4+
pass
5+
else:
6+
from alembic.testing.suite.test_op import (
7+
BackendAlterColumnTest as _BackendAlterColumnTest,
8+
)
9+
from alembic.testing.suite.test_autogen_diffs import (
10+
AutoincrementTest as _AutoincrementTest,
11+
)
12+
from alembic.testing.suite import * # noqa
13+
14+
class BackendAlterColumnTest(_BackendAlterColumnTest):
15+
def test_rename_column(self):
16+
# IRIS Uppercases new names
17+
self._run_alter_col({}, {"name": "NEWNAME"})
18+
19+
class AutoincrementTest(_AutoincrementTest):
20+
# pk don't change type
21+
def test_alter_column_autoincrement_pk_implicit_true(self):
22+
pass
23+
24+
def test_alter_column_autoincrement_pk_explicit_true(self):
25+
pass

0 commit comments

Comments
 (0)