Skip to content

Commit

Permalink
Implement checkfirst for Index.create(), Index.drop()
Browse files Browse the repository at this point in the history
The :meth:`.Index.create` and :meth:`.Index.drop` methods now have a
parameter :paramref:`.Index.create.checkfirst`, in the same way as that of
:class:`.Table` and :class:`.Sequence`, which when enabled will cause the
operation to detect if the index exists (or not) before performing a create
or drop operation.

Fixes: #527
Change-Id: Idf994bc016359d0ae86cc64ccb20378115cb66d6
  • Loading branch information
zzzeek committed Aug 7, 2019
1 parent 6a622c6 commit d8da7f5
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 9 deletions.
10 changes: 10 additions & 0 deletions doc/build/changelog/unreleased_14/527.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.. change::
:tags: usecase, sql
:tickets: 527

The :meth:`.Index.create` and :meth:`.Index.drop` methods now have a
parameter :paramref:`.Index.create.checkfirst`, in the same way as that of
:class:`.Table` and :class:`.Sequence`, which when enabled will cause the
operation to detect if the index exists (or not) before performing a create
or drop operation.

9 changes: 9 additions & 0 deletions lib/sqlalchemy/engine/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,15 @@ def get_pk_constraint(self, conn, table_name, schema=None, **kw):
)
}

def has_index(self, connection, table_name, index_name, schema=None):
if not self.has_table(connection, table_name, schema=schema):
return False
for idx in self.get_indexes(connection, table_name, schema=schema):
if idx["name"] == index_name:
return True
else:
return False

def validate_identifier(self, ident):
if len(ident) > self.max_identifier_length:
raise exc.IdentifierError(
Expand Down
18 changes: 18 additions & 0 deletions lib/sqlalchemy/engine/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,24 @@ def has_table(self, connection, table_name, schema=None, **kw):

raise NotImplementedError()

def has_index(self, connection, table_name, index_name, schema=None):
"""Check the existence of a particular index name in the database.
Given a :class:`.Connection` object, a string
`table_name` and stiring index name, return True if an index of the
given name on the given table exists, false otherwise.
The :class:`.DefaultDialect` implements this in terms of the
:meth:`.Dialect.has_table` and :meth:`.Dialect.get_indexes` methods,
however dialects can implement a more performant version.
.. versionadded:: 1.4
"""

raise NotImplementedError()

def has_sequence(self, connection, sequence_name, schema=None, **kw):
"""Check the existence of a particular sequence in the database.
Expand Down
33 changes: 30 additions & 3 deletions lib/sqlalchemy/sql/ddl.py
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,17 @@ def _can_create_table(self, table):
self.connection, table.name, schema=effective_schema
)

def _can_create_index(self, index):
effective_schema = self.connection.schema_for_object(index.table)
if effective_schema:
self.dialect.validate_identifier(effective_schema)
return not self.checkfirst or not self.dialect.has_index(
self.connection,
index.table.name,
index.name,
schema=effective_schema,
)

def _can_create_sequence(self, sequence):
effective_schema = self.connection.schema_for_object(sequence)

Expand Down Expand Up @@ -831,7 +842,7 @@ def visit_table(

if hasattr(table, "indexes"):
for index in table.indexes:
self.traverse_single(index)
self.traverse_single(index, create_ok=True)

if self.dialect.supports_comments and not self.dialect.inline_comments:
if table.comment is not None:
Expand Down Expand Up @@ -859,7 +870,9 @@ def visit_sequence(self, sequence, create_ok=False):
return
self.connection.execute(CreateSequence(sequence))

def visit_index(self, index):
def visit_index(self, index, create_ok=False):
if not create_ok and not self._can_create_index(index):
return
self.connection.execute(CreateIndex(index))


Expand Down Expand Up @@ -973,6 +986,17 @@ def _can_drop_table(self, table):
self.connection, table.name, schema=effective_schema
)

def _can_drop_index(self, index):
effective_schema = self.connection.schema_for_object(index.table)
if effective_schema:
self.dialect.validate_identifier(effective_schema)
return not self.checkfirst or self.dialect.has_index(
self.connection,
index.table.name,
index.name,
schema=effective_schema,
)

def _can_drop_sequence(self, sequence):
effective_schema = self.connection.schema_for_object(sequence)
return self.dialect.supports_sequences and (
Expand All @@ -985,7 +1009,10 @@ def _can_drop_sequence(self, sequence):
)
)

def visit_index(self, index):
def visit_index(self, index, drop_ok=False):
if not drop_ok and not self._can_drop_index(index):
return

self.connection.execute(DropIndex(index))

def visit_table(self, table, drop_ok=False, _is_metadata_operation=False):
Expand Down
8 changes: 4 additions & 4 deletions lib/sqlalchemy/sql/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -3670,7 +3670,7 @@ def bind(self):

return self.table.bind

def create(self, bind=None):
def create(self, bind=None, checkfirst=False):
"""Issue a ``CREATE`` statement for this
:class:`.Index`, using the given :class:`.Connectable`
for connectivity.
Expand All @@ -3682,10 +3682,10 @@ def create(self, bind=None):
"""
if bind is None:
bind = _bind_or_error(self)
bind._run_ddl_visitor(ddl.SchemaGenerator, self)
bind._run_ddl_visitor(ddl.SchemaGenerator, self, checkfirst=checkfirst)
return self

def drop(self, bind=None):
def drop(self, bind=None, checkfirst=False):
"""Issue a ``DROP`` statement for this
:class:`.Index`, using the given :class:`.Connectable`
for connectivity.
Expand All @@ -3697,7 +3697,7 @@ def drop(self, bind=None):
"""
if bind is None:
bind = _bind_or_error(self)
bind._run_ddl_visitor(ddl.SchemaDropper, self)
bind._run_ddl_visitor(ddl.SchemaDropper, self, checkfirst=checkfirst)

def __repr__(self):
return "Index(%s)" % (
Expand Down
66 changes: 65 additions & 1 deletion lib/sqlalchemy/testing/suite/test_reflection.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,65 @@ def test_has_table_schema(self):
)


class HasIndexTest(fixtures.TablesTest):
__backend__ = True

@classmethod
def define_tables(cls, metadata):
tt = Table(
"test_table",
metadata,
Column("id", Integer, primary_key=True),
Column("data", String(50)),
)
Index("my_idx", tt.c.data)

if testing.requires.schemas.enabled:
tt = Table(
"test_table",
metadata,
Column("id", Integer, primary_key=True),
Column("data", String(50)),
schema=config.test_schema,
)
Index("my_idx_s", tt.c.data)

def test_has_index(self):
with config.db.begin() as conn:
assert config.db.dialect.has_index(conn, "test_table", "my_idx")
assert not config.db.dialect.has_index(
conn, "test_table", "my_idx_s"
)
assert not config.db.dialect.has_index(
conn, "nonexistent_table", "my_idx"
)
assert not config.db.dialect.has_index(
conn, "test_table", "nonexistent_idx"
)

@testing.requires.schemas
def test_has_index_schema(self):
with config.db.begin() as conn:
assert config.db.dialect.has_index(
conn, "test_table", "my_idx_s", schema=config.test_schema
)
assert not config.db.dialect.has_index(
conn, "test_table", "my_idx", schema=config.test_schema
)
assert not config.db.dialect.has_index(
conn,
"nonexistent_table",
"my_idx_s",
schema=config.test_schema,
)
assert not config.db.dialect.has_index(
conn,
"test_table",
"nonexistent_idx_s",
schema=config.test_schema,
)


class ComponentReflectionTest(fixtures.TablesTest):
run_inserts = run_deletes = None

Expand Down Expand Up @@ -1129,4 +1188,9 @@ def test_get_table_names(self):
eq_(tablenames[1].upper(), tablenames[1].lower())


__all__ = ("ComponentReflectionTest", "HasTableTest", "NormalizedNameTest")
__all__ = (
"ComponentReflectionTest",
"HasTableTest",
"HasIndexTest",
"NormalizedNameTest",
)
118 changes: 117 additions & 1 deletion test/sql/test_ddlemit.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from sqlalchemy import Column
from sqlalchemy import ForeignKey
from sqlalchemy import Index
from sqlalchemy import Integer
from sqlalchemy import MetaData
from sqlalchemy import schema
Expand All @@ -16,11 +17,15 @@ def _mock_connection(self, item_exists):
def has_item(connection, name, schema):
return item_exists(name)

def has_index(connection, tablename, idxname, schema):
return item_exists(idxname)

return Mock(
dialect=Mock(
supports_sequences=True,
has_table=Mock(side_effect=has_item),
has_sequence=Mock(side_effect=has_item),
has_index=Mock(side_effect=has_index),
supports_comments=True,
inline_comments=False,
)
Expand Down Expand Up @@ -86,6 +91,12 @@ def _fk_fixture_one(self):
t2 = Table("t2", m, Column("id", Integer, primary_key=True))
return m, t1, t2

def _table_index_fixture(self):
m = MetaData()
t1 = Table("t1", m, Column("x", Integer), Column("y", Integer))
i1 = Index("my_idx", t1.c.x, t1.c.y)
return m, t1, i1

def _table_seq_fixture(self):
m = MetaData()

Expand Down Expand Up @@ -130,6 +141,105 @@ def test_drop_seq_checkfirst(self):

self._assert_drop([t1, s1], generator, m)

def test_create_table_index_checkfirst(self):
"""create table that doesn't exist should not require a check
on the index"""

m, t1, i1 = self._table_index_fixture()

def exists(name):
if name == "my_idx":
raise NotImplementedError()
else:
return False

generator = self._mock_create_fixture(True, [t1], item_exists=exists)
self._assert_create([t1, i1], generator, t1)

def test_create_table_exists_index_checkfirst(self):
"""for the moment, if the table *does* exist, we are not checking
for the index. this can possibly be changed."""

m, t1, i1 = self._table_index_fixture()

def exists(name):
if name == "my_idx":
raise NotImplementedError()
else:
return True

generator = self._mock_create_fixture(True, [t1], item_exists=exists)
# nothing is created
self._assert_create([], generator, t1)

def test_drop_table_index_checkfirst(self):
m, t1, i1 = self._table_index_fixture()

def exists(name):
if name == "my_idx":
raise NotImplementedError()
else:
return True

generator = self._mock_drop_fixture(True, [t1], item_exists=exists)
self._assert_drop_tables([t1], generator, t1)

def test_create_index_checkfirst_exists(self):
m, t1, i1 = self._table_index_fixture()
generator = self._mock_create_fixture(
True, [i1], item_exists=lambda idx: True
)
self._assert_create_index([], generator, i1)

def test_create_index_checkfirst_doesnt_exist(self):
m, t1, i1 = self._table_index_fixture()
generator = self._mock_create_fixture(
True, [i1], item_exists=lambda idx: False
)
self._assert_create_index([i1], generator, i1)

def test_create_index_nocheck_exists(self):
m, t1, i1 = self._table_index_fixture()
generator = self._mock_create_fixture(
False, [i1], item_exists=lambda idx: True
)
self._assert_create_index([i1], generator, i1)

def test_create_index_nocheck_doesnt_exist(self):
m, t1, i1 = self._table_index_fixture()
generator = self._mock_create_fixture(
False, [i1], item_exists=lambda idx: False
)
self._assert_create_index([i1], generator, i1)

def test_drop_index_checkfirst_exists(self):
m, t1, i1 = self._table_index_fixture()
generator = self._mock_drop_fixture(
True, [i1], item_exists=lambda idx: True
)
self._assert_drop_index([i1], generator, i1)

def test_drop_index_checkfirst_doesnt_exist(self):
m, t1, i1 = self._table_index_fixture()
generator = self._mock_drop_fixture(
True, [i1], item_exists=lambda idx: False
)
self._assert_drop_index([], generator, i1)

def test_drop_index_nocheck_exists(self):
m, t1, i1 = self._table_index_fixture()
generator = self._mock_drop_fixture(
False, [i1], item_exists=lambda idx: True
)
self._assert_drop_index([i1], generator, i1)

def test_drop_index_nocheck_doesnt_exist(self):
m, t1, i1 = self._table_index_fixture()
generator = self._mock_drop_fixture(
False, [i1], item_exists=lambda idx: False
)
self._assert_drop_index([i1], generator, i1)

def test_create_collection_checkfirst(self):
m, t1, t2, t3, t4, t5 = self._table_fixture()
generator = self._mock_create_fixture(
Expand Down Expand Up @@ -240,7 +350,7 @@ def _assert_drop_tables(self, elements, generator, argument):

def _assert_create(self, elements, generator, argument):
self._assert_ddl(
(schema.CreateTable, schema.CreateSequence),
(schema.CreateTable, schema.CreateSequence, schema.CreateIndex),
elements,
generator,
argument,
Expand Down Expand Up @@ -282,6 +392,12 @@ def _assert_create_comment(self, elements, generator, argument):
argument,
)

def _assert_create_index(self, elements, generator, argument):
self._assert_ddl((schema.CreateIndex,), elements, generator, argument)

def _assert_drop_index(self, elements, generator, argument):
self._assert_ddl((schema.DropIndex,), elements, generator, argument)

def _assert_ddl(self, ddl_cls, elements, generator, argument):
generator.traverse_single(argument)
for call_ in generator.connection.execute.mock_calls:
Expand Down

0 comments on commit d8da7f5

Please sign in to comment.