|
21 | 21 | import sqlalchemy.exc |
22 | 22 | from sqlalchemy.sql.functions import cube, grouping_sets, rollup |
23 | 23 |
|
| 24 | +from sqlalchemy_bigquery import SafeCast, safe_cast |
| 25 | + |
24 | 26 | from .conftest import setup_table, sqlalchemy_2_0_or_higher, sqlalchemy_before_2_0 |
25 | 27 |
|
26 | 28 |
|
@@ -487,3 +489,78 @@ def test_float_literal_pyformat_bindparam(): |
487 | 489 | stmt = sqlalchemy.select(sqlalchemy.literal(15.7563)) |
488 | 490 | compiled = stmt.compile(dialect=dialect) |
489 | 491 | assert "%(param_1:FLOAT64)s" in str(compiled) |
| 492 | + |
| 493 | + |
| 494 | +SAFE_CAST_TYPE_CASES = [ |
| 495 | + pytest.param(sqlalchemy.Integer, "INT64", id="integer_class"), |
| 496 | + pytest.param(sqlalchemy.Integer(), "INT64", id="integer_instance"), |
| 497 | + pytest.param(sqlalchemy.String, "STRING", id="string"), |
| 498 | + pytest.param(sqlalchemy.Float, "FLOAT64", id="float"), |
| 499 | + pytest.param(sqlalchemy.Boolean, "BOOL", id="boolean"), |
| 500 | + pytest.param(sqlalchemy.Date, "DATE", id="date"), |
| 501 | + pytest.param(sqlalchemy.Numeric, "NUMERIC", id="numeric"), |
| 502 | +] |
| 503 | + |
| 504 | + |
| 505 | +@pytest.mark.parametrize("target_type,expected_bq_type", SAFE_CAST_TYPE_CASES) |
| 506 | +def test_safe_cast_column(faux_conn, table, target_type, expected_bq_type): |
| 507 | + expr = safe_cast(table.c.foo, target_type) |
| 508 | + assert isinstance(expr, SafeCast) |
| 509 | + assert expr.inherit_cache is True |
| 510 | + expected_type_cls = ( |
| 511 | + target_type if isinstance(target_type, type) else type(target_type) |
| 512 | + ) |
| 513 | + assert isinstance(expr.type, expected_type_cls) |
| 514 | + |
| 515 | + stmt = sqlalchemy.select(expr.label("casted")) |
| 516 | + found_sql = stmt.compile(faux_conn).string |
| 517 | + expected_sql = ( |
| 518 | + f"SELECT SAFE_CAST(`table1`.`foo` AS {expected_bq_type}) AS `casted` \n" |
| 519 | + "FROM `table1`" |
| 520 | + ) |
| 521 | + assert found_sql == expected_sql |
| 522 | + |
| 523 | + |
| 524 | +def test_safe_cast_literal(faux_conn): |
| 525 | + stmt = sqlalchemy.select(safe_cast(sqlalchemy.literal("123"), sqlalchemy.Integer)) |
| 526 | + found_sql = stmt.compile(faux_conn, compile_kwargs={"literal_binds": True}).string |
| 527 | + assert found_sql == "SELECT SAFE_CAST('123' AS INT64) AS `anon_1`" |
| 528 | + |
| 529 | + |
| 530 | +def test_safe_cast_null(faux_conn): |
| 531 | + stmt = sqlalchemy.select( |
| 532 | + safe_cast(sqlalchemy.null(), sqlalchemy.Integer).label("val") |
| 533 | + ) |
| 534 | + found_sql = stmt.compile(faux_conn).string |
| 535 | + assert found_sql == "SELECT SAFE_CAST(NULL AS INT64) AS `val`" |
| 536 | + |
| 537 | + |
| 538 | +def test_safe_cast_nested(faux_conn, table): |
| 539 | + inner = safe_cast(table.c.foo, sqlalchemy.String) |
| 540 | + outer = safe_cast(inner, sqlalchemy.Integer).label("casted") |
| 541 | + stmt = sqlalchemy.select(outer) |
| 542 | + found_sql = stmt.compile(faux_conn).string |
| 543 | + expected_sql = ( |
| 544 | + "SELECT SAFE_CAST(SAFE_CAST(`table1`.`foo` AS STRING) AS INT64) AS `casted` \n" |
| 545 | + "FROM `table1`" |
| 546 | + ) |
| 547 | + assert found_sql == expected_sql |
| 548 | + |
| 549 | + |
| 550 | +@pytest.mark.skipif( |
| 551 | + not hasattr(sqlalchemy, "try_cast"), |
| 552 | + reason="sqlalchemy.try_cast requires SQLAlchemy 2.0.14 or higher", |
| 553 | +) |
| 554 | +@pytest.mark.parametrize("target_type,expected_bq_type", SAFE_CAST_TYPE_CASES) |
| 555 | +def test_sqlalchemy_try_cast_compiles_to_safe_cast( |
| 556 | + faux_conn, table, target_type, expected_bq_type |
| 557 | +): |
| 558 | + stmt = sqlalchemy.select( |
| 559 | + sqlalchemy.try_cast(table.c.foo, target_type).label("casted") |
| 560 | + ) |
| 561 | + found_sql = stmt.compile(faux_conn).string |
| 562 | + expected_sql = ( |
| 563 | + f"SELECT SAFE_CAST(`table1`.`foo` AS {expected_bq_type}) AS `casted` \n" |
| 564 | + "FROM `table1`" |
| 565 | + ) |
| 566 | + assert found_sql == expected_sql |
0 commit comments