Repository navigation
Expose anvil_file.file_path column from v6 schema (#8315) - #8376
Open
hannes-ucsc wants to merge 13 commits into
Open
hannes-ucsc wants to merge 13 commits into
hannes-ucsc wants to merge 13 commits into
Conversation
AnVIL entity IDs are currently the `datarepo_row_id` of the row an entity originates from. That column is not stable across releases of a dataset: of the 72 datasets re-released between the `anvil14` and `anvil15` catalogs, not one of the 1353006 rows compared kept its `datarepo_row_id`. Primary keys are far more stable, but they are only unique within a table of a snapshot, so they can't serve as entity IDs on their own. `Plugin._entity_id` therefore derives a v5 UUID from the dataset, the table and the primary key. It has no caller yet; the transition away from `datarepo_row_id` follows in a later commit. The dataset is identified by the one component of the snapshot name that survives a re-release, lower-cased. The deployment configurations derive the same name in their `source` function and key their catalogs by it, so that a new release of a dataset supersedes the previous one. `TestAnvilSnapshotNames` asserts that the two agree by feeding every configured snapshot name back through the configuration that declared it. `SnapshotName` also replaces the ad-hoc regular expression the plugin used to extract the schema version from a snapshot name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three places in the plugin derived the name of a table's primary key column by stripping the `anvil_` prefix from the table name and appending `_id`. The AnVIL schema declares that column for every table it describes, so there is no need to guess it. The lookup is per schema version, like the one for column names next to it, because a future version could rename a key. The schema has always declared exactly one primary key column per table, and always named it by that convention, so this changes no behavior today. `test_pk_column` pins the declaration against the convention, so that a future schema that departs from it fails the build instead of quietly producing queries against a column that doesn't exist. Reading the declaration through `one` likewise turns the assumption that a primary key is a single column into an assertion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entities from tables the AnVIL schema describes are now identified by a
UUID derived from the dataset, the table and the row's primary key,
instead of by the row's `datarepo_row_id`. Entity IDs are therefore
stable across releases of a dataset, where `datarepo_row_id` was not.
They are user-visible, in `/index/{entity_type}/{entity_id}`, in file
download URLs and in manifests, so they change once with this commit and
are expected to survive re-releases from here on.
The three places that identified a row all had to change together.
`anvil_file` rows are reached both by the traversal that builds a primary
bundle and by the batches that build a supplementary bundle, and
`anvil_dataset` rows both by that traversal and by `_get_dataset`.
Converting one site and not another would give a row two different IDs,
depending on which bundle contributed it. The new `_entity_ref` is the
one place that decides.
Tables the schema doesn't describe declare no primary key, so there is
nothing stable to derive an ID from and their rows keep their
`datarepo_row_id`. Such rows only ever occur as replicas.
The column itself is unaffected and remains part of every replica, which
is why the canned index documents still mention the old IDs in that
position, and why the verbatim manifest fixtures didn't change at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every place that slices a table by a prefix of a column's value now uses the primary key the schema declares for that table, falling back to `datarepo_row_id` only for tables the schema doesn't describe. These had to change together: a batch prefix extends the prefix of the partition it falls in, so partitioning a table by one column while batching it by another would let rows fall through the gap between them. Batch membership therefore no longer changes when a dataset is re-released, which makes the contents of a batched bundle stable, not just its UUID. It also means that two rows sharing a primary key now land in the same batch, and hence in the same bundle, where a later commit can assert that they don't. Primary keys are suitable for this because they are UUIDs: all 19754063 of them across the 464 snapshots of `anvil15` are, so the prefixes keep the meaning they had when they were taken from `datarepo_row_id`. Since tables without a declared primary key are now expected rather than exceptional, `_pk_column` returns None for them instead of raising. The five callers that require a key wrap the call in `not_none` to say so, and the two that tolerate its absence go through `_batch_column`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A primary bundle's UUID has always been the UUID of its bundle entity with the version nibble changed. That entity was identified by its `datarepo_row_id`, so the bundle UUID wasn't stable across releases either. It is now derived from the entity's ID, preserving the relationship between the two while making both stable. Bundle UUIDs are the entity IDs of the `bundles` index, so they are user-visible. Deriving the UUID with `uuid5` can't be inverted, so `_bundle_entity`, which recovered the bundle entity's primary key from the UUID, no longer can. It doesn't need to: the FQID is constructed from that key, and carries it to `fetch_bundle`. Fetching a primary bundle therefore costs one BigQuery query less than it used to. Because the UUID is now a function of the FQID's other attributes, the FQID derives it itself, and asserts that a UUID passed to it agrees with that derivation. Deserializing an FQID is the only thing that passes one, whether from a notification or from the arguments of `can_bundle.py`, which gains a `--primary-key` option, no longer being able to identify a primary bundle by its UUID alone. The derivations moved out of the plugin to module scope, so that the FQID can reach them. With the row ID no longer involved, `datarepo_row_uuid_version` has no remaining use. What is left of `batch_uuid_version` is the version of any UUID we derive with `uuid5`, which is what the new derivation produces, too, hence `derived_uuid_version`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An AnVIL bundle's UUID is derived from the table it is drawn from and either the batch or the bundle entity it contains, so requiring it as an argument meant deriving a v5 UUID by hand just to name the bundle one already knows. `--version` was worse: documented as ignored for AnVIL, it was in fact required, and omitting it failed with a `ValueError` that named neither the argument nor the script. Both are now optional and mutually exclusive with the AnVIL arguments, which is a constraint `argparse` can't express, hence the explicit check. Being able to give one of `--batch-prefix` and `--primary-key` and omit the other also retires the `"null"` sentinel that stood for the one that didn't apply. The script used to build an FQID by deserializing a dictionary of its arguments, but a serialized FQID is complete by definition, and `_from_json` type checks the UUID it requires. Constructing the FQID instead leaves it to the repository to derive what wasn't given. For AnVIL that includes the version, which every bundle and every entity in one shares. It was a property of the plugin, which is parameterized by the FQID class and constructs instances of it. Since the FQID now needs the version, too, in order to default it, the constant moved to module scope, where both can reach it without depending on each other. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entities from tables the schema describes are identified by their primary key, so two rows of a table that share one are indistinguishable to us. `add_entity` already rejected an entity it had seen before, but only among the entities, or only among the orphans, depending on where the row was headed. Two `anvil_file` rows that share a key but disagree on `is_supplementary` go to either side of that divide and used to slip through, leaving two replicas with the same ID but different content. Consulting both sides closes that, and makes the detection complete: partitioning, batching and the graph traversal are all keyed on the primary key now, so two rows sharing one always meet in the same bundle. Rows of `anvil_dataset` are the one exception, in that `_get_dataset` rejects a second one of those before it ever gets here. Whether such rows exist, and whether anything upstream prevents them, is not known. The most recent release has none. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The relations in a verbatim PFB manifest were declared `MANY_TO_ONE` on the grounds that a primary key is unique within its table. It isn't, between snapshots: a manifest can span datasets whose tables of the same name share primary keys, in which case a relation's right-hand side matches more than one entity. Declaring the links `MANY_TO_MANY` makes the schema describe the data we actually hand over, instead of promising a cardinality it doesn't honour. It doesn't make the references unambiguous, which is what the FIXME is for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #8376 +/- ##
===========================================
+ Coverage 84.92% 84.96% +0.03%
===========================================
Files 170 170
Lines 25593 25681 +88
===========================================
+ Hits 21734 21819 +85
- Misses 3859 3862 +3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The new --issues option names the issues the PR links, the first of which determines its title. The option is independent of the PR type, and in its absence the issues are inferred from the name of the current branch, as before, for every type. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Version 6 adds the optional `anvil_file.file_path` column. The canned snapshot was ingested under version 5, whose `anvil_file` table lacks that column, so no unit test could exercise a populated value. One of the three canned files leaves the column null, because it is optional even in version 6. The UUID of a batched bundle is derived from the source spec, which contains the name of the snapshot, so the two canned batched bundles are renamed. The UUID of a primary bundle is derived from the primary key of its bundle entity, and is therefore unaffected. The replicas carry the column, and the PFB schema has declared it since #8242, so the verbatim manifests expose it without any change to the source. The ID of a replica document is a hash of its contents, so the two canned file replicas are re-keyed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The column is exposed in the compact manifest and in the response to the /index/files endpoint. The verbatim manifests already carried it for snapshots that were ingested under version 6 of the AnVIL schema. Rows from a snapshot that was ingested under version 5 lack the column, and are now treated as if it were null in them, in the index and the verbatim manifests alike. The column is neither faceted, sorted nor filtered on, so it is excluded from the aggregation of files into entities of other types, where it would bloat the aggregates just like the file name would. The minor version of the service API is incremented, because adding a field to a response and a column to a manifest requires no updates to clients. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
hannes-ucsc
force-pushed
the
issues/hannes-ucsc/8315-file-path
branch
from
October 7, 2026 00:51
1fd4c4d to
84b1794
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linked issues: #8315
Checklist
Author
A01PR is assigned to the authorA02Status of PR is In progressA03PR is a draftA04Target branch isdevelopA05Name of PR branch matchesissues/<GitHub handle of author>/<issue#>-<slug>A06PR is linked to all issues it (partially) resolvesA07Status of linked issues is In progressA08PR description links to linked issuesA09PR title matches1 that of a linked issue or comment in PR explains why they're differentA10PR title references all linked issuesA11For each linked issue, there is at least one commit whose title references that issue1 when the issue title describes a problem, the corresponding PR
title is
Fix:followed by the issue titleAuthor (partiality)
B01Addedptag to titles of partial commitsB02This PR is labeledpartialor completely resolves all linked issuesB03This PR partially resolves each of the linked issues or does not have thepartiallabelAuthor (reindex)
C01Addedrtag to commit title or the changes introduced by this PR will not require reindexing of any deploymentC02This PR is labeledreindex:devor the changes introduced by it will not require reindexing ofdevC03This PR is labeledreindex:anvildevor the changes introduced by it will not require reindexing ofanvildevC04This PR is labeledreindex:anvilprodor the changes introduced by it will not require reindexing ofanvilprodC05This PR is labeledreindex:prodor the changes introduced by it will not require reindexing ofprodC06This PR is labeledreindex:partialand its description documents the specific reindexing procedure fordev,anvildev,anvilprodandprodor requires a full reindex or carries none of the labelsreindex:dev,reindex:anvildev,reindex:anvilprodandreindex:prodAuthor (mirror)
D01This PR is labeledmirror:devor the changes introduced by it will not require mirroring ofdevD02This PR is labeledmirror:anvildevor the changes introduced by it will not require mirroring ofanvildevD03This PR is labeledmirror:anvilprodor the changes introduced by it will not require mirroring ofanvilprodD04This PR is labeledmirror:prodor the changes introduced by it will not require mirroring ofprodD05This PR is labeledmirror:partialand its description documents the specific mirroring procedure fordev,anvildev,anvilprodandprodor requires a full mirroring or carries none of the labelsmirror:dev,mirror:anvildev,mirror:anvilprodandmirror:prodAuthor (API changes)
E01This PR and its linked issues are labeledAPIor this PR does not modify a REST APIE02Addeda(A) tag to commit title for backwards (in)compatible changes or this PR does not modify a REST APIE03Updated REST API version number inapp.pyor this PR does not modify a REST APIAuthor (upgrading deployments)
F01Ranmake docker_images.jsonand committed the resulting changes or this PR does not modifyazul_docker_images, or any other variables referenced in the definition of that variableF02Documented upgrading of deployments in UPGRADING.rst or this PR does not require upgrading deploymentsF03Addedutag to commit title or this PR does not require upgrading deploymentsF04This PR is labeledupgradeor does not require upgrading deploymentsF05This PR is labeleddeploy:sharedor does not modifydocker_images.json, and does not require deploying thesharedcomponent for any other reasonF06This PR is labeleddeploy:gitlabor does not require deploying thegitlabcomponentF07This PR is labeleddeploy:runneror does not require deploying therunnerimageAuthor (hotfixes)
G01AddedFtag to main commit title or this PR does not include permanent fix for a temporary hotfixG02Reverted the temporary hotfixes for any linked issues or the none of the stable branches (anvilprodandprod) have temporary hotfixes for any of the issues linked to this PRAuthor (before every review)
H01Rebased PR branch ondevelop, squashed fixups from prior reviewsH02Ranmake requirements_updateor this PR does not modifypyproject.tomlH03AddedRtag to commit title or this PR does not modifyuv.lockH04This PR is labeledreqsor does not modifyuv.lockH05make integration_testpasses in personal deployment or this PR does not modify functionality that could affect the IT outcomeH06PR is awaiting requested review from a peerH07Status of PR is Review requestedH08PR is assigned to only the peer and the authorPeer reviewer (after approval)
Note that after requesting changes, the PR must be assigned to only the author.
J01Actually approved the PRJ02PR is not a draftJ03PR is awaiting requested review from system administratorJ04Status of PR is Review requestedJ05PR is assigned to only the system administrator and the authorSystem administrator (after approval)
K01Actually approved the PRK02Labeled linked issues asdemoorno demoK03Commented on linked issues about demo expectations or all linked issues are labeledno demoK04Decided if PR can be labeledno sandboxK05A comment to this PR details the completed security design reviewK06PR title is appropriate as title of merge commitK07N reviewslabel is accurateK08Status of PR is ApprovedK09PR is assigned to only the operator and the authorOperator
L01Checkedreindex:…labels andrcommit title tagL02Checkedmirror:…labelsL03Checked that demo expectations are clear or all linked issues are labeledno demoL04Squashed PR branch and rebased ontodevelopL05Sanity-checked historyL06Pushed PR branch to GitHubOperator (deploy
.sharedand.gitlabcomponents)M01Ran_select dev.shared && CI_COMMIT_REF_NAME=develop make -C terraform/shared apply_keep_unusedor this PR is not labeleddeploy:sharedM02Ran_select dev.gitlab && CI_COMMIT_REF_NAME=develop make -C terraform/gitlab apply(an error from _login_docker_gitlab is benign if the instance was stopped for backup) or this PR is not labeleddeploy:gitlabM03Ran_select anvildev.shared && CI_COMMIT_REF_NAME=develop make -C terraform/shared apply_keep_unusedor this PR is not labeleddeploy:sharedM04Ran_select anvildev.gitlab && CI_COMMIT_REF_NAME=develop make -C terraform/gitlab apply(an error from _login_docker_gitlab is benign if the instance was stopped for backup) or this PR is not labeleddeploy:gitlabM05Checked the items in the next section or this PR is labeleddeploy:gitlabM06PR is assigned to only the system administrator and the author or this PR is not labeleddeploy:gitlabSystem administrator (post-deploy of
.gitlabcomponent)N01Background migrations fordev.gitlabare complete or this PR is not labeleddeploy:gitlabN02Background migrations foranvildev.gitlabare complete or this PR is not labeleddeploy:gitlabN03PR is assigned to only the operator and the authorOperator (deploy runner image)
P01Ran_select dev.gitlab && make -C terraform/gitlab/runneror this PR is not labeleddeploy:runnerP02Ran_select anvildev.gitlab && make -C terraform/gitlab/runneror this PR is not labeleddeploy:runnerOperator (sandbox build)
Q01Addedsandboxlabel or PR is labeledno sandboxQ02Pushed PR branch to GitLabdevor PR is labeledno sandboxQ03Pushed PR branch to GitLabanvildevor PR is labeledno sandboxQ04Build passes insandboxdeployment or PR is labeledno sandboxQ05Build passes inanvilboxdeployment or PR is labeledno sandboxQ06Reviewed build logs for anomalies insandboxdeployment or PR is labeledno sandboxQ07Reviewed build logs for anomalies inanvilboxdeployment or PR is labeledno sandboxQ08Applied upgrade instructions from UPGRADING.rst tosandboxor this PR is not labeledupgrade, or upgrade instructions do not apply tosandboxQ09Applied upgrade instructions from UPGRADING.rst toanvilboxor this PR is not labeledupgrade, or upgrade instructions do not apply toanvilboxQ10Insandbox, deleted the catalogs specified in the notes or this PR is missing either thereindex:partialor thereindex:devlabel, or bothQ11Inanvilbox, deleted the catalogs specified in the notes or this PR is missing either thereindex:partialor thereindex:anvildevlabel, or bothQ12Insandbox, deindexed the sources sepcified in the notes or this PR is missing either thereindex:partialor thereindex:devlabel, or bothQ13Inanvilbox, deindexed the sources sepcified in the notes or this PR is missing either thereindex:partialor thereindex:anvildevlabel, or bothQ14Insandbox, indexed the sources specified in the notes or this PR is missing either thereindex:partialor thereindex:devlabel, or bothQ15Inanvilbox, indexed the sources specified in the notes or this PR is missing either thereindex:partialor thereindex:anvildevlabel, or bothQ16Insandbox, indexed the catalogs specified in the notes or this PR is missing either thereindex:partialor thereindex:devlabel, or bothQ17Inanvilbox, indexed the catalogs specified in the notes or this PR is missing either thereindex:partialor thereindex:anvildevlabel, or bothQ18Started full reindex insandboxor this PR is not labeledreindex:devor it is labeled reindex:partialQ19Started full reindex inanvilboxor this PR is not labeledreindex:anvildevor it is labeled reindex:partialQ20Checked for failures insandboxor this PR is not labeledreindex:devQ21Checked for failures inanvilboxor this PR is not labeledreindex:anvildevQ22Started mirroring insandboxor this PR is not labeledmirror:devQ23Started mirroring inanvilboxor this PR is not labeledmirror:anvildevQ24Checked for failures insandboxor this PR is not labeledmirror:devQ25Checked for failures inanvilboxor this PR is not labeledmirror:anvildevOperator (merge the branch)
R01All status checks passed and the PR is mergeableR02The title of the merge commit starts with the title of this PRR03Added PR # reference to merge commit titleR04Collected commit title tags in merge commit title but only includedpif the PR is also labeledpartialR05Pushed merge commit to GitHubR06Status of PR is Merged lowerR07Status of blocked issues is Triage or no issues are blocked on the linked issuesOperator (main build)
S01Pushed merge commit to GitLabdevS02Pushed merge commit to GitLabanvildevS03Build passes on GitLabdevS04Reviewed build logs for anomalies on GitLabdevS05Build passes on GitLabanvildevS06Reviewed build logs for anomalies on GitLabanvildevS07Applied upgrade instructions from UPGRADING.rst todevor this PR is not labeledupgrade, or upgrade instructions do not apply todevS08Applied upgrade instructions from UPGRADING.rst toanvildevor this PR is not labeledupgrade, or upgrade instructions do not apply toanvildevS09Notified developers to apply upgrade instructions from UPGRADING.rst to their personal deployments or this PR is not labeledupgrade, or upgrade instructions do not apply to personal deploymentsS10Ran_select dev.shared && make -C terraform/shared applyor this PR is not labeleddeploy:sharedS11Ran_select anvildev.shared && make -C terraform/shared applyor this PR is not labeleddeploy:sharedS12Deleted PR branch from GitHubS13PR is assigned to only the operatorS14Deleted PR branch from GitLabdevS15Deleted PR branch from GitLabanvildevS16Status of linked issues is Lower, or Triage, if PR is partialOperator (reindex)
T01Indev, deleted the catalogs specified in the notes or this PR is missing either thereindex:partialor thereindex:devlabel, or bothT02Inanvildev, deleted the catalogs specified in the notes or this PR is missing either thereindex:partialor thereindex:anvildevlabel, or bothT03Indev, deindexed the sources sepcified in the notes or this PR is missing either thereindex:partialor thereindex:devlabel, or bothT04Inanvildev, deindexed the sources sepcified in the notes or this PR is missing either thereindex:partialor thereindex:anvildevlabel, or bothT05Indev, indexed the sources specified in the notes or this PR is missing either thereindex:partialor thereindex:devlabel, or bothT06Inanvildev, indexed the sources specified in the notes or this PR is missing either thereindex:partialor thereindex:anvildevlabel, or bothT07Indev, indexed the catalogs specified in the notes or this PR is missing either thereindex:partialor thereindex:devlabel, or bothT08Inanvildev, indexed the catalogs specified in the notes or this PR is missing either thereindex:partialor thereindex:anvildevlabel, or bothT09Started full reindex indevor this PR is not labeledreindex:devor it is labeled reindex:partialT10Started full reindex inanvildevor this PR is not labeledreindex:anvildevor it is labeled reindex:partialT11Checked for, triaged and possibly requeued messages in both fail queues indevor this PR is not labeledreindex:devor it is labeled reindex:partialT12Checked for, triaged and possibly requeued messages in both fail queues inanvildevor this PR is not labeledreindex:anvildevor it is labeled reindex:partialT13Emptied fail queues indevor this PR is not labeledreindex:devor it is labeled reindex:partialT14Emptied fail queues inanvildevor this PR is not labeledreindex:anvildevor it is labeled reindex:partialT15Restarted the Data Browser pipeline for the ucsc/hca/dev branch on GitLab indev, and it succeeded or this PR is not labeledreindex:devT16Restarted the Data Browser pipeline for the ucsc/lungmap/dev branch on GitLab indev, and it succeeded or this PR is not labeledreindex:devT17Restarteddeploy_browserjob in the GitLab pipeline for this PR indev, and it succeeded or this PR is not labeledreindex:devT18Restarted the Data Browser pipeline for the ucsc/anvil/anvildev branch on GitLab inanvildev, and it succeeded or this PR is not labeledreindex:anvildevT19Restarteddeploy_browserjob in the GitLab pipeline for this PR inanvildev, and it succeeded or this PR is not labeledreindex:anvildevOperator (mirroring)
U01Started mirroring indevor this PR is not labelledmirror:devU02Started mirroring inanvildevor this PR is not labelledmirror:anvildevU03Checked for, triaged and possibly requeued messages in mirror fail queue indevor this PR is not labelledmirror:devU04Checked for, triaged and possibly requeued messages in mirror fail queue inanvildevor this PR is not labelledmirror:anvildevU05Emptied mirror fail queue indevor this PR is not labelledmirror:devU06Emptied mirror fail queue inanvildevor this PR is not labelledmirror:anvildevOperator
V01Propagated theupgrade,API,deploy:shared,deploy:gitlab,deploy:runner,reindex:partial,reindex:anvilprod,reindex:prod,mirror:partial,mirror:anvilprodandmirror:prodlabels to any open promotion PRs or this PR carries none of these labels, or is not included in an open promotion PRV02Propagated any specific instructions related to those labels, from the description of this PR to that of any open promotion PRs or this PR carries none of those labels, or is not included in an open promotion PRV03PR is assigned to no oneShorthand for review comments
Lline is too longWline wrapping is wrongQbad quotesFother formatting problem