From 5bbe3fc01d182fba818c47f6964818e86f893b09 Mon Sep 17 00:00:00 2001 From: jiangjianyuan Date: Tue, 11 Aug 2026 09:15:11 -0700 Subject: [PATCH 1/9] fix: support lake SQLAlchemy URLs --- README.rst | 52 +++++++++---------- setup.cfg | 3 +- tests/conftest.py | 1 + tests/unit/test_tidbcloudlake_dialect.py | 5 ++ .../tidbcloudlake_dialect.py | 15 +----- 5 files changed, 34 insertions(+), 42 deletions(-) diff --git a/README.rst b/README.rst index dbf4a59..9b8970f 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ tidbcloudlake-sqlalchemy -=================== +========================= TiDB Cloud Lake dialect for SQLAlchemy. @@ -13,24 +13,19 @@ The package is installable through PIP:: Usage ----- -The DSN format is similar to that of regular Postgres:: +Use a ``lake://`` URL with SQLAlchemy. The database name and warehouse are +part of the URL:: - from sqlalchemy import create_engine, text - from sqlalchemy.engine.base import Connection, Engine - engine = create_engine( - f"tidbcloudlake://{username}:{password}@{host_port_name}/{database_name}?sslmode=disable" - ) - connection = engine.connect() - result = connection.execute(text("SELECT 1")) - assert len(result.fetchall()) == 1 + from sqlalchemy import create_engine, text - import connector - cursor = connector.connect('tidbcloudlake://root:@localhost:8000?sslmode=disable').cursor() - cursor.execute('SELECT * FROM test') - # print(cursor.fetchone()) - # print(cursor.fetchall()) - for row in cursor: - print(row) + engine = create_engine( + "lake://:@:443/default?warehouse=default" + ) + with engine.connect() as connection: + assert connection.execute(text("SELECT 1")).scalar_one() == 1 + +``tidbcloudlake://`` remains accepted as a backward-compatible alias, but new +applications should use ``lake://``. Merge Command Support @@ -44,7 +39,7 @@ The Merge command can be used as below:: from sqlalchemy import MetaData, create_engine from tidbcloudlake_sqlalchemy.tidbcloudlake_dialect import Merge - engine = create_engine(db.url, echo=False) + engine = create_engine("lake://:@:443/default?warehouse=default") session = sessionmaker(bind=engine)() connection = engine.connect() @@ -64,18 +59,21 @@ The Merge command can be used as below:: Copy Into Command Support --------------------- -TiDB Cloud Lake SQLAlchemy supports copy into operations through it's CopyIntoTable and CopyIntoLocation methods. See `CopyIntoLocation `_ or `CopyIntoTable `_ for full documentation. +TiDB Cloud Lake SQLAlchemy supports copy into operations through its +``CopyIntoTable`` and ``CopyIntoLocation`` methods. See `CopyIntoLocation `_ or `CopyIntoTable `_ for full documentation. The CopyIntoTable command can be used as below:: + import base64 + from sqlalchemy.orm import sessionmaker from sqlalchemy import MetaData, create_engine from tidbcloudlake_sqlalchemy import ( CopyIntoTable, GoogleCloudStorage, ParquetFormat, CopyIntoTableOptions, - FileColumnClause, CSVFormat, + FileColumnClause, CSVFormat, Compression, ) - engine = create_engine(db.url, echo=False) + engine = create_engine("lake://:@:443/default?warehouse=default") session = sessionmaker(bind=engine)() connection = engine.connect() @@ -134,13 +132,15 @@ The CopyIntoTable command can be used as below:: The CopyIntoLocation command can be used as below:: + import base64 + + from sqlalchemy import MetaData, create_engine, select from sqlalchemy.orm import sessionmaker - from sqlalchemy import MetaData, create_engine from tidbcloudlake_sqlalchemy import ( CopyIntoLocation, GoogleCloudStorage, ParquetFormat, CopyIntoLocationOptions, ) - engine = create_engine(db.url, echo=False) + engine = create_engine("lake://:@:443/default?warehouse=default") session = sessionmaker(bind=engine)() connection = engine.connect() @@ -173,10 +173,9 @@ TiDB Cloud Lake SQLAlchemy supports tidbcloudlake specific table options for Eng The table options can be used as below:: - from sqlalchemy import Table, Column - from sqlalchemy import MetaData, create_engine + from sqlalchemy import Column, Integer, MetaData, String, Table, cast, create_engine - engine = create_engine(db.url, echo=False) + engine = create_engine("lake://:@:443/default?warehouse=default") meta = MetaData() # Example of Transient Table @@ -214,4 +213,3 @@ The table options can be used as below:: ) meta.create_all(engine) - diff --git a/setup.cfg b/setup.cfg index 6207ce9..d783d1c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,7 +3,7 @@ name = tidbcloudlake-sqlalchemy version = attr: tidbcloudlake_sqlalchemy.VERSION description = Sqlalchemy adapter for TiDB Cloud Lake long_description = file: README.rst -long_description_content_type = text/markdown +long_description_content_type = text/x-rst url = https://github.com/tidbcloud/lake-sqlalchemy author = TiDB Cloud license = Apache-2.0 @@ -37,6 +37,7 @@ where = . [options.entry_points] sqlalchemy.dialects = + lake = tidbcloudlake_sqlalchemy.tidbcloudlake_dialect:TiDBCloudLakeDialect tidbcloudlake = tidbcloudlake_sqlalchemy.tidbcloudlake_dialect:TiDBCloudLakeDialect tidbcloudlake.tidbcloudlake = tidbcloudlake_sqlalchemy.tidbcloudlake_dialect:TiDBCloudLakeDialect diff --git a/tests/conftest.py b/tests/conftest.py index 40df4dd..8c0cf66 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ registry.register("tidbcloudlake.tidbcloudlake", "tidbcloudlake_sqlalchemy.tidbcloudlake_dialect", "TiDBCloudLakeDialect") registry.register("tidbcloudlake", "tidbcloudlake_sqlalchemy.tidbcloudlake_dialect", "TiDBCloudLakeDialect") +registry.register("lake", "tidbcloudlake_sqlalchemy.tidbcloudlake_dialect", "TiDBCloudLakeDialect") pytest.register_assert_rewrite("sa.testing.assertions") diff --git a/tests/unit/test_tidbcloudlake_dialect.py b/tests/unit/test_tidbcloudlake_dialect.py index 3d0cf1f..b0141b1 100644 --- a/tests/unit/test_tidbcloudlake_dialect.py +++ b/tests/unit/test_tidbcloudlake_dialect.py @@ -33,6 +33,11 @@ def test_create_dialect(self, dialect: TiDBCloudLakeDialect): assert dialect.context == {} def test_create_connect_args(self, dialect: TiDBCloudLakeDialect): + u = url.make_url("lake://user:pass@host:443/db?warehouse=test") + args, kwargs = dialect.create_connect_args(u) + assert args == [] + assert kwargs["dsn"] == "lake://user:pass@host:443/db?warehouse=test" + u = url.make_url("tidbcloudlake://user:pass@localhost:8000/testdb") result_list, result_dict = dialect.create_connect_args(u) assert result_dict["dsn"] == "tidbcloudlake://user:pass@localhost:8000/testdb" diff --git a/tidbcloudlake_sqlalchemy/tidbcloudlake_dialect.py b/tidbcloudlake_sqlalchemy/tidbcloudlake_dialect.py index f7017ef..e46c6b8 100644 --- a/tidbcloudlake_sqlalchemy/tidbcloudlake_dialect.py +++ b/tidbcloudlake_sqlalchemy/tidbcloudlake_dialect.py @@ -1468,20 +1468,7 @@ def connect(self, *cargs, **cparams): return self.dbapi.connect(*cargs, **cparams) def create_connect_args(self, url): - parameters = dict(url.query) - kwargs = { - "dsn": "tidbcloudlake://%s:%s@%s:%d/%s" - % (url.username, url.password, url.host, url.port or 8000, url.database), - } - - if parameters: - kwargs["dsn"] += "?" - param_strings = [] - for k, v in parameters.items(): - param_strings.append(f"{k}={v}") - kwargs["dsn"] += "&".join(param_strings) - - return ([], kwargs) + return ([], {"dsn": url.render_as_string(hide_password=False)}) def create_server_side_cursor(self): return self.create_default_cursor() From febe43e6b11ec3f366a2f1db4849d3f230cb311b Mon Sep 17 00:00:00 2001 From: jiangjianyuan Date: Tue, 11 Aug 2026 09:18:32 -0700 Subject: [PATCH 2/9] fix: normalize SQLAlchemy URL aliases for Lake driver --- tests/unit/test_tidbcloudlake_dialect.py | 11 ++++++++--- tidbcloudlake_sqlalchemy/tidbcloudlake_dialect.py | 7 ++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_tidbcloudlake_dialect.py b/tests/unit/test_tidbcloudlake_dialect.py index b0141b1..1e94ce9 100644 --- a/tests/unit/test_tidbcloudlake_dialect.py +++ b/tests/unit/test_tidbcloudlake_dialect.py @@ -40,17 +40,22 @@ def test_create_connect_args(self, dialect: TiDBCloudLakeDialect): u = url.make_url("tidbcloudlake://user:pass@localhost:8000/testdb") result_list, result_dict = dialect.create_connect_args(u) - assert result_dict["dsn"] == "tidbcloudlake://user:pass@localhost:8000/testdb" + assert result_dict["dsn"] == "lake://user:pass@localhost:8000/testdb" + + u = url.make_url("tidbcloudlake+tidbcloudlake://user:pass@localhost:8000/testdb") + args, kwargs = dialect.create_connect_args(u) + assert args == [] + assert kwargs["dsn"] == "lake://user:pass@localhost:8000/testdb" u = url.make_url("tidbcloudlake://user:pass@host:443/db") args, kwargs = dialect.create_connect_args(u) assert args == [] - assert kwargs["dsn"] == "tidbcloudlake://user:pass@host:443/db" + assert kwargs["dsn"] == "lake://user:pass@host:443/db" u = url.make_url("tidbcloudlake://user:pass@host:443/db?warehouse=test&secure=True") args, kwargs = dialect.create_connect_args(u) assert args == [] - assert kwargs["dsn"] == "tidbcloudlake://user:pass@host:443/db?warehouse=test&secure=True" + assert kwargs["dsn"] == "lake://user:pass@host:443/db?warehouse=test&secure=True" def test_do_execute( self, dialect: TiDBCloudLakeDialect, cursor: mock.Mock(spec=MockCursor) diff --git a/tidbcloudlake_sqlalchemy/tidbcloudlake_dialect.py b/tidbcloudlake_sqlalchemy/tidbcloudlake_dialect.py index e46c6b8..f20be66 100644 --- a/tidbcloudlake_sqlalchemy/tidbcloudlake_dialect.py +++ b/tidbcloudlake_sqlalchemy/tidbcloudlake_dialect.py @@ -1468,7 +1468,12 @@ def connect(self, *cargs, **cparams): return self.dbapi.connect(*cargs, **cparams) def create_connect_args(self, url): - return ([], {"dsn": url.render_as_string(hide_password=False)}) + # The Lake driver only accepts the public ``lake://`` scheme. Keep the + # legacy SQLAlchemy aliases working by normalizing them at this boundary. + return ( + [], + {"dsn": url.set(drivername="lake").render_as_string(hide_password=False)}, + ) def create_server_side_cursor(self): return self.create_default_cursor() From c2080a01b94b897ab6de219248b932aaf399f483 Mon Sep 17 00:00:00 2001 From: jiangjianyuan Date: Tue, 11 Aug 2026 09:23:11 -0700 Subject: [PATCH 3/9] fix: require Lake URLs --- README.rst | 4 ---- setup.cfg | 4 +--- tests/conftest.py | 2 -- tests/integration/conftest.py | 2 +- tests/integration/test_sqlalchemy_integration.py | 2 +- tests/unit/test_tidbcloudlake_dialect.py | 13 ++----------- tidbcloudlake_sqlalchemy/connector.py | 10 ++-------- tidbcloudlake_sqlalchemy/tidbcloudlake_dialect.py | 2 -- 8 files changed, 7 insertions(+), 32 deletions(-) diff --git a/README.rst b/README.rst index 9b8970f..24f8b37 100644 --- a/README.rst +++ b/README.rst @@ -24,10 +24,6 @@ part of the URL:: with engine.connect() as connection: assert connection.execute(text("SELECT 1")).scalar_one() == 1 -``tidbcloudlake://`` remains accepted as a backward-compatible alias, but new -applications should use ``lake://``. - - Merge Command Support --------------------- diff --git a/setup.cfg b/setup.cfg index d783d1c..4413a86 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,8 +38,6 @@ where = . [options.entry_points] sqlalchemy.dialects = lake = tidbcloudlake_sqlalchemy.tidbcloudlake_dialect:TiDBCloudLakeDialect - tidbcloudlake = tidbcloudlake_sqlalchemy.tidbcloudlake_dialect:TiDBCloudLakeDialect - tidbcloudlake.tidbcloudlake = tidbcloudlake_sqlalchemy.tidbcloudlake_dialect:TiDBCloudLakeDialect [options.extras_require] dev = @@ -71,4 +69,4 @@ requirement_cls=tidbcloudlake_sqlalchemy.requirements:Requirements profile_file = .profiles.txt [db] -default=tidbcloudlake://tidbcloudlake:tidbcloudlake@localhost:8000/default?sslmode=disable +default=lake://tidbcloudlake:tidbcloudlake@localhost:8000/default?sslmode=disable diff --git a/tests/conftest.py b/tests/conftest.py index 8c0cf66..f327508 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,6 @@ from sqlalchemy.dialects import registry import pytest -registry.register("tidbcloudlake.tidbcloudlake", "tidbcloudlake_sqlalchemy.tidbcloudlake_dialect", "TiDBCloudLakeDialect") -registry.register("tidbcloudlake", "tidbcloudlake_sqlalchemy.tidbcloudlake_dialect", "TiDBCloudLakeDialect") registry.register("lake", "tidbcloudlake_sqlalchemy.tidbcloudlake_dialect", "TiDBCloudLakeDialect") pytest.register_assert_rewrite("sa.testing.assertions") diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 0fc27f7..b8c1eba 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -44,7 +44,7 @@ def engine( username: str, password: str, host_port_name: str, database_name: str ) -> Engine: return create_engine( - f"tidbcloudlake://{username}:{password}@{host_port_name}/{database_name}?sslmode=disable" + f"lake://{username}:{password}@{host_port_name}/{database_name}?sslmode=disable" ) diff --git a/tests/integration/test_sqlalchemy_integration.py b/tests/integration/test_sqlalchemy_integration.py index 1b5c859..69e37f8 100644 --- a/tests/integration/test_sqlalchemy_integration.py +++ b/tests/integration/test_sqlalchemy_integration.py @@ -12,7 +12,7 @@ def test_set_params( self, username: str, password: str, database_name: str, host_port_name: str ): engine = create_engine( - f"tidbcloudlake://{username}:{password}@{host_port_name}/{database_name}?sslmode=disable" + f"lake://{username}:{password}@{host_port_name}/{database_name}?sslmode=disable" ) connection = engine.connect() result = connection.execute(text("SELECT 1")) diff --git a/tests/unit/test_tidbcloudlake_dialect.py b/tests/unit/test_tidbcloudlake_dialect.py index 1e94ce9..b59977c 100644 --- a/tests/unit/test_tidbcloudlake_dialect.py +++ b/tests/unit/test_tidbcloudlake_dialect.py @@ -38,21 +38,12 @@ def test_create_connect_args(self, dialect: TiDBCloudLakeDialect): assert args == [] assert kwargs["dsn"] == "lake://user:pass@host:443/db?warehouse=test" - u = url.make_url("tidbcloudlake://user:pass@localhost:8000/testdb") - result_list, result_dict = dialect.create_connect_args(u) - assert result_dict["dsn"] == "lake://user:pass@localhost:8000/testdb" - - u = url.make_url("tidbcloudlake+tidbcloudlake://user:pass@localhost:8000/testdb") - args, kwargs = dialect.create_connect_args(u) - assert args == [] - assert kwargs["dsn"] == "lake://user:pass@localhost:8000/testdb" - - u = url.make_url("tidbcloudlake://user:pass@host:443/db") + u = url.make_url("lake://user:pass@host:443/db") args, kwargs = dialect.create_connect_args(u) assert args == [] assert kwargs["dsn"] == "lake://user:pass@host:443/db" - u = url.make_url("tidbcloudlake://user:pass@host:443/db?warehouse=test&secure=True") + u = url.make_url("lake://user:pass@host:443/db?warehouse=test&secure=True") args, kwargs = dialect.create_connect_args(u) assert args == [] assert kwargs["dsn"] == "lake://user:pass@host:443/db?warehouse=test&secure=True" diff --git a/tidbcloudlake_sqlalchemy/connector.py b/tidbcloudlake_sqlalchemy/connector.py index e9f0d5a..a6e3629 100644 --- a/tidbcloudlake_sqlalchemy/connector.py +++ b/tidbcloudlake_sqlalchemy/connector.py @@ -98,19 +98,13 @@ def connect(*args, **kwargs): return Connection(*args, **kwargs) -def _normalize_driver_dsn(dsn: str) -> str: - if dsn.startswith("tidbcloudlake://"): - return "lake://" + dsn[len("tidbcloudlake://") :] - return dsn - - class Connection: """ These objects are small stateless factories for cursors, which do all the real work. """ - def __init__(self, dsn="tidbcloudlake://root:@localhost:8000/?sslmode=disable"): - self.client = BlockingLakeClient(_normalize_driver_dsn(dsn)) + def __init__(self, dsn="lake://root:@localhost:8000/?sslmode=disable"): + self.client = BlockingLakeClient(dsn) def close(self): pass diff --git a/tidbcloudlake_sqlalchemy/tidbcloudlake_dialect.py b/tidbcloudlake_sqlalchemy/tidbcloudlake_dialect.py index f20be66..48c26aa 100644 --- a/tidbcloudlake_sqlalchemy/tidbcloudlake_dialect.py +++ b/tidbcloudlake_sqlalchemy/tidbcloudlake_dialect.py @@ -1468,8 +1468,6 @@ def connect(self, *cargs, **cparams): return self.dbapi.connect(*cargs, **cparams) def create_connect_args(self, url): - # The Lake driver only accepts the public ``lake://`` scheme. Keep the - # legacy SQLAlchemy aliases working by normalizing them at this boundary. return ( [], {"dsn": url.set(drivername="lake").render_as_string(hide_password=False)}, From 6f87bfcae81b6d6987f9014795a00a142a33b91f Mon Sep 17 00:00:00 2001 From: jiangjianyuan Date: Tue, 11 Aug 2026 09:26:12 -0700 Subject: [PATCH 4/9] fix: expose Lake as the SQLAlchemy dialect --- tests/conftest.py | 1 + tests/test_sqlalchemy.py | 2 +- tests/unit/test_tidbcloudlake_dialect.py | 4 ++-- tidbcloudlake_sqlalchemy/tidbcloudlake_dialect.py | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index f327508..bac0551 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,7 @@ import pytest registry.register("lake", "tidbcloudlake_sqlalchemy.tidbcloudlake_dialect", "TiDBCloudLakeDialect") +registry.register("lake.lake", "tidbcloudlake_sqlalchemy.tidbcloudlake_dialect", "TiDBCloudLakeDialect") pytest.register_assert_rewrite("sa.testing.assertions") diff --git a/tests/test_sqlalchemy.py b/tests/test_sqlalchemy.py index ee67a76..7bb38d8 100644 --- a/tests/test_sqlalchemy.py +++ b/tests/test_sqlalchemy.py @@ -254,7 +254,7 @@ class ServerSideCursorsTest(_ServerSideCursorsTest): def _is_server_side(self, cursor): # ToDo - requires implementation of `stream_results` option, so True always for now - if self.engine.dialect.driver == "tidbcloudlake": + if self.engine.dialect.driver == "lake": return True return super() diff --git a/tests/unit/test_tidbcloudlake_dialect.py b/tests/unit/test_tidbcloudlake_dialect.py index b59977c..e6e6b83 100644 --- a/tests/unit/test_tidbcloudlake_dialect.py +++ b/tests/unit/test_tidbcloudlake_dialect.py @@ -23,8 +23,8 @@ class TestTiDBCloudLakeDialect: def test_create_dialect(self, dialect: TiDBCloudLakeDialect): assert issubclass(dialect_definition, TiDBCloudLakeDialect) assert isinstance(TiDBCloudLakeDialect.dbapi(), type(tidbcloudlake_sqlalchemy)) - assert dialect.name == "tidbcloudlake" - assert dialect.driver == "tidbcloudlake" + assert dialect.name == "lake" + assert dialect.driver == "lake" assert issubclass(dialect.preparer, TiDBCloudLakeIdentifierPreparer) assert issubclass(dialect.statement_compiler, TiDBCloudLakeCompiler) # SQLAlchemy's DefaultDialect creates an instance of diff --git a/tidbcloudlake_sqlalchemy/tidbcloudlake_dialect.py b/tidbcloudlake_sqlalchemy/tidbcloudlake_dialect.py index 48c26aa..afbad1c 100644 --- a/tidbcloudlake_sqlalchemy/tidbcloudlake_dialect.py +++ b/tidbcloudlake_sqlalchemy/tidbcloudlake_dialect.py @@ -1396,8 +1396,8 @@ def visit_set_column_comment(self, create, **kw): class TiDBCloudLakeDialect(default.DefaultDialect): - name = "tidbcloudlake" - driver = "tidbcloudlake" + name = "lake" + driver = "lake" supports_cast = True supports_sane_rowcount = False supports_sane_multi_rowcount = False From 4e5694cf905587eb3a762fe371fd18796c1d5f52 Mon Sep 17 00:00:00 2001 From: jiangjianyuan Date: Tue, 11 Aug 2026 09:28:11 -0700 Subject: [PATCH 5/9] test: use Lake dialect provisioning --- tests/test_sqlalchemy.py | 60 +++++++++++++-------------- tidbcloudlake_sqlalchemy/provision.py | 22 +++++----- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/tests/test_sqlalchemy.py b/tests/test_sqlalchemy.py index 7bb38d8..ee07062 100644 --- a/tests/test_sqlalchemy.py +++ b/tests/test_sqlalchemy.py @@ -39,12 +39,12 @@ class ComponentReflectionTest(_ComponentReflectionTest): - @testing.skip("tidbcloudlake") + @testing.skip("lake") def test_get_indexes(self): pass class ComponentReflectionTestExtra(_ComponentReflectionTestExtra): - @testing.skip("tidbcloudlake") #ToDo No length in TiDB Cloud Lake + @testing.skip("lake") #ToDo No length in TiDB Cloud Lake @testing.requires.table_reflection def test_varchar_reflection(self, connection, metadata): typ = self._type_round_trip( @@ -53,7 +53,7 @@ def test_varchar_reflection(self, connection, metadata): assert isinstance(typ, sql_types.String) eq_(typ.length, 52) - @testing.skip("tidbcloudlake") # ToDo No length in TiDB Cloud Lake + @testing.skip("lake") # ToDo No length in TiDB Cloud Lake @testing.requires.table_reflection @testing.combinations( sql_types.String, @@ -114,29 +114,29 @@ def test_whereclause(self): class CompoundSelectTest(_CompoundSelectTest): - @testing.skip("tidbcloudlake") + @testing.skip("lake") def test_limit_offset_aliased_selectable_in_unions(self): pass - @testing.skip("tidbcloudlake") + @testing.skip("lake") def test_limit_offset_selectable_in_unions(self): pass - @testing.skip("tidbcloudlake") + @testing.skip("lake") def test_limit_offset_in_unions_from_alias(self): pass class DeprecatedCompoundSelectTest(_DeprecatedCompoundSelectTest): - @testing.skip("tidbcloudlake") + @testing.skip("lake") def test_limit_offset_aliased_selectable_in_unions(self): pass - @testing.skip("tidbcloudlake") + @testing.skip("lake") def test_limit_offset_selectable_in_unions(self): pass - @testing.skip("tidbcloudlake") + @testing.skip("lake") def test_limit_offset_in_unions_from_alias(self): pass @@ -146,54 +146,54 @@ class HasIndexTest(_HasIndexTest): class InsertBehaviorTest(_InsertBehaviorTest): - @testing.skip("tidbcloudlake") # required autoinc columns + @testing.skip("lake") # required autoinc columns def test_insert_from_select_autoinc(self, connection): pass - @testing.skip("tidbcloudlake") # required autoinc columns + @testing.skip("lake") # required autoinc columns def test_insert_from_select_autoinc_no_rows(self, connection): pass - @testing.skip("tidbcloudlake") # required autoinc columns + @testing.skip("lake") # required autoinc columns def test_no_results_for_non_returning_insert(self, connection): pass class LikeFunctionsTest(_LikeFunctionsTest): - @testing.skip("tidbcloudlake") + @testing.skip("lake") def test_contains_autoescape(self): pass - @testing.skip("tidbcloudlake") + @testing.skip("lake") def test_contains_escape(self): pass - @testing.skip("tidbcloudlake") + @testing.skip("lake") def test_contains_autoescape_escape(self): pass - @testing.skip("tidbcloudlake") + @testing.skip("lake") def test_endswith_autoescape(self): pass - @testing.skip("tidbcloudlake") + @testing.skip("lake") def test_endswith_escape(self): pass - @testing.skip("tidbcloudlake") + @testing.skip("lake") def test_endswith_autoescape_escape(self): pass - @testing.skip("tidbcloudlake") + @testing.skip("lake") def test_startswith_autoescape(self): pass - @testing.skip("tidbcloudlake") + @testing.skip("lake") def test_startswith_escape(self): pass - @testing.skip("tidbcloudlake") + @testing.skip("lake") def test_startswith_autoescape_escape(self): pass @@ -300,19 +300,19 @@ def test_ss_cursor_status( ): super() - @testing.skip("tidbcloudlake") # ToDo - requires implementation of `stream_results` option + @testing.skip("lake") # ToDo - requires implementation of `stream_results` option def test_stmt_enabled_conn_option_disabled(self): pass - @testing.skip("tidbcloudlake") # ToDo - requires implementation of `stream_results` option + @testing.skip("lake") # ToDo - requires implementation of `stream_results` option def test_aliases_and_ss(self): pass - @testing.skip("tidbcloudlake") # Skipped because requires auto increment primary key + @testing.skip("lake") # Skipped because requires auto increment primary key def test_roundtrip_fetchall(self): pass - @testing.skip("tidbcloudlake") # Skipped because requires auto increment primary key + @testing.skip("lake") # Skipped because requires auto increment primary key def test_roundtrip_fetchmany(self): pass @@ -320,7 +320,7 @@ def test_roundtrip_fetchmany(self): class EnumTest(_EnumTest): __backend__ = True - @testing.skip("tidbcloudlake") # Skipped because no supporting enums yet + @testing.skip("lake") # Skipped because no supporting enums yet def test_round_trip_executemany(self, connection): pass @@ -723,11 +723,11 @@ def define_tables(cls, metadata): Column("id", Integer, primary_key=True), ) - @testing.skip("tidbcloudlake") # ToDo - requires definition of sequences with schema + @testing.skip("lake") # ToDo - requires definition of sequences with schema def test_has_sequence_remote_not_in_default(self, connection): eq_(inspect(connection).has_sequence("schema_seq"), False) - @testing.skip("tidbcloudlake") # ToDo - requires definition of sequences with schema + @testing.skip("lake") # ToDo - requires definition of sequences with schema def test_get_sequence_names(self, connection): exp = {"other_seq", "user_id_seq"} @@ -735,7 +735,7 @@ def test_get_sequence_names(self, connection): is_true(res.intersection(exp) == exp) is_true("schema_seq" not in res) - @testing.skip("tidbcloudlake") # ToDo - requires definition of sequences with schema + @testing.skip("lake") # ToDo - requires definition of sequences with schema @testing.requires.schemas def test_get_sequence_names_no_sequence_schema(self, connection): eq_( @@ -745,7 +745,7 @@ def test_get_sequence_names_no_sequence_schema(self, connection): [], ) - @testing.skip("tidbcloudlake") # ToDo - requires definition of sequences with schema + @testing.skip("lake") # ToDo - requires definition of sequences with schema @testing.requires.schemas def test_get_sequence_names_sequences_schema(self, connection): eq_( diff --git a/tidbcloudlake_sqlalchemy/provision.py b/tidbcloudlake_sqlalchemy/provision.py index f3d0826..3008706 100644 --- a/tidbcloudlake_sqlalchemy/provision.py +++ b/tidbcloudlake_sqlalchemy/provision.py @@ -4,11 +4,11 @@ from sqlalchemy.testing.provision import configure_follower, update_db_opts, temp_table_keyword_args -@create_db.for_db("tidbcloudlake") -def _tidbcloudlake_create_db(cfg, eng, ident): +@create_db.for_db("lake") +def _lake_create_db(cfg, eng, ident): with eng.begin() as conn: try: - _tidbcloudlake_drop_db(cfg, conn, ident) + _lake_drop_db(cfg, conn, ident) except Exception: pass @@ -24,24 +24,24 @@ def _tidbcloudlake_create_db(cfg, eng, ident): ) -@drop_db.for_db("tidbcloudlake") -def _tidbcloudlake_drop_db(cfg, eng, ident): +@drop_db.for_db("lake") +def _lake_drop_db(cfg, eng, ident): with eng.begin() as conn: conn.exec_driver_sql("DROP DATABASE IF EXISTS %s_test_schema" % ident) conn.exec_driver_sql("DROP DATABASE IF EXISTS %s_test_schema_2" % ident) conn.exec_driver_sql("DROP DATABASE IF EXISTS %s" % ident) -@temp_table_keyword_args.for_db("tidbcloudlake") -def _tidbcloudlake_temp_table_keyword_args(cfg, eng): +@temp_table_keyword_args.for_db("lake") +def _lake_temp_table_keyword_args(cfg, eng): return {"prefixes": ["TEMPORARY"]} -@configure_follower.for_db("tidbcloudlake") -def _tidbcloudlake_configure_follower(config, ident): +@configure_follower.for_db("lake") +def _lake_configure_follower(config, ident): config.test_schema = "%s_test_schema" % ident config.test_schema_2 = "%s_test_schema_2" % ident # Uncomment to debug SQL Statements in tests -# @update_db_opts.for_db("tidbcloudlake") -# def _tidbcloudlake_update_db_opts(db_url, db_opts): +# @update_db_opts.for_db("lake") +# def _lake_update_db_opts(db_url, db_opts): # db_opts["echo"] = True From 35bca405437633e6c477f99c2c1a0ea024787414 Mon Sep 17 00:00:00 2001 From: jiangjianyuan Date: Tue, 11 Aug 2026 09:28:52 -0700 Subject: [PATCH 6/9] test: finish Lake skip migration --- tests/test_sqlalchemy.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_sqlalchemy.py b/tests/test_sqlalchemy.py index ee07062..bfc17ee 100644 --- a/tests/test_sqlalchemy.py +++ b/tests/test_sqlalchemy.py @@ -210,17 +210,17 @@ def quote_fixtures(fn): )(fn) @quote_fixtures - @testing.skip("tidbcloudlake") + @testing.skip("lake") def test_get_pk_constraint(self, name): pass @quote_fixtures - @testing.skip("tidbcloudlake") + @testing.skip("lake") def test_get_foreign_keys(self, name): pass @quote_fixtures - @testing.skip("tidbcloudlake") + @testing.skip("lake") def test_get_indexes(self, name): pass @@ -241,11 +241,11 @@ class BinaryTest(_BinaryTest): # It is possible to do this # INSERT INTO binary_test (x) values (TO_BINARY('7\xe7\x9f')) # but that's not really a solution I don't think - @testing.skip("tidbcloudlake") + @testing.skip("lake") def test_binary_roundtrip(self): pass - @testing.skip("tidbcloudlake") + @testing.skip("lake") def test_pickle_roundtrip(self): pass From 96e334cf9fd83ec3613a8a5662e33031dc5f2183 Mon Sep 17 00:00:00 2001 From: jiangjianyuan Date: Tue, 11 Aug 2026 09:31:20 -0700 Subject: [PATCH 7/9] fix: rename table options for Lake dialect --- README.rst | 10 ++--- tests/test_copy_into.py | 5 +-- tests/test_table_options.py | 38 +++++++++---------- .../tidbcloudlake_dialect.py | 16 ++++---- 4 files changed, 34 insertions(+), 35 deletions(-) diff --git a/README.rst b/README.rst index 24f8b37..cc78b50 100644 --- a/README.rst +++ b/README.rst @@ -165,7 +165,7 @@ The CopyIntoLocation command can be used as below:: Table Options --------------------- -TiDB Cloud Lake SQLAlchemy supports tidbcloudlake specific table options for Engine, Cluster Keys and Transient tables +TiDB Cloud Lake SQLAlchemy supports Lake-specific table options for Engine, Cluster Keys and Transient tables The table options can be used as below:: @@ -179,7 +179,7 @@ The table options can be used as below:: "t_transient", meta, Column("c1", Integer), - tidbcloudlake_transient=True, + lake_transient=True, ) # Example of Engine @@ -187,7 +187,7 @@ The table options can be used as below:: "t_engine", meta, Column("c1", Integer), - tidbcloudlake_engine='Memory', + lake_engine='Memory', ) # Examples of Table with Cluster Keys @@ -195,7 +195,7 @@ The table options can be used as below:: "t_cluster_1", meta, Column("c1", Integer), - tidbcloudlake_cluster_by=[c1], + lake_cluster_by=[c1], ) # c = Column("id", Integer) @@ -205,7 +205,7 @@ The table options can be used as below:: meta, c, c2, - tidbcloudlake_cluster_by=[cast(c, String), c2], + lake_cluster_by=[cast(c, String), c2], ) meta.create_all(engine) diff --git a/tests/test_copy_into.py b/tests/test_copy_into.py index 0ce4056..e3f4d62 100644 --- a/tests/test_copy_into.py +++ b/tests/test_copy_into.py @@ -33,7 +33,7 @@ class CompileTiDBCloudLakeCopyIntoTableTest(fixtures.TestBase, AssertsCompiledSQL): - __only_on__ = "tidbcloudlake" + __only_on__ = "lake" def test_copy_into_table(self): m = MetaData() @@ -208,7 +208,7 @@ def define_tables(cls, metadata): metadata, Column("id", Integer), Column("data", String(50)), - tidbcloudlake_engine='Random', + lake_engine='Random', ) Table( "loaded", @@ -265,4 +265,3 @@ def test_copy_into_stage_and_table(self, connection): eq_(result['first_error'], None) eq_(result['first_error_line'], None) - diff --git a/tests/test_table_options.py b/tests/test_table_options.py index 5a6ae56..850d4f9 100644 --- a/tests/test_table_options.py +++ b/tests/test_table_options.py @@ -6,13 +6,13 @@ class CompileTiDBCloudLakeTableOptionsTest(fixtures.TestBase, AssertsCompiledSQL): - __only_on__ = "tidbcloudlake" + __only_on__ = "lake" def test_create_table_transient_on(self): m = MetaData() tbl = Table( 'atable', m, Column("id", Integer), - tidbcloudlake_transient=True, + lake_transient=True, ) self.assert_compile( schema.CreateTable(tbl), @@ -22,7 +22,7 @@ def test_create_table_transient_off(self): m = MetaData() tbl = Table( 'atable', m, Column("id", Integer), - tidbcloudlake_transient=False, + lake_transient=False, ) self.assert_compile( schema.CreateTable(tbl), @@ -32,7 +32,7 @@ def test_create_table_engine(self): m = MetaData() tbl = Table( 'atable', m, Column("id", Integer), - tidbcloudlake_engine='Memory', + lake_engine='Memory', ) self.assert_compile( schema.CreateTable(tbl), @@ -42,7 +42,7 @@ def test_create_table_cluster_by_column_str(self): m = MetaData() tbl = Table( 'atable', m, Column("id", Integer), - tidbcloudlake_cluster_by='id', + lake_cluster_by='id', ) self.assert_compile( schema.CreateTable(tbl), @@ -52,7 +52,7 @@ def test_create_table_cluster_by_column_strs(self): m = MetaData() tbl = Table( 'atable', m, Column("id", Integer), Column("Name", String), - tidbcloudlake_cluster_by=['id', 'Name'], + lake_cluster_by=['id', 'Name'], ) self.assert_compile( schema.CreateTable(tbl), @@ -63,7 +63,7 @@ def test_create_table_cluster_by_column_object(self): c = Column("id", Integer) tbl = Table( 'atable', m, c, - tidbcloudlake_cluster_by=[c], + lake_cluster_by=[c], ) self.assert_compile( schema.CreateTable(tbl), @@ -75,7 +75,7 @@ def test_create_table_cluster_by_column_objects(self): c2 = Column("Name", String) tbl = Table( 'atable', m, c, c2, - tidbcloudlake_cluster_by=[c, c2], + lake_cluster_by=[c, c2], ) self.assert_compile( schema.CreateTable(tbl), @@ -87,7 +87,7 @@ def test_create_table_cluster_by_column_expr(self): c2 = Column("Name", String) tbl = Table( 'atable', m, c, c2, - tidbcloudlake_cluster_by=[cast(c, String), c2], + lake_cluster_by=[cast(c, String), c2], ) self.assert_compile( schema.CreateTable(tbl), @@ -99,7 +99,7 @@ def test_create_table_cluster_by_str(self): c2 = Column("Name", String) tbl = Table( 'atable', m, c, c2, - tidbcloudlake_cluster_by="CAST(id AS VARCHAR), \"Name\"", + lake_cluster_by="CAST(id AS VARCHAR), \"Name\"", ) self.assert_compile( schema.CreateTable(tbl), @@ -121,7 +121,7 @@ def test_create_table_cluster_by_str(self): class ReflectTiDBCloudLakeTableOptionsTest(fixtures.TablesTest): __backend__ = True - __only_on__ = "tidbcloudlake" + __only_on__ = "lake" # 'once', 'each', None run_inserts = "None" @@ -136,7 +136,7 @@ def define_tables(cls, metadata): metadata, Column("id", Integer, primary_key=True), Column("Name", String), - tidbcloudlake_engine="Memory", + lake_engine="Memory", ) c2 = Column("id", Integer, primary_key=True) Table( @@ -144,7 +144,7 @@ def define_tables(cls, metadata): metadata, c2, Column("Name", String), - tidbcloudlake_cluster_by=[c2, "Name"], + lake_cluster_by=[c2, "Name"], ) c3 = Column("id", Integer, primary_key=True) Table( @@ -152,7 +152,7 @@ def define_tables(cls, metadata): metadata, c3, Column("Name", String), - tidbcloudlake_cluster_by=[cast(c3, String), "Name"], + lake_cluster_by=[cast(c3, String), "Name"], ) c4 = Column("id", Integer, primary_key=True) Table( @@ -160,7 +160,7 @@ def define_tables(cls, metadata): metadata, c4, Column("Name", String), - tidbcloudlake_cluster_by='CAST(id AS STRING), "Name"', + lake_cluster_by='CAST(id AS STRING), "Name"', ) def test_reflect_table_engine(self): @@ -168,25 +168,25 @@ def test_reflect_table_engine(self): t1_ref = Table( "t2_engine", m2, autoload_with=config.db ) - assert t1_ref.dialect_options['tidbcloudlake']['engine'] == 'MEMORY' + assert t1_ref.dialect_options['lake']['engine'] == 'MEMORY' def test_reflect_table_cluster_by_column(self): m2 = MetaData() t2_ref = Table( "t2_cluster_by_column", m2, autoload_with=config.db ) - assert t2_ref.dialect_options['tidbcloudlake']['cluster_by'] == 'id, "Name"' + assert t2_ref.dialect_options['lake']['cluster_by'] == 'id, "Name"' def test_reflect_table_cluster_by_expr(self): m2 = MetaData() t3_ref = Table( "t3_cluster_by_expr", m2, autoload_with=config.db ) - assert t3_ref.dialect_options['tidbcloudlake']['cluster_by'] == 'CAST(id AS STRING), "Name"' + assert t3_ref.dialect_options['lake']['cluster_by'] == 'CAST(id AS STRING), "Name"' def test_reflect_table_cluster_by_str(self): m2 = MetaData() t4_ref = Table( "t4_cluster_by_str", m2, autoload_with=config.db ) - assert t4_ref.dialect_options['tidbcloudlake']['cluster_by'] == 'CAST(id AS STRING), "Name"' \ No newline at end of file + assert t4_ref.dialect_options['lake']['cluster_by'] == 'CAST(id AS STRING), "Name"' diff --git a/tidbcloudlake_sqlalchemy/tidbcloudlake_dialect.py b/tidbcloudlake_sqlalchemy/tidbcloudlake_dialect.py index afbad1c..ef43f2e 100644 --- a/tidbcloudlake_sqlalchemy/tidbcloudlake_dialect.py +++ b/tidbcloudlake_sqlalchemy/tidbcloudlake_dialect.py @@ -12,15 +12,15 @@ * ``ENGINE``:: - Table("some_table", metadata, ..., tidbcloudlake_engine=FUSE|Memory|Random|Iceberg|Delta) + Table("some_table", metadata, ..., lake_engine=FUSE|Memory|Random|Iceberg|Delta) * ``CLUSTER KEY``:: - Table("some_table", metadata, ..., tidbcloudlake_cluster_by=str|LIST(expr|str)) + Table("some_table", metadata, ..., lake_cluster_by=str|LIST(expr|str)) * ``TRANSIENT``:: - Table("some_table", metadata, ..., tidbcloudlake_transient=True|False) + Table("some_table", metadata, ..., lake_transient=True|False) """ import decimal @@ -1322,7 +1322,7 @@ def visit_drop_schema(self, drop, **kw): def visit_create_table(self, create, **kw): table = create.element - db_opts = table.dialect_options["tidbcloudlake"] + db_opts = table.dialect_options["lake"] if "transient" in db_opts and db_opts["transient"]: if "transient" not in [p.lower() for p in table._prefixes]: table._prefixes.append("TRANSIENT") @@ -1330,7 +1330,7 @@ def visit_create_table(self, create, **kw): def post_create_table(self, table): table_opts = [] - db_opts = table.dialect_options["tidbcloudlake"] + db_opts = table.dialect_options["lake"] engine = db_opts.get("engine") if engine is not None: @@ -1714,12 +1714,12 @@ def get_table_options(self, connection, table_name, schema=None, **kw): ) if result.engine_full: - options["tidbcloudlake_engine"] = result.engine_full + options["lake_engine"] = result.engine_full if result.cluster_by: cluster_by = re.match(r"\((.*)\)", result.cluster_by).group(1) - options["tidbcloudlake_cluster_by"] = cluster_by + options["lake_cluster_by"] = cluster_by if result.is_transient: - options["tidbcloudlake_is_transient"] = result.is_transient + options["lake_is_transient"] = result.is_transient # engine options From 0617c1f0f82105d962804c2d3968109760eb6185 Mon Sep 17 00:00:00 2001 From: jiangjianyuan Date: Tue, 11 Aug 2026 09:33:06 -0700 Subject: [PATCH 8/9] test: remove obsolete table option alias --- tests/test_table_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_table_options.py b/tests/test_table_options.py index 850d4f9..97825d4 100644 --- a/tests/test_table_options.py +++ b/tests/test_table_options.py @@ -110,7 +110,7 @@ def test_create_table_cluster_by_str(self): # m = MetaData() # tbl = Table( # 'atable', m, Column("id", Integer), - # tidbcloudlake_engine_options=( + # lake_engine_options=( # ("compression", "snappy"), # ("storage_format", "parquet"), # )) From ddf28154340b0b3845f9eada87ea35b0ccabb0e0 Mon Sep 17 00:00:00 2001 From: jiangjianyuan Date: Tue, 11 Aug 2026 10:17:54 -0700 Subject: [PATCH 9/9] chore: release version 0.5.6 --- tidbcloudlake_sqlalchemy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tidbcloudlake_sqlalchemy/__init__.py b/tidbcloudlake_sqlalchemy/__init__.py index 3be1d14..c87b150 100644 --- a/tidbcloudlake_sqlalchemy/__init__.py +++ b/tidbcloudlake_sqlalchemy/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python -VERSION = (0, 5, 5) +VERSION = (0, 5, 6) __version__ = ".".join(str(x) for x in VERSION)