The primary use case of LinkML is to map data to data. However, because the LinkML metamodel is expressed as a LinkML schema, it is possible to map to and from the metamodel itself.
NOTE this workflow is not yet fully matured, and is subject to change.
Let's assume that we have data represented using models conforming to an ad-hoc metamodel with core constructs: Schema
, Table
, and Column
. We can create a LinkML schema that represents this metamodel as follows:
import yaml
from linkml_runtime.linkml_model import SlotDefinition
from linkml.utils.schema_builder import SchemaBuilder
sb = SchemaBuilder()
sb.add_class("Schema",
tree_root=True,
slots=[
SlotDefinition("id", required=True, description="The name of the schema"),
SlotDefinition("tables", range="Table", multivalued=True, inlined_as_list=True),
],
use_attributes=True,
)
sb.add_class("Table",
slots=[
SlotDefinition("id", required=True, description="The name of the table"),
SlotDefinition("columns", range="Column", multivalued=True, inlined_as_list=True),
],
use_attributes=True,
)
sb.add_class("Column",
slots=[
SlotDefinition("id", required=True, description="The name of the column"),
SlotDefinition("primary_key", range="boolean"),
SlotDefinition("datatype", range="string"),
],
use_attributes=True,
)
my_metamodel = sb.as_dict()
print(yaml.dump(sb.as_dict(), sort_keys=False))
name: test-schema id: http://example.org/test-schema default_prefix: http://example.org/test-schema/ classes: Schema: attributes: id: description: The name of the schema required: true tables: multivalued: true range: Table inlined_as_list: true tree_root: true Table: attributes: id: description: The name of the table required: true columns: multivalued: true range: Column inlined_as_list: true Column: attributes: id: description: The name of the column required: true primary_key: range: boolean datatype: range: string prefixes: {}
Now we'll make an example schema that conforms to this metamodel; this will be a fairly boring schema with a single table Person
with three columns: id
, name
, and description
:
my_schema = {
"id": "my_schema",
"tables": [
{
"id": "Person",
"columns": [
{
"id": "id",
"primary_key": True,
"datatype": "integer"
},
{
"id": "name",
"datatype": "string"
},
{
"id": "description",
"datatype": "string"
}
]
},
]
}
Now we'll create mappings from the ad-hoc metamodel to the LinkML metamodel, where Table maps to a LinkML ClassDefinition, Column maps to a LinkML *SlotDefinition.
metamap = {
"class_derivations": {
"SchemaDefinition": {
"populated_from": "Schema",
"slot_derivations": {
"name": {
"populated_from": "id",
},
"id": {
"expr": "'https://example.org/' + id",
},
"classes": {
"populated_from": "tables",
"dictionary_key": "name",
"cast_collection_as": "MultiValuedDict",
},
}
},
"ClassDefinition": {
"populated_from": "Table",
"slot_derivations": {
"name": {
"populated_from": "id",
},
"attributes": {
"populated_from": "columns",
"dictionary_key": "name",
"cast_collection_as": "MultiValuedDict",
},
}
},
"SlotDefinition": {
"populated_from": "Column",
"slot_derivations": {
"name": {
"populated_from": "id",
},
"identifier": {
"populated_from": "primary_key",
},
"range": {
"populated_from": "datatype",
},
},
},
}
}
from linkml_map.session import Session
session = Session()
session.set_source_schema(my_metamodel)
session.set_object_transformer(metamap)
session.transform(my_schema)
WARNING:linkml_map.transformer.object_transformer:Unexpected: my_schema for type Schema WARNING:linkml_map.transformer.object_transformer:Unexpected: Person for type Schema WARNING:linkml_map.transformer.object_transformer:Unexpected: id for type Schema WARNING:linkml_map.transformer.object_transformer:Unexpected: True for type boolean WARNING:linkml_map.transformer.object_transformer:Unexpected: integer for type string WARNING:linkml_map.transformer.object_transformer:Unexpected: name for type Schema WARNING:linkml_map.transformer.object_transformer:Unexpected: string for type string WARNING:linkml_map.transformer.object_transformer:Unexpected: description for type Schema WARNING:linkml_map.transformer.object_transformer:Unexpected: string for type string WARNING:linkml_map.transformer.transformer:Unknown target range SlotDefinition WARNING:linkml_map.transformer.transformer:Unknown target range SlotDefinition WARNING:linkml_map.transformer.transformer:Unknown target range SlotDefinition WARNING:linkml_map.transformer.transformer:Unknown target range SlotDefinition WARNING:linkml_map.transformer.transformer:Unknown target range SlotDefinition WARNING:linkml_map.transformer.transformer:Unknown target range SlotDefinition WARNING:linkml_map.transformer.transformer:Unknown target range SlotDefinition WARNING:linkml_map.transformer.transformer:Unknown target range SlotDefinition WARNING:linkml_map.transformer.transformer:Unknown target range SlotDefinition WARNING:linkml_map.transformer.transformer:Unknown target range ClassDefinition WARNING:linkml_map.transformer.transformer:Unknown target range ClassDefinition WARNING:linkml_map.transformer.transformer:Unknown target range ClassDefinition WARNING:linkml_map.transformer.transformer:Unknown target range ClassDefinition WARNING:linkml_map.transformer.transformer:Unknown target range ClassDefinition WARNING:linkml_map.transformer.transformer:Unknown target range ClassDefinition WARNING:linkml_map.transformer.transformer:Unknown target range ClassDefinition
{'name': 'my_schema', 'id': 'https://example.org/my_schema', 'classes': {'Person': {'attributes': {'id': {'identifier': True, 'range': 'integer'}, 'name': {'identifier': None, 'range': 'string'}, 'description': {'identifier': None, 'range': 'string'}}}}}
A different scenario is where you might want to customize the existing LinkML metamodel, in particular, adding additional constraints. For example:
TODO