LaminDB

repository·main·Indexed 42 days ago

https://github.com/laminlabs/lamindb

An open-source data management tool for traceable, multimodal AI. LaminDB provides lakehouse-style governance, including ACID transactions, time travel, and schema evolution for data formats such as Parquet, Zarr, and AnnData. It enables reproducible AI workflows through automatic lineage tracking of code, data, and agents using `ln.track()` and the `@ln.flow()` decorator.

Tokens
39.9K
Snippets
144
Records
170
Agent score
58%

What's inside LaminDB

  1. Overview of LaminDB capabilities

    main

    LaminDB is a distributed data management tool designed for traceable, multimodal AI workflows. It provides lakehouse guarantees (ACID transactions, time travel, schema evolution) for non-tabular data formats like parquet, zarr, AnnData, and images.

    Key capabilities include:

    • Lineage: Trace results across agent sessions, notebooks, scripts, and workflows.
    • Lakehouse: Manage multimodal datasets with ACID guarantees and query them using engines like Polars or DuckDB.
    • LIMS & ELN: Unified schema-based records management with support for ontologies and notes.
    • FAIR datasets: Validate and annotate files, DataFrame, AnnData, and SpatialData.
    • Governance: Manage changes via branching and versioning of both data and code.
  2. Prevent Artifact duplication using hash lookups

    main

    When instantiating an Artifact from data intended for managed storage, the Artifact() constructor performs a hash lookup to return the existing artifact if it already exists. This ensures that re-running code does not duplicate files or metadata.

    Hash Lookup Behavior:

    • Managed Storage: Hash lookup is performed by default.
    • Registered Storage Locations: For paths that already live in a registered storage location, hash lookup is skipped by default. To force a hash lookup for these paths, set skip_hash_lookup=False.
    • Force Skip: To explicitly skip the lookup, set skip_hash_lookup=True.
    import lamindb as ln
    
    filepath = "path/to/your/file.ext"
    # Default behavior: returns existing artifact via hash lookup if in managed storage
    artifact = ln.Artifact(filepath, key="my_key.ext").save()
    
    # Re-instantiating returns the same object
    artifact2 = ln.Artifact(filepath, key="my_key.ext")
    assert artifact.id == artifact2.id
    
    # Force a hash lookup even if the path is in a registered storage location
    artifact_forced = ln.Artifact(filepath, key="my_key.ext", skip_hash_lookup=False)
  3. Annotate artifacts with Data Lineage

    main

    LaminDB automatically tracks and annotates artifacts with lineage information when using tracking utilities. When you use ln.track() or the @ln.flow decorator, the resulting artifacts are automatically linked to:

    • ln.Run objects (representing the execution)
    • ln.Transform objects (representing the code/logic applied)

    You can also pass a project argument to ln.track() to ensure all objects created during that run are automatically annotated with that project.

  4. Track run features using the Feature registry

    main

    While params are for arbitrary values, features are validated against a Feature registry. This allows you to express formal relationships with entities (like Record types) in your registries.

    1. Define a Record type (e.g., for an experiment type).
    2. Create a Record instance of that type.
    3. Define a Feature with a specific dtype (e.g., str, int).
    4. Pass these features during a run.

    You can then filter runs by these feature values and retrieve them using run.features.get_values().

    # 1. Setup Registry
    experiment_type = ln.Record(name="Experiments", is_type=True).save()
    experiment_label = ln.Record(name="Experiment1", type=experiment_type).save()
    ln.Feature(name="s3_folder", dtype=str).save()
    ln.Feature(name="experiment", dtype=experiment_type).save()
    
    # 2. Run with features (passed via CLI or script)
    # python scripts/run_track_with_features_and_params.py --s3-folder s3://my-bucket/my-folder --experiment Experiment1
    
    # 3. Query by features
    ln.Run.filter(s3_folder="s3://my-bucket/my-folder").to_dataframe()
    
    # 4. Access feature values
    run2 = ln.Run.filter(
        s3_folder="s3://my-bucket/my-folder", 
        experiment="Experiment1"
    ).last()
    print(run2.features.get_values())
  5. How branching and idempotency work in LaminDB

    main

    LaminDB provides mechanisms to support non-destructive experimentation and repeatable execution:

    Branching (Write-Audit-Publish)

    LaminDB uses database-level branching (stage, review, merge). Isolation is achieved via the branch field in SQLRecord.

    • Isolated objects: Newly created objects live on their creation branch.
    • Versioned objects: Changes to artifacts, collections, transforms, and blocks/notes create new versions isolated to the current branch.
    • Note: In-place updates to existing objects are shared across all branches; to isolate such changes, one must create new objects or new database instances.

    Idempotent Execution

    LaminDB validates content hashes and metadata before writing. This prevents duplicate artifacts or dangling storage objects when re-running scripts, workflows (Nextflow, Snakemake), or agent traces.

  6. Atomicity during bulk creation with ln.save()

    main

    Atomicity is guaranteed at the individual artifact level.

    When passing a list of objects to ln.save():

    • If an upload fails mid-way through a list, all successful uploads up to that point are maintained, and a RuntimeError is raised.
    • If a failure occurs before any uploads begin (e.g., due to a validation error), nothing is saved to the database or storage.
    # If one item in the list fails during upload, previous successful ones remain.
    # If a validation error occurs before any upload, nothing is saved.
    artifacts = [artifact1, artifact2, "invalid_data"]
    
    try:
        ln.save(artifacts)
    except Exception as error:
        print(f"Failed: {error}")
  7. The Two-Phase Commit mechanism for artifact saves

    main

    To prevent dangling metadata or orphaned files, LaminDB executes a save operation in three distinct phases:

    1. Phase 1: Metadata is written to the database with an internal flag _storage_ongoing = True.
    2. Phase 2: The actual file bytes are uploaded to the storage backend.
    3. Phase 3: The database is updated to set _storage_ongoing = False.

    If an upload is interrupted during Phase 2, the artifact remains flagged with _storage_ongoing = True. This state is visible in the UI. To recover, you can re-run lamin save or artifact.save() to attempt the upload again.

  8. How `lamindb.Schema` works for dataset curation

    main

    A lamindb.Schema is a specification that defines the expected structure, data types, and validation rules for a dataset. Unlike standard Pydantic or Pandera schemas, a LaminDB schema enables database queries and supports complex, nested data structures.

    Schemas ensure consistency by defining:

    • Which features (columns/slots) exist.
    • The required data types for those features.
    • Valid values for categorical features (via registries).
    • Whether features are required or optional.

    For composite data structures like AnnData, you use slots to specify which component contains which schema (e.g., "obs" for observations or "var.T" for gene-derived features).

    # Example of a simple DataFrame schema
    schema = ln.Schema(
        name="experiment_schema",
        features=[
            ln.Feature(name="cell_type", dtype=bt.CellType),
            ln.Feature(name="treatment", dtype=str),
        ],
        otype="DataFrame"
    )
    
    # Example of a composite AnnData schema using slots
    schema = ln.Schema(
        otype="AnnData",
        slots={
            "obs": cell_metadata_schema,
            "var.T": gene_id_schema
        }
    )
  9. Prevent SQLRecord duplication using the `name` field

    main

    LaminDB provides idempotency for SQLRecord (and Record) objects through name-based lookups. When you instantiate a record with a name that exactly matches an existing record in the registry, the constructor returns the existing object instead of creating a new one.

    If the name is similar to existing records, LaminDB provides search results to help you decide whether to save a new entry or use an existing one.

    To disable this name-searching behavior (e.g., for performance reasons), set ln.settings.creation.search_names to False.

    import lamindb as ln
    
    # Enable name searching (default)
    ln.settings.creation.search_names = True
    
    # Create or retrieve a record by name
    label = ln.Record(name="My label 1").save()
    
    # If you want to bypass name checks for performance
    ln.settings.creation.search_names = False
    label = ln.Record(name="My label 1c")
  10. How ACID guarantees work for Collection appends

    main

    When calling Collection.append(), LaminDB uses a versioning pattern similar to Iceberg or DuckLake to provide ACID guarantees and snapshot isolation without mutating existing files:

    1. Artifact creation: New data is saved as a new Artifact using the two-phase commit process (staged in storage).
    2. Collection versioning: A new version of the Collection is created that links to the new artifact alongside existing ones.

    This provides:

    • Atomicity: The new collection version is only created if the new artifact is successfully stored and registered.
    • Isolation (Snapshot/Time Travel): Concurrent readers querying the original version are unaffected. The previous state remains addressable via its original version.
  11. Query by features in the Feature registry

    main

    The Feature registry indexes variables across datasets. You can use Feature objects directly in .filter() calls for Artifact, Record, or Run registries to query by dimensions (e.g., perturbation, temperature).

    feature_type = db.Feature.get(name="mini_immuno")
    perturbation = db.Feature.get(name="perturbation", type=feature_type)
    temperature = db.Feature.get(name="temperature")
    
    # Filter artifacts by feature values
    db.Artifact.filter(
        perturbation == "DMSO",
        temperature > 21,
    ).to_dataframe(include="features")
    
    # Check if a feature is present (not null)
    db.Artifact.filter(perturbation.is_null(False)).to_dataframe(include="features")
  12. Compare data validation with Pydantic, Pandera, and LaminDB

    main

    LaminDB provides a different approach to data validation compared to pydantic and pandera. While pydantic and pandera define schemas as code (requiring code changes to update allowed values), LaminDB defines schemas as persisted objects in a database. This allows for updating ontologies and labels (e.g., adding a new CellType) without modifying the application code. LaminDB also supports built-in validation against public ontologies and allows datasets to remain queryable and annotated via the schema.

    Comparison Summary

    Propertypydanticpanderalamindb
    Define schema as codeYes (BaseModel)Yes (DataFrameSchema)Yes (ln.Schema)
    Update labels without code changeNoNoYes (via registries)
    Built-in ontology validationNoNoYes
    Sync labels with registriesNoNoYes
    Re-use features across schemasLimitedSession-onlyYes (persisted)
    Schema changes invalidate dataYesYesNo (versioned)
    Columnar organization supportNoYesYes
    Dataset is queryableNoNoYes
    Dataset is annotatedNoNoYes