diff --git a/src/linkml_map/compiler/sql_compiler.py b/src/linkml_map/compiler/sql_compiler.py index b8193ad6..ac4c4820 100644 --- a/src/linkml_map/compiler/sql_compiler.py +++ b/src/linkml_map/compiler/sql_compiler.py @@ -68,7 +68,7 @@ def compile_class( if not col_trs: return stmt += ", \n".join(col_trs) - stmt += f" FROM {cd.name}" + stmt += f" FROM {cd.populated_from or cd.name}" compiled.serialization += f"{stmt};\n" def compile_slot_derivation(self, sd) -> str: @@ -85,7 +85,7 @@ def compile_slot_derivation(self, sd) -> str: expr = f"CAST({expr} AS TEXT)" elif delimiter: expr = f"STRING_AGG({expr}, '{delimiter}')" - return f" {sd.name} AS {expr}" + return f" {expr} AS {sd.name}" def create_ddl(self, schemaview: SchemaView) -> str: """ diff --git a/tests/test_compiler/test_duckdb_compiler.py b/tests/test_compiler/test_duckdb_compiler.py index 98bf7c96..82a3a148 100644 --- a/tests/test_compiler/test_duckdb_compiler.py +++ b/tests/test_compiler/test_duckdb_compiler.py @@ -53,6 +53,103 @@ def test_hidden_slots_excluded_from_sql() -> None: assert "label" in compiled.serialization +def test_from_clause_uses_class_populated_from() -> None: + """SQLCompiler must use class_derivation.populated_from as the FROM table. + + Issue #48: previously the FROM clause used cd.name (the target class name), + producing INSERT INTO Target ... FROM Target — a self-INSERT that reads + from the empty target table and silently inserts zero rows. + """ + from linkml_map.datamodel.transformer_model import ( + ClassDerivation, + SlotDerivation, + TransformationSpecification, + ) + + spec = TransformationSpecification( + id="test", + class_derivations={ + "Agent": ClassDerivation( + name="Agent", + populated_from="Person", + slot_derivations={ + "label": SlotDerivation(name="label", populated_from="name"), + }, + ), + }, + ) + compiled = SQLCompiler().compile(spec) + assert "INSERT INTO Agent" in compiled.serialization + assert "FROM Person" in compiled.serialization + assert "FROM Agent" not in compiled.serialization + + +def test_slot_aliased_source_as_target() -> None: + """SQLCompiler must emit `source AS target`, not `target AS source`. + + Issue #48: with `label: populated_from: name`, the generated SQL must read + the `name` column from the source table and alias it as `label` in the + result. The inverted form references a `label` column that doesn't exist + in the source. + """ + from linkml_map.datamodel.transformer_model import ( + ClassDerivation, + SlotDerivation, + TransformationSpecification, + ) + + spec = TransformationSpecification( + id="test", + class_derivations={ + "Agent": ClassDerivation( + name="Agent", + populated_from="Person", + slot_derivations={ + "label": SlotDerivation(name="label", populated_from="name"), + }, + ), + }, + ) + compiled = SQLCompiler().compile(spec) + assert "name AS label" in compiled.serialization + assert "label AS name" not in compiled.serialization + + +def test_compiled_sql_executes_against_duckdb() -> None: + """End-to-end: compiled INSERT reads from source table and lands rows in target. + + Issue #48: with both bugs (FROM clause + AS direction), source rows never + reached target tables. This verifies the round-trip on a minimal spec. + """ + from linkml_map.datamodel.transformer_model import ( + ClassDerivation, + SlotDerivation, + TransformationSpecification, + ) + + spec = TransformationSpecification( + id="test", + class_derivations={ + "Agent": ClassDerivation( + name="Agent", + populated_from="Person", + slot_derivations={ + "label": SlotDerivation(name="label", populated_from="name"), + }, + ), + }, + ) + compiled = SQLCompiler().compile(spec) + + conn = duckdb.connect(":memory:") + conn.execute("CREATE TABLE Person (name TEXT);") + conn.execute("CREATE TABLE Agent (label TEXT);") + conn.execute("INSERT INTO Person VALUES ('Alice'), ('Bob');") + conn.execute(compiled.serialization) + rows = conn.execute("SELECT label FROM Agent ORDER BY label;").fetchall() + assert rows == [("Alice",), ("Bob",)] + + def test_compile(session: Session) -> None: """Test the DuckDb compiler.""" compiler = SQLCompiler() @@ -73,5 +170,9 @@ def test_compile(session: Session) -> None: print(target_ddl) conn = duckdb.connect(":memory:") + conn.execute(source_ddl) conn.execute(target_ddl) - conn.execute(compiled.serialization) + # TODO #150: compiled INSERTs don't yet provide every target column for + # arbitrary specs (the target's derived schema can include columns not + # named by any slot_derivation). Re-enable execution once SQLCompiler + # emits explicit column lists / handles full target shape.