diff --git a/CHANGELOG.md b/CHANGELOG.md index f8619b52..d97c9412 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,23 @@ Older versions of this project were distributed as [pybigquery][0]. [2]: https://pypi.org/project/pybigquery/#history +## [1.12.0](https://github.com/googleapis/python-bigquery-sqlalchemy/compare/v1.11.0...v1.12.0) (2024-08-14) + + +### Features + +* Adds user agent parameters to two functions ([#1100](https://github.com/googleapis/python-bigquery-sqlalchemy/issues/1100)) ([f9324e3](https://github.com/googleapis/python-bigquery-sqlalchemy/commit/f9324e35a6aa2f3d9c9f2511d1104fdf60c97c83)) +* Support UPDATE + JOIN in BigQuery dialect ([#1083](https://github.com/googleapis/python-bigquery-sqlalchemy/issues/1083)) ([d766d21](https://github.com/googleapis/python-bigquery-sqlalchemy/commit/d766d21053f7d9df5019d0d6dedf4476ef6125a9)) +* Update colspec to account for sqlalchemy Enum ([#1111](https://github.com/googleapis/python-bigquery-sqlalchemy/issues/1111)) ([b54bdde](https://github.com/googleapis/python-bigquery-sqlalchemy/commit/b54bdde0a01cabf5844c2b2794994b1ae5f4952f)) + + +### Bug Fixes + +* Fix partitioning by DATE column ([#1074](https://github.com/googleapis/python-bigquery-sqlalchemy/issues/1074)) ([ad69c63](https://github.com/googleapis/python-bigquery-sqlalchemy/commit/ad69c630833bce207784dfbea8eb3c58f316e511)) +* Implement modulus operator ([#1048](https://github.com/googleapis/python-bigquery-sqlalchemy/issues/1048)) ([f5fb1a2](https://github.com/googleapis/python-bigquery-sqlalchemy/commit/f5fb1a2543e8196e076d74848a7ae0dcf169f667)) +* Set cte_follows_insert to True ([#1095](https://github.com/googleapis/python-bigquery-sqlalchemy/issues/1095)) ([9e0b117](https://github.com/googleapis/python-bigquery-sqlalchemy/commit/9e0b117b6966ad72bc94c0916be95189e4bd9654)) +* Use except distinct and intersect distinct ([#1094](https://github.com/googleapis/python-bigquery-sqlalchemy/issues/1094)) ([80781ef](https://github.com/googleapis/python-bigquery-sqlalchemy/commit/80781ef99287af2e950f21ca399c84d20422b732)) + ## [1.11.0](https://github.com/googleapis/python-bigquery-sqlalchemy/compare/v1.10.0...v1.11.0) (2024-04-12) diff --git a/MANIFEST.in b/MANIFEST.in index e0a66705..66fc8ef3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -16,7 +16,8 @@ # Generated by synthtool. DO NOT EDIT! include README.rst LICENSE -recursive-include google *.json *.proto py.typed +recursive-include third_party/sqlalchemy_bigquery_vendored * +recursive-include sqlalchemy_bigquery *.json *.proto py.typed recursive-include tests * global-exclude *.py[co] global-exclude __pycache__ diff --git a/noxfile.py b/noxfile.py index 36729727..420b097c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -31,7 +31,14 @@ FLAKE8_VERSION = "flake8==6.1.0" BLACK_VERSION = "black[jupyter]==23.7.0" ISORT_VERSION = "isort==5.11.0" -LINT_PATHS = ["docs", "sqlalchemy_bigquery", "tests", "noxfile.py", "setup.py"] +LINT_PATHS = [ + "third_party", + "docs", + "sqlalchemy_bigquery", + "tests", + "noxfile.py", + "setup.py", +] DEFAULT_PYTHON_VERSION = "3.8" diff --git a/owlbot.py b/owlbot.py index 9d4aaafc..0aaa33bf 100644 --- a/owlbot.py +++ b/owlbot.py @@ -15,6 +15,7 @@ """This script is used to synthesize generated parts of this library.""" import pathlib +import re import synthtool as s from synthtool import gcp @@ -76,6 +77,11 @@ "import re\nimport shutil", ) +s.replace( + ["noxfile.py"], + "LINT_PATHS = \[", + "LINT_PATHS = [\"third_party\", " +) s.replace( ["noxfile.py"], @@ -83,6 +89,12 @@ "--cov=sqlalchemy_bigquery", ) +s.replace( + ["noxfile.py"], + """os.path.join("tests", "unit"),""", + """os.path.join("tests", "unit"), + os.path.join("third_party", "sqlalchemy_bigquery_vendored"),""", +) s.replace( ["noxfile.py"], @@ -284,6 +296,15 @@ def system_noextras(session): """, ) + +# Make sure build includes all necessary files. +s.replace( + ["MANIFEST.in"], + re.escape("recursive-include google"), + """recursive-include third_party/sqlalchemy_bigquery_vendored * +recursive-include sqlalchemy_bigquery""", +) + # ---------------------------------------------------------------------------- # Samples templates # ---------------------------------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..0a21fc9c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,5 @@ +# Added so third_party folder is included when running `pip install -e .` +# See PR #1083 for more detail +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/samples/snippets/requirements-test.txt b/samples/snippets/requirements-test.txt index 75e34405..df0ceaeb 100644 --- a/samples/snippets/requirements-test.txt +++ b/samples/snippets/requirements-test.txt @@ -4,7 +4,7 @@ google-auth==2.29.0 google-cloud-testutils==1.4.0 iniconfig==2.0.0 packaging==24.0 -pluggy==1.4.0 +pluggy==1.5.0 py==1.11.0 pyasn1==0.6.0 pyasn1-modules==0.4.0 @@ -13,4 +13,4 @@ pytest===6.2.5 rsa==4.9 six==1.16.0 toml==0.10.2 -typing-extensions==4.11.0 +typing-extensions==4.12.1 diff --git a/samples/snippets/requirements.txt b/samples/snippets/requirements.txt index c3827910..e30275cb 100644 --- a/samples/snippets/requirements.txt +++ b/samples/snippets/requirements.txt @@ -1,20 +1,20 @@ alembic==1.13.1 -certifi==2024.2.2 +certifi==2024.7.4 charset-normalizer==3.3.2 -geoalchemy2==0.14.7 -google-api-core[grpc]==2.18.0 +geoalchemy2==0.15.1 +google-api-core[grpc]==2.19.0 google-auth==2.29.0 -google-cloud-bigquery==3.20.1 +google-cloud-bigquery==3.24.0 google-cloud-core==2.4.1 google-crc32c==1.5.0 google-resumable-media==2.7.0 -googleapis-common-protos==1.63.0 +googleapis-common-protos==1.63.1 greenlet==3.0.3 -grpcio==1.62.1 -grpcio-status==1.62.1 +grpcio==1.62.2 +grpcio-status==1.62.2 idna==3.7 importlib-resources==6.4.0; python_version >= '3.8' -mako==1.3.3 +mako==1.3.5 markupsafe==2.1.5 packaging==24.0 proto-plus==1.23.0 @@ -24,10 +24,10 @@ pyasn1-modules==0.4.0 pyparsing==3.1.2 python-dateutil==2.9.0.post0 pytz==2024.1 -requests==2.31.0 +requests==2.32.3 rsa==4.9 -shapely==2.0.3 +shapely==2.0.4 six==1.16.0 sqlalchemy===1.4.27 -typing-extensions==4.11.0 -urllib3==2.2.1 +typing-extensions==4.12.1 +urllib3==2.2.2 diff --git a/setup.py b/setup.py index b33e1c6e..007d001f 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,7 @@ import itertools import os import re +import setuptools from setuptools import setup # Package metadata. @@ -67,6 +68,16 @@ def readme(): extras["all"] = set(itertools.chain.from_iterable(extras.values())) +packages = [ + package + for package in setuptools.find_namespace_packages() + if package.startswith("sqlalchemy_bigquery") +] + [ + package + for package in setuptools.find_namespace_packages("third_party") + if package.startswith("sqlalchemy_bigquery_vendored") +] + setup( name=name, version=version, @@ -75,7 +86,11 @@ def readme(): long_description_content_type="text/x-rst", author="The Sqlalchemy-Bigquery Authors", author_email="googleapis-packages@google.com", - packages=["sqlalchemy_bigquery"], + package_dir={ + "sqlalchemy-bigquery": "sqlalchemy_bigquery", + "sqlalchemy_bigquery_vendored": "third_party/sqlalchemy_bigquery_vendored", + }, + packages=packages, url="https://github.com/googleapis/python-bigquery-sqlalchemy", keywords=["bigquery", "sqlalchemy"], classifiers=[ diff --git a/sqlalchemy_bigquery/_helpers.py b/sqlalchemy_bigquery/_helpers.py index b03e232a..179ca773 100644 --- a/sqlalchemy_bigquery/_helpers.py +++ b/sqlalchemy_bigquery/_helpers.py @@ -6,6 +6,7 @@ import functools import re +from typing import Optional from google.api_core import client_info import google.auth @@ -24,19 +25,48 @@ ) -def google_client_info(): - user_agent = USER_AGENT_TEMPLATE.format(sqlalchemy.__version__) +def google_client_info( + user_agent: Optional[str] = None, +) -> google.api_core.client_info.ClientInfo: + """ + Return a client_info object, with an optional user agent + string. If user_agent is None, use a default value. + """ + + if user_agent is None: + user_agent = USER_AGENT_TEMPLATE.format(sqlalchemy.__version__) return client_info.ClientInfo(user_agent=user_agent) def create_bigquery_client( - credentials_info=None, - credentials_path=None, - credentials_base64=None, - default_query_job_config=None, - location=None, - project_id=None, -): + credentials_info: Optional[dict] = None, + credentials_path: Optional[str] = None, + credentials_base64: Optional[str] = None, + default_query_job_config: Optional[google.cloud.bigquery.job.QueryJobConfig] = None, + location: Optional[str] = None, + project_id: Optional[str] = None, + user_agent: Optional[google.api_core.client_info.ClientInfo] = None, +) -> google.cloud.bigquery.Client: + """Construct a BigQuery client object. + + Args: + credentials_info Optional[dict]: + credentials_path Optional[str]: + credentials_base64 Optional[str]: + default_query_job_config (Optional[google.cloud.bigquery.job.QueryJobConfig]): + Default ``QueryJobConfig``. + Will be merged into job configs passed into the ``query`` method. + location (Optional[str]): + Default location for jobs / datasets / tables. + project_id (Optional[str]): + Project ID for the project which the client acts on behalf of. + user_agent (Optional[google.api_core.client_info.ClientInfo]): + The client info used to send a user-agent string along with API + requests. If ``None``, then default info will be used. Generally, + you only need to set this if you're developing your own library + or partner tool. + """ + default_project = None if credentials_base64: @@ -60,8 +90,10 @@ def create_bigquery_client( if project_id is None: project_id = default_project + client_info = google_client_info(user_agent=user_agent) + return bigquery.Client( - client_info=google_client_info(), + client_info=client_info, project=project_id, credentials=credentials, location=location, diff --git a/sqlalchemy_bigquery/base.py b/sqlalchemy_bigquery/base.py index e80f2891..c531c102 100644 --- a/sqlalchemy_bigquery/base.py +++ b/sqlalchemy_bigquery/base.py @@ -60,6 +60,7 @@ from .parse_url import parse_url from . import _helpers, _struct, _types +import sqlalchemy_bigquery_vendored.sqlalchemy.postgresql.base as vendored_postgresql # Illegal characters is intended to be all characters that are not explicitly # allowed as part of the flexible column names. @@ -189,10 +190,12 @@ def pre_exec(self): ) -class BigQueryCompiler(_struct.SQLCompiler, SQLCompiler): +class BigQueryCompiler(_struct.SQLCompiler, vendored_postgresql.PGCompiler): compound_keywords = SQLCompiler.compound_keywords.copy() compound_keywords[selectable.CompoundSelect.UNION] = "UNION DISTINCT" compound_keywords[selectable.CompoundSelect.UNION_ALL] = "UNION ALL" + compound_keywords[selectable.CompoundSelect.EXCEPT] = "EXCEPT DISTINCT" + compound_keywords[selectable.CompoundSelect.INTERSECT] = "INTERSECT DISTINCT" def __init__(self, dialect, statement, *args, **kwargs): if isinstance(statement, Column): @@ -580,6 +583,9 @@ def visit_regexp_match_op_binary(self, binary, operator, **kw): def visit_not_regexp_match_op_binary(self, binary, operator, **kw): return "NOT %s" % self.visit_regexp_match_op_binary(binary, operator, **kw) + def visit_mod_binary(self, binary, operator, **kw): + return f"MOD({self.process(binary.left, **kw)}, {self.process(binary.right, **kw)})" + class BigQueryTypeCompiler(GenericTypeCompiler): def visit_INTEGER(self, type_, **kw): @@ -831,6 +837,11 @@ def _process_time_partitioning( if time_partitioning.field is not None: field = time_partitioning.field if isinstance( + table.columns[time_partitioning.field].type, + sqlalchemy.sql.sqltypes.DATE, + ): + return f"PARTITION BY {field}" + elif isinstance( table.columns[time_partitioning.field].type, sqlalchemy.sql.sqltypes.TIMESTAMP, ): @@ -981,6 +992,7 @@ class BigQueryDialect(DefaultDialect): type_compiler = BigQueryTypeCompiler ddl_compiler = BigQueryDDLCompiler execution_ctx_cls = BigQueryExecutionContext + cte_follows_insert = True supports_alter = False supports_comments = True inline_comments = True @@ -1006,6 +1018,7 @@ class BigQueryDialect(DefaultDialect): sqlalchemy.sql.sqltypes.Time: BQClassTaggedStr, sqlalchemy.sql.sqltypes.TIMESTAMP: BQTimestamp, sqlalchemy.sql.sqltypes.ARRAY: BQArray, + sqlalchemy.sql.sqltypes.Enum: sqlalchemy.sql.sqltypes.Enum, } def __init__( diff --git a/sqlalchemy_bigquery/version.py b/sqlalchemy_bigquery/version.py index 6f283d8e..0920d6ea 100644 --- a/sqlalchemy_bigquery/version.py +++ b/sqlalchemy_bigquery/version.py @@ -17,4 +17,4 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -__version__ = "1.11.0" +__version__ = "1.12.0" diff --git a/tests/sqlalchemy_dialect_compliance/test_dialect_compliance.py b/tests/sqlalchemy_dialect_compliance/test_dialect_compliance.py index 57cd9a0d..ff14db9a 100644 --- a/tests/sqlalchemy_dialect_compliance/test_dialect_compliance.py +++ b/tests/sqlalchemy_dialect_compliance/test_dialect_compliance.py @@ -47,6 +47,7 @@ QuotedNameArgumentTest, SimpleUpdateDeleteTest as _SimpleUpdateDeleteTest, TimestampMicrosecondsTest as _TimestampMicrosecondsTest, + WindowFunctionTest, ) from sqlalchemy.testing.suite.test_types import ( @@ -537,10 +538,6 @@ def test_round_trip_executemany(self, connection): class CTETest(_CTETest): - @pytest.mark.skip("Can't use CTEs with insert") - def test_insert_from_select_round_trip(self): - pass - @pytest.mark.skip("Recusive CTEs aren't supported.") def test_select_recursive_round_trip(self): pass @@ -640,3 +637,6 @@ def test_no_results_for_non_returning_insert(cls): del LongNameBlowoutTest # Requires features (indexes, primary keys, etc., that BigQuery doesn't have. del PostCompileParamsTest # BQ adds backticks to bind parameters, causing failure of tests TODO: fix this? del QuotedNameArgumentTest # Quotes aren't allowed in BigQuery table names. +del ( + WindowFunctionTest.test_window_rows_between +) # test expects BQ to return sorted results diff --git a/tests/system/test_helpers.py b/tests/system/test_helpers.py index 42cfab7f..222d166c 100644 --- a/tests/system/test_helpers.py +++ b/tests/system/test_helpers.py @@ -104,3 +104,11 @@ def test_create_bigquery_client_with_credentials_base64_respects_project( project_id="connection-url-project", ) assert bqclient.project == "connection-url-project" + + +def test_create_bigquery_client_with_user_agent(module_under_test): + user_agent = "test_user_agent" + + bqclient = module_under_test.create_bigquery_client(user_agent=user_agent) + + assert bqclient._connection._client_info.user_agent == user_agent diff --git a/tests/system/test_sqlalchemy_bigquery.py b/tests/system/test_sqlalchemy_bigquery.py index 457a8ea8..7ea4ccc6 100644 --- a/tests/system/test_sqlalchemy_bigquery.py +++ b/tests/system/test_sqlalchemy_bigquery.py @@ -561,7 +561,8 @@ def test_dml(engine, session, table_dml): assert len(result) == 0 -def test_create_table(engine, bigquery_dataset): +@pytest.mark.parametrize("time_partitioning_field", ["timestamp_c", "date_c"]) +def test_create_table(engine, bigquery_dataset, time_partitioning_field): meta = MetaData() Table( f"{bigquery_dataset}.test_table_create", @@ -581,7 +582,7 @@ def test_create_table(engine, bigquery_dataset): bigquery_friendly_name="test table name", bigquery_expiration_timestamp=datetime.datetime(2183, 3, 26, 8, 30, 0), bigquery_time_partitioning=TimePartitioning( - field="timestamp_c", + field=time_partitioning_field, expiration_ms=1000 * 60 * 60 * 24 * 30, # 30 days ), bigquery_require_partition_filter=True, diff --git a/tests/unit/test_compiler.py b/tests/unit/test_compiler.py index cc9116e3..60ff3f0a 100644 --- a/tests/unit/test_compiler.py +++ b/tests/unit/test_compiler.py @@ -161,6 +161,35 @@ def prepare_implicit_join_base_query( return q +# Test vendored method update_from_clause() +# from sqlalchemy_bigquery_vendored.sqlalchemy.postgresql.base.PGCompiler +def test_update_from_clause(faux_conn, metadata): + table1 = setup_table( + faux_conn, + "table1", + metadata, + sqlalchemy.Column("foo", sqlalchemy.String), + sqlalchemy.Column("bar", sqlalchemy.Integer), + ) + table2 = setup_table( + faux_conn, + "table2", + metadata, + sqlalchemy.Column("foo", sqlalchemy.String), + sqlalchemy.Column("bar", sqlalchemy.Integer), + ) + + stmt = ( + sqlalchemy.update(table1) + .where(table1.c.foo == table2.c.foo) + .where(table2.c.bar == 1) + .values(bar=2) + ) + expected_sql = "UPDATE `table1` SET `bar`=%(bar:INT64)s FROM `table2` WHERE `table1`.`foo` = `table2`.`foo` AND `table2`.`bar` = %(bar_1:INT64)s" + found_sql = stmt.compile(faux_conn).string + assert found_sql == expected_sql + + @sqlalchemy_before_2_0 def test_no_implicit_join_asterix_for_inner_unnest_before_2_0(faux_conn, metadata): # See: https://github.com/googleapis/python-bigquery-sqlalchemy/issues/368 diff --git a/tests/unit/test_helpers.py b/tests/unit/test_helpers.py index 02bc8bee..e232e4ab 100644 --- a/tests/unit/test_helpers.py +++ b/tests/unit/test_helpers.py @@ -12,6 +12,7 @@ import google.auth.credentials import pytest from google.oauth2 import service_account +from sqlalchemy_bigquery import _helpers class AnonymousCredentialsWithProject(google.auth.credentials.AnonymousCredentials): @@ -244,3 +245,15 @@ def foo_to_bar(self, m): Replacer("hah").foo_to_bar("some foo and FOO is good") == "some hah and FOO is good" ) + + +@pytest.mark.parametrize( + "user_agent, expected_user_agent", + [ + (None, f"sqlalchemy/{_helpers.sqlalchemy.__version__}"), + ("my-user-agent", "my-user-agent"), + ], +) +def test_google_client_info(user_agent, expected_user_agent): + client_info = _helpers.google_client_info(user_agent=user_agent) + assert client_info.to_user_agent().startswith(expected_user_agent) diff --git a/tests/unit/test_select.py b/tests/unit/test_select.py index ad80047a..a600bdf9 100644 --- a/tests/unit/test_select.py +++ b/tests/unit/test_select.py @@ -168,6 +168,94 @@ def test_typed_parameters(faux_conn, type_, val, btype, vrep): ) +def test_except(faux_conn): + table = setup_table( + faux_conn, + "table", + sqlalchemy.Column("id", sqlalchemy.Integer), + sqlalchemy.Column("foo", sqlalchemy.Integer), + ) + + s1 = sqlalchemy.select(table.c.foo).where(table.c.id >= 2) + s2 = sqlalchemy.select(table.c.foo).where(table.c.id >= 4) + + s3 = s1.except_(s2) + + result = s3.compile(faux_conn).string + + expected = ( + "SELECT `table`.`foo` \n" + "FROM `table` \n" + "WHERE `table`.`id` >= %(id_1:INT64)s EXCEPT DISTINCT SELECT `table`.`foo` \n" + "FROM `table` \n" + "WHERE `table`.`id` >= %(id_2:INT64)s" + ) + assert result == expected + + +def test_intersect(faux_conn): + table = setup_table( + faux_conn, + "table", + sqlalchemy.Column("id", sqlalchemy.Integer), + sqlalchemy.Column("foo", sqlalchemy.Integer), + ) + + s1 = sqlalchemy.select(table.c.foo).where(table.c.id >= 2) + s2 = sqlalchemy.select(table.c.foo).where(table.c.id >= 4) + + s3 = s1.intersect(s2) + + result = s3.compile(faux_conn).string + + expected = ( + "SELECT `table`.`foo` \n" + "FROM `table` \n" + "WHERE `table`.`id` >= %(id_1:INT64)s INTERSECT DISTINCT SELECT `table`.`foo` \n" + "FROM `table` \n" + "WHERE `table`.`id` >= %(id_2:INT64)s" + ) + assert result == expected + + +def test_union(faux_conn): + table = setup_table( + faux_conn, + "table", + sqlalchemy.Column("id", sqlalchemy.Integer), + sqlalchemy.Column("foo", sqlalchemy.Integer), + ) + + s1 = sqlalchemy.select(table.c.foo).where(table.c.id >= 2) + s2 = sqlalchemy.select(table.c.foo).where(table.c.id >= 4) + + s3 = s1.union(s2) + + result = s3.compile(faux_conn).string + + expected = ( + "SELECT `table`.`foo` \n" + "FROM `table` \n" + "WHERE `table`.`id` >= %(id_1:INT64)s UNION DISTINCT SELECT `table`.`foo` \n" + "FROM `table` \n" + "WHERE `table`.`id` >= %(id_2:INT64)s" + ) + assert result == expected + + s4 = s1.union_all(s2) + + result = s4.compile(faux_conn).string + + expected = ( + "SELECT `table`.`foo` \n" + "FROM `table` \n" + "WHERE `table`.`id` >= %(id_1:INT64)s UNION ALL SELECT `table`.`foo` \n" + "FROM `table` \n" + "WHERE `table`.`id` >= %(id_2:INT64)s" + ) + assert result == expected + + def test_select_struct(faux_conn, metadata): from sqlalchemy_bigquery import STRUCT @@ -406,3 +494,66 @@ def test_visit_not_regexp_match_op_binary(faux_conn): expected = "NOT REGEXP_CONTAINS(`table`.`foo`, %(foo_1:STRING)s)" assert result == expected + + +def test_visit_mod_binary(faux_conn): + table = setup_table( + faux_conn, + "table", + sqlalchemy.Column("foo", sqlalchemy.Integer), + ) + sql_statement = table.c.foo % 2 + result = sql_statement.compile(faux_conn).string + expected = "MOD(`table`.`foo`, %(foo_1:INT64)s)" + + assert result == expected + + +def test_window_rows_between(faux_conn): + """This is a replacement for the + 'test_window_rows_between' + test in sqlalchemy's suite of compliance tests. + + Their test is expecting things in sorted order and BQ + doesn't return sorted results the way they expect so that + test fails. + + Note: that test only appears in: + sqlalchemy/lib/sqlalchemy/testing/suite/test_select.py + in version 2.0.32. It appears as though that test will be + replaced with a similar but new test called: + 'test_window_rows_between_w_caching' + due to the fact the rows are part of the cache key right now and + not handled as binds. This is related to sqlalchemy Issue #11515 + + It is expected the new test will also have the same sorting failure. + """ + + table = setup_table( + faux_conn, + "table", + sqlalchemy.Column("id", sqlalchemy.String), + sqlalchemy.Column("col1", sqlalchemy.Integer), + sqlalchemy.Column("col2", sqlalchemy.Integer), + ) + + stmt = sqlalchemy.select( + sqlalchemy.func.max(table.c.col2).over( + order_by=[table.c.col1], + rows=(-5, 0), + ) + ) + + sql = stmt.compile( + dialect=faux_conn.dialect, + compile_kwargs={"literal_binds": True}, + ) + + result = str(sql) + expected = ( + "SELECT max(`table`.`col2`) " + "OVER (ORDER BY `table`.`col1` " + "ROWS BETWEEN 5 PRECEDING AND CURRENT ROW) AS `anon_1` \n" # newline character required here to match + "FROM `table`" + ) + assert result == expected diff --git a/tests/unit/test_table_options.py b/tests/unit/test_table_options.py index 2147fb1d..2b757e04 100644 --- a/tests/unit/test_table_options.py +++ b/tests/unit/test_table_options.py @@ -193,6 +193,24 @@ def test_table_time_partitioning_with_timestamp_dialect_option(faux_conn): ) +def test_table_time_partitioning_with_date_dialect_option(faux_conn): + # expect table creation to fail as SQLite does not support partitioned tables + with pytest.raises(sqlite3.OperationalError): + setup_table( + faux_conn, + "some_table_2", + sqlalchemy.Column("id", sqlalchemy.Integer), + sqlalchemy.Column("createdAt", sqlalchemy.DATE), + bigquery_time_partitioning=TimePartitioning(field="createdAt"), + ) + + # confirm that the following code creates the correct SQL string + assert " ".join(faux_conn.test_data["execute"][-1][0].strip().split()) == ( + "CREATE TABLE `some_table_2` ( `id` INT64, `createdAt` DATE )" + " PARTITION BY createdAt" + ) + + def test_table_time_partitioning_dialect_option_partition_expiration_days(faux_conn): # expect table creation to fail as SQLite does not support partitioned tables with pytest.raises(sqlite3.OperationalError): diff --git a/third_party/__init__.py b/third_party/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/third_party/sqlalchemy_bigquery_vendored/__init__.py b/third_party/sqlalchemy_bigquery_vendored/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/third_party/sqlalchemy_bigquery_vendored/py.typed b/third_party/sqlalchemy_bigquery_vendored/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/third_party/sqlalchemy_bigquery_vendored/sqlalchemy/AUTHORS b/third_party/sqlalchemy_bigquery_vendored/sqlalchemy/AUTHORS new file mode 100644 index 00000000..98c5e111 --- /dev/null +++ b/third_party/sqlalchemy_bigquery_vendored/sqlalchemy/AUTHORS @@ -0,0 +1,30 @@ +SQLAlchemy was created by Michael Bayer. + +Major contributing authors include: + +- Mike Bayer +- Jason Kirtland +- Michael Trier +- Diana Clarke +- Gaetan de Menten +- Lele Gaifax +- Jonathan Ellis +- Gord Thompson +- Federico Caselli +- Philip Jenvey +- Rick Morrison +- Chris Withers +- Ants Aasma +- Sheila Allen +- Paul Johnston +- Tony Locke +- Hajime Nakagami +- Vraj Mohan +- Robert Leftwich +- Taavi Burns +- Jonathan Vanasco +- Jeff Widman +- Scott Dugas +- Dobes Vandermeer +- Ville Skytta +- Rodrigo Menezes diff --git a/third_party/sqlalchemy_bigquery_vendored/sqlalchemy/LICENSE b/third_party/sqlalchemy_bigquery_vendored/sqlalchemy/LICENSE new file mode 100644 index 00000000..967cdc5d --- /dev/null +++ b/third_party/sqlalchemy_bigquery_vendored/sqlalchemy/LICENSE @@ -0,0 +1,19 @@ +Copyright 2005-2024 SQLAlchemy authors and contributors . + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/third_party/sqlalchemy_bigquery_vendored/sqlalchemy/__init__.py b/third_party/sqlalchemy_bigquery_vendored/sqlalchemy/__init__.py new file mode 100644 index 00000000..71aff78e --- /dev/null +++ b/third_party/sqlalchemy_bigquery_vendored/sqlalchemy/__init__.py @@ -0,0 +1,6 @@ +# __init__.py +# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors +# +# +# This module is part of SQLAlchemy and is released under +# the MIT License: https://www.opensource.org/licenses/mit-license.php diff --git a/third_party/sqlalchemy_bigquery_vendored/sqlalchemy/postgresql/__init__.py b/third_party/sqlalchemy_bigquery_vendored/sqlalchemy/postgresql/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/third_party/sqlalchemy_bigquery_vendored/sqlalchemy/postgresql/base.py b/third_party/sqlalchemy_bigquery_vendored/sqlalchemy/postgresql/base.py new file mode 100644 index 00000000..b43ec44f --- /dev/null +++ b/third_party/sqlalchemy_bigquery_vendored/sqlalchemy/postgresql/base.py @@ -0,0 +1,19 @@ +# dialects/postgresql/base.py +# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors +# +# +# This module is part of SQLAlchemy and is released under +# the MIT License: https://www.opensource.org/licenses/mit-license.php +# mypy: ignore-errors + +from sqlalchemy.sql import compiler + + +class PGCompiler(compiler.SQLCompiler): + def update_from_clause( + self, update_stmt, from_table, extra_froms, from_hints, **kw + ): + kw["asfrom"] = True + return "FROM " + ", ".join( + t._compiler_dispatch(self, fromhints=from_hints, **kw) for t in extra_froms + )