Skip to content

Commit 50563e8

Browse files
committed
fix(doris): preserve table alias on self-referencing replace_query, wait on index SCHEMA_CHANGE, and refine Lance TVF docs
Signed-off-by: Bruce Arctor <5032356+brucearctor@users.noreply.github.com>
1 parent fdab389 commit 50563e8

5 files changed

Lines changed: 139 additions & 21 deletions

File tree

‎docs/integrations/engines/doris.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,7 @@ FROM hive_catalog.raw.orders;
467467

468468
### Pattern 5: Reading External Lance, Hudi & Delta Lake Tables (Doris 4.1+)
469469

470-
Apache Doris 4.1+ supports reading multimodal and vector datasets stored in **Lance** format (`'type' = 'lance'` catalog or `s3(...)` / `gcs(...)` / `hdfs(...)` / `local(...)` table-valued functions with `'format' = 'lance'`), as well as external **Apache Hudi** (`'type' = 'hudi'`) and **Delta Lake** (`'type' = 'deltalake'`) catalogs.
470+
Apache Doris 4.1.4+ supports reading multimodal and vector datasets stored in **Lance** format (`'type' = 'lance'` catalog or `s3(...)` / `local(...)` table-valued functions with `'format' = 'lance'`), as well as external **Apache Hudi** (`'type' = 'hudi'`) and **Delta Lake** (`'type' = 'deltalake'`) catalogs.
471471

472472
```yaml
473473
# config.yaml
@@ -773,7 +773,7 @@ MODEL (
773773

774774
| SQLMesh Model Kind | Doris Internal OLAP Catalog (`internal` on Local Disk or S3/GCS Storage Vault) | External Iceberg & Paimon Catalogs (`table_format iceberg \| paimon` on S3/GCS/HDFS) | External Hive Catalog (`table_format hive` on S3/GCS/HDFS) | External Read-Only Lakehouse Catalogs (`lance`, `hudi`, `deltalake`, `lakesoul`) |
775775
| --- | :---: | :---: | :---: | :---: |
776-
| Source / Upstream `SELECT` | Supported | Supported (incl. `FOR TIME/VERSION AS OF`, `@branch`, `@tag`) | Supported | Supported (Read-Only via Catalog or S3/GCS TVF) |
776+
| Source / Upstream `SELECT` | Supported | Supported (incl. `FOR TIME/VERSION AS OF`, `@branch`, `@tag`) | Supported | Supported (Read-Only via corresponding External Catalog; Lance also supports `s3()` / `local()` TVFs) |
777777
| `FULL` | Supported (`CREATE TABLE ... AS SELECT` / `DELETE` + `INSERT`) | Supported (`CREATE TABLE ... ENGINE=iceberg\|paimon` / `INSERT OVERWRITE TABLE`) | Supported (`CREATE TABLE ... ENGINE=hive` / `INSERT OVERWRITE TABLE`) | Read-Only in Doris (Write Guarded) |
778778
| `INCREMENTAL_BY_TIME_RANGE` | Supported (`DELETE WHERE time_col >= low AND time_col <= high` + `INSERT`) | Supported (`DELETE` + `INSERT`; requires Doris 4.1.0+ & Iceberg v2 / Paimon PK) | Not supported by Hive in Doris (No row-level `DELETE`) | Read-Only in Doris (Write Guarded) |
779779
| `INCREMENTAL_BY_UNIQUE_KEY` | Supported (Auto-promotes `unique_key` to `UNIQUE KEY`, `DELETE ... USING` + `INSERT` or native `MERGE INTO` when `when_matched` / `merge_filter` is specified) | Supported (Batched key `DELETE` + `INSERT` or native `MERGE INTO`; requires Doris 4.1.0+ & Iceberg v2 / Paimon PK) | Not supported by Hive in Doris (No `DELETE` / `MERGE INTO`) | Read-Only in Doris (Write Guarded) |

‎sqlmesh/core/engine_adapter/base.py‎

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,11 @@ def replace_query(
531531
default_catalog_loaded = False
532532
qualified_target: t.Optional[exp.Table] = None
533533

534+
def _table_parts(
535+
table: exp.Table,
536+
) -> t.Tuple[t.Optional[exp.Expr], t.Optional[exp.Expr], t.Optional[exp.Expr]]:
537+
return (table.args.get("catalog"), table.args.get("db"), table.this)
538+
534539
def _qualified(table: exp.Table, catalog: str) -> exp.Table:
535540
table = table.copy()
536541
if not table.catalog and table.db:
@@ -542,7 +547,7 @@ def _is_target_table(node: exp.Expr) -> bool:
542547
if not isinstance(node, exp.Table):
543548
return False
544549
quoted_node = quote_identifiers(node.copy())
545-
if quoted_node == quoted_target:
550+
if _table_parts(quoted_node) == _table_parts(quoted_target):
546551
return True
547552
if quoted_node.this != quoted_target.this or quoted_node.args.get(
548553
"db"
@@ -555,9 +560,19 @@ def _is_target_table(node: exp.Expr) -> bool:
555560
qualified_target = _qualified(target_table, default_catalog)
556561
return (
557562
default_catalog is not None
558-
and _qualified(node, default_catalog) == qualified_target
563+
and qualified_target is not None
564+
and _table_parts(_qualified(node, default_catalog))
565+
== _table_parts(qualified_target)
559566
)
560567

568+
def _replace_target_table(node: exp.Expr, replacement_table: exp.Table) -> exp.Expr:
569+
if not _is_target_table(node):
570+
return node
571+
replacement = replacement_table.copy()
572+
if isinstance(node, exp.Table) and node.args.get("alias") is not None:
573+
replacement.set("alias", node.args["alias"].copy())
574+
return replacement
575+
561576
self_referencing = any(_is_target_table(table) for table in query.find_all(exp.Table))
562577
# If a query references itself then it must have a table created regardless of approach used.
563578
if self_referencing:
@@ -601,9 +616,7 @@ def _is_target_table(node: exp.Expr) -> bool:
601616
) as temp_table:
602617
for source_query in source_queries:
603618
source_query.add_transform(
604-
lambda node: ( # type: ignore
605-
temp_table if _is_target_table(node) else node # type: ignore
606-
)
619+
lambda node: _replace_target_table(node, temp_table) # type: ignore
607620
)
608621
return self._insert_overwrite_by_condition(
609622
target_table,

‎sqlmesh/core/engine_adapter/doris.py‎

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1644,9 +1644,11 @@ def create_index(
16441644
exists=exists,
16451645
)
16461646
)
1647+
self._wait_for_schema_change(table_name)
16471648
return
16481649
col_names = tuple(c.name if isinstance(c, exp.Expr) else str(c) for c in columns)
16491650
super().create_index(table_name, index_name, col_names, exists=exists)
1651+
self._wait_for_schema_change(table_name)
16501652

16511653
def _create_table_like(
16521654
self,
@@ -4098,17 +4100,32 @@ def _create_table_from_source_queries(
40984100
first_query = source_queries[0].query_factory() if source_queries else None
40994101

41004102
def _matches_target_table(t_node: exp.Table) -> bool:
4101-
if quote_identifiers(t_node.copy()) == quote_identifiers(table.copy()):
4103+
q_node = quote_identifiers(t_node.copy())
4104+
q_target = quote_identifiers(table.copy())
4105+
if (q_node.args.get("catalog"), q_node.args.get("db"), q_node.this) == (
4106+
q_target.args.get("catalog"),
4107+
q_target.args.get("db"),
4108+
q_target.this,
4109+
):
41024110
return True
4103-
if self.default_catalog:
4104-
norm_node = t_node.copy()
4105-
if not norm_node.catalog and norm_node.db:
4106-
norm_node.set("catalog", exp.parse_identifier(self.default_catalog))
4107-
norm_target = table.copy()
4108-
if not norm_target.catalog and norm_target.db:
4109-
norm_target.set("catalog", exp.parse_identifier(self.default_catalog))
4110-
return quote_identifiers(norm_node) == quote_identifiers(norm_target)
4111-
return False
4111+
default_cat = self.default_catalog or "internal"
4112+
norm_node = t_node.copy()
4113+
if not norm_node.catalog and norm_node.db:
4114+
norm_node.set("catalog", exp.parse_identifier(default_cat))
4115+
norm_target = table.copy()
4116+
if not norm_target.catalog and norm_target.db:
4117+
norm_target.set("catalog", exp.parse_identifier(default_cat))
4118+
q_norm_node = quote_identifiers(norm_node)
4119+
q_norm_target = quote_identifiers(norm_target)
4120+
return (
4121+
q_norm_node.args.get("catalog"),
4122+
q_norm_node.args.get("db"),
4123+
q_norm_node.this,
4124+
) == (
4125+
q_norm_target.args.get("catalog"),
4126+
q_norm_target.args.get("db"),
4127+
q_norm_target.this,
4128+
)
41124129

41134130
self_referencing = bool(
41144131
first_query

‎tests/core/engine_adapter/integration/test_integration_doris.py‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,3 +872,63 @@ def test_doris_4_1_native_merge_segment_v3_and_ann_index(ctx: TestContext):
872872
)
873873
seq_rows = doris_adapter.fetchall(exp.select("id", "val", "ver").from_(seq_tbl))
874874
assert list(seq_rows) == [(1, "v10", 10)]
875+
876+
# 7. Self-referencing replace_query with aliased, catalog-qualified `internal.<db>.<tbl> AS t`
877+
self_ref_tbl = ctx.table("test_doris_self_ref")
878+
self_ref_cols = {
879+
"id": exp.DataType.build("INT", dialect="doris"),
880+
"val": exp.DataType.build("INT", dialect="doris"),
881+
}
882+
doris_adapter.create_table(self_ref_tbl, self_ref_cols)
883+
doris_adapter.execute(
884+
f"INSERT INTO {doris_adapter._to_sql(self_ref_tbl)} (`id`, `val`) VALUES (1, 10), (2, 20)"
885+
)
886+
prev_default_cat = doris_adapter._default_catalog
887+
try:
888+
doris_adapter._default_catalog = "internal"
889+
doris_adapter.replace_query(
890+
self_ref_tbl,
891+
parse_one(
892+
f"SELECT t.id, t.val + 5 AS val FROM `internal`.`{self_ref_tbl.db}`.`{self_ref_tbl.name}` AS t",
893+
read="doris",
894+
),
895+
target_columns_to_types=self_ref_cols,
896+
)
897+
finally:
898+
doris_adapter._default_catalog = prev_default_cat
899+
self_ref_rows = doris_adapter.fetchall(
900+
exp.select("id", "val").from_(self_ref_tbl).order_by("id")
901+
)
902+
assert list(self_ref_rows) == [(1, 15), (2, 25)]
903+
904+
# 8. NGRAM_BF index creation + BUILD INDEX on INVERTED index + TIMESTAMPTZ cross-timezone normalize_value equivalence
905+
doris_adapter.create_index(
906+
fts_tbl,
907+
"idx_msg_ngram",
908+
[exp.to_column("msg")],
909+
index_type="NGRAM_BF",
910+
index_properties={"gram_size": "3", "bf_size": "256"},
911+
)
912+
doris_adapter.execute(
913+
parse_one(
914+
f"BUILD INDEX idx_msg ON {doris_adapter._to_sql(fts_tbl)}",
915+
read="doris",
916+
)
917+
)
918+
norm_tz = doris_adapter.normalize_value(
919+
exp.to_column("ts"),
920+
exp.DataType.build("TIMESTAMPTZ(6)", dialect="doris"),
921+
timestamp_precision=6,
922+
)
923+
tz_check_rows = doris_adapter.fetchall(
924+
exp.select(norm_tz.as_("utc_str")).from_(
925+
parse_one(
926+
"(SELECT CAST('2024-06-01 12:00:00.123456+08:00' AS TIMESTAMPTZ(6)) AS ts "
927+
"UNION ALL "
928+
"SELECT CAST('2024-06-01 04:00:00.123456+00:00' AS TIMESTAMPTZ(6)) AS ts) t",
929+
read="doris",
930+
)
931+
)
932+
)
933+
assert len(tz_check_rows) == 2
934+
assert tz_check_rows[0][0] == tz_check_rows[1][0] == "2024-06-01 04:00:00.123456"

‎tests/core/engine_adapter/test_doris.py‎

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4865,11 +4865,12 @@ def _flaky_execute(sql: str, **kwargs: t.Any) -> None:
48654865
adapter.cursor.execute.side_effect = _flaky_execute
48664866
adapter.alter_table(
48674867
[
4868-
parse_one(
4869-
"ALTER TABLE `db`.`t` MODIFY COLUMN `a` BIGINT", into=exp.Alter, read="doris"
4868+
t.cast(
4869+
exp.Alter, parse_one("ALTER TABLE `db`.`t` MODIFY COLUMN `a` BIGINT", read="doris")
48704870
),
4871-
parse_one(
4872-
"ALTER TABLE `db`.`t` MODIFY COLUMN `b` DATETIME(6)", into=exp.Alter, read="doris"
4871+
t.cast(
4872+
exp.Alter,
4873+
parse_one("ALTER TABLE `db`.`t` MODIFY COLUMN `b` DATETIME(6)", read="doris"),
48734874
),
48744875
]
48754876
)
@@ -5064,6 +5065,7 @@ def test_iceberg_reading_time_travel_branch_and_compaction_ddl(
50645065

50655066
def test_doris_4_1_search_indexes_compaction_and_sequence_col(
50665067
make_mocked_engine_adapter: t.Callable[..., DorisEngineAdapter],
5068+
mocker: MockerFixture,
50675069
):
50685070
adapter = make_mocked_engine_adapter(DorisEngineAdapter)
50695071

@@ -5172,3 +5174,29 @@ def test_doris_4_1_search_indexes_compaction_and_sequence_col(
51725174
assert any(
51735175
"PROPERTIES ('function_column.sequence_col'='updated_at')" in call for call in sql_calls
51745176
)
5177+
5178+
# 5. Self-referencing replace_query with aliased, default-catalog-qualified table reference preserves alias
5179+
adapter._default_catalog = "internal"
5180+
adapter.cursor.execute.reset_mock()
5181+
mocker.patch("sqlmesh.core.engine_adapter.doris.random_id", return_value="abcd1234")
5182+
mocker.patch.object(
5183+
adapter,
5184+
"get_data_object",
5185+
return_value=DataObject(
5186+
catalog="internal", schema="db", name="tbl", type=DataObjectType.TABLE
5187+
),
5188+
)
5189+
adapter.replace_query(
5190+
"db.tbl",
5191+
parse_one("SELECT t.id, t.val FROM internal.db.tbl AS t", read="doris"),
5192+
target_columns_to_types={
5193+
"id": exp.DataType.build("INT", dialect="doris"),
5194+
"val": exp.DataType.build("INT", dialect="doris"),
5195+
},
5196+
)
5197+
self_ref_calls = to_sql_calls(adapter)
5198+
assert any(
5199+
"CREATE TABLE IF NOT EXISTS `internal`.`db`.`__temp_tbl_abcd1234`" in c
5200+
for c in self_ref_calls
5201+
)
5202+
assert any("FROM `internal`.`db`.`__temp_tbl_abcd1234` AS `t`" in c for c in self_ref_calls)

0 commit comments

Comments
 (0)