Uh oh!
There was an error while loading. Please reload this page.
This repository was archived by the owner on May 14, 2026. It is now read-only.
- Notifications
You must be signed in to change notification settings - Fork 35
feat: support computed columns#139
Merged
Uh oh!
There was an error while loading. Please reload this page.
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
0c0c9be
feat: support computed columns
2a2d3e3
fix tests
f3ca2b6
override fixture
3d31dfd
override test case
9eabb68
fix imports
1fc815e
skip unsupported case
3e086d3
reflect computed
276a62a
fix
4b3695c
fix cols desc
9887447
erase unsupported condition
13ec44c
add import
ddbb1f7
erase unsupported statement
adf7819
try
55fceef
Revert "try"
4ec4ed8
Merge branch 'main' into computed_columns
larkee 1a86b75
add docstrings
3c8a592
Merge branch 'computed_columns' of https://github.com/cloudspannereco…
12d6d63
Merge branch 'main' into computed_columns
skuruppu 438ccfc
fix: lint
skuruppu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
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 |
|---|---|---|
| @@ -267,6 +267,13 @@ def limit_clause(self, select, **kw): | ||
| class SpannerDDLCompiler(DDLCompiler): | ||
| """Spanner DDL statements compiler.""" | ||
| def visit_computed_column(self, generated, **kw): | ||
| """Computed column operator.""" | ||
| text = "AS (%s) STORED" % self.sql_compiler.process( | ||
| generated.sqltext, include_table=False, literal_binds=True | ||
| ) | ||
| return text | ||
| def visit_drop_table(self, drop_table): | ||
| """ | ||
| Cloud Spanner doesn't drop tables which have indexes | ||
| @@ -492,7 +499,7 @@ def get_columns(self, connection, table_name, schema=None, **kw): | ||
| list: The table every column dict-like description. | ||
| """ | ||
| sql = """ | ||
| SELECT column_name, spanner_type, is_nullable | ||
| SELECT column_name, spanner_type, is_nullable, generation_expression | ||
| FROM information_schema.columns | ||
| WHERE | ||
| table_catalog = '' | ||
| @@ -512,14 +519,20 @@ def get_columns(self, connection, table_name, schema=None, **kw): | ||
| columns = snap.execute_sql(sql) | ||
| for col in columns: | ||
| cols_desc.append( | ||
| { | ||
| "name": col[0], | ||
| "type": self._designate_type(col[1]), | ||
| "nullable": col[2] == "YES", | ||
| "default": None, | ||
| col_desc = { | ||
| "name": col[0], | ||
| "type": self._designate_type(col[1]), | ||
| "nullable": col[2] == "YES", | ||
| "default": None, | ||
| } | ||
| if col[3] is not None: | ||
| col_desc["computed"] = { | ||
| "persisted": True, | ||
| "sqltext": col[3], | ||
| } | ||
| ) | ||
| cols_desc.append(col_desc) | ||
IlyaFaer marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| return cols_desc | ||
| def _designate_type(self, str_repr): | ||
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 |
|---|---|---|
| @@ -29,11 +29,13 @@ | ||
| from sqlalchemy import ForeignKey | ||
| from sqlalchemy import MetaData | ||
| from sqlalchemy.schema import DDL | ||
| from sqlalchemy.schema import Computed | ||
| from sqlalchemy.testing import config | ||
| from sqlalchemy.testing import engines | ||
| from sqlalchemy.testing import eq_ | ||
| from sqlalchemy.testing import provide_metadata, emits_warning | ||
| from sqlalchemy.testing import fixtures | ||
| from sqlalchemy.testing import is_true | ||
| from sqlalchemy.testing.provision import temp_table_keyword_args | ||
| from sqlalchemy.testing.schema import Column | ||
| from sqlalchemy.testing.schema import Table | ||
| @@ -54,6 +56,9 @@ | ||
| from sqlalchemy.types import Numeric | ||
| from sqlalchemy.types import Text | ||
| from sqlalchemy.testing import requires | ||
| from sqlalchemy.testing.fixtures import ( | ||
| ComputedReflectionFixtureTest as _ComputedReflectionFixtureTest, | ||
| ) | ||
| from google.api_core.datetime_helpers import DatetimeWithNanoseconds | ||
| @@ -89,6 +94,7 @@ | ||
| QuotedNameArgumentTest as _QuotedNameArgumentTest, | ||
| ComponentReflectionTest as _ComponentReflectionTest, | ||
| CompositeKeyReflectionTest as _CompositeKeyReflectionTest, | ||
| ComputedReflectionTest as _ComputedReflectionTest, | ||
| ) | ||
| from sqlalchemy.testing.suite.test_results import RowFetchTest as _RowFetchTest | ||
| from sqlalchemy.testing.suite.test_types import ( # noqa: F401, F403 | ||
| @@ -1608,3 +1614,95 @@ def test_staleness(self): | ||
| with self._engine.connect() as connection: | ||
| assert connection.connection.staleness is None | ||
| class ComputedReflectionFixtureTest(_ComputedReflectionFixtureTest): | ||
| @classmethod | ||
| def define_tables(cls, metadata): | ||
| """SPANNER OVERRIDE: | ||
| Avoid using default values for computed columns. | ||
| """ | ||
| Table( | ||
| "computed_default_table", | ||
| metadata, | ||
| Column("id", Integer, primary_key=True), | ||
| Column("normal", Integer), | ||
| Column("computed_col", Integer, Computed("normal + 42")), | ||
| Column("with_default", Integer), | ||
| ) | ||
| t = Table( | ||
| "computed_column_table", | ||
| metadata, | ||
| Column("id", Integer, primary_key=True), | ||
| Column("normal", Integer), | ||
| Column("computed_no_flag", Integer, Computed("normal + 42")), | ||
| ) | ||
| if testing.requires.schemas.enabled: | ||
| t2 = Table( | ||
| "computed_column_table", | ||
| metadata, | ||
| Column("id", Integer, primary_key=True), | ||
| Column("normal", Integer), | ||
| Column("computed_no_flag", Integer, Computed("normal / 42")), | ||
| schema=config.test_schema, | ||
| ) | ||
| if testing.requires.computed_columns_virtual.enabled: | ||
| t.append_column( | ||
| Column( | ||
| "computed_virtual", | ||
| Integer, | ||
| Computed("normal + 2", persisted=False), | ||
| ) | ||
| ) | ||
| if testing.requires.schemas.enabled: | ||
| t2.append_column( | ||
| Column( | ||
| "computed_virtual", | ||
| Integer, | ||
| Computed("normal / 2", persisted=False), | ||
| ) | ||
| ) | ||
| if testing.requires.computed_columns_stored.enabled: | ||
| t.append_column( | ||
| Column( | ||
| "computed_stored", Integer, Computed("normal - 42", persisted=True), | ||
| ) | ||
| ) | ||
| if testing.requires.schemas.enabled: | ||
| t2.append_column( | ||
| Column( | ||
| "computed_stored", | ||
| Integer, | ||
| Computed("normal * 42", persisted=True), | ||
| ) | ||
| ) | ||
| class ComputedReflectionTest(_ComputedReflectionTest, ComputedReflectionFixtureTest): | ||
| @pytest.mark.skip("Default values are not supported.") | ||
| def test_computed_col_default_not_set(self): | ||
IlyaFaer marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| pass | ||
| def test_get_column_returns_computed(self): | ||
IlyaFaer marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| """ | ||
| SPANNER OVERRIDE: | ||
| In Spanner all the generated columns are STORED, | ||
| meaning there are no persisted and not persisted | ||
| (in the terms of the SQLAlchemy) columns. The | ||
| method override omits the persistence reflection checks. | ||
| """ | ||
| insp = inspect(config.db) | ||
| cols = insp.get_columns("computed_default_table") | ||
| data = {c["name"]: c for c in cols} | ||
| for key in ("id", "normal", "with_default"): | ||
| is_true("computed" not in data[key]) | ||
| compData = data["computed_col"] | ||
| is_true("computed" in compData) | ||
| is_true("sqltext" in compData["computed"]) | ||
| eq_(self.normalize(compData["computed"]["sqltext"]), "normal+42") | ||
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.
Overriding the method to drop
GENERATED ALWAYSpart of the statement