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 + ) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy