This repository was archived by the owner on Mar 13, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 35
feat: implement UNIQUE constraints creation mechanism #24
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
f4b82cf
feat: implement UNIQUE constraints creation mechanism
d2534e0
don't run all the reflection tests yet
9fbbc68
Merge remote-tracking branch 'origin/main' into unique_indexes
fa8a9ca
Merge branch 'main' into unique_indexes
b376932
Merge branch 'main' into unique_indexes
1ea934e
Merge branch 'main' into unique_indexes
fd774c9
Merge branch 'main' into unique_indexes
c876f03
EOF
456cd11
override the test data definitions
6f0c8c7
Merge branch 'unique_indexes' of https://github.com/cloudspannerecosy…
be9208f
Merge branch 'main' into unique_indexes
84cb11f
Merge branch 'main' into unique_indexes
AVaksman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,16 +14,29 @@ | |
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| import operator | ||
| import pytest | ||
|
|
||
| from sqlalchemy.testing import config, db | ||
| import sqlalchemy | ||
| from sqlalchemy import inspect | ||
| from sqlalchemy import testing | ||
| from sqlalchemy import ForeignKey | ||
| from sqlalchemy import MetaData | ||
| from sqlalchemy.schema import DDL | ||
| from sqlalchemy.testing import config | ||
| from sqlalchemy.testing import db | ||
| from sqlalchemy.testing import eq_ | ||
| from sqlalchemy.testing import provide_metadata | ||
| from sqlalchemy.testing.provision import temp_table_keyword_args | ||
| from sqlalchemy.testing.schema import Column | ||
| from sqlalchemy.testing.schema import Table | ||
| from sqlalchemy import bindparam | ||
| from sqlalchemy import case | ||
| from sqlalchemy import literal | ||
| from sqlalchemy import literal_column | ||
|
|
||
| from sqlalchemy import bindparam, case, literal, select, util | ||
| from sqlalchemy import select | ||
| from sqlalchemy import util | ||
| from sqlalchemy import event | ||
| from sqlalchemy import exists | ||
| from sqlalchemy import Boolean | ||
| from sqlalchemy import String | ||
|
|
@@ -32,8 +45,10 @@ | |
|
|
||
| from google.api_core.datetime_helpers import DatetimeWithNanoseconds | ||
|
|
||
| from sqlalchemy.testing.suite.test_ddl import * # noqa: F401, F403 | ||
| from google.cloud import spanner_dbapi | ||
|
|
||
| from sqlalchemy.testing.suite.test_cte import * # noqa: F401, F403 | ||
| from sqlalchemy.testing.suite.test_ddl import * # noqa: F401, F403 | ||
| from sqlalchemy.testing.suite.test_dialect import * # noqa: F401, F403 | ||
| from sqlalchemy.testing.suite.test_update_delete import * # noqa: F401, F403 | ||
|
|
||
|
|
@@ -42,8 +57,10 @@ | |
| from sqlalchemy.testing.suite.test_ddl import ( | ||
| LongNameBlowoutTest as _LongNameBlowoutTest, | ||
| ) | ||
|
|
||
| from sqlalchemy.testing.suite.test_dialect import EscapingTest as _EscapingTest | ||
| from sqlalchemy.testing.suite.test_reflection import ( | ||
| ComponentReflectionTest as _ComponentReflectionTest, | ||
| ) | ||
| from sqlalchemy.testing.suite.test_select import ExistsTest as _ExistsTest | ||
| from sqlalchemy.testing.suite.test_types import BooleanTest as _BooleanTest | ||
| from sqlalchemy.testing.suite.test_types import IntegerTest as _IntegerTest | ||
|
|
@@ -102,6 +119,29 @@ def test_percent_sign_round_trip(self): | |
|
|
||
|
|
||
| class CTETest(_CTETest): | ||
| @classmethod | ||
| def define_tables(cls, metadata): | ||
| """ | ||
| The original method creates a foreign key without a name, | ||
| which causes troubles on test cleanup. Overriding the | ||
| method to explicitly set a foreign key name. | ||
| """ | ||
| Table( | ||
| "some_table", | ||
| metadata, | ||
| Column("id", Integer, primary_key=True), | ||
| Column("data", String(50)), | ||
| Column("parent_id", ForeignKey("some_table.id", name="fk_some_table")), | ||
| ) | ||
|
|
||
| Table( | ||
| "some_other_table", | ||
| metadata, | ||
| Column("id", Integer, primary_key=True), | ||
| Column("data", String(50)), | ||
| Column("parent_id", Integer), | ||
| ) | ||
|
|
||
| @pytest.mark.skip("INSERT from WITH subquery is not supported") | ||
| def test_insert_from_select_round_trip(self): | ||
| """ | ||
|
|
@@ -509,3 +549,136 @@ def _literal_round_trip(self, type_, input_, output, filter_=None): | |
| if filter_ is not None: | ||
| value = filter_(value) | ||
| assert value in output | ||
|
|
||
|
|
||
| class ComponentReflectionTest(_ComponentReflectionTest): | ||
| @classmethod | ||
| def define_temp_tables(cls, metadata): | ||
| """ | ||
| SPANNER OVERRIDE: | ||
|
|
||
| In Cloud Spanner unique indexes are used instead of directly | ||
| creating unique constraints. Overriding the test to replace | ||
| constraints with indexes in testing data. | ||
| """ | ||
| kw = temp_table_keyword_args(config, config.db) | ||
| user_tmp = Table( | ||
| "user_tmp", | ||
| metadata, | ||
| Column("id", sqlalchemy.INT, primary_key=True), | ||
| Column("name", sqlalchemy.VARCHAR(50)), | ||
| Column("foo", sqlalchemy.INT), | ||
| sqlalchemy.Index("user_tmp_uq", "name", unique=True), | ||
| sqlalchemy.Index("user_tmp_ix", "foo"), | ||
| **kw | ||
| ) | ||
| if ( | ||
| testing.requires.view_reflection.enabled | ||
| and testing.requires.temporary_views.enabled | ||
| ): | ||
| event.listen( | ||
| user_tmp, | ||
| "after_create", | ||
| DDL("create temporary view user_tmp_v as " "select * from user_tmp"), | ||
| ) | ||
| event.listen(user_tmp, "before_drop", DDL("drop view user_tmp_v")) | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The method creates test tables. On line 244 instead of |
||
|
|
||
| @testing.provide_metadata | ||
| def _test_get_unique_constraints(self, schema=None): | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test is not passing yet because of another error: one of the test columns is called |
||
| """ | ||
| SPANNER OVERRIDE: | ||
|
|
||
| In Cloud Spanner unique indexes are used instead of directly | ||
| creating unique constraints. Overriding the test to replace | ||
| constraints with indexes in testing data. | ||
| """ | ||
| # SQLite dialect needs to parse the names of the constraints | ||
| # separately from what it gets from PRAGMA index_list(), and | ||
| # then matches them up. so same set of column_names in two | ||
| # constraints will confuse it. Perhaps we should no longer | ||
| # bother with index_list() here since we have the whole | ||
| # CREATE TABLE? | ||
| uniques = sorted( | ||
| [ | ||
| {"name": "unique_a", "column_names": ["a"]}, | ||
| {"name": "unique_a_b_c", "column_names": ["a", "b", "c"]}, | ||
| {"name": "unique_c_a_b", "column_names": ["c", "a", "b"]}, | ||
| {"name": "unique_asc_key", "column_names": ["asc", "key"]}, | ||
| {"name": "i.have.dots", "column_names": ["b"]}, | ||
| {"name": "i have spaces", "column_names": ["c"]}, | ||
| ], | ||
| key=operator.itemgetter("name"), | ||
| ) | ||
| orig_meta = self.metadata | ||
| table = Table( | ||
| "testtbl", | ||
| orig_meta, | ||
| Column("a", sqlalchemy.String(20)), | ||
| Column("b", sqlalchemy.String(30)), | ||
| Column("c", sqlalchemy.Integer), | ||
| # reserved identifiers | ||
| Column("asc", sqlalchemy.String(30)), | ||
| Column("key", sqlalchemy.String(30)), | ||
| schema=schema, | ||
| ) | ||
| for uc in uniques: | ||
| table.append_constraint( | ||
| sqlalchemy.Index(uc["name"], *uc["column_names"], unique=True) | ||
| ) | ||
| orig_meta.create_all() | ||
|
|
||
| inspector = inspect(orig_meta.bind) | ||
| reflected = sorted( | ||
| inspector.get_unique_constraints("testtbl", schema=schema), | ||
| key=operator.itemgetter("name"), | ||
| ) | ||
|
|
||
| names_that_duplicate_index = set() | ||
|
|
||
| for orig, refl in zip(uniques, reflected): | ||
| # Different dialects handle duplicate index and constraints | ||
| # differently, so ignore this flag | ||
| dupe = refl.pop("duplicates_index", None) | ||
| if dupe: | ||
| names_that_duplicate_index.add(dupe) | ||
| eq_(orig, refl) | ||
|
|
||
| reflected_metadata = MetaData() | ||
| reflected = Table( | ||
| "testtbl", reflected_metadata, autoload_with=orig_meta.bind, schema=schema, | ||
| ) | ||
|
|
||
| # test "deduplicates for index" logic. MySQL and Oracle | ||
| # "unique constraints" are actually unique indexes (with possible | ||
| # exception of a unique that is a dupe of another one in the case | ||
| # of Oracle). make sure # they aren't duplicated. | ||
| idx_names = set([idx.name for idx in reflected.indexes]) | ||
| uq_names = set( | ||
| [ | ||
| uq.name | ||
| for uq in reflected.constraints | ||
| if isinstance(uq, sqlalchemy.UniqueConstraint) | ||
| ] | ||
| ).difference(["unique_c_a_b"]) | ||
|
|
||
| assert not idx_names.intersection(uq_names) | ||
| if names_that_duplicate_index: | ||
| eq_(names_that_duplicate_index, idx_names) | ||
| eq_(uq_names, set()) | ||
|
|
||
| @testing.provide_metadata | ||
| def test_unique_constraint_raises(self): | ||
| """ | ||
| Checking that unique constraint creation | ||
| fails due to a ProgrammingError. | ||
| """ | ||
| Table( | ||
| "user_tmp_failure", | ||
| self.metadata, | ||
| Column("id", sqlalchemy.INT, primary_key=True), | ||
| Column("name", sqlalchemy.VARCHAR(50)), | ||
| sqlalchemy.UniqueConstraint("name", name="user_tmp_uq"), | ||
| ) | ||
|
|
||
| with pytest.raises(spanner_dbapi.exceptions.ProgrammingError): | ||
| self.metadata.create_all() | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Explicitly setting a foreign key name