diff --git a/e2e/test/scenarios/collections/trash.cy.spec.js b/e2e/test/scenarios/collections/trash.cy.spec.js
index b64a12f9e2c3007648c1de8ef13c123419e3c13e..0b84b8070e079da3b87060623cc595f36ce8fb4f 100644
--- a/e2e/test/scenarios/collections/trash.cy.spec.js
+++ b/e2e/test/scenarios/collections/trash.cy.spec.js
@@ -62,7 +62,7 @@ describe("scenarios > collections > trash", () => {
     popover().within(() => {
       cy.findByText("Move to trash").should("not.exist");
       cy.findByText("Restore").should("exist");
-      cy.findByText("Delete permanently").should("exist");
+      cy.findByText("Delete permanently").should("not.exist");
     });
     toggleEllipsisMenuFor("Collection A");
 
@@ -338,12 +338,15 @@ describe("scenarios > collections > trash", () => {
 
     cy.log("can delete from trash list");
     toggleEllipsisMenuFor("Collection A");
-    popover().findByText("Delete permanently").click();
-    modal().findByText("Delete Collection A permanently?").should("exist");
-    modal().findByText("Delete permanently").click();
-    collectionTable().within(() => {
-      cy.findByText("Collection A").should("not.exist");
-    });
+    // FUTURE: replace following two lines with commented out code when collections can be deleted
+    popover().findByText("Delete permanently").should("not.exist");
+    toggleEllipsisMenuFor("Collection A");
+    // popover().findByText("Delete permanently").click();
+    // modal().findByText("Delete Collection A permanently?").should("exist");
+    // modal().findByText("Delete permanently").click();
+    // collectionTable().within(() => {
+    //   cy.findByText("Collection A").should("not.exist");
+    // });
 
     toggleEllipsisMenuFor("Dashboard A");
     popover().findByText("Delete permanently").click();
@@ -365,12 +368,15 @@ describe("scenarios > collections > trash", () => {
     collectionTable().within(() => {
       cy.findByText("Collection B").click();
     });
-    archiveBanner().findByText("Delete permanently").click();
-    modal().findByText("Delete Collection B permanently?").should("exist");
-    modal().findByText("Delete permanently").click();
-    collectionTable().within(() => {
-      cy.findByText("Collection B").should("not.exist");
-    });
+    // FUTURE: replace following two lines with commented out code when collections can be deleted
+    archiveBanner().findByText("Delete permanently").should("not.exist");
+    cy.visit("/trash");
+    // archiveBanner().findByText("Delete permanently").click();
+    // modal().findByText("Delete Collection B permanently?").should("exist");
+    // modal().findByText("Delete permanently").click();
+    // collectionTable().within(() => {
+    //   cy.findByText("Collection B").should("not.exist");
+    // });
 
     collectionTable().within(() => {
       cy.findByText("Dashboard B").click();
@@ -402,12 +408,13 @@ describe("scenarios > collections > trash", () => {
         true,
       );
       cy.visit("/trash");
+    });
+
+    it("user should be able to bulk restore", () => {
       selectItem("Collection A");
       selectItem("Dashboard A");
       selectItem("Question A");
-    });
 
-    it("user should be able to bulk restore", () => {
       cy.findByTestId("toast-card")
         .should("be.visible")
         .within(() => {
@@ -424,6 +431,10 @@ describe("scenarios > collections > trash", () => {
     });
 
     it("user should be able to bulk move out of trash", () => {
+      selectItem("Collection A");
+      selectItem("Dashboard A");
+      selectItem("Question A");
+
       cy.findByTestId("toast-card")
         .should("be.visible")
         .within(() => {
@@ -455,6 +466,9 @@ describe("scenarios > collections > trash", () => {
     });
 
     it("user should be able to bulk delete", () => {
+      selectItem("Dashboard A");
+      selectItem("Question A");
+
       cy.findByTestId("toast-card")
         .should("be.visible")
         .within(() => {
@@ -464,12 +478,13 @@ describe("scenarios > collections > trash", () => {
         });
 
       modal().within(() => {
-        cy.findByText("Delete 3 items permanently?");
+        cy.findByText("Delete 2 items permanently?");
         cy.findByText("Delete permanently").click();
       });
 
       collectionTable().within(() => {
-        cy.findByText("Collection A").should("not.exist");
+        cy.findByText("Collection A").should("exist");
+        cy.findByText("Dashboard A").should("not.exist");
         cy.findByText("Question A").should("not.exist");
       });
     });
diff --git a/e2e/test/scenarios/organization/timelines-question.cy.spec.js b/e2e/test/scenarios/organization/timelines-question.cy.spec.js
index 06c2fd8f24bd84624d32a793ab86c5ace8bf3ae3..b63fb8278f86ec6f290fd0561cd65c38a40bef7e 100644
--- a/e2e/test/scenarios/organization/timelines-question.cy.spec.js
+++ b/e2e/test/scenarios/organization/timelines-question.cy.spec.js
@@ -309,7 +309,7 @@ describe("scenarios > organization > timelines > question", () => {
       });
 
       cy.createTimelineWithEvents({
-        timeline: { name: "Timeline for collection", collection_id: 1 },
+        timeline: { name: "Timeline for collection", collection_id: 2 },
         events: [
           { name: "TC1", timestamp: "2022-05-20T00:00:00Z", icon: "warning" },
         ],
diff --git a/enterprise/backend/src/metabase_enterprise/snippet_collections/api/native_query_snippet.clj b/enterprise/backend/src/metabase_enterprise/snippet_collections/api/native_query_snippet.clj
index 7b4937951f585733c63b7d616c155ba96351d351..47be96123bc8e39cc0370d06b2c037ebcd997240 100644
--- a/enterprise/backend/src/metabase_enterprise/snippet_collections/api/native_query_snippet.clj
+++ b/enterprise/backend/src/metabase_enterprise/snippet_collections/api/native_query_snippet.clj
@@ -13,7 +13,7 @@
   "Collection children query for snippets on EE."
   :feature :snippet-collections
   [collection {:keys [archived?]}]
-  {:select [:id :name :entity_id [(h2x/literal "snippet") :model]]
+  {:select [:id :collection_id :name :entity_id [(h2x/literal "snippet") :model]]
    :from   [[:native_query_snippet :nqs]]
    :where  [:and
             [:= :collection_id (:id collection)]
diff --git a/enterprise/backend/test/metabase_enterprise/serialization/v2/storage_test.clj b/enterprise/backend/test/metabase_enterprise/serialization/v2/storage_test.clj
index 4922aca2f697fe36c64d582e7e323cf020631708..7382160f272a3c2c037d29901207e17928c80db6 100644
--- a/enterprise/backend/test/metabase_enterprise/serialization/v2/storage_test.clj
+++ b/enterprise/backend/test/metabase_enterprise/serialization/v2/storage_test.clj
@@ -51,7 +51,6 @@
             (is (= (-> (into {} (t2/select-one Collection :id (:id parent)))
                        (dissoc :id :location)
                        (assoc :parent_id nil)
-                       (assoc :trashed_from_parent_id nil)
                        (update :created_at t/offset-date-time))
                    (-> (yaml/from-file (io/file dump-dir "collections" parent-filename (str parent-filename ".yaml")))
                        (dissoc :serdes/meta)
@@ -60,7 +59,6 @@
             (is (= (-> (into {} (t2/select-one Collection :id (:id child)))
                        (dissoc :id :location)
                        (assoc :parent_id (:entity_id parent))
-                       (assoc :trashed_from_parent_id nil)
                        (update :created_at t/offset-date-time))
                    (-> (yaml/from-file (io/file dump-dir "collections" parent-filename
                                                 child-filename (str child-filename ".yaml")))
diff --git a/frontend/src/metabase-types/api/card.ts b/frontend/src/metabase-types/api/card.ts
index e24412595164ee4f2fabbee32d1c50d3711cc1ae..f3069552640cbb407bd612997cf57489542b80a0 100644
--- a/frontend/src/metabase-types/api/card.ts
+++ b/frontend/src/metabase-types/api/card.ts
@@ -33,6 +33,7 @@ export interface Card<Q extends DatasetQuery = DatasetQuery>
   can_write: boolean;
   can_run_adhoc_query: boolean;
   can_restore: boolean;
+  can_delete: boolean;
   initially_published_at: string | null;
 
   database_id?: DatabaseId;
diff --git a/frontend/src/metabase-types/api/collection.ts b/frontend/src/metabase-types/api/collection.ts
index 6fb6771c8409e35b55822568b5889e935568cd05..6a024d98366e72c742d7c7dfb514847f508ab049 100644
--- a/frontend/src/metabase-types/api/collection.ts
+++ b/frontend/src/metabase-types/api/collection.ts
@@ -58,6 +58,7 @@ export interface Collection {
   description: string | null;
   can_write: boolean;
   can_restore: boolean;
+  can_delete: boolean;
   archived: boolean;
   children?: Collection[];
   authority_level?: "official" | null;
@@ -114,6 +115,7 @@ export interface CollectionItem {
   below?: CollectionItemModel[];
   can_write?: boolean;
   can_restore?: boolean;
+  can_delete?: boolean;
   "last-edit-info"?: LastEditInfo;
   location?: string;
   effective_location?: string;
diff --git a/frontend/src/metabase-types/api/dashboard.ts b/frontend/src/metabase-types/api/dashboard.ts
index 30177e50617d43a8012a3f2b261f9f048de7b9b2..14f401e32e21112a946f32ce8c0088c66f0f17ef 100644
--- a/frontend/src/metabase-types/api/dashboard.ts
+++ b/frontend/src/metabase-types/api/dashboard.ts
@@ -48,6 +48,7 @@ export interface Dashboard {
   collection_authority_level?: CollectionAuthorityLevel;
   can_write: boolean;
   can_restore: boolean;
+  can_delete: boolean;
   cache_ttl: number | null;
   "last-edit-info": {
     id: number;
diff --git a/frontend/src/metabase-types/api/mocks/card.ts b/frontend/src/metabase-types/api/mocks/card.ts
index da8d5283ab8862744e598ee5296bda8c4c4e5fb8..79bc9dae92f092252945ee8bf867b6fc61a4069b 100644
--- a/frontend/src/metabase-types/api/mocks/card.ts
+++ b/frontend/src/metabase-types/api/mocks/card.ts
@@ -31,6 +31,7 @@ export const createMockCard = (opts?: Partial<Card>): Card => ({
   can_write: true,
   can_run_adhoc_query: true,
   can_restore: false,
+  can_delete: false,
   cache_ttl: null,
   collection: null,
   collection_id: null,
diff --git a/frontend/src/metabase-types/api/mocks/collection.ts b/frontend/src/metabase-types/api/mocks/collection.ts
index 44f7b277fa03d590f4f5039590906f5f1a017003..9ce36565f1a225a0815e7d3b4205bbba6becb50d 100644
--- a/frontend/src/metabase-types/api/mocks/collection.ts
+++ b/frontend/src/metabase-types/api/mocks/collection.ts
@@ -8,7 +8,8 @@ export const createMockCollection = (
   description: null,
   location: "/",
   can_write: true,
-  can_restore: true,
+  can_restore: false,
+  can_delete: false,
   archived: false,
   is_personal: false,
   authority_level: null,
diff --git a/frontend/src/metabase-types/api/mocks/dashboard.ts b/frontend/src/metabase-types/api/mocks/dashboard.ts
index 8ff4af99940895eccb055276041538e36a5c5034..a86039d927117040c23ce71eedf09345551b56de 100644
--- a/frontend/src/metabase-types/api/mocks/dashboard.ts
+++ b/frontend/src/metabase-types/api/mocks/dashboard.ts
@@ -18,7 +18,8 @@ export const createMockDashboard = (opts?: Partial<Dashboard>): Dashboard => ({
   name: "Dashboard",
   dashcards: [],
   can_write: true,
-  can_restore: true,
+  can_restore: false,
+  can_delete: false,
   description: "",
   cache_ttl: null,
   "last-edit-info": {
diff --git a/frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/QueryActionContextProvider.tsx b/frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/QueryActionContextProvider.tsx
index abb54eb2cfb8f643d7d875122d3952c0e63f91f1..a48db5ad6493eb40d369790a90af600cb34981f6 100644
--- a/frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/QueryActionContextProvider.tsx
+++ b/frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/QueryActionContextProvider.tsx
@@ -68,6 +68,7 @@ function convertActionToQuestionCard(
     can_write: true,
     can_run_adhoc_query: true,
     can_restore: false,
+    can_delete: false,
     public_uuid: null,
     collection_id: null,
     collection_position: null,
diff --git a/frontend/src/metabase/archive/components/ArchivedEntityBanner/ArchivedEntityBanner.tsx b/frontend/src/metabase/archive/components/ArchivedEntityBanner/ArchivedEntityBanner.tsx
index 0322d1ba235c468e15618fa9b8cc1fb1f53d95e7..ff14d6d25a83283fb7952341cd21942b89e76f50 100644
--- a/frontend/src/metabase/archive/components/ArchivedEntityBanner/ArchivedEntityBanner.tsx
+++ b/frontend/src/metabase/archive/components/ArchivedEntityBanner/ArchivedEntityBanner.tsx
@@ -13,6 +13,7 @@ type ArchivedEntityBannerProps = {
   entityType: string;
   canWrite: boolean;
   canRestore: boolean;
+  canDelete: boolean;
   onUnarchive: () => void;
   onMove: (collection: CollectionPickerValueItem) => void;
   onDeletePermanently: () => void;
@@ -23,11 +24,13 @@ export const ArchivedEntityBanner = ({
   entityType,
   canRestore,
   canWrite,
+  canDelete,
   onUnarchive,
   onMove,
   onDeletePermanently,
 }: ArchivedEntityBannerProps) => {
   const [modal, setModal] = useState<"move" | "delete" | null>(null);
+  const hasAction = canWrite || canDelete || canRestore;
 
   return (
     <>
@@ -52,19 +55,26 @@ export const ArchivedEntityBanner = ({
               ).t`This ${entityType} is in the trash.`}
             </Text>
           </Flex>
-          {canWrite && (
+          {hasAction && (
             <Flex gap={{ base: "sm", sm: "md" }}>
               {canRestore && (
                 <BannerButton iconName="revert" onClick={onUnarchive}>
                   {t`Restore`}
                 </BannerButton>
               )}
-              <BannerButton iconName="move" onClick={() => setModal("move")}>
-                {t`Move`}
-              </BannerButton>
-              <BannerButton iconName="trash" onClick={() => setModal("delete")}>
-                {t`Delete permanently`}
-              </BannerButton>
+              {canWrite && (
+                <BannerButton iconName="move" onClick={() => setModal("move")}>
+                  {t`Move`}
+                </BannerButton>
+              )}
+              {canDelete && (
+                <BannerButton
+                  iconName="trash"
+                  onClick={() => setModal("delete")}
+                >
+                  {t`Delete permanently`}
+                </BannerButton>
+              )}
             </Flex>
           )}
         </Flex>
diff --git a/frontend/src/metabase/collections/components/ActionMenu/ActionMenu.tsx b/frontend/src/metabase/collections/components/ActionMenu/ActionMenu.tsx
index a41b97fd86d3cd481a03620a5bea108dc83c6075..7e47af99264993bfd08a83b95b367fd1b165990d 100644
--- a/frontend/src/metabase/collections/components/ActionMenu/ActionMenu.tsx
+++ b/frontend/src/metabase/collections/components/ActionMenu/ActionMenu.tsx
@@ -18,7 +18,6 @@ import {
   canPreviewItem,
   isItemPinned,
   isPreviewEnabled,
-  canDeleteItem,
 } from "metabase/collections/utils";
 import { ConfirmDeleteModal } from "metabase/components/ConfirmDeleteModal";
 import EventSandbox from "metabase/components/EventSandbox";
@@ -100,7 +99,7 @@ function ActionMenu({
   const canMove = canMoveItem(item, collection);
   const canArchive = canArchiveItem(item, collection);
   const canRestore = item.can_restore;
-  const canDelete = canDeleteItem(item, collection);
+  const canDelete = item.can_delete;
   const canCopy = canCopyItem(item);
   const canUseMetabot =
     database != null && canUseMetabotOnDatabase(database) && isMetabotEnabled;
diff --git a/frontend/src/metabase/collections/components/CollectionBulkActions/ArchivedBulkActions.tsx b/frontend/src/metabase/collections/components/CollectionBulkActions/ArchivedBulkActions.tsx
index 8a571f1684795524453b9420ec68115b37d6904b..9700962d98c3a123680ddeb56545c9df1fd6499d 100644
--- a/frontend/src/metabase/collections/components/CollectionBulkActions/ArchivedBulkActions.tsx
+++ b/frontend/src/metabase/collections/components/CollectionBulkActions/ArchivedBulkActions.tsx
@@ -3,11 +3,7 @@ import { msgid, ngettext, t } from "ttag";
 import _ from "underscore";
 
 import { BulkDeleteConfirmModal } from "metabase/archive/components/BulkDeleteConfirmModal";
-import {
-  canDeleteItem,
-  canMoveItem,
-  isRootTrashCollection,
-} from "metabase/collections/utils";
+import { canMoveItem, isRootTrashCollection } from "metabase/collections/utils";
 import {
   BulkActionButton,
   BulkActionDangerButton,
@@ -64,8 +60,8 @@ export const ArchivedBulkActions = ({
 
   // delete
   const canDelete = useMemo(() => {
-    return selected.every(item => canDeleteItem(item, collection));
-  }, [selected, collection]);
+    return selected.every(item => item.can_delete);
+  }, [selected]);
 
   const handleBulkDeletePermanentlyStart = async () => {
     setSelectedItems(selected);
diff --git a/frontend/src/metabase/collections/components/CollectionContent/CollectionContentView.tsx b/frontend/src/metabase/collections/components/CollectionContent/CollectionContentView.tsx
index fb36a295e9298107b8a198597f4cd7c65877d67c..c66e169308deca39465fb2930da81ccf31d07161 100644
--- a/frontend/src/metabase/collections/components/CollectionContent/CollectionContentView.tsx
+++ b/frontend/src/metabase/collections/components/CollectionContent/CollectionContentView.tsx
@@ -272,6 +272,7 @@ export const CollectionContentView = ({
                 entityType="collection"
                 canWrite={collection.can_write}
                 canRestore={collection.can_restore}
+                canDelete={collection.can_delete}
                 onUnarchive={() => {
                   const input = { ...actionId, name: collection.name };
                   dispatch(Collections.actions.setArchived(input, false));
diff --git a/frontend/src/metabase/collections/components/PinnedItemCard/PinnedItemCard.stories.tsx b/frontend/src/metabase/collections/components/PinnedItemCard/PinnedItemCard.stories.tsx
index d2f71693e7da61899438195c39cdc4e2de44175c..c8cb966b6a9d5bf425195f08f616dffe576d74f8 100644
--- a/frontend/src/metabase/collections/components/PinnedItemCard/PinnedItemCard.stories.tsx
+++ b/frontend/src/metabase/collections/components/PinnedItemCard/PinnedItemCard.stories.tsx
@@ -15,6 +15,7 @@ const collection = {
   description: null,
   archived: false,
   can_restore: false,
+  can_delete: false,
   location: "/",
 };
 
diff --git a/frontend/src/metabase/collections/utils.ts b/frontend/src/metabase/collections/utils.ts
index 97067f62870cbdd2171bd86d122e83594e31e48f..e404f8d32b2236071aea786bc176d2473a3631a6 100644
--- a/frontend/src/metabase/collections/utils.ts
+++ b/frontend/src/metabase/collections/utils.ts
@@ -189,10 +189,6 @@ export function canArchiveItem(item: CollectionItem, collection?: Collection) {
   );
 }
 
-export function canDeleteItem(item: CollectionItem, collection?: Collection) {
-  return item.archived && (item.can_write ?? collection?.can_write ?? true);
-}
-
 export function canCopyItem(item: CollectionItem) {
   return item.copy && !item.archived;
 }
diff --git a/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.tsx b/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.tsx
index 937b58cdc04cd72474ebc0496f884af6b47ba37d..688fcd7b552a00432fe19d93a87a49e750ef998b 100644
--- a/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.tsx
+++ b/frontend/src/metabase/dashboard/components/Dashboard/Dashboard.tsx
@@ -221,6 +221,7 @@ function DashboardInner(props: DashboardProps) {
 
   const canWrite = Boolean(dashboard?.can_write);
   const canRestore = Boolean(dashboard?.can_restore);
+  const canDelete = Boolean(dashboard?.can_delete);
   const tabHasCards = currentTabDashcards.length > 0;
   const dashboardHasCards = dashboard && dashboard.dashcards.length > 0;
 
@@ -433,6 +434,7 @@ function DashboardInner(props: DashboardProps) {
                 entityType="dashboard"
                 canWrite={canWrite}
                 canRestore={canRestore}
+                canDelete={canDelete}
                 onUnarchive={() => dispatch(setArchivedDashboard(false))}
                 onMove={({ id }) => dispatch(moveDashboardToCollection({ id }))}
                 onDeletePermanently={() => {
diff --git a/frontend/src/metabase/dashboard/components/DashboardTabs/test-utils.ts b/frontend/src/metabase/dashboard/components/DashboardTabs/test-utils.ts
index 44417905517a143fa9e17b58828d89a86e9181e1..5e1a4c018a29cafcc2943f888b810ef78ee5ce35 100644
--- a/frontend/src/metabase/dashboard/components/DashboardTabs/test-utils.ts
+++ b/frontend/src/metabase/dashboard/components/DashboardTabs/test-utils.ts
@@ -30,7 +30,8 @@ export const TEST_DASHBOARD_STATE: DashboardState = {
       name: "",
       description: "",
       can_write: true,
-      can_restore: true,
+      can_restore: false,
+      can_delete: false,
       cache_ttl: null,
       auto_apply_filters: true,
       archived: false,
diff --git a/frontend/src/metabase/query_builder/components/view/View.jsx b/frontend/src/metabase/query_builder/components/view/View.jsx
index 6709684679dc07d92a1547ab759240ff7cb52fec..6f935f1a75f58eb7a00c7cbada5aa186237ce7a9 100644
--- a/frontend/src/metabase/query_builder/components/view/View.jsx
+++ b/frontend/src/metabase/query_builder/components/view/View.jsx
@@ -245,6 +245,7 @@ class View extends Component {
             entityType={card.type}
             canWrite={card.can_write}
             canRestore={card.can_restore}
+            canDelete={card.can_delete}
             onUnarchive={() => onUnarchive(question)}
             onMove={collection => onMove(question, collection)}
             onDeletePermanently={() => onDeletePermanently(card.id)}
diff --git a/resources/migrations/001_update_migrations.yaml b/resources/migrations/001_update_migrations.yaml
index bb7fedf2101ba27f67f811caeed243792205fa52..21079b2a08415cb4f8dbc48fed12918024dbbd9a 100644
--- a/resources/migrations/001_update_migrations.yaml
+++ b/resources/migrations/001_update_migrations.yaml
@@ -7673,187 +7673,6 @@ databaseChangeLog:
                   constraints:
                     nullable: false
 
-  - changeSet:
-      id: v50.2024-05-14T12:13:22
-      author: johnswanson
-      comment: Add `collection.trashed_from_location`
-      changes:
-        - addColumn:
-            tableName: collection
-            columns:
-              - column:
-                  name: trashed_from_location
-                  type: ${text.type}
-                  remarks: "The previous location this collection was trashed from"
-                  constraints:
-                    nullable: true
-
-  - changeSet:
-      id: v50.2024-05-14T12:13:33
-      author: johnswanson
-      comment: Add `report_card.trashed_from_collection_id`
-      changes:
-        - addColumn:
-            tableName: report_card
-            columns:
-              - column:
-                  name: trashed_from_collection_id
-                  type: int
-                  remarks: "The previous parent collection this card was trashed *from*"
-                  constraints:
-                    nullable: true
-
-  - changeSet:
-      id: v50.2024-05-14T12:13:39
-      author: johnswanson
-      comment: Add `report_dashboard.trashed_from_collection_id`
-      changes:
-        - addColumn:
-            tableName: report_dashboard
-            columns:
-              - column:
-                  name: trashed_from_collection_id
-                  type: int
-                  remarks: "The previous parent collection this dashboard was trashed *from*"
-                  constraints:
-                    nullable: true
-
-  - changeSet:
-      id: v50.2024-05-14T12:42:16
-      author: johnswanson
-      comment: Drop foreign key constraint fk_snippet_collection_id
-      rollback:
-        - addForeignKeyConstraint:
-            baseTableName: native_query_snippet
-            baseColumnNames: collection_id
-            referencedTableName: collection
-            referencedColumnNames: id
-            constraintName: fk_snippet_collection_id
-            onDelete: SET NULL
-      changes:
-        - dropForeignKeyConstraint:
-            baseTableName: native_query_snippet
-            constraintName: fk_snippet_collection_id
-
-  - changeSet:
-      id: v50.2024-05-14T12:42:27
-      author: johnswanson
-      comment: Add foreign key constraint fk_snippet_collection_id with CASCADE delete
-      rollback:
-        - dropForeignKeyConstraint:
-            baseTableName: native_query_snippet
-            constraintName: fk_snippet_collection_id
-      changes:
-        - addForeignKeyConstraint:
-            baseTableName: native_query_snippet
-            baseColumnNames: collection_id
-            referencedTableName: collection
-            referencedColumnNames: id
-            constraintName: fk_snippet_collection_id
-            onDelete: CASCADE
-
-  - changeSet:
-      id: v50.2024-05-14T12:42:29
-      author: johnswanson
-      comment: Drop foreign key constraint fk_pulse_collection_id
-      rollback:
-        - addForeignKeyConstraint:
-            baseTableName: pulse
-            baseColumnNames: collection_id
-            referencedTableName: collection
-            referencedColumnNames: id
-            constraintName: fk_pulse_collection_id
-            onDelete: SET NULL
-      changes:
-        - dropForeignKeyConstraint:
-            baseTableName: pulse
-            constraintName: fk_pulse_collection_id
-
-  - changeSet:
-      id: v50.2024-05-14T12:42:32
-      author: johnswanson
-      comment: Add foreign key constraint fk_pulse_collection_id with CASCADE delete
-      rollback:
-        - dropForeignKeyConstraint:
-            baseTableName: pulse
-            constraintName: fk_pulse_collection_id
-      changes:
-        - addForeignKeyConstraint:
-            baseTableName: pulse
-            baseColumnNames: collection_id
-            referencedTableName: collection
-            referencedColumnNames: id
-            constraintName: fk_pulse_collection_id
-            onDelete: CASCADE
-
-  - changeSet:
-      id: v50.2024-05-14T12:42:33
-      author: johnswanson
-      comment: Drop foreign key constraint fk_card_collection_id
-      rollback:
-        - addForeignKeyConstraint:
-            baseTableName: report_card
-            baseColumnNames: collection_id
-            referencedTableName: collection
-            referencedColumnNames: id
-            constraintName: fk_card_collection_id
-            onDelete: SET NULL
-      changes:
-        - dropForeignKeyConstraint:
-            baseTableName: report_card
-            constraintName: fk_card_collection_id
-
-  - changeSet:
-      id: v50.2024-05-14T12:42:36
-      author: johnswanson
-      comment: Add foreign key constraint fk_card_collection_id with CASCADE delete
-      rollback:
-        - dropForeignKeyConstraint:
-            baseTableName: report_card
-            constraintName: fk_card_collection_id
-      changes:
-        - addForeignKeyConstraint:
-            baseTableName: report_card
-            baseColumnNames: collection_id
-            referencedTableName: collection
-            referencedColumnNames: id
-            constraintName: fk_card_collection_id
-            onDelete: CASCADE
-
-  - changeSet:
-      id: v50.2024-05-14T12:42:37
-      author: johnswanson
-      comment: Drop foreign key constraint fk_dashboard_collection_id
-      rollback:
-        - addForeignKeyConstraint:
-            baseTableName: report_dashboard
-            baseColumnNames: collection_id
-            referencedTableName: collection
-            referencedColumnNames: id
-            constraintName: fk_dashboard_collection_id
-            onDelete: SET NULL
-      changes:
-        - dropForeignKeyConstraint:
-            baseTableName: report_dashboard
-            constraintName: fk_dashboard_collection_id
-
-  - changeSet:
-      id: v50.2024-05-14T12:42:40
-      author: johnswanson
-      comment: Add foreign key constraint fk_dashboard_collection_id with CASCADE delete
-      rollback:
-        - dropForeignKeyConstraint:
-            baseTableName: report_dashboard
-            constraintName: fk_dashboard_collection_id
-      changes:
-        - addForeignKeyConstraint:
-            baseTableName: report_dashboard
-            baseColumnNames: collection_id
-            referencedTableName: collection
-            referencedColumnNames: id
-            constraintName: fk_dashboard_collection_id
-            onDelete: CASCADE
-
   - changeSet:
       id: v50.2024-05-14T12:42:42
       author: johnswanson
@@ -7876,92 +7695,7 @@ databaseChangeLog:
               WHERE permissions.object IN (
                 SELECT CONCAT('/collection/', collection.id, '/') FROM collection WHERE collection.type = 'trash'
               );
-              UPDATE collection
-              SET
-                entity_id = NULL,
-                archived = true,
-                name = 'Trash (Auto-Generated)',
-                type = NULL
-              WHERE type = 'trash';
-
-  - changeSet:
-      id: v50.2024-05-14T12:42:44
-      author: johnswanson
-      comment: Move existing archived collections to the Trash - (Postgres)
-      preConditions:
-        - onFail: MARK_RAN
-        - dbms:
-            type: postgresql
-      changes:
-        - sql:
-            sql: >-
-              UPDATE collection AS c1
-              SET trashed_from_location = c1.location,
-                  location = CONCAT('/', c2.id, '/')
-              FROM (SELECT id FROM collection WHERE type = 'trash') AS c2
-              WHERE c1.archived = true AND c1.namespace IS NULL;
-      rollback: # not needed. See above: `Trash` becomes a normal collection
-
-  - changeSet:
-      id: v50.2024-05-14T12:42:46
-      author: johnswanson
-      comment: Move existing archived collections to the Trash - (H2)
-      preConditions:
-        - onFail: MARK_RAN
-        - dbms:
-            type: h2
-      changes:
-        - sql:
-            sql: >-
-              UPDATE collection AS c1
-              SET trashed_from_location = c1.location,
-                  location = CONCAT('/', (SELECT id FROM collection WHERE type = 'trash'), '/')
-              WHERE c1.archived = true AND c1.namespace IS NULL;
-      rollback: # Not needed.
-
-  - changeSet:
-      id: v50.2024-05-14T12:42:48
-      author: johnswanson
-      comment: Move existing archived collections to the Trash - (MySQL/MariaDB)
-      preConditions:
-        - onFail: MARK_RAN
-        - dbms:
-            type: mysql,mariadb
-      changes:
-        - sql:
-            sql: >-
-              UPDATE collection AS c1
-              JOIN (
-                  SELECT id FROM collection WHERE type = 'trash'
-              ) AS c2
-              SET c1.trashed_from_location = c1.location,
-                  c1.location = CONCAT('/', c2.id, '/')
-              WHERE c1.archived = true AND c1.namespace IS NULL;
-      rollback: # Not needed
-
-  - changeSet:
-      id: v50.2024-05-14T12:42:50
-      author: johnswanson
-      comment: Move existing archived dashboards to the Trash
-      changes:
-        - sql:
-            sql: >-
-              UPDATE report_dashboard
-              SET trashed_from_collection_id = collection_id, collection_id = (SELECT id FROM collection WHERE type = 'trash')
-              WHERE archived = true;
-      rollback: # Not needed
-
-  - changeSet:
-      id: v50.2024-05-14T12:42:52
-      author: johnswanson
-      comment: Move existing archived cards to the Trash
-      changes:
-        - sql:
-            sql: >-
-              UPDATE report_card
-              SET trashed_from_collection_id = collection_id, collection_id = (SELECT id FROM collection WHERE type = 'trash')
-              WHERE archived = true;
-      rollback: # Not needed
+              DELETE FROM collection WHERE type = 'trash';
 
   - changeSet:
       id: v50.2024-05-15T13:13:13
@@ -8032,6 +7766,112 @@ databaseChangeLog:
         - customChange:
             class: "metabase.db.custom_migrations.CreateSampleContent"
 
+  - changeSet:
+      id: v50.2024-05-29T14:04:47
+      author: johnswanson
+      comment: Add `report_dashboard.archived_directly`
+      changes:
+        - addColumn:
+            tableName: report_dashboard
+            columns:
+              - column:
+                  name: archived_directly
+                  type: ${boolean.type}
+                  defaultValueBoolean: false
+                  remarks: "Was this thing trashed directly"
+                  constraints:
+                    nullable: false
+
+  - changeSet:
+      id: v50.2024-05-29T14:04:53
+      author: johnswanson
+      comment: Add `report_card.archived_directly`
+      changes:
+        - addColumn:
+            tableName: report_card
+            columns:
+              - column:
+                  name: archived_directly
+                  type: ${boolean.type}
+                  remarks: "Was this thing trashed directly"
+                  defaultValueBoolean: false
+                  constraints:
+                    nullable: false
+
+  - changeSet:
+      id: v50.2024-05-29T14:04:58
+      author: johnswanson
+      comment: Add `collection.archive_operation_id`
+      changes:
+        - addColumn:
+            tableName: collection
+            columns:
+              - column:
+                  name: archive_operation_id
+                  type: char(36)
+                  remarks: "The UUID of the trash operation. Each time you trash a collection subtree, you get a unique ID."
+                  constraints:
+                    nullable: true
+
+  - changeSet:
+      id: v50.2024-05-29T14:05:01
+      author: johnswanson
+      comment: Add `collection.archived_directly`
+      changes:
+        - addColumn:
+            tableName: collection
+            columns:
+              - column:
+                  name: archived_directly
+                  type: ${boolean.type}
+                  remarks: "Whether the item was trashed independently or as a subcollection"
+                  constraints:
+                    nullable: true
+
+  - changeSet:
+      id: v50.2024-05-29T14:05:03
+      author: johnswanson
+      comment: Populate `archived_directly` and `archive_operation_id` (Postgres)
+      changes:
+        - sqlFile:
+            dbms: postgresql
+            path: trash/postgres.sql
+            relativeToChangelogFile: true
+      rollback: # not needed, columns will be deleted
+
+  - changeSet:
+      id: v50.2024-05-29T18:42:13
+      author: johnswanson
+      comment: Populate `archived_directly` and `archive_operation_id` (H2)
+      changes:
+        - sqlFile:
+            dbms: h2
+            path: trash/h2.sql
+            relativeToChangelogFile: true
+      rollback: # not needed, columns will be deleted
+
+  - changeSet:
+      id: v50.2024-05-29T18:42:14
+      author: johnswanson
+      comment: Populate `archived_directly` and `archive_operation_id` (MySQL)
+      changes:
+        - sqlFile:
+            dbms: mysql
+            path: trash/mysql.sql
+            relativeToChangelogFile: true
+      rollback: # not needed, columns will be deleted
+
+  - changeSet:
+      id: v50.2024-05-29T18:42:15
+      author: johnswanson
+      comment: Populate `archived_directly` and `archive_operation_id` (MariaDB)
+      changes:
+        - sqlFile:
+            dbms: mariadb
+            path: trash/mariadb.sql
+            relativeToChangelogFile: true
+      rollback: # not needed, columns will be deleted
+
   - changeSet:
       id: v51.2024-05-13T15:30:57
       author: metamben
diff --git a/resources/migrations/trash/h2.sql b/resources/migrations/trash/h2.sql
new file mode 100644
index 0000000000000000000000000000000000000000..afcea5786c266f67f3350ab684f2966df8231cd0
--- /dev/null
+++ b/resources/migrations/trash/h2.sql
@@ -0,0 +1,101 @@
+-- DASHBOARDS
+-- Set `archived_directly`.
+UPDATE report_dashboard
+SET archived_directly = COALESCE(
+  -- If the collection is archived then it was *not* archived directly
+  (SELECT NOT collection.archived FROM collection WHERE collection.id = report_dashboard.collection_id),
+  false
+  )
+WHERE archived = true;
+
+-- CARDS
+-- Set `archived_directly`.
+UPDATE report_card
+SET archived_directly = COALESCE(
+  -- If the collection is archived then it was *not* archived directly
+  (SELECT NOT collection.archived FROM collection WHERE collection.id = report_card.collection_id),
+  false
+  )
+WHERE archived = true;
+
+-- COLLECTIONS
+-- Set `collection.archived_directly`.
+WITH CollectionWithParentID AS (
+  SELECT
+  id,
+  archived,
+  CASE
+      WHEN location = '/' THEN NULL
+      ELSE REGEXP_SUBSTR(location, '/([0-9]+)(/$)', 1, 1, '', 1)::INTEGER
+  END AS parent_id
+  FROM
+  collection
+)
+
+UPDATE collection c
+SET archived_directly = true
+WHERE EXISTS (
+  SELECT 1
+  FROM CollectionWithParentID cp
+  WHERE c.id = cp.id
+  AND cp.archived = true
+  AND (
+    cp.parent_id IS NULL
+    OR NOT EXISTS (
+      SELECT 1
+      FROM CollectionWithParentID pp
+      WHERE pp.id = cp.parent_id
+      AND pp.archived = true
+    )
+  )
+);
+
+-- Set `collection.archive_operation_id` for collections that were archived directly
+UPDATE collection
+SET archive_operation_id =
+CASE
+    WHEN LENGTH(CAST(id AS VARCHAR)) <= 12 THEN
+        CONCAT('00000000-0000-0000-0000-', LPAD(CAST(id AS VARCHAR), 12, '0'))
+    WHEN LENGTH(CAST(id AS VARCHAR)) > 12 AND LENGTH(CAST(id AS VARCHAR)) <= 16 THEN
+        CONCAT('00000000-0000-0000-',
+               LPAD(SUBSTRING(CAST(id AS VARCHAR), 1, LENGTH(CAST(id AS VARCHAR)) - 12), 4, '0'), '-',
+               SUBSTRING(CAST(id AS VARCHAR), LENGTH(CAST(id AS VARCHAR)) - 11, 12))
+    WHEN LENGTH(CAST(id AS VARCHAR)) > 16 AND LENGTH(CAST(id AS VARCHAR)) <= 20 THEN
+        CONCAT('00000000-0000-',
+               LPAD(SUBSTRING(CAST(id AS VARCHAR), 1, 4), 4, '0'), '-',
+               LPAD(SUBSTRING(CAST(id AS VARCHAR), 5, 4), 4, '0'), '-',
+               SUBSTRING(CAST(id AS VARCHAR), 9))
+    WHEN LENGTH(CAST(id AS VARCHAR)) > 20 THEN
+        CONCAT(
+               LPAD(SUBSTRING(CAST(id AS VARCHAR), 1, 8), 8, '0'), '-',
+               LPAD(SUBSTRING(CAST(id AS VARCHAR), 9, 4), 4, '0'), '-',
+               LPAD(SUBSTRING(CAST(id AS VARCHAR), 13, 4), 4, '0'), '-',
+               LPAD(SUBSTRING(CAST(id AS VARCHAR), 17, 12), 12, '0')
+        )
+    -- If someone has >10^20 collections, they have bigger problems than a wrong `archive_operation_id`
+    ELSE '00000000-0000-0000-0000-000000000000'
+END
+WHERE archived AND archived_directly;
+
+WITH Ancestors(id, archived, archived_directly, archive_operation_id, location) AS (
+    SELECT
+    id,
+    archived,
+    archived_directly,
+    archive_operation_id,
+    location
+  FROM
+    collection
+  WHERE
+    archived_directly = true
+    AND archived = true
+)
+UPDATE collection
+SET archive_operation_id = (
+  SELECT a.archive_operation_id
+  FROM Ancestors a
+  WHERE collection.location LIKE concat(a.location, a.id, '/%')
+  ORDER BY LENGTH(a.location) DESC
+  LIMIT 1
+), archived_directly = false
+WHERE archived_directly IS NULL AND archive_operation_id IS NULL AND archived = true;
diff --git a/resources/migrations/trash/mariadb.sql b/resources/migrations/trash/mariadb.sql
new file mode 100644
index 0000000000000000000000000000000000000000..d8d740ad2d7218faca54b64972a5d25a5f6c403c
--- /dev/null
+++ b/resources/migrations/trash/mariadb.sql
@@ -0,0 +1,96 @@
+-- DASHBOARDS
+-- Set `archived_directly`.
+UPDATE report_dashboard
+SET archived_directly = COALESCE(
+  -- If the collection is archived too, then it was not archived directly
+  (SELECT NOT archived FROM collection WHERE id = collection_id),
+  false
+  )
+WHERE archived = true;
+
+-- CARDS
+UPDATE report_card
+SET archived_directly = COALESCE(
+  -- If the collection is archived too, then it was not archived directly
+  (SELECT NOT archived FROM collection WHERE id = collection_id),
+  false
+  )
+WHERE archived = true;
+
+-- COLLECTIONS
+-- Set `collection.archived_directly`.
+UPDATE collection AS child
+JOIN (
+  SELECT
+    id,
+    CASE
+      WHEN location = '/' THEN NULL
+      ELSE SUBSTRING_INDEX(SUBSTRING_INDEX(location, '/', -2), '/', 1)
+    END AS parent_id
+    FROM collection
+) AS with_parent_id ON child.id = with_parent_id.id
+LEFT JOIN (
+  SELECT id, archived
+  FROM collection
+) AS parent ON with_parent_id.parent_id = parent.id
+SET archived_directly = (
+  (with_parent_id.parent_id IS NULL OR NOT parent.archived)
+)
+WHERE child.archived;
+
+-- Set `collection.archive_operation_id` for collections that were archived directly
+UPDATE collection
+SET archive_operation_id =
+CASE
+    WHEN LENGTH(CAST(id AS CHAR)) <= 12 THEN
+        CONCAT('00000000-0000-0000-0000-', LPAD(CAST(id AS CHAR), 12, '0'))
+    WHEN LENGTH(CAST(id AS CHAR)) > 12 AND LENGTH(CAST(id AS CHAR)) <= 16 THEN
+        CONCAT('00000000-0000-0000-',
+               LPAD(SUBSTRING(CAST(id AS CHAR), 1, LENGTH(CAST(id AS CHAR)) - 12), 4, '0'), '-',
+               SUBSTRING(CAST(id AS CHAR), LENGTH(CAST(id AS CHAR)) - 11, 12))
+    WHEN LENGTH(CAST(id AS CHAR)) > 16 AND LENGTH(CAST(id AS CHAR)) <= 20 THEN
+        CONCAT('00000000-0000-',
+               LPAD(SUBSTRING(CAST(id AS CHAR), 1, 4), 4, '0'), '-',
+               LPAD(SUBSTRING(CAST(id AS CHAR), 5, 4), 4, '0'), '-',
+               SUBSTRING(CAST(id AS CHAR), 9))
+    WHEN LENGTH(CAST(id AS CHAR)) > 20 THEN
+        CONCAT(
+               LPAD(SUBSTRING(CAST(id AS CHAR), 1, 8), 8, '0'), '-',
+               LPAD(SUBSTRING(CAST(id AS CHAR), 9, 4), 4, '0'), '-',
+               LPAD(SUBSTRING(CAST(id AS CHAR), 13, 4), 4, '0'), '-',
+               LPAD(SUBSTRING(CAST(id AS CHAR), 17, 12), 12, '0')
+        )
+    ELSE '00000000-0000-0000-0000-000000000000'
+END
+WHERE archived AND archived_directly;
+
+-- Set `collection.archive_operation_id` for descendants of collections that were archived directly
+UPDATE collection
+JOIN (
+  WITH RECURSIVE Ancestors AS (
+  SELECT
+    id,
+    archived,
+    archived_directly,
+    archive_operation_id,
+    location
+  FROM
+    collection
+  WHERE
+    archived_directly = true
+    AND archived = true
+
+  UNION ALL
+
+  SELECT
+    collection.id,
+    collection.archived,
+    collection.archived_directly,
+    parent.archive_operation_id,
+    collection.location
+    FROM collection
+    JOIN Ancestors parent ON collection.location = concat(parent.location, parent.id, '/')
+  WHERE collection.archived = true
+  ) SELECT * FROM Ancestors) AS ancestor ON collection.id = ancestor.id
+SET collection.archive_operation_id = ancestor.archive_operation_id, collection.archived_directly = false
+WHERE collection.archive_operation_id IS NULL AND collection.archived = true;
diff --git a/resources/migrations/trash/mysql.sql b/resources/migrations/trash/mysql.sql
new file mode 100644
index 0000000000000000000000000000000000000000..6c8ccb2b65b19086e2616308c47fb3e77bb58243
--- /dev/null
+++ b/resources/migrations/trash/mysql.sql
@@ -0,0 +1,97 @@
+-- DASHBOARDS
+-- Set `archived_directly`.
+UPDATE report_dashboard
+SET archived_directly = COALESCE(
+  -- If the collection the dashboard is in is archived, then it was *not* archived directly
+  (SELECT NOT collection.archived FROM collection WHERE collection.id = report_dashboard.collection_id),
+  false
+)
+WHERE archived = true;
+
+-- CARDS
+-- Set `archived_directly`.
+UPDATE report_card
+SET archived_directly = COALESCE(
+  -- If the collection the card is in is archived, then it was *not* archived directly
+  (SELECT NOT collection.archived FROM collection WHERE collection.id = report_card.collection_id),
+  false
+  )
+WHERE archived = true;
+
+-- COLLECTIONS
+-- Set `collection.archived_directly`.
+UPDATE collection AS child
+JOIN (
+  SELECT
+    id,
+    CASE
+      WHEN location = '/' THEN NULL
+      ELSE SUBSTRING_INDEX(SUBSTRING_INDEX(location, '/', -2), '/', 1)
+    END AS parent_id
+    FROM collection
+) AS with_parent_id ON child.id = with_parent_id.id
+LEFT JOIN (
+  SELECT id, archived
+  FROM collection
+) AS parent ON with_parent_id.parent_id = parent.id
+SET archived_directly = (
+  (with_parent_id.parent_id IS NULL OR NOT parent.archived)
+)
+WHERE child.archived;
+
+-- Set `collection.archive_operation_id` for collections that were archived directly
+UPDATE collection
+SET archive_operation_id =
+CASE
+    WHEN LENGTH(CAST(id AS CHAR)) <= 12 THEN
+        CONCAT('00000000-0000-0000-0000-', LPAD(CAST(id AS CHAR), 12, '0'))
+    WHEN LENGTH(CAST(id AS CHAR)) > 12 AND LENGTH(CAST(id AS CHAR)) <= 16 THEN
+        CONCAT('00000000-0000-0000-',
+               LPAD(SUBSTRING(CAST(id AS CHAR), 1, LENGTH(CAST(id AS CHAR)) - 12), 4, '0'), '-',
+               SUBSTRING(CAST(id AS CHAR), LENGTH(CAST(id AS CHAR)) - 11, 12))
+    WHEN LENGTH(CAST(id AS CHAR)) > 16 AND LENGTH(CAST(id AS CHAR)) <= 20 THEN
+        CONCAT('00000000-0000-',
+               LPAD(SUBSTRING(CAST(id AS CHAR), 1, 4), 4, '0'), '-',
+               LPAD(SUBSTRING(CAST(id AS CHAR), 5, 4), 4, '0'), '-',
+               SUBSTRING(CAST(id AS CHAR), 9))
+    WHEN LENGTH(CAST(id AS CHAR)) > 20 THEN
+        CONCAT(
+               LPAD(SUBSTRING(CAST(id AS CHAR), 1, 8), 8, '0'), '-',
+               LPAD(SUBSTRING(CAST(id AS CHAR), 9, 4), 4, '0'), '-',
+               LPAD(SUBSTRING(CAST(id AS CHAR), 13, 4), 4, '0'), '-',
+               LPAD(SUBSTRING(CAST(id AS CHAR), 17, 12), 12, '0')
+        )
+    ELSE '00000000-0000-0000-0000-000000000000'
+END
+WHERE archived AND archived_directly;
+
+-- Set `collection.archive_operation_id` for descendants of collections that were archived directly
+WITH RECURSIVE Ancestors (id, archived, archived_directly, archive_operation_id, location) AS (
+  SELECT
+    id,
+    archived,
+    archived_directly,
+    archive_operation_id,
+    location
+  FROM
+    collection
+  WHERE
+    archived_directly = true
+    AND archived = true
+
+  UNION ALL
+
+  SELECT
+    collection.id,
+    collection.archived,
+    collection.archived_directly,
+    parent.archive_operation_id,
+    collection.location
+    FROM collection
+    JOIN Ancestors parent ON collection.location = concat(parent.location, parent.id, '/')
+  WHERE collection.archived = true
+)
+UPDATE collection
+JOIN Ancestors ancestor ON collection.id = ancestor.id
+SET collection.archive_operation_id = ancestor.archive_operation_id, collection.archived_directly = false
+WHERE collection.archive_operation_id IS NULL AND collection.archived = true;
diff --git a/resources/migrations/trash/postgres.sql b/resources/migrations/trash/postgres.sql
new file mode 100644
index 0000000000000000000000000000000000000000..ce2de944eb33edc4e1b59b079395fed91096d39f
--- /dev/null
+++ b/resources/migrations/trash/postgres.sql
@@ -0,0 +1,99 @@
+-- DASHBOARDS
+-- Next: set `archived_directly`.
+UPDATE report_dashboard
+SET archived_directly = COALESCE(
+  -- If the collection the dashboard is in is archived, then it was *not* archived directly
+  (SELECT NOT archived FROM collection WHERE id = collection_id),
+  false
+  )
+WHERE archived = true;
+
+-- CARDS
+-- Set `archived_directly`.
+UPDATE report_card
+SET archived_directly = COALESCE(
+  -- If the collection the card is in is archived, then it was *not* archived directly
+  (SELECT NOT archived FROM collection WHERE id = collection_id),
+  false
+  )
+WHERE archived = true;
+
+-- COLLECTIONS
+-- Set `collection.archived_directly`.
+WITH CollectionWithParentID AS (
+  SELECT
+  id,
+  archived,
+  CASE
+      WHEN location = '/' THEN NULL
+      ELSE RIGHT(TRIM(TRAILING '/' FROM location), POSITION('/' IN REVERSE(TRIM(TRAILING '/' FROM location))) - 1)::INTEGER
+  END AS parent_id
+  FROM
+  collection
+)
+
+UPDATE collection c
+SET archived_directly = (
+  cp.parent_id IS NULL
+  OR NOT EXISTS (
+    SELECT 1
+    FROM CollectionWithParentID pp
+    WHERE pp.id = cp.parent_id
+    AND pp.archived = true
+  )
+)
+FROM CollectionWithParentID cp
+WHERE c.id = cp.id
+AND cp.archived = true;
+
+-- Set `collection.archive_operation_id` for collections that were archived directly
+
+UPDATE collection
+SET archive_operation_id =
+CASE
+    WHEN LENGTH(id::text) <= 12 THEN
+        CONCAT('00000000-0000-0000-0000-', LPAD(id::text, 12, '0'))
+    WHEN LENGTH(id::text) > 12 AND LENGTH(id::text) <= 16 THEN
+        CONCAT('00000000-0000-0000-',
+               LPAD(SUBSTRING(id::text, 1, LENGTH(id::text) - 12), 4, '0'), '-',
+               SUBSTRING(id::text, LENGTH(id::text) - 11, 12))
+    WHEN LENGTH(id::text) > 16 AND LENGTH(id::text) <= 20 THEN
+        CONCAT('00000000-0000-',
+               LPAD(SUBSTRING(id::text, 1, 4), 4, '0'), '-',
+               LPAD(SUBSTRING(id::text, 5, 4), 4, '0'), '-',
+               SUBSTRING(id::text, 9))
+    WHEN LENGTH(id::text) > 20 THEN
+        CONCAT(
+               LPAD(SUBSTRING(id::text, 1, 8), 8, '0'), '-',
+               LPAD(SUBSTRING(id::text, 9, 4), 4, '0'), '-',
+               LPAD(SUBSTRING(id::text, 13, 4), 4, '0'), '-',
+               LPAD(SUBSTRING(id::text, 17, 12), 12, '0')
+        )
+    -- If someone has >10^20 collections, they have bigger problems than a wrong `archive_operation_id`
+    ELSE '00000000-0000-0000-0000-000000000000'
+END
+WHERE archived AND archived_directly;
+
+-- Set `collection.archive_operation_id` for descendants of collections that were archived directly
+WITH Ancestors(id, archived, archived_directly, archive_operation_id, location) AS (
+    SELECT
+    id,
+    archived,
+    archived_directly,
+    archive_operation_id,
+    location
+  FROM
+    collection
+  WHERE
+    archived_directly = true
+    AND archived = true
+)
+UPDATE collection
+SET archive_operation_id = (
+  SELECT a.archive_operation_id
+  FROM Ancestors a
+  WHERE collection.location LIKE concat(a.location, a.id, '/%')
+  ORDER BY LENGTH(a.location) DESC
+  LIMIT 1
+), archived_directly = false
+WHERE archive_operation_id IS NULL AND archived = true;
diff --git a/resources/sample-content.edn b/resources/sample-content.edn
index d8f0ce12e73d7bb0fcdd50e47e08a6b73d9d88e0..140bc60b5caf0b417c8ca45fd199c645d9ddb48a 100644
--- a/resources/sample-content.edn
+++ b/resources/sample-content.edn
@@ -30,7 +30,6 @@
    :archived false,
    :collection_position 2,
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :enable_embedding false,
    :collection_id 2,
    :show_in_getting_started false,
@@ -3176,7 +3175,6 @@
    :result_metadata
    "[{\"description\":\"The date and time an order was submitted.\",\"semantic_type\":\"type/CreationTimestamp\",\"coercion_strategy\":null,\"unit\":\"month\",\"name\":\"CREATED_AT\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",41,{\"base-type\":\"type/DateTime\",\"temporal-unit\":\"month\"}],\"effective_type\":\"type/DateTime\",\"id\":41,\"visibility_type\":\"normal\",\"display_name\":\"Created At\",\"fingerprint\":{\"global\":{\"distinct-count\":10001,\"nil%\":0.0},\"type\":{\"type/DateTime\":{\"earliest\":\"2022-04-30T18:56:13.352Z\",\"latest\":\"2026-04-19T14:07:15.657Z\"}}},\"base_type\":\"type/DateTime\"},{\"display_name\":\"Sum of Quantity\",\"semantic_type\":\"type/Quantity\",\"settings\":null,\"field_ref\":[\"aggregation\",0],\"name\":\"sum\",\"base_type\":\"type/BigInteger\",\"effective_type\":\"type/BigInteger\",\"fingerprint\":{\"global\":{\"distinct-count\":7,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":1.0,\"q1\":3.25,\"q3\":6.0,\"max\":74.0,\"sd\":22.086949389477336,\"avg\":11.5}}}}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -3209,7 +3207,6 @@
    :result_metadata
    "[{\"description\":\"This is a unique ID for the product. It is also called the “Invoice number” or “Confirmation number” in customer facing emails and screens.\",\"semantic_type\":\"type/PK\",\"coercion_strategy\":null,\"name\":\"ID\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",37,null],\"effective_type\":\"type/BigInteger\",\"id\":37,\"visibility_type\":\"normal\",\"display_name\":\"ID\",\"fingerprint\":null,\"base_type\":\"type/BigInteger\"},{\"description\":\"The id of the user who made this order. Note that in some cases where an order was created on behalf of a customer who phoned the order in, this might be the employee who handled the request.\",\"semantic_type\":\"type/FK\",\"coercion_strategy\":null,\"name\":\"USER_ID\",\"settings\":null,\"fk_target_field_id\":46,\"field_ref\":[\"field\",43,null],\"effective_type\":\"type/Integer\",\"id\":43,\"visibility_type\":\"normal\",\"display_name\":\"User ID\",\"fingerprint\":{\"global\":{\"distinct-count\":929,\"nil%\":0}},\"base_type\":\"type/Integer\"},{\"description\":\"The product ID. This is an internal identifier for the product, NOT the SKU.\",\"semantic_type\":\"type/FK\",\"coercion_strategy\":null,\"name\":\"PRODUCT_ID\",\"settings\":null,\"fk_target_field_id\":62,\"field_ref\":[\"field\",40,null],\"effective_type\":\"type/Integer\",\"id\":40,\"visibility_type\":\"normal\",\"display_name\":\"Product ID\",\"fingerprint\":{\"global\":{\"distinct-count\":200,\"nil%\":0}},\"base_type\":\"type/Integer\"},{\"description\":\"The raw, pre-tax cost of the order. Note that this might be different in the future from the product price due to promotions, credits, etc.\",\"semantic_type\":null,\"coercion_strategy\":null,\"name\":\"SUBTOTAL\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",44,null],\"effective_type\":\"type/Float\",\"id\":44,\"visibility_type\":\"normal\",\"display_name\":\"Subtotal\",\"fingerprint\":{\"global\":{\"distinct-count\":340,\"nil%\":0},\"type\":{\"type/Number\":{\"min\":15.691943673970439,\"q1\":49.74894519060184,\"q3\":105.42965746993103,\"max\":148.22900526552291,\"sd\":32.53705013056317,\"avg\":77.01295465356547}}},\"base_type\":\"type/Float\"},{\"description\":\"This is the amount of local and federal taxes that are collected on the purchase. Note that other governmental fees on some products are not included here, but instead are accounted for in the subtotal.\",\"semantic_type\":null,\"coercion_strategy\":null,\"name\":\"TAX\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",38,null],\"effective_type\":\"type/Float\",\"id\":38,\"visibility_type\":\"normal\",\"display_name\":\"Tax\",\"fingerprint\":{\"global\":{\"distinct-count\":797,\"nil%\":0},\"type\":{\"type/Number\":{\"min\":0,\"q1\":2.273340386603857,\"q3\":5.337275338216307,\"max\":11.12,\"sd\":2.3206651358900316,\"avg\":3.8722100000000004}}},\"base_type\":\"type/Float\"},{\"description\":\"The total billed amount.\",\"semantic_type\":null,\"coercion_strategy\":null,\"name\":\"TOTAL\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",42,null],\"effective_type\":\"type/Float\",\"id\":42,\"visibility_type\":\"normal\",\"display_name\":\"Total\",\"fingerprint\":{\"global\":{\"distinct-count\":4426,\"nil%\":0},\"type\":{\"type/Number\":{\"min\":8.93914247937167,\"q1\":51.34535490743823,\"q3\":110.29428389265787,\"max\":159.34900526552292,\"sd\":34.26469575709948,\"avg\":80.35871658771228}}},\"base_type\":\"type/Float\"},{\"description\":\"Discount amount.\",\"semantic_type\":\"type/Discount\",\"coercion_strategy\":null,\"name\":\"DISCOUNT\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",36,null],\"effective_type\":\"type/Float\",\"id\":36,\"visibility_type\":\"normal\",\"display_name\":\"Discount\",\"fingerprint\":{\"global\":{\"distinct-count\":701,\"nil%\":0.898},\"type\":{\"type/Number\":{\"min\":0.17088996672584322,\"q1\":2.9786226681458743,\"q3\":7.338187788658235,\"max\":61.69684269960571,\"sd\":3.053663125001991,\"avg\":5.161255547580326}}},\"base_type\":\"type/Float\"},{\"description\":\"The date and time an order was submitted.\",\"semantic_type\":\"type/CreationTimestamp\",\"coercion_strategy\":null,\"unit\":\"default\",\"name\":\"CREATED_AT\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",41,{\"temporal-unit\":\"default\"}],\"effective_type\":\"type/DateTime\",\"id\":41,\"visibility_type\":\"normal\",\"display_name\":\"Created At\",\"fingerprint\":{\"global\":{\"distinct-count\":10001,\"nil%\":0},\"type\":{\"type/DateTime\":{\"earliest\":\"2022-04-30T18:56:13.352Z\",\"latest\":\"2026-04-19T14:07:15.657Z\"}}},\"base_type\":\"type/DateTime\"},{\"description\":\"Number of products bought.\",\"semantic_type\":\"type/Quantity\",\"coercion_strategy\":null,\"name\":\"QUANTITY\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",39,null],\"effective_type\":\"type/Integer\",\"id\":39,\"visibility_type\":\"normal\",\"display_name\":\"Quantity\",\"fingerprint\":{\"global\":{\"distinct-count\":62,\"nil%\":0},\"type\":{\"type/Number\":{\"min\":0,\"q1\":1.755882607764982,\"q3\":4.882654507928044,\"max\":100,\"sd\":4.214258386403798,\"avg\":3.7015}}},\"base_type\":\"type/Integer\"},{\"display_name\":\"Age\",\"field_ref\":[\"field\",\"Age\",{\"base-type\":\"type/BigInteger\"}],\"name\":\"Age\",\"base_type\":\"type/BigInteger\",\"effective_type\":\"type/BigInteger\",\"fingerprint\":{\"global\":{\"distinct-count\":42,\"nil%\":0},\"type\":{\"type/Number\":{\"min\":24,\"q1\":33.36752836803635,\"q3\":55.20362176071121,\"max\":65,\"sd\":12.063315373018085,\"avg\":44.572}}},\"semantic_type\":null}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -3243,7 +3240,6 @@
    :result_metadata
    "[{\"field_ref\":[\"field\",\"Age\",{\"base-type\":\"type/BigInteger\",\"binning\":{\"strategy\":\"num-bins\",\"num-bins\":10,\"min-value\":20.0,\"max-value\":65.0,\"bin-width\":5.0}}],\"base_type\":\"type/BigInteger\",\"name\":\"Age\",\"effective_type\":\"type/BigInteger\",\"display_name\":\"Age\",\"fingerprint\":{\"global\":{\"distinct-count\":42,\"nil%\":0},\"type\":{\"type/Number\":{\"min\":24,\"q1\":33.36752836803635,\"q3\":55.20362176071121,\"max\":65,\"sd\":12.063315373018085,\"avg\":44.572}}},\"binning_info\":{\"num_bins\":10,\"min_value\":20.0,\"max_value\":65.0,\"bin_width\":5.0,\"binning_strategy\":\"num-bins\"},\"source\":\"breakout\"},{\"base_type\":\"type/Float\",\"name\":\"sum\",\"display_name\":\"Sum of Total\",\"source\":\"aggregation\",\"field_ref\":[\"aggregation\",0],\"aggregation_index\":0}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -3277,7 +3273,6 @@
    :result_metadata
    "[{\"display_name\":\"Age\",\"field_ref\":[\"field\",\"Age\",{\"base-type\":\"type/BigInteger\",\"binning\":{\"strategy\":\"num-bins\",\"num-bins\":10,\"min-value\":20,\"max-value\":65,\"bin-width\":5}}],\"name\":\"Age\",\"base_type\":\"type/Decimal\",\"effective_type\":\"type/Decimal\",\"fingerprint\":{\"global\":{\"distinct-count\":42,\"nil%\":0},\"type\":{\"type/Number\":{\"min\":24,\"q1\":33.340572873934306,\"q3\":55.17166516756599,\"max\":65,\"sd\":12.263883782175668,\"avg\":44.434}}},\"semantic_type\":null},{\"display_name\":\"Count\",\"semantic_type\":\"type/Quantity\",\"field_ref\":[\"aggregation\",0],\"name\":\"count\",\"base_type\":\"type/BigInteger\",\"effective_type\":\"type/BigInteger\",\"fingerprint\":{\"global\":{\"distinct-count\":9,\"nil%\":0},\"type\":{\"type/Number\":{\"min\":61,\"q1\":270,\"q3\":304,\"max\":334,\"sd\":99.91663191547909,\"avg\":250}}}}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -3311,7 +3306,6 @@
    :result_metadata
    "[{\"description\":\"The date and time an order was submitted.\",\"semantic_type\":\"type/CreationTimestamp\",\"coercion_strategy\":null,\"unit\":\"month\",\"name\":\"CREATED_AT\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",41,{\"base-type\":\"type/DateTime\",\"temporal-unit\":\"month\"}],\"effective_type\":\"type/DateTime\",\"id\":41,\"visibility_type\":\"normal\",\"display_name\":\"Created At\",\"fingerprint\":{\"global\":{\"distinct-count\":10001,\"nil%\":0.0},\"type\":{\"type/DateTime\":{\"earliest\":\"2022-04-30T18:56:13.352Z\",\"latest\":\"2026-04-19T14:07:15.657Z\"}}},\"base_type\":\"type/DateTime\"},{\"display_name\":\"Sum of Quantity\",\"semantic_type\":\"type/Quantity\",\"settings\":null,\"field_ref\":[\"aggregation\",0],\"name\":\"sum\",\"base_type\":\"type/BigInteger\",\"effective_type\":\"type/BigInteger\",\"fingerprint\":{\"global\":{\"distinct-count\":7,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":1.0,\"q1\":2.5,\"q3\":9.414213562373096,\"max\":10.0,\"sd\":3.693623849670827,\"avg\":5.75}}}}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -3344,7 +3338,6 @@
    :result_metadata
    "[{\"description\":\"The type of product, valid values include: Doohicky, Gadget, Gizmo and Widget\",\"semantic_type\":\"type/Category\",\"coercion_strategy\":null,\"name\":\"CATEGORY\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",58,{\"base-type\":\"type/Text\",\"source-field\":40}],\"effective_type\":\"type/Text\",\"id\":58,\"visibility_type\":\"normal\",\"display_name\":\"Product → Category\",\"fingerprint\":{\"global\":{\"distinct-count\":4,\"nil%\":0.0},\"type\":{\"type/Text\":{\"percent-json\":0.0,\"percent-url\":0.0,\"percent-email\":0.0,\"percent-state\":0.0,\"average-length\":6.375}}},\"base_type\":\"type/Text\"},{\"display_name\":\"Count\",\"semantic_type\":\"type/Quantity\",\"field_ref\":[\"aggregation\",0],\"name\":\"count\",\"base_type\":\"type/BigInteger\",\"effective_type\":\"type/BigInteger\",\"fingerprint\":{\"global\":{\"distinct-count\":4,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":3976.0,\"q1\":4380.0,\"q3\":5000.0,\"max\":5061.0,\"sd\":489.3103990992493,\"avg\":4690.0}}}}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -3378,7 +3371,6 @@
    :result_metadata
    "[{\"description\":\"The date and time an order was submitted.\",\"semantic_type\":\"type/CreationTimestamp\",\"coercion_strategy\":null,\"unit\":\"month\",\"name\":\"CREATED_AT\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",41,{\"base-type\":\"type/DateTime\",\"temporal-unit\":\"month\"}],\"effective_type\":\"type/DateTime\",\"id\":41,\"visibility_type\":\"normal\",\"display_name\":\"Created At\",\"fingerprint\":{\"global\":{\"distinct-count\":10001,\"nil%\":0.0},\"type\":{\"type/DateTime\":{\"earliest\":\"2022-04-30T18:56:13.352Z\",\"latest\":\"2026-04-19T14:07:15.657Z\"}}},\"base_type\":\"type/DateTime\"},{\"description\":\"The type of product, valid values include: Doohicky, Gadget, Gizmo and Widget\",\"semantic_type\":\"type/Category\",\"coercion_strategy\":null,\"name\":\"CATEGORY\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",58,{\"base-type\":\"type/Text\",\"source-field\":40}],\"effective_type\":\"type/Text\",\"id\":58,\"visibility_type\":\"normal\",\"display_name\":\"Product → Category\",\"fingerprint\":{\"global\":{\"distinct-count\":4,\"nil%\":0.0},\"type\":{\"type/Text\":{\"percent-json\":0.0,\"percent-url\":0.0,\"percent-email\":0.0,\"percent-state\":0.0,\"average-length\":6.375}}},\"base_type\":\"type/Text\"},{\"display_name\":\"Sum of Subtotal\",\"semantic_type\":null,\"settings\":null,\"field_ref\":[\"aggregation\",0],\"name\":\"sum\",\"base_type\":\"type/Float\",\"effective_type\":\"type/Float\",\"fingerprint\":{\"global\":{\"distinct-count\":96,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":194.15721878715678,\"q1\":1682.037094502916,\"q3\":5250.149916853326,\"max\":10113.40941589646,\"sd\":2757.8851826916916,\"avg\":3956.9519658839577}}}}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -3412,7 +3404,6 @@
    :result_metadata
    "[{\"description\":\"The date and time an order was submitted.\",\"semantic_type\":\"type/CreationTimestamp\",\"coercion_strategy\":null,\"unit\":\"month\",\"name\":\"CREATED_AT\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",41,{\"base-type\":\"type/DateTime\",\"temporal-unit\":\"month\"}],\"effective_type\":\"type/DateTime\",\"id\":41,\"visibility_type\":\"normal\",\"display_name\":\"Created At\",\"fingerprint\":{\"global\":{\"distinct-count\":10001,\"nil%\":0.0},\"type\":{\"type/DateTime\":{\"earliest\":\"2022-04-30T18:56:13.352Z\",\"latest\":\"2026-04-19T14:07:15.657Z\"}}},\"base_type\":\"type/DateTime\"},{\"display_name\":\"Sum of Quantity\",\"semantic_type\":\"type/Quantity\",\"settings\":null,\"field_ref\":[\"aggregation\",0],\"name\":\"sum\",\"base_type\":\"type/BigInteger\",\"effective_type\":\"type/BigInteger\",\"fingerprint\":{\"global\":{\"distinct-count\":11,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":3.0,\"q1\":4.76393202250021,\"q3\":17.0,\"max\":21.0,\"sd\":6.199964551476116,\"avg\":10.142857142857142}}}}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -3445,7 +3436,6 @@
    :result_metadata
    "[{\"description\":\"The state or province of the account’s billing address\",\"semantic_type\":\"type/State\",\"coercion_strategy\":null,\"name\":\"STATE\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",48,{\"base-type\":\"type/Text\",\"source-field\":43}],\"effective_type\":\"type/Text\",\"id\":48,\"visibility_type\":\"normal\",\"display_name\":\"User → State\",\"fingerprint\":{\"global\":{\"distinct-count\":49,\"nil%\":0.0},\"type\":{\"type/Text\":{\"percent-json\":0.0,\"percent-url\":0.0,\"percent-email\":0.0,\"percent-state\":1.0,\"average-length\":2.0}}},\"base_type\":\"type/Text\"},{\"display_name\":\"Sum of Total\",\"semantic_type\":null,\"settings\":null,\"field_ref\":[\"aggregation\",0],\"name\":\"sum\",\"base_type\":\"type/Float\",\"effective_type\":\"type/Float\",\"fingerprint\":{\"global\":{\"distinct-count\":48,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":1358.6880585850759,\"q1\":14606.102496036605,\"q3\":45423.71594332504,\"max\":108466.5974651383,\"sd\":21267.69039176716,\"avg\":31471.285063554824}}}}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -3479,7 +3469,6 @@
    :result_metadata
    "[{\"field_ref\":[\"field\",\"Age\",{\"base-type\":\"type/BigInteger\",\"binning\":{\"strategy\":\"num-bins\",\"num-bins\":10,\"min-value\":20.0,\"max-value\":65.0,\"bin-width\":5.0}}],\"base_type\":\"type/BigInteger\",\"name\":\"Age\",\"effective_type\":\"type/BigInteger\",\"display_name\":\"Age\",\"fingerprint\":{\"global\":{\"distinct-count\":42,\"nil%\":0},\"type\":{\"type/Number\":{\"min\":24,\"q1\":33.36752836803635,\"q3\":55.20362176071121,\"max\":65,\"sd\":12.063315373018085,\"avg\":44.572}}},\"binning_info\":{\"num_bins\":10,\"min_value\":20.0,\"max_value\":65.0,\"bin_width\":5.0,\"binning_strategy\":\"num-bins\"},\"source\":\"breakout\"},{\"description\":\"The channel through which we acquired this user. Valid values include: Affiliate, Facebook, Google, Organic and Twitter\",\"semantic_type\":\"type/Source\",\"table_id\":3,\"coercion_strategy\":null,\"name\":\"SOURCE\",\"settings\":null,\"source\":\"breakout\",\"fk_target_field_id\":null,\"field_ref\":[\"field\",45,{\"base-type\":\"type/Text\",\"join-alias\":\"People - User\"}],\"effective_type\":\"type/Text\",\"nfc_path\":null,\"parent_id\":null,\"id\":45,\"position\":8,\"visibility_type\":\"normal\",\"display_name\":\"People - User → Source\",\"fingerprint\":{\"global\":{\"distinct-count\":5,\"nil%\":0.0},\"type\":{\"type/Text\":{\"percent-json\":0.0,\"percent-url\":0.0,\"percent-email\":0.0,\"percent-state\":0.0,\"average-length\":7.4084}}},\"base_type\":\"type/Text\",\"source_alias\":\"People - User\"},{\"base_type\":\"type/Integer\",\"name\":\"count\",\"display_name\":\"Count\",\"semantic_type\":\"type/Quantity\",\"source\":\"aggregation\",\"field_ref\":[\"aggregation\",0],\"aggregation_index\":0}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -3513,7 +3502,6 @@
    :result_metadata
    "[{\"field_ref\":[\"field\",\"Age\",{\"base-type\":\"type/BigInteger\",\"binning\":{\"strategy\":\"num-bins\",\"num-bins\":10,\"min-value\":20.0,\"max-value\":65.0,\"bin-width\":5.0}}],\"base_type\":\"type/BigInteger\",\"name\":\"Age\",\"effective_type\":\"type/BigInteger\",\"display_name\":\"Age\",\"fingerprint\":{\"global\":{\"distinct-count\":42,\"nil%\":0},\"type\":{\"type/Number\":{\"min\":24,\"q1\":33.36752836803635,\"q3\":55.20362176071121,\"max\":65,\"sd\":12.063315373018085,\"avg\":44.572}}},\"binning_info\":{\"num_bins\":10,\"min_value\":20.0,\"max_value\":65.0,\"bin_width\":5.0,\"binning_strategy\":\"num-bins\"},\"source\":\"breakout\"},{\"description\":\"The type of product, valid values include: Doohicky, Gadget, Gizmo and Widget\",\"semantic_type\":\"type/Category\",\"table_id\":8,\"coercion_strategy\":null,\"name\":\"CATEGORY\",\"settings\":null,\"source\":\"breakout\",\"fk_target_field_id\":null,\"field_ref\":[\"field\",58,{\"base-type\":\"type/Text\",\"join-alias\":\"Products\"}],\"effective_type\":\"type/Text\",\"nfc_path\":null,\"parent_id\":null,\"id\":58,\"position\":3,\"visibility_type\":\"normal\",\"display_name\":\"Products → Category\",\"fingerprint\":{\"global\":{\"distinct-count\":4,\"nil%\":0.0},\"type\":{\"type/Text\":{\"percent-json\":0.0,\"percent-url\":0.0,\"percent-email\":0.0,\"percent-state\":0.0,\"average-length\":6.375}}},\"base_type\":\"type/Text\",\"source_alias\":\"Products\"},{\"base_type\":\"type/Integer\",\"name\":\"count\",\"display_name\":\"Count\",\"semantic_type\":\"type/Quantity\",\"source\":\"aggregation\",\"field_ref\":[\"aggregation\",0],\"aggregation_index\":0}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -3547,7 +3535,6 @@
    :result_metadata
    "[{\"description\":\"The date and time an order was submitted.\",\"semantic_type\":\"type/CreationTimestamp\",\"coercion_strategy\":null,\"unit\":\"month\",\"name\":\"CREATED_AT\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",41,{\"base-type\":\"type/DateTime\",\"temporal-unit\":\"month\"}],\"effective_type\":\"type/DateTime\",\"id\":41,\"visibility_type\":\"normal\",\"display_name\":\"Created At\",\"fingerprint\":{\"global\":{\"distinct-count\":10001,\"nil%\":0.0},\"type\":{\"type/DateTime\":{\"earliest\":\"2022-04-30T18:56:13.352Z\",\"latest\":\"2026-04-19T14:07:15.657Z\"}}},\"base_type\":\"type/DateTime\"},{\"display_name\":\"Sum of Total\",\"semantic_type\":null,\"settings\":null,\"field_ref\":[\"aggregation\",0],\"name\":\"sum\",\"base_type\":\"type/Float\",\"effective_type\":\"type/Float\",\"fingerprint\":{\"global\":{\"distinct-count\":24,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":1265.7162964063327,\"q1\":7814.799491244121,\"q3\":21566.481581896165,\"max\":38569.69761756364,\"sd\":11384.837065416124,\"avg\":16481.762292966578}}}},{\"display_name\":\"Sum of Quantity\",\"semantic_type\":\"type/Quantity\",\"settings\":null,\"field_ref\":[\"aggregation\",1],\"name\":\"sum_2\",\"base_type\":\"type/BigInteger\",\"effective_type\":\"type/BigInteger\",\"fingerprint\":{\"global\":{\"distinct-count\":24,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":75.0,\"q1\":369.0,\"q3\":1286.0,\"max\":2260.0,\"sd\":630.7594628699596,\"avg\":877.25}}}}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -3581,7 +3568,6 @@
    :result_metadata
    "[{\"display_name\":\"Cumulative sum of Total\",\"semantic_type\":null,\"settings\":null,\"field_ref\":[\"aggregation\",0],\"name\":\"sum\",\"base_type\":\"type/Float\",\"effective_type\":\"type/Float\",\"fingerprint\":{\"global\":{\"distinct-count\":1,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":123889.60365320997,\"q1\":123889.60365320997,\"q3\":123889.60365320997,\"max\":123889.60365320997,\"sd\":null,\"avg\":123889.60365320997}}}}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -3614,7 +3600,6 @@
    :result_metadata
    "[{\"description\":\"The type of product, valid values include: Doohicky, Gadget, Gizmo and Widget\",\"semantic_type\":\"type/Category\",\"coercion_strategy\":null,\"name\":\"CATEGORY\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",58,{\"base-type\":\"type/Text\",\"source-field\":40}],\"effective_type\":\"type/Text\",\"id\":58,\"visibility_type\":\"normal\",\"display_name\":\"Product → Category\",\"fingerprint\":{\"global\":{\"distinct-count\":4,\"nil%\":0.0},\"type\":{\"type/Text\":{\"percent-json\":0.0,\"percent-url\":0.0,\"percent-email\":0.0,\"percent-state\":0.0,\"average-length\":6.375}}},\"base_type\":\"type/Text\"},{\"description\":\"The date and time an order was submitted.\",\"semantic_type\":\"type/CreationTimestamp\",\"coercion_strategy\":null,\"unit\":\"quarter\",\"name\":\"CREATED_AT\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",41,{\"base-type\":\"type/DateTime\",\"temporal-unit\":\"quarter\"}],\"effective_type\":\"type/DateTime\",\"id\":41,\"visibility_type\":\"normal\",\"display_name\":\"Created At\",\"fingerprint\":{\"global\":{\"distinct-count\":10001,\"nil%\":0.0},\"type\":{\"type/DateTime\":{\"earliest\":\"2022-04-30T18:56:13.352Z\",\"latest\":\"2026-04-19T14:07:15.657Z\"}}},\"base_type\":\"type/DateTime\"},{\"display_name\":\"Count\",\"semantic_type\":\"type/Quantity\",\"field_ref\":[\"aggregation\",0],\"name\":\"count\",\"base_type\":\"type/BigInteger\",\"effective_type\":\"type/BigInteger\",\"fingerprint\":{\"global\":{\"distinct-count\":34,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":13.0,\"q1\":81.0,\"q3\":259.5,\"max\":338.0,\"sd\":102.85448188797824,\"avg\":169.38888888888889}}}}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -3648,7 +3633,6 @@
    :result_metadata
    "[{\"description\":\"The date and time an order was submitted.\",\"semantic_type\":\"type/CreationTimestamp\",\"coercion_strategy\":null,\"unit\":\"quarter\",\"name\":\"CREATED_AT\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",41,{\"base-type\":\"type/DateTime\",\"temporal-unit\":\"quarter\"}],\"effective_type\":\"type/DateTime\",\"id\":41,\"visibility_type\":\"normal\",\"display_name\":\"Created At\",\"fingerprint\":{\"global\":{\"distinct-count\":10001,\"nil%\":0.0},\"type\":{\"type/DateTime\":{\"earliest\":\"2022-04-30T18:56:13.352Z\",\"latest\":\"2026-04-19T14:07:15.657Z\"}}},\"base_type\":\"type/DateTime\"},{\"display_name\":\"Sum of Total\",\"semantic_type\":null,\"settings\":null,\"field_ref\":[\"aggregation\",0],\"name\":\"sum\",\"base_type\":\"type/Float\",\"effective_type\":\"type/Float\",\"fingerprint\":{\"global\":{\"distinct-count\":2,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":67863.39650548704,\"q1\":67863.39650548704,\"q3\":111121.25780141751,\"max\":111121.25780141751,\"sd\":30587.92706197953,\"avg\":89492.32715345226}}}}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -3681,7 +3665,6 @@
    :result_metadata
    "[{\"description\":\"The channel through which we acquired this user. Valid values include: Affiliate, Facebook, Google, Organic and Twitter\",\"semantic_type\":null,\"coercion_strategy\":null,\"name\":\"pivot-grouping\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"expression\",\"pivot-grouping\"],\"effective_type\":\"type/Text\",\"id\":45,\"visibility_type\":\"normal\",\"display_name\":\"pivot-grouping\",\"fingerprint\":{\"global\":{\"distinct-count\":1,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":3.0,\"q1\":3.0,\"q3\":3.0,\"max\":3.0,\"sd\":null,\"avg\":3.0}}},\"base_type\":\"type/Integer\"},{\"description\":\"The date and time an order was submitted.\",\"semantic_type\":null,\"coercion_strategy\":null,\"name\":\"sum\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"aggregation\",0],\"effective_type\":\"type/DateTime\",\"id\":41,\"visibility_type\":\"normal\",\"display_name\":\"Sum of Subtotal\",\"fingerprint\":{\"global\":{\"distinct-count\":1,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":379867.38872485986,\"q1\":379867.38872485986,\"q3\":379867.38872485986,\"max\":379867.38872485986,\"sd\":null,\"avg\":379867.38872485986}}},\"base_type\":\"type/Float\"}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -3716,7 +3699,6 @@
    :result_metadata
    "[{\"display_name\":\"Age\",\"field_ref\":[\"expression\",\"Age\"],\"name\":\"Age\",\"base_type\":\"type/BigInteger\",\"effective_type\":\"type/BigInteger\",\"semantic_type\":null,\"fingerprint\":{\"global\":{\"distinct-count\":43,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":24.0,\"q1\":34.25,\"q3\":55.75,\"max\":66.0,\"sd\":12.445906346880554,\"avg\":45.0}}}},{\"description\":\"The type of product, valid values include: Doohicky, Gadget, Gizmo and Widget\",\"semantic_type\":\"type/Category\",\"coercion_strategy\":null,\"name\":\"CATEGORY\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",58,{\"base-type\":\"type/Text\",\"source-field\":40}],\"effective_type\":\"type/Text\",\"id\":58,\"visibility_type\":\"normal\",\"display_name\":\"Product → Category\",\"fingerprint\":{\"global\":{\"distinct-count\":4,\"nil%\":0.0},\"type\":{\"type/Text\":{\"percent-json\":0.0,\"percent-url\":0.0,\"percent-email\":0.0,\"percent-state\":0.0,\"average-length\":6.375}}},\"base_type\":\"type/Text\"},{\"display_name\":\"Count\",\"semantic_type\":\"type/Quantity\",\"field_ref\":[\"aggregation\",0],\"name\":\"count\",\"base_type\":\"type/BigInteger\",\"effective_type\":\"type/BigInteger\",\"fingerprint\":{\"global\":{\"distinct-count\":84,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":11.0,\"q1\":95.5,\"q3\":124.48331477354789,\"max\":204.0,\"sd\":28.015985634286857,\"avg\":109.06976744186046}}}}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -3750,7 +3732,6 @@
    :result_metadata
    "[{\"description\":\"The date and time an order was submitted.\",\"semantic_type\":\"type/CreationTimestamp\",\"coercion_strategy\":null,\"unit\":\"quarter\",\"name\":\"CREATED_AT\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",41,{\"base-type\":\"type/DateTime\",\"temporal-unit\":\"quarter\"}],\"effective_type\":\"type/DateTime\",\"id\":41,\"visibility_type\":\"normal\",\"display_name\":\"Created At\",\"fingerprint\":{\"global\":{\"distinct-count\":10001,\"nil%\":0.0},\"type\":{\"type/DateTime\":{\"earliest\":\"2022-04-30T18:56:13.352Z\",\"latest\":\"2026-04-19T14:07:15.657Z\"}}},\"base_type\":\"type/DateTime\"},{\"display_name\":\"Sum of Discount\",\"semantic_type\":\"type/Discount\",\"settings\":null,\"field_ref\":[\"aggregation\",0],\"name\":\"sum\",\"base_type\":\"type/Float\",\"effective_type\":\"type/Float\",\"fingerprint\":{\"global\":{\"distinct-count\":2,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":738.5575011690233,\"q1\":738.5575011690233,\"q3\":795.9833992557524,\"max\":795.9833992557524,\"sd\":40.606241952853686,\"avg\":767.2704502123879}}}}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -3783,7 +3764,6 @@
    :result_metadata
    "[{\"description\":\"The average rating users have given the product. This ranges from 1 - 5\",\"semantic_type\":\"type/Score\",\"coercion_strategy\":null,\"name\":\"RATING\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",61,{\"base-type\":\"type/Float\",\"source-field\":40}],\"effective_type\":\"type/Float\",\"id\":61,\"visibility_type\":\"normal\",\"display_name\":\"Product → Rating\",\"fingerprint\":{\"global\":{\"distinct-count\":23,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":0.0,\"q1\":3.5120465053408525,\"q3\":4.216124969497314,\"max\":5.0,\"sd\":1.3605488657451452,\"avg\":3.4715}}},\"base_type\":\"type/Float\"},{\"description\":\"The type of product, valid values include: Doohicky, Gadget, Gizmo and Widget\",\"semantic_type\":\"type/Category\",\"coercion_strategy\":null,\"name\":\"CATEGORY\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",58,{\"base-type\":\"type/Text\",\"source-field\":40}],\"effective_type\":\"type/Text\",\"id\":58,\"visibility_type\":\"normal\",\"display_name\":\"Product → Category\",\"fingerprint\":{\"global\":{\"distinct-count\":4,\"nil%\":0.0},\"type\":{\"type/Text\":{\"percent-json\":0.0,\"percent-url\":0.0,\"percent-email\":0.0,\"percent-state\":0.0,\"average-length\":6.375}}},\"base_type\":\"type/Text\"},{\"display_name\":\"Count\",\"semantic_type\":\"type/Quantity\",\"field_ref\":[\"aggregation\",0],\"name\":\"count\",\"base_type\":\"type/BigInteger\",\"effective_type\":\"type/BigInteger\",\"fingerprint\":{\"global\":{\"distinct-count\":57,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":70.0,\"q1\":105.0,\"q3\":370.0,\"max\":1093.0,\"sd\":229.88812521865958,\"avg\":281.6060606060606}}}}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -3817,7 +3797,6 @@
    :result_metadata
    "[{\"description\":\"The date and time an order was submitted.\",\"semantic_type\":\"type/CreationTimestamp\",\"coercion_strategy\":null,\"unit\":\"month\",\"name\":\"CREATED_AT\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",41,{\"base-type\":\"type/DateTime\",\"temporal-unit\":\"month\"}],\"effective_type\":\"type/DateTime\",\"id\":41,\"visibility_type\":\"normal\",\"display_name\":\"Created At\",\"fingerprint\":{\"global\":{\"distinct-count\":10001,\"nil%\":0.0},\"type\":{\"type/DateTime\":{\"earliest\":\"2022-04-30T18:56:13.352Z\",\"latest\":\"2026-04-19T14:07:15.657Z\"}}},\"base_type\":\"type/DateTime\"},{\"display_name\":\"Sum of Quantity\",\"semantic_type\":\"type/Quantity\",\"settings\":null,\"field_ref\":[\"aggregation\",0],\"name\":\"sum\",\"base_type\":\"type/BigInteger\",\"effective_type\":\"type/BigInteger\",\"fingerprint\":{\"global\":{\"distinct-count\":2,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":487.0,\"q1\":487.0,\"q3\":1330.0,\"max\":1330.0,\"sd\":596.0910165402596,\"avg\":908.5}}}}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -3850,7 +3829,6 @@
    :result_metadata
    "[{\"display_name\":\"Age\",\"field_ref\":[\"expression\",\"Age\"],\"name\":\"Age\",\"base_type\":\"type/BigInteger\",\"effective_type\":\"type/BigInteger\",\"semantic_type\":null,\"fingerprint\":{\"global\":{\"distinct-count\":43,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":24.0,\"q1\":34.15,\"q3\":55.45,\"max\":66.0,\"sd\":12.328008896346248,\"avg\":44.80281690140845}}}},{\"description\":\"The channel through which we acquired this user. Valid values include: Affiliate, Facebook, Google, Organic and Twitter\",\"semantic_type\":\"type/Source\",\"coercion_strategy\":null,\"name\":\"SOURCE\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",45,{\"base-type\":\"type/Text\",\"join-alias\":\"People - User\"}],\"effective_type\":\"type/Text\",\"id\":45,\"visibility_type\":\"normal\",\"display_name\":\"People - User → Source\",\"fingerprint\":{\"global\":{\"distinct-count\":5,\"nil%\":0.0},\"type\":{\"type/Text\":{\"percent-json\":0.0,\"percent-url\":0.0,\"percent-email\":0.0,\"percent-state\":0.0,\"average-length\":7.4084}}},\"base_type\":\"type/Text\"},{\"display_name\":\"Count\",\"semantic_type\":\"type/Quantity\",\"field_ref\":[\"aggregation\",0],\"name\":\"count\",\"base_type\":\"type/BigInteger\",\"effective_type\":\"type/BigInteger\",\"fingerprint\":{\"global\":{\"distinct-count\":111,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":14.0,\"q1\":61.30452018978108,\"q3\":110.7900825561565,\"max\":214.0,\"sd\":36.95993697001328,\"avg\":88.07511737089202}}}}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -3884,7 +3862,6 @@
    :result_metadata
    "[{\"display_name\":\"Average of Rating\",\"semantic_type\":\"type/Score\",\"settings\":null,\"field_ref\":[\"aggregation\",0],\"name\":\"avg\",\"base_type\":\"type/Float\",\"effective_type\":\"type/Float\",\"fingerprint\":{\"global\":{\"distinct-count\":1,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":3.4715,\"q1\":3.4715,\"q3\":3.4715,\"max\":3.4715,\"sd\":null,\"avg\":3.4715}}}}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -3918,7 +3895,6 @@
    :result_metadata
    "[{\"description\":\"The name of the product as it should be displayed to customers.\",\"semantic_type\":\"type/Title\",\"coercion_strategy\":null,\"name\":\"TITLE\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",65,{\"base-type\":\"type/Text\",\"source-field\":40}],\"effective_type\":\"type/Text\",\"id\":65,\"visibility_type\":\"normal\",\"display_name\":\"Product → Title\",\"fingerprint\":{\"global\":{\"distinct-count\":199,\"nil%\":0.0},\"type\":{\"type/Text\":{\"percent-json\":0.0,\"percent-url\":0.0,\"percent-email\":0.0,\"percent-state\":0.0,\"average-length\":21.495}}},\"base_type\":\"type/Text\"},{\"description\":\"The type of product, valid values include: Doohicky, Gadget, Gizmo and Widget\",\"semantic_type\":\"type/Category\",\"coercion_strategy\":null,\"name\":\"CATEGORY\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",58,{\"base-type\":\"type/Text\",\"source-field\":40}],\"effective_type\":\"type/Text\",\"id\":58,\"visibility_type\":\"normal\",\"display_name\":\"Product → Category\",\"fingerprint\":{\"global\":{\"distinct-count\":4,\"nil%\":0.0},\"type\":{\"type/Text\":{\"percent-json\":0.0,\"percent-url\":0.0,\"percent-email\":0.0,\"percent-state\":0.0,\"average-length\":6.375}}},\"base_type\":\"type/Text\"},{\"display_name\":\"Count\",\"semantic_type\":\"type/Quantity\",\"field_ref\":[\"field\",\"count\",{\"base-type\":\"type/Integer\"}],\"name\":\"count\",\"base_type\":\"type/BigInteger\",\"effective_type\":\"type/BigInteger\",\"fingerprint\":{\"global\":{\"distinct-count\":8,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":109.0,\"q1\":109.36150801752578,\"q3\":117.41886116991581,\"max\":120.0,\"sd\":4.254710813035841,\"avg\":113.53846153846153}}}}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -3952,7 +3928,6 @@
    :result_metadata
    "[{\"description\":\"The date the user record was created. Also referred to as the user’s \\\"join date\\\"\",\"semantic_type\":\"type/CreationTimestamp\",\"coercion_strategy\":null,\"unit\":\"month\",\"name\":\"CREATED_AT\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",56,{\"base-type\":\"type/DateTime\",\"temporal-unit\":\"month\"}],\"effective_type\":\"type/DateTime\",\"id\":56,\"visibility_type\":\"normal\",\"display_name\":\"Created At\",\"fingerprint\":{\"global\":{\"distinct-count\":2500,\"nil%\":0.0},\"type\":{\"type/DateTime\":{\"earliest\":\"2022-04-19T21:35:18.752Z\",\"latest\":\"2025-04-19T14:06:27.3Z\"}}},\"base_type\":\"type/DateTime\"},{\"display_name\":\"Distinct values of Name\",\"semantic_type\":\"type/Quantity\",\"settings\":null,\"field_ref\":[\"aggregation\",0],\"name\":\"count\",\"base_type\":\"type/BigInteger\",\"effective_type\":\"type/BigInteger\",\"fingerprint\":{\"global\":{\"distinct-count\":2,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":70.0,\"q1\":70.0,\"q3\":76.0,\"max\":76.0,\"sd\":4.242640687119285,\"avg\":73.0}}}}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -3985,7 +3960,6 @@
    :result_metadata
    "[{\"display_name\":\"Age\",\"field_ref\":[\"expression\",\"Age\"],\"name\":\"Age\",\"base_type\":\"type/BigInteger\",\"effective_type\":\"type/BigInteger\",\"semantic_type\":null,\"fingerprint\":{\"global\":{\"distinct-count\":43,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":24.0,\"q1\":34.25,\"q3\":55.75,\"max\":66.0,\"sd\":12.556538801224908,\"avg\":45.0}}}},{\"display_name\":\"Sum of Total\",\"semantic_type\":null,\"settings\":null,\"field_ref\":[\"aggregation\",0],\"name\":\"sum\",\"base_type\":\"type/Float\",\"effective_type\":\"type/Float\",\"fingerprint\":{\"global\":{\"distinct-count\":43,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":4158.786206744129,\"q1\":31397.769615345165,\"q3\":38764.15212640109,\"max\":58676.42899556268,\"sd\":8106.619825736762,\"avg\":35130.73681513096}}}}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -4019,7 +3993,6 @@
    :result_metadata
    "[{\"description\":\"A unique identifier given to each user.\",\"semantic_type\":\"type/PK\",\"coercion_strategy\":null,\"name\":\"ID\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",46,null],\"effective_type\":\"type/BigInteger\",\"id\":46,\"visibility_type\":\"normal\",\"display_name\":\"ID\",\"fingerprint\":null,\"base_type\":\"type/BigInteger\"},{\"description\":\"The street address of the account’s billing address\",\"semantic_type\":null,\"coercion_strategy\":null,\"name\":\"ADDRESS\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",52,null],\"effective_type\":\"type/Text\",\"id\":52,\"visibility_type\":\"normal\",\"display_name\":\"Address\",\"fingerprint\":{\"global\":{\"distinct-count\":2490,\"nil%\":0.0},\"type\":{\"type/Text\":{\"percent-json\":0.0,\"percent-url\":0.0,\"percent-email\":0.0,\"percent-state\":0.0,\"average-length\":20.85}}},\"base_type\":\"type/Text\"},{\"description\":\"The contact email for the account.\",\"semantic_type\":\"type/Email\",\"coercion_strategy\":null,\"name\":\"EMAIL\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",51,null],\"effective_type\":\"type/Text\",\"id\":51,\"visibility_type\":\"normal\",\"display_name\":\"Email\",\"fingerprint\":{\"global\":{\"distinct-count\":2500,\"nil%\":0.0},\"type\":{\"type/Text\":{\"percent-json\":0.0,\"percent-url\":0.0,\"percent-email\":1.0,\"percent-state\":0.0,\"average-length\":24.1824}}},\"base_type\":\"type/Text\"},{\"description\":\"This is the salted password of the user. It should not be visible\",\"semantic_type\":null,\"coercion_strategy\":null,\"name\":\"PASSWORD\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",53,null],\"effective_type\":\"type/Text\",\"id\":53,\"visibility_type\":\"normal\",\"display_name\":\"Password\",\"fingerprint\":{\"global\":{\"distinct-count\":2500,\"nil%\":0.0},\"type\":{\"type/Text\":{\"percent-json\":0.0,\"percent-url\":0.0,\"percent-email\":0.0,\"percent-state\":0.0,\"average-length\":36.0}}},\"base_type\":\"type/Text\"},{\"description\":\"The name of the user who owns an account\",\"semantic_type\":\"type/Name\",\"coercion_strategy\":null,\"name\":\"NAME\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",47,null],\"effective_type\":\"type/Text\",\"id\":47,\"visibility_type\":\"normal\",\"display_name\":\"Name\",\"fingerprint\":{\"global\":{\"distinct-count\":2499,\"nil%\":0.0},\"type\":{\"type/Text\":{\"percent-json\":0.0,\"percent-url\":0.0,\"percent-email\":0.0,\"percent-state\":0.0,\"average-length\":13.532}}},\"base_type\":\"type/Text\"},{\"description\":\"The city of the account’s billing address\",\"semantic_type\":\"type/City\",\"coercion_strategy\":null,\"name\":\"CITY\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",55,null],\"effective_type\":\"type/Text\",\"id\":55,\"visibility_type\":\"normal\",\"display_name\":\"City\",\"fingerprint\":{\"global\":{\"distinct-count\":1966,\"nil%\":0.0},\"type\":{\"type/Text\":{\"percent-json\":0.0,\"percent-url\":0.0,\"percent-email\":0.0,\"percent-state\":0.002,\"average-length\":8.284}}},\"base_type\":\"type/Text\"},{\"description\":\"This is the longitude of the user on sign-up. It might be updated in the future to the last seen location.\",\"semantic_type\":\"type/Longitude\",\"coercion_strategy\":null,\"name\":\"LONGITUDE\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",50,null],\"effective_type\":\"type/Float\",\"id\":50,\"visibility_type\":\"normal\",\"display_name\":\"Longitude\",\"fingerprint\":{\"global\":{\"distinct-count\":2491,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":-166.5425726,\"q1\":-101.58350792373135,\"q3\":-84.65289348288829,\"max\":-67.96735199999999,\"sd\":15.399698968175663,\"avg\":-95.18741780363999}}},\"base_type\":\"type/Float\"},{\"description\":\"The state or province of the account’s billing address\",\"semantic_type\":\"type/State\",\"coercion_strategy\":null,\"name\":\"STATE\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",48,null],\"effective_type\":\"type/Text\",\"id\":48,\"visibility_type\":\"normal\",\"display_name\":\"State\",\"fingerprint\":{\"global\":{\"distinct-count\":49,\"nil%\":0.0},\"type\":{\"type/Text\":{\"percent-json\":0.0,\"percent-url\":0.0,\"percent-email\":0.0,\"percent-state\":1.0,\"average-length\":2.0}}},\"base_type\":\"type/Text\"},{\"description\":\"The channel through which we acquired this user. Valid values include: Affiliate, Facebook, Google, Organic and Twitter\",\"semantic_type\":\"type/Source\",\"coercion_strategy\":null,\"name\":\"SOURCE\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",45,null],\"effective_type\":\"type/Text\",\"id\":45,\"visibility_type\":\"normal\",\"display_name\":\"Source\",\"fingerprint\":{\"global\":{\"distinct-count\":5,\"nil%\":0.0},\"type\":{\"type/Text\":{\"percent-json\":0.0,\"percent-url\":0.0,\"percent-email\":0.0,\"percent-state\":0.0,\"average-length\":7.4084}}},\"base_type\":\"type/Text\"},{\"description\":\"The date of birth of the user\",\"semantic_type\":null,\"coercion_strategy\":null,\"unit\":\"default\",\"name\":\"BIRTH_DATE\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",49,{\"temporal-unit\":\"default\"}],\"effective_type\":\"type/Date\",\"id\":49,\"visibility_type\":\"normal\",\"display_name\":\"Birth Date\",\"fingerprint\":{\"global\":{\"distinct-count\":2308,\"nil%\":0.0},\"type\":{\"type/DateTime\":{\"earliest\":\"1958-04-26\",\"latest\":\"2000-04-03\"}}},\"base_type\":\"type/Date\"},{\"description\":\"The postal code of the account’s billing address\",\"semantic_type\":\"type/ZipCode\",\"coercion_strategy\":null,\"name\":\"ZIP\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",54,null],\"effective_type\":\"type/Text\",\"id\":54,\"visibility_type\":\"normal\",\"display_name\":\"Zip\",\"fingerprint\":{\"global\":{\"distinct-count\":2234,\"nil%\":0.0},\"type\":{\"type/Text\":{\"percent-json\":0.0,\"percent-url\":0.0,\"percent-email\":0.0,\"percent-state\":0.0,\"average-length\":5.0}}},\"base_type\":\"type/Text\"},{\"description\":\"This is the latitude of the user on sign-up. It might be updated in the future to the last seen location.\",\"semantic_type\":\"type/Latitude\",\"coercion_strategy\":null,\"name\":\"LATITUDE\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",57,null],\"effective_type\":\"type/Float\",\"id\":57,\"visibility_type\":\"normal\",\"display_name\":\"Latitude\",\"fingerprint\":{\"global\":{\"distinct-count\":2491,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":25.775827,\"q1\":35.302705923023126,\"q3\":43.773802584662,\"max\":70.6355001,\"sd\":6.390832341883712,\"avg\":39.87934670484002}}},\"base_type\":\"type/Float\"},{\"description\":\"The date the user record was created. Also referred to as the user’s \\\"join date\\\"\",\"semantic_type\":\"type/CreationTimestamp\",\"coercion_strategy\":null,\"unit\":\"default\",\"name\":\"CREATED_AT\",\"settings\":null,\"fk_target_field_id\":null,\"field_ref\":[\"field\",56,{\"temporal-unit\":\"default\"}],\"effective_type\":\"type/DateTime\",\"id\":56,\"visibility_type\":\"normal\",\"display_name\":\"Created At\",\"fingerprint\":{\"global\":{\"distinct-count\":2500,\"nil%\":0.0},\"type\":{\"type/DateTime\":{\"earliest\":\"2022-04-19T21:35:18.752Z\",\"latest\":\"2025-04-19T14:06:27.3Z\"}}},\"base_type\":\"type/DateTime\"},{\"display_name\":\"Age\",\"field_ref\":[\"expression\",\"Age\"],\"name\":\"Age\",\"base_type\":\"type/BigInteger\",\"effective_type\":\"type/BigInteger\",\"semantic_type\":null,\"fingerprint\":{\"global\":{\"distinct-count\":42,\"nil%\":0.0},\"type\":{\"type/Number\":{\"min\":24.0,\"q1\":33.340572873934306,\"q3\":55.17166516756599,\"max\":65.0,\"sd\":12.263883782175668,\"avg\":44.434}}}}]",
    :initially_published_at nil,
-   :trashed_from_collection_id nil,
    :database_id 1,
    :enable_embedding false,
    :collection_id 2,
@@ -4050,7 +4023,6 @@
  ({:authority_level nil,
    :description nil,
    :archived false,
-   :trashed_from_location nil,
    :slug "examples",
    :name "Examples",
    :personal_owner_id nil,
diff --git a/src/metabase/api/activity.clj b/src/metabase/api/activity.clj
index ca7b526e1f6e64bbe248a8cc66a5ecc2473adf47..e3097c16fd37b6016e35b4886d4b9951c7bfd3ea 100644
--- a/src/metabase/api/activity.clj
+++ b/src/metabase/api/activity.clj
@@ -22,13 +22,11 @@
      "card"      [Card
                   :id :name :collection_id :description :display
                   :dataset_query :type :archived
-                  :collection.authority_level [:collection.name :collection_name]
-                  :trashed_from_collection_id]
+                  :collection.authority_level [:collection.name :collection_name]]
      "dashboard" [Dashboard
                   :id :name :collection_id :description
                   :archived
-                  :collection.authority_level [:collection.name :collection_name]
-                  :trashed_from_collection_id]
+                  :collection.authority_level [:collection.name :collection_name]]
      "table"     [Table
                   :id :name :db_id :active
                   :display_name [:metabase_database.initial_sync_status :initial-sync-status]
diff --git a/src/metabase/api/card.clj b/src/metabase/api/card.clj
index af5147510a7e86e79cc2b1e5ee29654fddd4f9f6..f4e08d62c17c091ae0fdac6098b4839a79743d94 100644
--- a/src/metabase/api/card.clj
+++ b/src/metabase/api/card.clj
@@ -190,6 +190,7 @@
                     :last_query_start
                     :parameter_usage_count
                     :can_restore
+                    :can_delete
                     [:collection :is_personal]
                     [:moderation_reviews :moderator_details])
         (cond->                                             ; card
@@ -205,7 +206,8 @@
         hydrate-card-details
         ;; Cal 2023-11-27: why is last-edit-info hydrated differently for GET vs PUT and POST
         with-last-edit-info
-        collection.root/hydrate-root-collection)))
+        collection.root/hydrate-root-collection
+        (api/present-in-trash-if-archived-directly (collection/trash-collection-id)))))
 
 (api/defendpoint GET "/:id"
   "Get `Card` with ID."
@@ -537,10 +539,14 @@
   (check-if-card-can-be-saved dataset_query type)
   (let [card-before-update     (t2/hydrate (api/write-check Card id)
                                            [:moderation_reviews :moderator_details])
-        card-updates           (api/move-on-archive-or-unarchive card-before-update card-updates (collection/trash-collection-id))
+        card-updates           (api/updates-with-archived-directly card-before-update card-updates)
         is-model-after-update? (if (nil? type)
                                  (card/model? card-before-update)
                                  (card/model? card-updates))]
+    ;; Can't move a card to the trash
+    (when (and collection_id (= collection_id (collection/trash-collection-id)))
+      (throw (ex-info (tru "Cannot move a card to the trash collection.")
+                      {:status-code 400})))
     ;; Do various permissions checks
     (doseq [f [collection/check-allowed-to-change-collection
                check-allowed-to-modify-query
diff --git a/src/metabase/api/collection.clj b/src/metabase/api/collection.clj
index 6d2fe85b5b1c9ea3c68f438d75289dad04d7ce72..5bcb8d380016a5e4e0894fdfe78bda3f9a0da956 100644
--- a/src/metabase/api/collection.clj
+++ b/src/metabase/api/collection.clj
@@ -75,8 +75,10 @@
   the root, if `collection-id` is `nil`) and its immediate children, to avoid reading the entire collection tree when it
   is not necessary.
 
-  For archived, we can either include everthing (when archived is `nil`), only archived (when `archived` is true),
-  or only non-archived (when `archived` is false).
+  For archived, we can either include only archived items (when archived is truthy) or exclude archived items (when
+  archived is falsey).
+
+  The Trash Collection itself (the container for archived items) is *always* included.
 
   To select only personal collections, pass in `personal-only` as `true`.
   This will select only collections where `personal_owner_id` is not `nil`."
@@ -101,7 +103,14 @@
                        (perms/audit-namespace-clause :namespace namespace)
                        (collection/visible-collection-ids->honeysql-filter-clause
                         :id
-                        (collection/permissions-set->visible-collection-ids permissions-set))]
+                        (collection/permissions-set->visible-collection-ids
+                         permissions-set
+                         {:include-archived-items (if archived
+                                                    :only
+                                                    :exclude)
+                          :include-trash-collection? true
+                          :permission-level :read
+                          :archive-operation-id nil}))]
                ;; Order NULL collection types first so that audit collections are last
                :order-by [[[[:case [:= :authority_level "official"] 0 :else 1]] :asc]
                           [[[:case
@@ -109,7 +118,7 @@
                              [:= :type collection/trash-collection-type] 1
                              :else 2]] :asc]
                           [:%lower.name :asc]]})
-   exclude-other-user-collections (remove-other-users-personal-subcollections api/*current-user-id*)))
+    exclude-other-user-collections (remove-other-users-personal-subcollections api/*current-user-id*)))
 
 (api/defendpoint GET "/"
   "Fetch a list of all Collections that the current user has read permissions for (`:can_write` is returned as an
@@ -141,7 +150,7 @@
         (cond->> collections
           (mi/can-read? root)
           (cons root))))
-    (t2/hydrate collections :can_write :is_personal)
+    (t2/hydrate collections :can_write :is_personal :can_delete)
     ;; remove the :metabase.models.collection.root/is-root? tag since FE doesn't need it
     ;; and for personal collections we translate the name to user's locale
     (for [collection collections]
@@ -329,6 +338,7 @@
                          :p.name
                          :p.entity_id
                          :p.collection_position
+                         :p.collection_id
                          [(h2x/literal "pulse") :model]]
        :from            [[:pulse :p]]
        :left-join       [[:pulse_card :pc] [:= :p.id :pc.pulse_id]]
@@ -363,7 +373,7 @@
 
 (defmethod collection-children-query :timeline
   [_ collection {:keys [archived? pinned-state]}]
-  {:select [:id :name [(h2x/literal "timeline") :model] :description :entity_id :icon]
+  {:select [:id :collection_id :name [(h2x/literal "timeline") :model] :description :entity_id :icon]
    :from   [[:timeline :timeline]]
    :where  [:and
             (poison-when-pinned-clause pinned-state)
@@ -388,7 +398,8 @@
 (defn- card-query [card-type collection {:keys [archived? pinned-state]}]
   (-> {:select    (cond->
                     [:c.id :c.name :c.description :c.entity_id :c.collection_position :c.display :c.collection_preview
-                     :c.trashed_from_collection_id
+                     :c.collection_id
+                     :c.archived_directly
                      :c.archived
                      :c.dataset_query
                      [(h2x/literal (case card-type
@@ -421,7 +432,19 @@
                                    [:= :r.model (h2x/literal "Card")]]
                    [:core_user :u] [:= :u.id :r.user_id]]
        :where     [:and
-                   [:= :collection_id (:id collection)]
+                   (collection/visible-collection-ids->honeysql-filter-clause
+                    :collection_id
+                    (collection/permissions-set->visible-collection-ids @api/*current-user-permissions-set*
+                                                                        {:include-archived-items :all
+                                                                         :permission-level (if archived?
+                                                                                             :write
+                                                                                             :read)
+                                                                         :archive-operation-id nil}))
+                   (if (collection/is-trash? collection)
+                     [:= :c.archived_directly true]
+                     [:and
+                      [:= :collection_id (:id collection)]
+                      [:= :c.archived_directly false]])
                    [:= :archived (boolean archived?)]
                    (case card-type
                      :model
@@ -505,7 +528,8 @@
   (-> (t2/instance :model/Card row)
       (update :collection_preview api/bit->boolean)
       (update :archived api/bit->boolean)
-      (t2/hydrate :can_write :can_restore)
+      (update :archived_directly api/bit->boolean)
+      (t2/hydrate :can_write :can_restore :can_delete)
       (dissoc :authority_level :icon :personal_owner_id :dataset_query :table_id :query_type :is_upload)
       (assoc :fully_parameterized (fully-parameterized-query? row))))
 
@@ -519,7 +543,8 @@
 
 (defn- dashboard-query [collection {:keys [archived? pinned-state]}]
   (-> {:select    [:d.id :d.name :d.description :d.entity_id :d.collection_position
-                   :d.trashed_from_collection_id
+                   :d.collection_id
+                   :d.archived_directly
                    [(h2x/literal "dashboard") :model]
                    [:u.id :last_edit_user]
                    :archived
@@ -534,7 +559,19 @@
                                    [:= :r.model (h2x/literal "Dashboard")]]
                    [:core_user :u] [:= :u.id :r.user_id]]
        :where     [:and
-                   [:= :collection_id (:id collection)]
+                   (collection/visible-collection-ids->honeysql-filter-clause
+                    :collection_id
+                    (collection/permissions-set->visible-collection-ids @api/*current-user-permissions-set*
+                                                                        {:include-archived-items :all
+                                                                         :archive-operation-id nil
+                                                                         :permission-level (if archived?
+                                                                                             :write
+                                                                                             :read)}))
+                   (if (collection/is-trash? collection)
+                     [:= :d.archived_directly true]
+                     [:and
+                      [:= :collection_id (:id collection)]
+                      [:not= :d.archived_directly true]])
                    [:= :archived (boolean archived?)]]}
       (sql.helpers/where (pinned-state->clause pinned-state))))
 
@@ -545,7 +582,8 @@
 (defn- post-process-dashboard [dashboard]
   (-> (t2/instance :model/Dashboard dashboard)
       (update :archived api/bit->boolean)
-      (t2/hydrate :can_write :can_restore)
+      (update :archived_directly api/bit->boolean)
+      (t2/hydrate :can_write :can_restore :can_delete)
       (dissoc :display :authority_level :moderated_status :icon :personal_owner_id :collection_preview
               :dataset_query :table_id :query_type :is_upload)))
 
@@ -564,25 +602,30 @@
 
 (defn- collection-query
   [collection {:keys [archived? collection-namespace pinned-state]}]
-  (-> (assoc (collection/effective-children-query
-              collection
-              (if archived?
-                [:or [:= :archived true] [:= :id (collection/trash-collection-id)]]
-                [:and [:= :archived false] [:not= :id (collection/trash-collection-id)]])
-              (perms/audit-namespace-clause :namespace (u/qualified-name collection-namespace))
-              (snippets-collection-filter-clause))
-             ;; We get from the effective-children-query a normal set of columns selected:
-             ;; want to make it fit the others to make UNION ALL work
-             :select [:id
-                      :archived
-                      :name
-                      :description
-                      :entity_id
-                      :personal_owner_id
-                      :location
-                      [:type :collection_type]
-                      [(h2x/literal "collection") :model]
-                      :authority_level])
+  (-> (assoc
+       (collection/effective-children-query
+        collection
+        (if archived?
+          [:or
+           [:= :archived true]
+           [:= :id (collection/trash-collection-id)]]
+          [:and [:= :archived false] [:not= :id (collection/trash-collection-id)]])
+        (perms/audit-namespace-clause :namespace (u/qualified-name collection-namespace))
+        (snippets-collection-filter-clause))
+       ;; We get from the effective-children-query a normal set of columns selected:
+       ;; want to make it fit the others to make UNION ALL work
+       :select [:id
+                [:id :collection_id]
+                :archived
+                :name
+                :description
+                :entity_id
+                :personal_owner_id
+                :location
+                :archived_directly
+                [:type :collection_type]
+                [(h2x/literal "collection") :model]
+                :authority_level])
       ;; the nil indicates that collections are never pinned.
       (sql.helpers/where (pinned-state->clause pinned-state nil))))
 
@@ -592,8 +635,10 @@
 
 (defn- annotate-collections
   [parent-coll colls]
-  (let [visible-collection-ids (collection/permissions-set->visible-collection-ids
-                                @api/*current-user-permissions-set*)
+  (let [visible-collection-ids (collection/permissions-set->visible-collection-ids @api/*current-user-permissions-set*
+                                                                                   {:include-archived-items :all
+                                                                                    :archive-operation-id nil
+                                                                                    :permission-level :read})
 
         descendant-collections (collection/descendants-flat parent-coll (collection/visible-collection-ids->honeysql-filter-clause
                                                                          :id
@@ -610,18 +655,20 @@
                 {:dataset #{}
                  :metric  #{}
                  :card    #{}}
-                (t2/reducible-query {:select-distinct [:collection_id :type]
-                                     :from            [:report_card]
-                                     :where           [:and
-                                                       [:= :archived false]
-                                                       [:in :collection_id descendant-collection-ids]]}))
+                (when (seq descendant-collection-ids)
+                  (t2/reducible-query {:select-distinct [:collection_id :type]
+                                       :from            [:report_card]
+                                       :where           [:and
+                                                         [:= :archived false]
+                                                         [:in :collection_id descendant-collection-ids]]})))
 
         collections-containing-dashboards
-        (->> (t2/query {:select-distinct [:collection_id]
-                        :from :report_dashboard
-                        :where [:and
-                                [:= :archived false]
-                                [:in :collection_id descendant-collection-ids]]})
+        (->> (when (seq descendant-collection-ids)
+               (t2/query {:select-distinct [:collection_id]
+                          :from :report_dashboard
+                          :where [:and
+                                  [:= :archived false]
+                                  [:in :collection_id descendant-collection-ids]]}))
              (map :collection_id)
              (into #{}))
 
@@ -659,7 +706,7 @@
       (-> (t2/instance :model/Collection row)
           collection/maybe-localize-trash-name
           (update :archived api/bit->boolean)
-          (t2/hydrate :can_write :effective_location :collection/can_restore)
+          (t2/hydrate :can_write :effective_location :can_restore :can_delete)
           (dissoc :collection_position :display :moderated_status :icon
                   :collection_preview :dataset_query :table_id :query_type :is_upload)
           update-personal-collection))))
@@ -682,7 +729,7 @@
         (:last_edit_user row) (assoc :last-edit-info (select-as row mapping))))))
 
 (defn- remove-unwanted-keys [row]
-  (dissoc row :collection_type :model_ranking :trashed_from_collection_id))
+  (dissoc row :collection_type :model_ranking :archived_directly))
 
 (defn- model-name->toucan-model [model-name]
   (case (keyword model-name)
@@ -695,38 +742,18 @@
     :snippet    :model/NativeQuerySnippet
     :timeline   :model/Timeline))
 
-(defn- can-read-in-trash? [collection row]
-  (mi/can-write?
-   (t2/instance
-    (model-name->toucan-model (:model row))
-    (assoc (select-keys row [:id :trashed_from_collection_id :archived])
-           :collection_id (:id collection)))))
-
-(defn- maybe-check-permissions
-  "Generally, if you have permission to read a collection, you have permission to read everything in it.
-  This is *not* true for the Trash collection. This contains all trashed items. In this case, we need to filter the
-  list down to those items for which the user actually has permissions.
-
-  Because this is the trash collection, we only want to show the user items which they could move out of the trash if
-  desired. Therefore, we want *write* permissions, not just read."
-  [collection rows]
-  (cond->> rows
-    (collection/is-trash? collection)
-    (filter #(can-read-in-trash? collection %))))
-
 (defn- post-process-rows
   "Post process any data. Have a chance to process all of the same type at once using
   `post-process-collection-children`. Must respect the order passed in."
   [collection rows]
   (->> (map-indexed (fn [i row] (vary-meta row assoc ::index i)) rows) ;; keep db sort order
-       (map #(assoc % :collection_id (:id collection)))
-       (maybe-check-permissions collection)
        (group-by :model)
        (into []
              (comp (map (fn [[model rows]]
                           (post-process-collection-children (keyword model) collection rows)))
                    cat
                    (map coalesce-edit-info)))
+       (map #(api/present-in-trash-if-archived-directly % (collection/trash-collection-id)))
        (map remove-unwanted-keys)
        (sort-by (comp ::index meta))))
 
@@ -747,11 +774,12 @@
   are optional (not id, but last_edit_user for example) must have a type so that the union-all can unify the nil with
   the correct column type."
   [:id :name :description :entity_id :display [:collection_preview :boolean] :dataset_query
+   :collection_id
+   [:archived_directly :boolean]
    :model :collection_position :authority_level [:personal_owner_id :integer] :location
    :last_edit_email :last_edit_first_name :last_edit_last_name :moderated_status :icon
    [:last_edit_user :integer] [:last_edit_timestamp :timestamp] [:database_id :integer]
    :collection_type [:archived :boolean]
-   [:trashed_from_collection_id :integer]
    ;; for determining whether a model is based on a csv-uploaded table
    [:table_id :integer] [:is_upload :boolean] :query_type])
 
@@ -902,7 +930,13 @@
   [collection :- collection/CollectionWithLocationAndIDOrRoot]
   (-> collection
       collection/personal-collection-with-ui-details
-      (t2/hydrate :parent_id :effective_location [:effective_ancestors :can_write] :can_write :is_personal :collection/can_restore)))
+      (t2/hydrate :parent_id
+                  :effective_location
+                  [:effective_ancestors :can_write]
+                  :can_write
+                  :is_personal
+                  :can_restore
+                  :can_delete)))
 
 (api/defendpoint GET "/:id"
   "Fetch a specific Collection with standard details added"
@@ -1089,6 +1123,10 @@
         (api/check-403
          (perms/set-has-full-permissions-for-set? @api/*current-user-permissions-set*
            (collection/perms-for-moving collection-before-update new-parent)))
+
+        ;; We can't move a collection to the Trash
+        (api/check-400
+         (not (collection/is-trash? new-parent)))
         ;; ok, we're good to move!
         (collection/move-collection! collection-before-update new-location)))))
 
@@ -1112,24 +1150,10 @@
   appropriate permissions checks and changes."
   [collection-before-update collection-updates]
   (condp #(api/column-will-change? %1 collection-before-update %2) collection-updates
-    ;; note that archiving includes a move. So if they include `archived` (with or without `parent_id`), that's an
-    ;; archive/unarchive. If they only include `parent_id`, that's a move.
     :archived (archive-collection! collection-before-update collection-updates)
     :parent_id (move-collection! collection-before-update collection-updates)
     :no-op))
 
-(api/defendpoint DELETE "/:id"
-  "Delete a collection entirely."
-  [id]
-  {id ms/PositiveInt}
-  (let [collection-before-delete (t2/select-one :model/Collection :id id)]
-    (api/check-403
-     (perms/set-has-full-permissions-for-set? @api/*current-user-permissions-set*
-                                              (collection/perms-for-archiving collection-before-delete)))
-    ;; we can only delete archived collections
-    (api/check-400 (:archived collection-before-delete))
-    (t2/delete! :model/Collection :id id)))
-
 (api/defendpoint PUT "/:id"
   "Modify an existing Collection, including archiving or unarchiving it, or moving it."
   [id, :as {{:keys [name description archived parent_id authority_level], :as collection-updates} :body}]
diff --git a/src/metabase/api/common.clj b/src/metabase/api/common.clj
index a256dc9d35917191278b3dd25b9713b1bd6b6752..e015f17c87c50665b7679a2cbc2ab999a3a007a0 100644
--- a/src/metabase/api/common.clj
+++ b/src/metabase/api/common.clj
@@ -658,21 +658,18 @@
     [(parse-fn xs)]))
 
 
-;;; ---------------------------------------- MOVING TO TRASH ON ARCHIVE --------------------------------
+;;; ---------------------------------------- SET `archived_directly` ---------------------------------
 
-(defn move-on-archive-or-unarchive
-  "Given a current instance with a `collection_id` and `trashed_from_collection_id` and a set of updates to that
-  instance, return a possibly modified version of the updates reflecting the fact that archiving or unarchiving also
-  moves the instance to/from the Trash."
-  [current-obj obj-updates trash-collection-id]
+(defn updates-with-archived-directly
+  "Sets `archived_directly` to `true` iff `:archived` is being set to `true`."
+  [current-obj obj-updates]
   (cond-> obj-updates
     (column-will-change? :archived current-obj obj-updates)
-    (assoc :collection_id (cond
-                            (:archived obj-updates) trash-collection-id
-
-                            (column-will-change? :collection_id current-obj obj-updates)
-                            (:collection_id obj-updates)
-
-                            :else (:trashed_from_collection_id current-obj))
-           :trashed_from_collection_id (when (:archived obj-updates)
-                                         (:collection_id current-obj)))))
+    (assoc :archived_directly (boolean (:archived obj-updates)))))
+
+(defn present-in-trash-if-archived-directly
+  "If `:archived_directly` is `true`, set `:collection_id` to the trash collection ID."
+  [item trash-collection-id]
+  (cond-> item
+    (:archived_directly item)
+    (assoc :collection_id trash-collection-id)))
diff --git a/src/metabase/api/dashboard.clj b/src/metabase/api/dashboard.clj
index 99be77f777f1fe6ec72844b75f19044267fdf349..bd4b362cbb77630ff9f6e3f78c7714b1e921fb50 100644
--- a/src/metabase/api/dashboard.clj
+++ b/src/metabase/api/dashboard.clj
@@ -98,6 +98,7 @@
                            :dashcard/action
                            :dashcard/linkcard-info]
                 :can_restore
+                :can_delete
                 :last_used_param_values
                 :tabs
                 :collection_authority_level
@@ -254,7 +255,8 @@
         hydrate-dashboard-details
         collection.root/hydrate-root-collection
         hide-unreadable-cards
-        add-query-average-durations)))
+        add-query-average-durations
+        (api/present-in-trash-if-archived-directly (collection/trash-collection-id)))))
 
 (defn- cards-to-copy
   "Returns a map of which cards we need to copy and which are not to be copied. The `:copy` key is a map from id to
@@ -704,9 +706,13 @@
           ;; tabs are always sent in production as well when dashcards are updated, but there are lots of
           ;; tests that exclude it. so this only checks for dashcards
           update-dashcards-and-tabs?         (contains? dash-updates :dashcards)
-          dash-updates                       (api/move-on-archive-or-unarchive current-dash dash-updates (collection/trash-collection-id))]
+          dash-updates                       (api/updates-with-archived-directly current-dash dash-updates)]
       (collection/check-allowed-to-change-collection current-dash dash-updates)
       (check-allowed-to-change-embedding current-dash dash-updates)
+      ;; Can't move things to the Trash.
+      (when (some-> dash-updates :collection_id (= (collection/trash-collection-id)))
+        (throw (ex-info (tru "Cannot move a card to the trash collection.")
+                        {:status-code 400})))
       (api/check-500
         (do
           (t2/with-transaction [_conn]
@@ -716,7 +722,7 @@
             (when-let [updates (not-empty
                                 (u/select-keys-when
                                     dash-updates
-                                  :present #{:description :position :width :collection_id :collection_position :cache_ttl :trashed_from_collection_id}
+                                  :present #{:description :position :width :collection_id :collection_position :cache_ttl :archived_directly}
                                   :non-nil #{:name :parameters :caveats :points_of_interest :show_in_getting_started :enable_embedding
                                              :embedding_params :archived :auto_apply_filters}))]
               (t2/update! Dashboard id updates)
diff --git a/src/metabase/config.clj b/src/metabase/config.clj
index 9dc066c4ad4715fba97630a989c4532ef6c941eb..ed4f20efc1271332179c2c1b642167c0b3f116df 100644
--- a/src/metabase/config.clj
+++ b/src/metabase/config.clj
@@ -159,3 +159,7 @@
   Using this effectively means `MB_LOAD_SAMPLE_CONTENT` defaults to true."
   []
   (not (false? (config-bool :mb-load-sample-content))))
+
+(def ^:dynamic *request-id*
+  "A unique identifier for the current request. This is bound by `metabase.server.middleware.request-id/wrap-request-id`."
+  nil)
diff --git a/src/metabase/models/card.clj b/src/metabase/models/card.clj
index 99a4ab7fdad44fd32cf25163b659822ca3c3bc38..cb105f3e14c95ad9ba590c9792899ec8d3955796 100644
--- a/src/metabase/models/card.clj
+++ b/src/metabase/models/card.clj
@@ -86,7 +86,6 @@
    :type                   mi/transform-keyword})
 
 (doto :model/Card
-  (derive ::mi/has-trashed-from-collection-id)
   (derive :metabase/model)
   ;; You can read/write a Card if you can read/write its parent Collection
   (derive ::perms/use-parent-collection-perms)
@@ -892,7 +891,7 @@ saved later when it is ready."
                 ;; `collection_id` and `description` can be `nil` (in order to unset them).
                 ;; Other values should only be modified if they're passed in as non-nil
                 (u/select-keys-when card-updates
-                                    :present #{:collection_id :collection_position :description :cache_ttl :trashed_from_collection_id}
+                                    :present #{:collection_id :collection_position :description :cache_ttl :archived_directly}
                                     :non-nil #{:dataset_query :display :name :visualization_settings :archived
                                                :enable_embedding :type :parameters :parameter_mappings :embedding_params
                                                :result_metadata :collection_preview :verified-result-metadata?})))
@@ -1036,7 +1035,7 @@ saved later when it is ready."
 
 (defmethod serdes/dependencies "Card"
   [{:keys [collection_id database_id dataset_query parameters parameter_mappings
-           result_metadata table_id visualization_settings trashed_from_collection_id]}]
+           result_metadata table_id visualization_settings]}]
   (->> (map serdes/mbql-deps parameter_mappings)
        (reduce set/union #{})
        (set/union (serdes/parameters-deps parameters))
@@ -1044,7 +1043,6 @@ saved later when it is ready."
        ; table_id and collection_id are nullable.
        (set/union (when table_id #{(serdes/table->path table_id)}))
        (set/union (when collection_id #{[{:model "Collection" :id collection_id}]}))
-       (set/union (when trashed_from_collection_id #{[{:model "Collection" :id trashed_from_collection_id}]}))
        (set/union (result-metadata-deps result_metadata))
        (set/union (serdes/mbql-deps dataset_query))
        (set/union (serdes/visualization-settings-deps visualization_settings))
diff --git a/src/metabase/models/collection.clj b/src/metabase/models/collection.clj
index e1745c2f13a21e9d378204e73ed74bd2e2215457..772183131050b46c9a4672debdfcdf387930768f 100644
--- a/src/metabase/models/collection.clj
+++ b/src/metabase/models/collection.clj
@@ -11,6 +11,7 @@
     :as api
     :refer [*current-user-id* *current-user-permissions-set*]]
    [metabase.audit :as audit]
+   [metabase.config :refer [*request-id*]]
    [metabase.db :as mdb]
    [metabase.events :as events]
    [metabase.models.collection.root :as collection.root]
@@ -43,6 +44,11 @@
    {:error/message (str "an instance of the root Collection")}
    #'collection.root/is-root-collection?])
 
+(def ^:private ^:const archived-directly-models #{:model/Card :model/Dashboard})
+(def ^:private ^:const collectable-models
+  (set/union archived-directly-models
+             #{:model/Pulse :model/NativeQuerySnippet :model/Timeline}))
+
 (def ^:private ^:const collection-slug-max-length
   "Maximum number of characters allowed in a Collection `slug`."
   510)
@@ -96,8 +102,6 @@
   [_original-model _k]
   :model/Collection)
 
-(methodical/defmethod t2/model-for-automagic-hydration [:default :trashed_from_collection] [_original-model _k] :model/Collection)
-
 (t2/deftransforms :model/Collection
   {:namespace       mi/transform-keyword
    :authority_level mi/transform-keyword})
@@ -287,9 +291,7 @@
   top-level Collection. Note that the `parent` of a `collection` that's in the trash is the collection it was trashed
   *from*."
   [collection :- CollectionWithLocationOrRoot]
-  (if-let [new-parent-id (location-path->parent-id (or (when (:archived collection)
-                                                         (:trashed_from_location collection))
-                                                       (:location collection)))]
+  (if-let [new-parent-id (location-path->parent-id (:location collection))]
     (t2/select-one Collection :id new-parent-id)
     root-collection))
 
@@ -305,48 +307,111 @@
 ;; breadcrumbing in the frontend.
 
 (def ^:private VisibleCollections
-  "Includes the possible values for visible collections, either `:all` or a set of ids, possibly including `\"root\"` to
-  represent the root collection."
-  [:or
-   [:= :all]
-   [:set
-    [:or [:= "root"] ms/PositiveInt]]])
+  "Includes the possible values for visible collections, possibly including `\"root\"` to represent the root
+  collection."
+  [:set
+   [:or [:= "root"] ms/PositiveInt]])
+
+(def ^:private ^{:arglists '([])} collection-id->collection
+  "Cached function to fetch *all* collections, as a map of, well, `collection-id->collection`."
+  (memoize/ttl
+   ^{::memoize/args-fn (fn [& _]
+                         ;; If this is running in the context of a request, cache it for the duration of that request.
+                         ;; Otherwise, don't cache the results at all.)
+                         (if-let [req-id *request-id*]
+                           [req-id]
+                           [(random-uuid)]))}
+   (fn collection-id->collection*
+     []
+     (into {} (t2/select-fn-vec (juxt :id identity) :model/Collection)))
+   ;; cache the results for 10 seconds. This is a bit arbitrary but should be long enough to cover ~all requests.
+   :ttl/threshold (* 10 1000)))
+
+(defn- permissions-set->collection-id->collection
+  [permissions-set & [read-or-write]]
+  (let [collection-id->collection (collection-id->collection)
+        ids-with-perm (set
+                       (for [path  permissions-set
+                             :let  [[_ id-str] (case read-or-write
+                                                 :read
+                                                 (re-matches #"/collection/((?:\d+)|root)/(read/)?" path)
+
+                                                 :write
+                                                 (re-matches #"/collection/((?:\d+)|root)/" path))]
+                             :when id-str]
+                         (cond-> id-str
+                           (not= id-str "root") Integer/parseInt)))
+        has-root-permission? (or (contains? permissions-set "/") (contains? ids-with-perm "root"))
+        root-map (when has-root-permission?
+                   {"root" collection.root/root-collection})
+        has-permission? (fn [[id _]]
+                          (if (contains? permissions-set "/")
+                            true
+                            (contains? ids-with-perm id)))]
+    (->> collection-id->collection
+         (filter has-permission?)
+         (merge root-map))))
+
+(def ^:private IncludeArchivedItems
+  [:enum :only :exclude :all])
+(def ^:private IncludeTrashCollection
+  [:boolean])
+(def ^:private ArchiveOperationId
+  [:maybe :string])
+(def ^:private PermissionLevel
+  [:enum :read :write])
+(def ^:private CollectionVisibilityConfig
+  [:map
+   [:include-trash-collection? {:optional true} IncludeTrashCollection]
+   [:include-archived-items {:optional true} IncludeArchivedItems]
+   [:archive-operation-id {:optional true} ArchiveOperationId]
+   [:permission-level {:optional true} PermissionLevel]])
+
+(defn- should-remove-for-archived? [include-archived-items collection]
+  (case include-archived-items
+    :all false
+    :exclude (:archived collection)
+    :only (not (or (:archived collection)
+                   (is-trash? collection)))))
+
+(defn- should-remove-for-trash? [include-trash-collection? collection]
+  (if include-trash-collection?
+    false
+    (is-trash? collection)))
+
+(defn- should-remove-for-archive-operation-id [archive-operation-id collection]
+  (and (some? archive-operation-id)
+       (not= archive-operation-id (:archive_operation_id collection))))
 
 (mu/defn permissions-set->visible-collection-ids :- VisibleCollections
-  "Given a `permissions-set` (presumably those of the current user), return a set of IDs of Collections that the
-  permissions set allows you to view. For those with *root* permissions (e.g., an admin), this function will return
-  `:all`, signifying that you are allowed to view all Collections. For *Root Collection* permissions, the response
-  will include \"root\".
-
-    (permissions-set->visible-collection-ids #{\"/collection/10/\"})   ; -> #{10}
-    (permissions-set->visible-collection-ids #{\"/\"})                 ; -> :all
-    (permissions-set->visible-collection-ids #{\"/collection/root/\"}) ; -> #{\"root\"}
-
-  You probably don't want to consume the results of this function directly -- most of the time, the reason you are
-  calling this function in the first place is because you want add a `FILTER` clause to an application DB query (e.g.
-  to only fetch Cards that belong to Collections visible to the current User). Use
-  [[visible-collection-ids->honeysql-filter-clause]] to generate a filter clause that handles all possible outputs of
-  this function correctly.
-
-  !!! IMPORTANT NOTE !!!
-
-  Because the result may include `nil` for the Root Collection, or may be `:all`, MAKE SURE YOU HANDLE THOSE
-  SITUATIONS CORRECTLY before using these IDs to make a DB call. Better yet, use
-  [[visible-collection-ids->honeysql-filter-clause]] to generate appropriate HoneySQL."
-  [permissions-set]
-  (if (contains? permissions-set "/")
-    :all
-    (set
-     (for [path  permissions-set
-           :let  [[_ id-str] (re-matches #"/collection/((?:\d+)|root)/(read/)?" path)]
-           :when id-str]
-       (cond-> id-str
-         (not= id-str "root") Integer/parseInt)))))
+  "There are four knobs we need to take into account when turning the permissions set into visible collection IDs.
+  - permission-level: generally collections with `read` permission are visible. Sometimes we want to change this and only
+    view collections with `write` permissions.
+  - include-archived-items: are archived collections currently visible, or not?
+  - archive-operation-id: when looking at a archived collection, we want to restrict visible collections to those that share
+  that operation ID.
+  - include-trash-collection?: is the Trash Collection itself visible, or not? Sometimes we want to show the Trash but
+    not the archived items inside it."
+  ([permissions-set :- [:set :string]]
+   (permissions-set->visible-collection-ids permissions-set {}))
+  ([permissions-set :- [:set :string]
+    visibility-config :- CollectionVisibilityConfig]
+   (let [visibility-config (merge {:include-archived-items :exclude
+                                   :include-trash-collection? false
+                                   :archive-operation-id nil
+                                   :permission-level :read}
+                                  visibility-config)]
+     (->> (permissions-set->collection-id->collection permissions-set (:permission-level visibility-config))
+          (remove #(should-remove-for-archived? (:include-archived-items visibility-config) (val %)))
+          (remove #(should-remove-for-archive-operation-id (:archive-operation-id visibility-config) (val %)))
+          (remove #(should-remove-for-trash? (:include-trash-collection? visibility-config) (val %)))
+          (map key)
+          (into #{})))))
 
 (mu/defn visible-collection-ids->honeysql-filter-clause
   "Generate an appropriate HoneySQL `:where` clause to filter something by visible Collection IDs, such as the ones
   returned by `permissions-set->visible-collection-ids`. Correctly handles all possible values returned by that
-  function, including `:all` and `nil` Collection IDs (for the Root Collection).
+  function, including `root` Collection IDs (for the Root Collection).
 
   Guaranteed to always generate a valid HoneySQL form, so this can be used directly in a query without further checks.
 
@@ -359,59 +424,60 @@
 
   ([collection-id-field :- [:or [:tuple [:= :coalesce] :keyword :keyword] :keyword] ;; `[:vector :keyword]` allows `[:coalesce :option-1 :option-2]`
     collection-ids      :- VisibleCollections]
-   (if (= collection-ids :all)
-     true
-     (let [{non-root-ids false, root-id true} (group-by (partial = "root") collection-ids)
-           non-root-clause                    (when (seq non-root-ids)
-                                                [:in collection-id-field non-root-ids])
-           root-clause                        (when (seq root-id)
-                                                [:= collection-id-field nil])]
-       (cond
-         (and root-clause non-root-clause)
-         [:or root-clause non-root-clause]
-
-         (or root-clause non-root-clause)
-         (or root-clause non-root-clause)
-
-         :else
-         false)))))
+   (let [{non-root-ids false, root-id true} (group-by (partial = "root") collection-ids)
+         non-root-clause                    (when (seq non-root-ids)
+                                              [:in collection-id-field non-root-ids])
+         root-clause                        (when (seq root-id)
+                                              [:= collection-id-field nil])]
+     (cond
+       (and root-clause non-root-clause)
+       [:or root-clause non-root-clause]
+
+       (or root-clause non-root-clause)
+       (or root-clause non-root-clause)
+
+       :else
+       false))))
 
 (mu/defn visible-collection-ids->direct-visible-descendant-clause
   "Generates an appropriate HoneySQL `:where` clause to filter out descendants of a collection A with a specific property.
   This property is being a descendant of a visible collection other than A. Used for effective children calculations"
   [parent-collection :- CollectionWithLocationAndIDOrRoot, collection-ids :- VisibleCollections]
-  (let [parent-id           (or (:id parent-collection) "")
-        child-literal       (if (collection.root/is-root-collection? parent-collection)
-                              "/"
-                              (format "%%/%s/" (str parent-id)))]
-    (into
-     ;; if the collection-ids are empty, the whole into turns into nil and we have a dangling [:and] clause in query.
-     ;; the (1 = 1) is to prevent this
-     [:and [:= [:inline 1] [:inline 1]]]
-     (if (= collection-ids :all)
-       ;; In the case that visible-collection-ids is all, that means there's no invisible collection ids
-       ;; meaning, the effective children are always the direct children. So check for being a direct child.
-       [[:like :location (h2x/literal child-literal)]]
+  (if (is-trash? parent-collection)
+    ;; the only direct descendants of the Trash are those that were trashed directly. TODO: theoretically if someone
+    ;; Trashes collection A, which I don't have permissions on, and then trashes collection B, which I do - I should
+    ;; see collection B in the Trash. But this is not something we can figure out here, because we need to look at
+    ;; whether a visible ancestor was `archived_directly`.
+    [:= :archived_directly true]
+    (let [parent-id (or (:id parent-collection) "")]
+      (into
+       ;; if the collection-ids are empty, the whole into turns into nil and we have a dangling [:and] clause in query.
+       ;; the (1 = 1) is to prevent this
+       [:and [:= [:inline 1] [:inline 1]]]
        (let [to-disj-ids         (location-path->ids (or (:effective_location parent-collection) "/"))
              disj-collection-ids (apply disj collection-ids (conj to-disj-ids parent-id))]
          (for [visible-collection-id disj-collection-ids]
            [:not-like :location (h2x/literal (format "%%/%s/%%" (str visible-collection-id)))]))))))
 
-
 (mu/defn ^:private effective-location-path* :- [:maybe LocationPath]
   ([collection :- CollectionWithLocationOrRoot]
-   (if (collection.root/is-root-collection? collection)
-     nil
-     (effective-location-path* (:location collection)
-                               (permissions-set->visible-collection-ids @*current-user-permissions-set*))))
-
+   (when-not (collection.root/is-root-collection? collection)
+     (effective-location-path* (if (:archived_directly collection)
+                                 (trash-path)
+                                 (:location collection))
+                               (permissions-set->visible-collection-ids
+                                @*current-user-permissions-set*
+                                {:include-archived-items    (if (:archived collection)
+                                                              :only
+                                                              :exclude)
+                                 :include-trash-collection? true
+                                 :archive-operation-id      (:archive_operation_id collection)
+                                 :permission-level          :read}))))
   ([real-location-path     :- LocationPath
     allowed-collection-ids :- VisibleCollections]
-   (if (= allowed-collection-ids :all)
-     real-location-path
-     (apply location-path (for [id    (location-path->ids real-location-path)
-                                :when (contains? allowed-collection-ids id)]
-                            id)))))
+   (apply location-path (for [id    (location-path->ids real-location-path)
+                              :when (contains? allowed-collection-ids id)]
+                          id))))
 
 (mi/define-simple-hydration-method effective-location-path
   :effective_location
@@ -507,16 +573,6 @@
   [{:keys [location]} :- CollectionWithLocationOrRoot]
   (some-> location location-path->parent-id))
 
-(mu/defn ^:private trashed-from-parent-id* :- [:maybe ms/PositiveInt]
-  [{:keys [trashed_from_location]} :- CollectionWithLocationOrRoot]
-  (some-> trashed_from_location location-path->parent-id))
-
-(methodical/defmethod t2/simple-hydrate [:default :trashed_from_parent_id]
-  "Get the immediate parent `collection` id this collection was *trashed* from, if set."
-  [_model k collection]
-  (cond-> collection
-    (:archived collection) (assoc k (trashed-from-parent-id* collection))))
-
 (methodical/defmethod t2/simple-hydrate [:default :parent_id]
   "Get the immediate parent `collection` id, if set."
   [_model k collection]
@@ -598,15 +654,32 @@
 
 (mu/defn ^:private effective-children-where-clause
   [collection & additional-honeysql-where-clauses]
-  (let [visible-collection-ids (permissions-set->visible-collection-ids @*current-user-permissions-set*)]
+  (let [visible-collection-ids (permissions-set->visible-collection-ids
+                                @*current-user-permissions-set*
+                                {:include-archived-items    (if (or (:archived collection)
+                                                                    (is-trash? collection))
+                                                              :only
+                                                              :exclude)
+                                 :include-trash-collection? true
+                                 :archive-operation-id      (:archive_operation_id collection)
+                                 :permission-level          (if (or (:archived collection)
+                                                                    (is-trash? collection))
+                                                              :write
+                                                              :read)})]
     ;; Collection B is an effective child of Collection A if...
     (into
       [:and
-       ;; it is a descendant of Collection A
-       [:like :location (h2x/literal (str (children-location collection) "%"))]
+       ;; it is a descendant of Collection A. For the Trash, this just means the collection is archived. Otherwise
+       ;; we check that Collection B's location contains A's location as a prefix
+       (if (is-trash? collection)
+         [:= :archived true]
+         [:like :location (h2x/literal (str (children-location collection) "%"))])
+       ;; when we're looking at a particular archived collection, we only see the subtree that was archived together.
+       (when (:archive_operation_id collection)
+         [:= :archive_operation_id (:archive_operation_id collection)])
        ;; it is visible.
        (visible-collection-ids->honeysql-filter-clause :id visible-collection-ids)
-       ;; it is NOT a descendant of a visible Collection other than A
+       ;; it is NOT a descendant of a visible Collection other than A - e.g. a visible subcollection.
        (visible-collection-ids->direct-visible-descendant-clause (t2/hydrate collection :effective_location) visible-collection-ids)
        ;; don't want personal collections in collection items. Only on the sidebar
        [:= :personal_owner_id nil]]
@@ -732,34 +805,53 @@
          :location [:like (str (children-location collection) "%")]
          additional-conditions))
 
-(mu/defn archive-or-unarchive-collection!
-  "Archive or un-archive a collection, moving it to the trash if necessary. Note that namespaced collections are never
-  moved to the trash."
+(mu/defn archive-collection!
+  "Mark a collection as archived, along with all its children."
+  [collection :- CollectionWithLocationAndIDOrRoot]
+  (api/check-403
+   (perms/set-has-full-permissions-for-set?
+    @api/*current-user-permissions-set*
+    (perms-for-archiving collection)))
+  (t2/with-transaction [_conn]
+    (let [archive-operation-id    (str (random-uuid))
+          affected-collection-ids (cons (u/the-id collection)
+                                        (collection->descendant-ids collection
+                                                                    :archived [:not= true]))]
+      (t2/update! :model/Collection (u/the-id collection)
+                  {:archive_operation_id archive-operation-id
+                   :archived_directly    true
+                   :archived             true})
+      (t2/query-one
+       {:update :collection
+        :set    {:archive_operation_id archive-operation-id
+                 :archived_directly    false
+                 :archived             true}
+        :where  [:and
+                 [:like :location (str (children-location collection) "%")]
+                 [:not :archived]]})
+      (doseq [model (apply disj collectable-models archived-directly-models)]
+        (t2/update! model {:collection_id [:in affected-collection-ids]}
+                    {:archived true}))
+      (doseq [model archived-directly-models]
+        (t2/update! model {:collection_id    [:in affected-collection-ids]
+                           :archived_directly false}
+                    {:archived true})))))
+
+(mu/defn unarchive-collection!
+  "Mark a collection as unarchived, along with any children that were archived along with the collection."
   [collection :- CollectionWithLocationAndIDOrRoot
    ;; `updates` is a map *possibly* containing `parent_id`. This allows us to distinguish
    ;; between specifying a `nil` parent_id (move to the root) and not specifying a parent_id.
-   updates :- [:map [:parent_id {:optional true} [:maybe ms/PositiveInt]
-                     :archived :boolean]]]
-  (let [namespaced?   (some? (:namespace collection))
-        archived? (:archived updates)
-        new-parent-id (cond
-                        ;; don't move namespaced collections.
-                        namespaced? (location-path->parent-id
-                                     (:location collection))
-
-                        ;; move archived things to the trash, no matter what.
-                        archived? (trash-collection-id)
-
-                        (contains? updates :parent_id)
-                        (:parent_id updates)
-
-                        (and (= (:location collection) (trash-path))
-                             (:trashed_from_location collection))
-                        (location-path->parent-id
-                         (:trashed_from_location collection))
-
-                        :else (throw (ex-info (tru "You must specify a new `parent_id` to un-trash to.")
-                                              {:status-code 400})))
+   updates :- [:map [:parent_id {:optional true} [:maybe ms/PositiveInt]]]]
+  (assert (:archive_operation_id collection))
+  (when (not (contains? updates :parent_id))
+    (api/check-400
+     (:can_restore (t2/hydrate collection :can_restore))))
+  (let [archive-operation-id    (:archive_operation_id collection)
+        current-parent-id       (:parent_id (t2/hydrate collection :parent_id))
+        new-parent-id           (if (contains? updates :parent_id)
+                                  (:parent_id updates)
+                                  current-parent-id)
         new-parent              (if new-parent-id
                                   (t2/select-one :model/Collection :id new-parent-id)
                                   root-collection)
@@ -767,44 +859,57 @@
         orig-children-location  (children-location collection)
         new-children-location   (children-location (assoc collection :location new-location))
         affected-collection-ids (cons (u/the-id collection)
-                                      (collection->descendant-ids collection))]
-    (api/check-403
-     (perms/set-has-full-permissions-for-set?
-      @api/*current-user-permissions-set*
-      (perms-for-moving collection new-parent)))
-
+                                      (collection->descendant-ids collection
+                                                                  :archive_operation_id [:= archive-operation-id]
+                                                                  :archived [:= true]))]
     (api/check-400
-     ;; we can never move a collection to a trashed collection. (the Trash itself isn't archived)
      (and (some? new-parent) (not (:archived new-parent))))
 
     (t2/with-transaction [_conn]
       (t2/update! :model/Collection (u/the-id collection)
-                  {:location              new-location
-                   :trashed_from_location (when archived? (:location collection))
-                   :archived              archived?})
+                  {:location             new-location
+                   :archive_operation_id nil
+                   :archived_directly    nil
+                   :archived             false})
       (t2/query-one
        {:update :collection
-        :set    {:location              [:replace :location orig-children-location new-children-location]
-                 :trashed_from_location nil
-                 :archived              archived?}
-        :where  [:like :location (str orig-children-location "%")]})
-      (doseq [model [:model/Card
-                     :model/Dashboard
-                     :model/NativeQuerySnippet
-                     :model/Pulse
-                     :model/Timeline]]
+        :set    {:location             [:replace :location orig-children-location new-children-location]
+                 :archive_operation_id nil
+                 :archived_directly    nil
+                 :archived             false}
+        :where  [:and
+                 [:like :location (str orig-children-location "%")]
+                 [:= :archive_operation_id (:archive_operation_id collection)]
+                 [:not= :archived_directly true]]})
+      (doseq [model (apply disj collectable-models archived-directly-models)]
         (t2/update! model {:collection_id [:in affected-collection-ids]}
-                    {:archived archived?})))))
+                    {:archived false}))
+      (doseq [model archived-directly-models]
+        (t2/update! model {:collection_id     [:in affected-collection-ids]
+                           :archived_directly false}
+                    {:archived false})))))
+
+(mu/defn archive-or-unarchive-collection!
+  "Archive or un-archive a collection. When unarchiving, you may need to specify a new `parent_id`."
+  [collection :- CollectionWithLocationAndIDOrRoot
+   ;; `updates` is a map *possibly* containing `parent_id`. This allows us to distinguish
+   ;; between specifying a `nil` parent_id (move to the root) and not specifying a parent_id.
+   updates :- [:map [:parent_id {:optional true} [:maybe ms/PositiveInt]
+                     :archived :boolean]]]
+  (if (:archived updates)
+    (archive-collection! collection)
+    (unarchive-collection! collection updates)))
 
 (mu/defn move-collection!
   "Move a Collection and all its descendant Collections from its current `location` to a `new-location`."
   [collection :- CollectionWithLocationAndIDOrRoot, new-location :- LocationPath]
   (let [orig-children-location (children-location collection)
         new-children-location  (children-location (assoc collection :location new-location))
-        will-be-trashed? (str/starts-with? new-location (trash-path))]
-    (when will-be-trashed?
+        will-be-in-trash? (str/starts-with? new-location (trash-path))]
+    (when will-be-in-trash?
       (throw (ex-info "Cannot `move-collection!` into the Trash. Call `archive-collection!` instead."
-                      {:collection collection :new-location new-location})))
+                      {:collection collection
+                       :new-location new-location})))
     ;; first move this Collection
     (log/infof "Moving Collection %s and its descendants from %s to %s"
                (u/the-id collection) (:location collection) new-location)
@@ -839,13 +944,9 @@
 
     ;; Try to get the ID of its highest-level ancestor, e.g. if `location` is `/1/2/3/` we would get `1`. Then see if
     ;; the root-level ancestor is a Personal Collection (Personal Collections can only exist in the Root Collection.)
-    ;; Note that if the collection is archived, we check this against the `trashed_from_location`.
     (t2/exists? Collection
                 :id                (first (location-path->ids
-                                           (or
-                                            (when (:archived collection)
-                                              (:trashed_from_location collection))
-                                            (:location collection))))
+                                           (:location collection)))
                 :personal_owner_id [:not= nil]))))
 
 ;;; ----------------------------------------------------- INSERT -----------------------------------------------------
@@ -1041,8 +1142,8 @@
   ;; This should never happen, but just to make sure...
   (when (= (u/the-id collection) (trash-collection-id))
     (throw (ex-info "Fatal error: the trash collection cannot be trashed" {})))
-  ;; Delete all the Children of this Collection
-  (t2/delete! Collection :location (children-location collection))
+  ;; delete all collection children
+  (t2/delete! :model/Collection :location (children-location collection))
   (let [affected-collection-ids (cons (u/the-id collection) (collection->descendant-ids collection))]
     (doseq [model [:model/Card
                    :model/Dashboard
@@ -1115,26 +1216,21 @@
   ;; Also transform :personal_owner_id from a database ID to the email string, if it's defined.
   ;; Use the :slug as the human-readable label.
   [_model-name _opts coll]
-  (let [fetch-collection       (fn [id]
-                                 (t2/select-one Collection :id id))
-        {:keys [trashed_from_parent_id
-                parent_id]}    (some-> coll :id fetch-collection (t2/hydrate :parent_id :trashed_from_parent_id))
-        parent                 (some-> parent_id fetch-collection)
-        trashed-from-parent    (some-> trashed_from_parent_id fetch-collection)
-        parent-id              (when parent
-                                 (or (:entity_id parent) (serdes/identity-hash parent)))
-        trashed-from-parent-id (when trashed-from-parent
-                                 (or (:entity_id trashed-from-parent) (serdes/identity-hash trashed-from-parent)))
-        owner-email            (when (:personal_owner_id coll)
-                                 (t2/select-one-fn :email 'User :id (:personal_owner_id coll)))]
+  (let [fetch-collection    (fn [id]
+                              (t2/select-one Collection :id id))
+        {:keys [parent_id]} (some-> coll :id fetch-collection (t2/hydrate :parent_id))
+        parent              (some-> parent_id fetch-collection)
+        parent-id           (when parent
+                              (or (:entity_id parent) (serdes/identity-hash parent)))
+        owner-email         (when (:personal_owner_id coll)
+                              (t2/select-one-fn :email 'User :id (:personal_owner_id coll)))]
     (-> (serdes/extract-one-basics "Collection" coll)
         (dissoc :location)
         (assoc :parent_id parent-id
-               :personal_owner_id owner-email
-               :trashed_from_parent_id trashed-from-parent-id)
+               :personal_owner_id owner-email)
         (assoc-in [:serdes/meta 0 :label] (:slug coll)))))
 
-(defmethod serdes/load-xform "Collection" [{:keys [parent_id trashed_from_parent_id archived] :as contents}]
+(defmethod serdes/load-xform "Collection" [{:keys [parent_id] :as contents}]
   (let [loc (fn [col-id]
               (if col-id
                 (let [{:keys [id location]} (serdes/lookup-by-id Collection col-id)]
@@ -1142,47 +1238,32 @@
                 "/"))]
     (-> contents
         (dissoc :parent_id)
-        (dissoc :trashed_from_parent_id)
-        (assoc :location (loc parent_id)
-               :trashed_from_location (when archived (loc trashed_from_parent_id)))
+        (assoc :location (loc parent_id))
         (update :personal_owner_id serdes/*import-user*)
         serdes/load-xform-basics)))
 
 (defmethod serdes/dependencies "Collection"
-  [{:keys [parent_id trashed_from_parent_id]}]
-  (set/union (when parent_id
-                   #{[{:model "Collection" :id parent_id}]})
-             (when trashed_from_parent_id
-               #{[{:model "Collection" :id trashed_from_parent_id}]})))
+  [{:keys [parent_id]}]
+  (when parent_id
+    #{[{:model "Collection" :id parent_id}]}))
 
 (defmethod serdes/generate-path "Collection" [_ coll]
   (serdes/maybe-labeled "Collection" coll :slug))
 
 (defmethod serdes/ascendants "Collection" [_ id]
-  (let [{:keys [location trashed_from_location]} (t2/select-one :model/Collection :id id)]
+  (let [{:keys [location]} (t2/select-one :model/Collection :id id)]
     ;; it would work returning just one, but why not return all if it's cheap
-    (set (concat (map vector (repeat "Collection") (location-path->ids location))
-                 (when trashed_from_location
-                   (map vector (repeat "Collection") (location-path->ids trashed_from_location)))))))
+    (set (map vector (repeat "Collection") (location-path->ids location)))))
 
 (defmethod serdes/descendants "Collection" [_model-name id]
-  ;; the Trash collection has no descendants, for our purposes. This allows us to only include trashed items that were
-  ;; explicitly included in the export.
-  (when-not (is-trash? (t2/select-one :model/Collection :id id))
-    (let [location    (t2/select-one-fn :location Collection :id id)
-          child-colls (set (for [child-id (t2/select-pks-set :model/Collection {:where [:or
-                                                                                        [:like :location (str location id "/%")]
-                                                                                        [:like :trashed_from_location (str location id "/%")]]})]
-                             ["Collection" child-id]))
-          dashboards  (set (for [dash-id (t2/select-pks-set :model/Dashboard {:where [:or
-                                                                                      [:= :collection_id id]
-                                                                                      [:= :trashed_from_collection_id id]]})]
-                             ["Dashboard" dash-id]))
-          cards       (set (for [card-id (t2/select-pks-set :model/Card {:where [:or
-                                                                                 [:= :collection_id id]
-                                                                                 [:= :trashed_from_collection_id id]]})]
-                             ["Card" card-id]))]
-      (set/union child-colls dashboards cards))))
+  (let [location    (t2/select-one-fn :location Collection :id id)
+        child-colls (set (for [child-id (t2/select-pks-set :model/Collection {:where [:like :location (str location id "/%")]})]
+                           ["Collection" child-id]))
+        dashboards  (set (for [dash-id (t2/select-pks-set :model/Dashboard {:where [:= :collection_id id]})]
+                           ["Dashboard" dash-id]))
+        cards       (set (for [card-id (t2/select-pks-set :model/Card {:where [:= :collection_id id]})]
+                           ["Card" card-id]))]
+    (set/union child-colls dashboards cards)))
 
 (defmethod serdes/storage-path "Collection" [coll {:keys [collections]}]
   (let [parental (get collections (:entity_id coll))]
@@ -1491,70 +1572,70 @@
                         (original-position coll-id))))))
      (annotate-collections child-type->parent-ids collections))))
 
-(mi/define-batched-hydration-method can-restore
-  :can_restore
-  "Efficiently hydrate the `:can_restore` of a sequence of items with a `trashed_from_collection_id`."
-  [items]
-  (for [{trashed-from-coll :trashed_from_collection
-         :as item*} (t2/hydrate items :trashed_from_collection)
-        :let [item (dissoc item* :trashed_from_collection)]]
+(defmulti hydrate-can-restore
+  "Can these items be restored?"
+  (fn [model _] model))
+
+(defmethod hydrate-can-restore :model/Collection [_model colls]
+  (when (seq colls)
+    (let [coll-id->parent-id (into {} (map (fn [{:keys [id parent_id]}]
+                                             [id parent_id])
+                                           (t2/hydrate (filter :archived colls) :parent_id)))
+          parent-ids (keep val coll-id->parent-id)
+          parent-id->archived? (when (seq parent-ids)
+                                 (t2/select-pk->fn :archived :model/Collection :id [:in parent-ids]))]
+      (for [coll colls
+            :let [parent-id (coll-id->parent-id (:id coll))
+                  archived-directly? (:archived_directly coll)
+                  parent-archived? (get parent-id->archived? parent-id false)]]
+        (cond-> coll
+          (:archived coll) (assoc :can_restore (and archived-directly?
+                                                    (not parent-archived?)
+                                                    (perms/set-has-full-permissions-for-set?
+                                                     @api/*current-user-permissions-set*
+                                                     (perms-for-archiving coll)))))))))
+
+(defmethod hydrate-can-restore :default [_model items]
+  (for [{collection :collection
+         :as item*} (t2/hydrate items :collection)
+        :let [item (dissoc item* :collection)]]
     (cond-> item
       (:archived item)
       (assoc :can_restore (and
-                           ;; the item is directly in the trash (it was moved to the trash independently, not as
+                           ;; the item is directly in the trash (it was archived independently, not as
                            ;; part of a collection)
-                           (= (:collection_id item) (trash-collection-id))
+                           (:archived_directly item)
 
                            ;; EITHER:
                            (or
-                            ;; the item was trashed from the root collection
-                            (nil? (:trashed_from_collection_id item))
+                            ;; the item was archived from the root collection
+                            (nil? (:collection_id item))
                             ;; or the collection we'll restore to actually exists.
-                            (some? trashed-from-coll))
+                            (some? collection))
 
                            ;; the collection we'll restore to is not archived
-                           (not (:archived trashed-from-coll))
+                           (not (:archived collection))
 
                            ;; we have perms on the collection
-                           (mi/can-write? (or trashed-from-coll root-collection)))))))
+                           (mi/can-write? (or collection root-collection)))))))
 
-(mi/define-batched-hydration-method collection-can-restore
-  :collection/can_restore
-  "Collections have a `trashed_from_location` rather than a `trashed_from_collection_id`. Efficiently hydrate the
-  `:can_restore` property of a sequence of Collections."
-  [colls]
-  (when (seq colls)
-    (let [;; skip any collections that aren't archived, or are the root collection
-          ids-to-fetch      (->> colls
-                                 (remove collection.root/is-root-collection?)
-                                 (filter :archived)
-                                 (map u/the-id))
-          coll-id->restore-coll* (if (seq ids-to-fetch)
-                                   (into {}
-                                         (map (juxt :unarchiving_coll_id identity))
-                                         (t2/select :model/Collection
-                                                    {:select    [:trashed_from_coll.*
-                                                                 [:coll.id :unarchiving_coll_id]
-                                                                 [:coll.trashed_from_location :unarchiving_trashed_from_location]]
-                                                     :from      [[:collection :coll]]
-                                                     :left-join [[:collection :trashed_from_coll] [:=
-                                                                                                   [:concat :trashed_from_coll.location :trashed_from_coll.id "/"]
-                                                                                                   :coll.trashed_from_location]]
-                                                     :where     [:in :coll.id (into #{} ids-to-fetch)]}))
-                                   {})
-          ;; given a collection, return the collection we will be restoring TO.
-          coll->restore-coll  (fn [coll]
-                                (when (not (or (collection.root/is-root-collection? coll)
-                                               (not (:archived coll))))
-                                  (let [restore-destination (coll-id->restore-coll* (u/the-id coll))]
-                                    (cond
-                                      (:id restore-destination) restore-destination
-                                      (= "/" (:unarchiving_trashed_from_location restore-destination)) collection.root/root-collection
-                                      :else nil))))]
-      (for [coll colls]
-        (cond-> coll
-          (:archived coll) (assoc :can_restore (if-let [restore-destination (coll->restore-coll coll)]
-                                                 (perms/set-has-full-permissions-for-set?
-                                                  @api/*current-user-permissions-set*
-                                                  (perms-for-moving coll restore-destination))
-                                                 false)))))))
+(mi/define-batched-hydration-method can-restore
+  :can_restore
+  "Efficiently hydrate the `:can_restore` of a sequence of items with a `archived_directly` field."
+  [items]
+  (->> (map-indexed (fn [i item] (vary-meta item assoc ::i i)) items)
+       (group-by t2/model)
+       (mapcat (fn [[model items]]
+                 (hydrate-can-restore model items)))
+       (sort-by (comp ::i meta))))
+
+(mi/define-batched-hydration-method can-delete
+  :can_delete
+  "Efficiently hydrate the `:can_delete` of a sequence of items"
+  [items]
+  (when (seq items)
+    (for [item items]
+      (assoc item :can_delete (if (or (= :model/Collection (t2/model item))
+                                      (collection.root/is-root-collection? item))
+                                false
+                                (mi/can-write? item))))))
diff --git a/src/metabase/models/dashboard.clj b/src/metabase/models/dashboard.clj
index 23c56417d48c19042f7398f57bae198718c2d94f..60e349d9c0d2bb01b9bbe4e43bb6397e2d4506a3 100644
--- a/src/metabase/models/dashboard.clj
+++ b/src/metabase/models/dashboard.clj
@@ -48,7 +48,6 @@
 (methodical/defmethod t2/table-name :model/Dashboard [_model] :report_dashboard)
 
 (doto :model/Dashboard
-  (derive ::mi/has-trashed-from-collection-id)
   (derive :metabase/model)
   (derive ::perms/use-parent-collection-perms)
   (derive :hook/timestamped?)
@@ -686,11 +685,10 @@
        set))
 
 (defmethod serdes/dependencies "Dashboard"
-  [{:keys [collection_id dashcards parameters trashed_from_collection_id]}]
+  [{:keys [collection_id dashcards parameters]}]
   (->> (map serdes-deps-dashcard dashcards)
        (reduce set/union #{})
        (set/union (when collection_id #{[{:model "Collection" :id collection_id}]}))
-       (set/union (when trashed_from_collection_id #{[{:model "Collection" :id trashed_from_collection_id}]}))
        (set/union (serdes/parameters-deps parameters))))
 
 (defmethod serdes/descendants "Dashboard" [_model-name id]
diff --git a/src/metabase/models/interface.clj b/src/metabase/models/interface.clj
index 2d76c27d8df70fc5e212e245169d1b11822c06c2..c624a01050ece020a931a3843da6aef117bcf779 100644
--- a/src/metabase/models/interface.clj
+++ b/src/metabase/models/interface.clj
@@ -734,34 +734,3 @@
 (defmethod exclude-internal-content-hsql :default
   [_model & _]
   [:= [:inline 1] [:inline 1]])
-
-(defmulti parent-collection-id-for-perms
-  "What is the ID of the parent collection that should determine the perms objects set for this object?"
-  {:arglists '([instance])}
-  dispatch-on-model)
-
-(defmethod parent-collection-id-for-perms ::has-trashed-from-collection-id
-  [instance]
-  (cond
-    (not (:archived instance)) (:collection_id instance)
-    (contains? instance :trashed_from_collection_id) (:trashed_from_collection_id instance)
-    ;; If we're supposed to get permissions from the `trashed_from_collection_id` but it isn't present, we can't check
-    ;; permissions correctly.
-    :else (throw (ex-info "Missing trashed_from_collection_id" {:instance instance}))))
-
-(defmethod parent-collection-id-for-perms :default
-  [instance]
-  (:collection_id instance))
-
-(defmulti parent-collection-id-column-for-perms
-  "What column should we use to determine the perms for this model?"
-  {:arglists '([model])}
-  identity)
-
-(defmethod parent-collection-id-column-for-perms :default
-  [_model]
-  :collection_id)
-
-(defmethod parent-collection-id-column-for-perms ::has-trashed-from-collection-id
-  [_model]
-  [:coalesce :trashed_from_collection_id :collection_id])
diff --git a/src/metabase/models/permissions.clj b/src/metabase/models/permissions.clj
index c327b74460c8e53258284b8bec1ca0cc78eedf03..20eb4b62e06e9658e0dd63ef8a3cf8fc98de8b8e 100644
--- a/src/metabase/models/permissions.clj
+++ b/src/metabase/models/permissions.clj
@@ -308,10 +308,7 @@
 (mu/defn perms-objects-set-for-parent-collection :- [:set perms.u/PathSchema]
   "Implementation of `perms-objects-set` for models with a `collection_id`, such as Card, Dashboard, or Pulse.
   This simply returns the `perms-objects-set` of the parent Collection (based on `collection_id`) or for the Root
-  Collection if `collection_id` is `nil`.
-
-  If the model contains an `archived` key and a `trashed_from_collection_id` key, we will check permissions of that
-  collection instead."
+  Collection if `collection_id` is `nil`."
   ([this read-or-write]
    (perms-objects-set-for-parent-collection nil this read-or-write))
 
@@ -323,7 +320,7 @@
    (let [path-fn (case read-or-write
                    :read  collection-read-path
                    :write collection-readwrite-path)
-         collection-id (mi/parent-collection-id-for-perms this)]
+         collection-id (:collection_id this)]
      ;; now pass that function our collection_id if we have one, or if not, pass it an object representing the Root
      ;; Collection
      #{(path-fn (or collection-id
diff --git a/src/metabase/models/recent_views.clj b/src/metabase/models/recent_views.clj
index aceb8234e3ba29a25aa73f74267fa2f46b188050..d11b3ebf9696ec3d5d180f6f9c55d5d17e78640f 100644
--- a/src/metabase/models/recent_views.clj
+++ b/src/metabase/models/recent_views.clj
@@ -208,7 +208,6 @@
                          :card.archived
                          :card.id
                          :card.display
-                         :card.trashed_from_collection_id
                          [:card.collection_id :entity-coll-id]
                          [:mr.status :moderated-status]
                          [:collection.id :collection_id]
@@ -304,7 +303,6 @@
                          :dash.name
                          :dash.description
                          :dash.archived
-                         :dash.trashed_from_collection_id
                          [:dash.collection_id :entity-coll-id]
                          [:c.id :collection_id]
                          [:c.name :collection_name]
@@ -338,7 +336,7 @@
     (let [ ;; these have their parent collection id in effective_location, but we need the id, name, and authority_level.
           collections (t2/select :model/Collection
                                  {:select [:id :name :description :authority_level
-                                           :archived :location :trashed_from_location]
+                                           :archived :location]
                                   :where [:and
                                           [:in :id collection-ids]
                                           [:= :archived false]]})]
diff --git a/src/metabase/models/revision/diff.clj b/src/metabase/models/revision/diff.clj
index 2d10fbe49c11f06f415000404a49bb963701e266..611f432b00aa430cfa6ddeb40df0f2716f9cd3d9 100644
--- a/src/metabase/models/revision/diff.clj
+++ b/src/metabase/models/revision/diff.clj
@@ -2,7 +2,6 @@
   (:require
    [clojure.core.match :refer [match]]
    [clojure.data :as data]
-   [metabase.models.collection :as collection]
    [metabase.util.i18n :refer [deferred-tru]]
    [toucan2.core :as t2]))
 
@@ -50,21 +49,17 @@
     (deferred-tru "changed pin position")
 
     [:collection_id nil coll-id]
-    ;; trash/untrash is handled by `archived`
-    (when-not (= coll-id (collection/trash-collection-id))
-      (deferred-tru "moved {0} to {1}" identifier (if coll-id
-                                                    (t2/select-one-fn :name 'Collection coll-id)
-                                                    (deferred-tru "Our analytics"))))
+    (deferred-tru "moved {0} to {1}" identifier (if coll-id
+                                                  (t2/select-one-fn :name 'Collection coll-id)
+                                                  (deferred-tru "Our analytics")))
 
     [:collection_id (prev-coll-id :guard int?) coll-id]
-    ;; trash/untrash is handled by `archived`
-    (when-not (or (= prev-coll-id (collection/trash-collection-id)) (= coll-id (collection/trash-collection-id)))
-      (deferred-tru "moved {0} from {1} to {2}"
-        identifier
-        (t2/select-one-fn :name 'Collection prev-coll-id)
-        (if coll-id
-          (t2/select-one-fn :name 'Collection coll-id)
-          (deferred-tru "Our analytics"))))
+    (deferred-tru "moved {0} from {1} to {2}"
+      identifier
+      (t2/select-one-fn :name 'Collection prev-coll-id)
+      (if coll-id
+        (t2/select-one-fn :name 'Collection coll-id)
+        (deferred-tru "Our analytics")))
 
 
     [:visualization_settings _ _]
diff --git a/src/metabase/models/serialization.clj b/src/metabase/models/serialization.clj
index 27d624ef2d88e8835b6326a68e05a69c1a161ee4..7ffc35c8e8822f7f310f4205072387eef27a5863 100644
--- a/src/metabase/models/serialization.clj
+++ b/src/metabase/models/serialization.clj
@@ -353,14 +353,7 @@
     (t2/reducible-select model {:where [:or
                                         [:in :collection_id collection-set]
                                         (when (contains? collection-set nil)
-                                          [:= :collection_id nil])
-                                        (when (contains? #{:model/Card :model/Dashboard} model)
-                                          [:and
-                                           :archived
-                                           [:or
-                                            [:in :trashed_from_collection_id collection-set]
-                                            (when (contains? collection-set nil)
-                                              [:= :trashed_from_collection_id nil])]])]})
+                                          [:= :collection_id nil])]})
     ;; If collection-set is nil, just select everything.
     (t2/reducible-select model)))
 
diff --git a/src/metabase/search/config.clj b/src/metabase/search/config.clj
index 8e9ea2505ed5423373a2447051d0f7c38f10468a..a7a7b59cf4585e7c816d5a4aa3073dfd7ed27f76 100644
--- a/src/metabase/search/config.clj
+++ b/src/metabase/search/config.clj
@@ -147,7 +147,7 @@
    :collection_type     :text
    :collection_location :text
    :collection_authority_level :text
-   :trashed_from_collection_id :integer
+   :archived_directly   :boolean
    ;; returned for Card and Dashboard
    :collection_position :integer
    :creator_id          :integer
@@ -281,7 +281,7 @@
 
 (defmethod columns-for-model "card"
   [_]
-  (conj default-columns :collection_id :trashed_from_collection_id :collection_position :dataset_query :display :creator_id
+  (conj default-columns :collection_id :archived_directly :collection_position :dataset_query :display :creator_id
         [:collection.name :collection_name]
         [:collection.type :collection_type]
         [:collection.location :collection_location]
@@ -302,7 +302,7 @@
 
 (defmethod columns-for-model "dashboard"
   [_]
-  (conj default-columns :trashed_from_collection_id :collection_id :collection_position :creator_id bookmark-col
+  (conj default-columns :archived_directly :collection_id :collection_position :creator_id bookmark-col
         [:collection.name :collection_name]
         [:collection.type :collection_type]
         [:collection.authority_level :collection_authority_level]))
@@ -318,6 +318,7 @@
         [:name :collection_name]
         [:type :collection_type]
         [:authority_level :collection_authority_level]
+        :archived_directly
         :location
         bookmark-col))
 
diff --git a/src/metabase/search/impl.clj b/src/metabase/search/impl.clj
index 86eaf5f5eef4ac066b978fb73526b3f170cccf75..04b8b503fee9672d42d9a1f53e7a2d61d8c66a34 100644
--- a/src/metabase/search/impl.clj
+++ b/src/metabase/search/impl.clj
@@ -107,30 +107,24 @@
 
 (mu/defn add-collection-join-and-where-clauses
   "Add a `WHERE` clause to the query to only return Collections the Current User has access to; join against Collection
-  so we can return its `:name`.
-
-  A brief note here on `collection-join-id` and `collection-permission-id`. What the heck do these represent?
-
-  Permissions on Trashed items work differently than normal permissions. If something is in the trash, you can only
-  see it if you have the relevant permissions on the *original* collection the item was trashed from. This is set as
-  `trashed_from_collection_id`.
-
-  However, the item is actually *in* the Trash, and we want to show that to the frontend. Therefore, we need two
-  different collection IDs. One, the ID we should be checking permissions on, and two, the ID we should be joining to
-  Collections on."
+  so we can return its `:name`."
   [honeysql-query                                :- ms/Map
    model                                         :- :string
    {:keys [current-user-perms
-           filter-items-in-personal-collection]} :- SearchContext]
-  (let [visible-collections      (collection/permissions-set->visible-collection-ids current-user-perms)
-        collection-join-id       (if (= model "collection")
+           filter-items-in-personal-collection
+           archived]} :- SearchContext]
+  (let [visible-collections      (collection/permissions-set->visible-collection-ids
+                                  current-user-perms
+                                  {:include-archived-items :all
+                                   :include-trash-collection? true
+                                   :permission-level (if archived
+                                                       :write
+                                                       :read)})
+        collection-id-col        (if (= model "collection")
                                    :collection.id
                                    :collection_id)
-        collection-permission-id (if (= model "collection")
-                                   :collection.id
-                                   (mi/parent-collection-id-column-for-perms (:db-model (search.config/model-to-db-model model))))
         collection-filter-clause (collection/visible-collection-ids->honeysql-filter-clause
-                                  collection-permission-id
+                                  collection-id-col
                                   visible-collections)]
     (cond-> honeysql-query
       true
@@ -138,7 +132,7 @@
       ;; add a JOIN against Collection *unless* the source table is already Collection
       (not= model "collection")
       (sql.helpers/left-join [:collection :collection]
-                             [:= collection-join-id :collection.id])
+                             [:= collection-id-col :collection.id])
 
       (some? filter-items-in-personal-collection)
       (sql.helpers/where
@@ -159,7 +153,7 @@
                 [:and [:= :collection.personal_owner_id nil]]
                 (for [id (t2/select-pks-set :model/Collection :personal_owner_id [:not= nil])]
                   [:not-like :collection.location (format "/%d/%%" id)]))
-               [:= collection-join-id nil]))))))
+               [:= collection-id-col nil]))))))
 
 (mu/defn ^:private add-table-db-id-clause
   "Add a WHERE clause to only return tables with the given DB id.
@@ -302,16 +296,14 @@
             (or (contains? current-user-perms "/collection/root/")
                 (contains? current-user-perms "/collection/root/read/"))
 
-            collection-id [:coalesce :model.trashed_from_collection_id :collection_id]
-
             collection-perm-clause
             [:or
-             (when has-root-access? [:= collection-id nil])
+             (when has-root-access? [:= :collection_id nil])
              [:and
-              [:not= collection-id nil]
+              [:not= :collection_id nil]
               [:or
-               (has-perm-clause "/collection/" collection-id "/")
-               (has-perm-clause "/collection/" collection-id "/read/")]]]]
+               (has-perm-clause "/collection/" :collection_id "/")
+               (has-perm-clause "/collection/" :collection_id "/read/")]]]]
         (sql.helpers/where
          query
          collection-perm-clause)))))
@@ -541,7 +533,8 @@
 (defn serialize
   "Massage the raw result from the DB and match data into something more useful for the client"
   [{:as result :keys [all-scores relevant-scores name display_name collection_id collection_name
-                      collection_authority_level collection_type collection_effective_ancestors effective_parent]}]
+                      collection_authority_level collection_type collection_effective_ancestors effective_parent
+                      archived_directly model]}]
   (let [matching-columns    (into #{} (remove nil? (map :column relevant-scores)))
         match-context-thunk (first (keep :match-context-thunk relevant-scores))]
     (-> result
@@ -553,14 +546,17 @@
                                     (empty?
                                      (remove matching-columns search.config/displayed-columns)))
                            (match-context-thunk))
-         :collection     (merge {:id              collection_id
-                                 :name            collection_name
-                                 :authority_level collection_authority_level
-                                 :type            collection_type}
-                                (when effective_parent
-                                  effective_parent)
-                                (when collection_effective_ancestors
-                                  {:effective_ancestors collection_effective_ancestors}))
+         :collection     (if (and archived_directly (not= "collection" model))
+                           (select-keys (collection/trash-collection)
+                                        [:id :name :authority_level :type])
+                           (merge {:id              collection_id
+                                   :name            collection_name
+                                   :authority_level collection_authority_level
+                                   :type            collection_type}
+                                  ;; for  non-root collections, override :collection with the values for its effective parent
+                                  effective_parent
+                                  (when collection_effective_ancestors
+                                    {:effective_ancestors collection_effective_ancestors})))
          :scores          all-scores)
         (update :dataset_query (fn [dataset-query]
                                  (when-let [query (some-> dataset-query json/parse-string)]
@@ -571,11 +567,11 @@
          :all-scores
          :relevant-scores
          :collection_effective_ancestors
-         :trashed_from_collection_id
          :collection_id
          :collection_location
          :collection_name
          :collection_type
+         :archived_directly
          :display_name
          :effective_parent))))
 
@@ -608,14 +604,19 @@
         xf                 (comp
                             (map t2.realize/realize)
                             (map to-toucan-instance)
+                            (map #(if (and (t2/instance-of? :model/Collection %)
+                                           (:archived_directly %))
+                                    (assoc % :location (collection/trash-path))
+                                    %))
                             (map #(cond-> %
                                     (t2/instance-of? :model/Collection %) (assoc :type (:collection_type %))))
                             (map #(cond-> % (t2/instance-of? :model/Collection %) collection/maybe-localize-trash-name))
-                            ;; MySQL returns `:bookmark` and `:archived` as `1` or `0` so convert those to boolean as
+                            ;; MySQL returns booleans as `1` or `0` so convert those to boolean as
                             ;; needed
                             (map #(update % :bookmark bit->boolean))
-
                             (map #(update % :archived bit->boolean))
+                            (map #(update % :archived_directly bit->boolean))
+
                             (filter (partial check-permissions-for-model search-ctx))
 
                             (map #(update % :pk_ref json/parse-string))
diff --git a/src/metabase/server/handler.clj b/src/metabase/server/handler.clj
index f6ab6c3597ca5d70733adee4ba0e9ba49d1b81ef..40f962564f4ccdc701884bddaedce946446495e6 100644
--- a/src/metabase/server/handler.clj
+++ b/src/metabase/server/handler.clj
@@ -9,6 +9,7 @@
    [metabase.server.middleware.log :as mw.log]
    [metabase.server.middleware.misc :as mw.misc]
    [metabase.server.middleware.offset-paging :as mw.offset-paging]
+   [metabase.server.middleware.request-id :as mw.request-id]
    [metabase.server.middleware.security :as mw.security]
    [metabase.server.middleware.session :as mw.session]
    [metabase.server.middleware.ssl :as mw.ssl]
@@ -62,6 +63,7 @@
    #'mw.misc/add-content-type                   ; Adds a Content-Type header for any response that doesn't already have one
    #'mw.misc/disable-streaming-buffering        ; Add header to streaming (async) responses so ngnix doesn't buffer keepalive bytes
    #'wrap-gzip                                  ; GZIP response if client can handle it
+   #'mw.request-id/wrap-request-id              ; Add a unique request ID to the request
    #'mw.misc/bind-request                       ; bind `metabase.middleware.misc/*request*` for the duration of the request
    #'mw.ssl/redirect-to-https-middleware])
 ;; ▲▲▲ PRE-PROCESSING ▲▲▲ happens from BOTTOM-TO-TOP
diff --git a/src/metabase/server/middleware/request_id.clj b/src/metabase/server/middleware/request_id.clj
new file mode 100644
index 0000000000000000000000000000000000000000..f5cbf9fdca4b964849293bfb4c36d6a2b9f32e51
--- /dev/null
+++ b/src/metabase/server/middleware/request_id.clj
@@ -0,0 +1,9 @@
+(ns metabase.server.middleware.request-id
+  (:require [metabase.config :refer [*request-id*]]))
+
+(defn wrap-request-id
+  "Attach a unique request ID to the request"
+  [handler]
+  (fn [request response raise]
+    (binding [*request-id* (random-uuid)]
+      (handler (assoc request :request-id *request-id*) response raise))))
diff --git a/test/metabase/api/bookmark_test.clj b/test/metabase/api/bookmark_test.clj
index 6a79714188ca15c96f3185e96b9752801b4465e5..0bbf53a410278f4f702b42be131d13af6c9f71fe 100644
--- a/test/metabase/api/bookmark_test.clj
+++ b/test/metabase/api/bookmark_test.clj
@@ -78,9 +78,7 @@
 (deftest bookmarks-on-archived-items-test
   (testing "POST /api/bookmark/:model/:model-id"
     (mt/with-temp [Collection archived-collection {:name "Test Collection"
-                                                   :archived true
-                                                   :location (collection/trash-path)
-                                                   :trashed_from_location "/"}
+                                                   :archived true}
                    Card       archived-card {:name "Test Card" :archived true}
                    Dashboard  archived-dashboard {:name "Test Dashboard" :archived true}]
       (bookmark-models (mt/user->id :rasta) archived-collection archived-card archived-dashboard)
diff --git a/test/metabase/api/card_test.clj b/test/metabase/api/card_test.clj
index cd1158c6c83efa665a0f496b3765c82973481f4d..5c7c1676f54056ab6ae5979b7a8c7a16c42ab98c 100644
--- a/test/metabase/api/card_test.clj
+++ b/test/metabase/api/card_test.clj
@@ -62,31 +62,31 @@
 
 (def card-defaults
   "The default card params."
-  {:archived                   false
-   :collection_id              nil
-   :collection_position        nil
-   :collection_preview         true
-   :dataset_query              {}
-   :type                       "question"
-   :description                nil
-   :display                    "scalar"
-   :enable_embedding           false
-   :initially_published_at     nil
-   :entity_id                  nil
-   :embedding_params           nil
-   :made_public_by_id          nil
-   :parameters                 []
-   :parameter_mappings         []
-   :moderation_reviews         ()
-   :public_uuid                nil
-   :query_type                 nil
-   :cache_ttl                  nil
-   :average_query_time         nil
-   :last_query_start           nil
-   :result_metadata            nil
-   :cache_invalidated_at       nil
-   :view_count                 0
-   :trashed_from_collection_id nil})
+  {:archived               false
+   :collection_id          nil
+   :collection_position    nil
+   :collection_preview     true
+   :dataset_query          {}
+   :type                   "question"
+   :description            nil
+   :display                "scalar"
+   :enable_embedding       false
+   :initially_published_at nil
+   :entity_id              nil
+   :embedding_params       nil
+   :made_public_by_id      nil
+   :parameters             []
+   :parameter_mappings     []
+   :moderation_reviews     ()
+   :public_uuid            nil
+   :query_type             nil
+   :cache_ttl              nil
+   :average_query_time     nil
+   :last_query_start       nil
+   :result_metadata        nil
+   :cache_invalidated_at   nil
+   :view_count             0
+   :archived_directly      false})
 
 ;; Used in dashboard tests
 (def card-defaults-no-hydrate
diff --git a/test/metabase/api/collection_test.clj b/test/metabase/api/collection_test.clj
index 1cdf9020f5929fe7e233309b7b01a7ce42970599..02cd1406c248a33972365bbc356ef914033adbfb 100644
--- a/test/metabase/api/collection_test.clj
+++ b/test/metabase/api/collection_test.clj
@@ -75,8 +75,9 @@
                  :name                "Our analytics"
                  :authority_level     nil
                  :is_personal         false
-                 :id                  "root"}
-                (assoc (into {:is_personal false} collection) :can_write true)]
+                 :id                  "root"
+                 :can_delete          false}
+                (assoc (into {:is_personal false} collection) :can_write true :can_delete false)]
                (filter #(#{(:id collection) "root"} (:id %))
                        (mt/user-http-request :crowberto :get 200 "collection"))))))))
 
@@ -638,6 +639,7 @@
         (is (= (mt/obj->json->obj
                 [{:collection_id       (:id collection)
                   :can_write           true
+                  :can_delete          true
                   :id                  card-id
                   :archived            false
                   :location            nil
@@ -818,7 +820,7 @@
         (is (:archived (mt/user-http-request :crowberto :get 200 (str "collection/" (u/the-id collection-b)))))
         (is (:archived (mt/user-http-request :crowberto :get 200 (str "collection/" (u/the-id collection-a)))))
         ;; we can't unarchive collection B without specifying a location, because it wasn't trashed directly.
-        (is (= "You must specify a new `parent_id` to un-trash to." (mt/user-http-request :crowberto :put 400 (str "collection/" (u/the-id collection-b)) {:archived false})))
+        (is (mt/user-http-request :crowberto :put 400 (str "collection/" (u/the-id collection-b)) {:archived false}))
 
         (mt/user-http-request :crowberto :put 200 (str "collection/" (u/the-id collection-b)) {:archived false :parent_id (u/the-id destination)})
         ;; collection A is still here!
@@ -883,20 +885,7 @@
                  "sub-C"
                  "C"
                  "dashboard-C"}
-               (set-of-item-names (collection/trash-collection-id)))))
-      (testing "after hard deletion of a collection, only admins can see things trashed from them"
-        (doseq [coll [collection-a collection-b collection-c]]
-          (mt/user-http-request :crowberto :delete 200 (str "collection/" (u/the-id coll))))
-        ;; Rasta can no longer see:
-        ;; - Collection C because it's deleted
-        ;; - dashboard C because permissions records are deleted along with the collection
-        (is (= #{"sub-A" "sub-B" "sub-C"}
-               (set-of-item-names (collection/trash-collection-id))))
-        ;; Crowberto can see all of the still-existent things.
-        (is (= #{"sub-A" "sub-B" "sub-C" "dashboard-A" "dashboard-B" "dashboard-C"}
-               (->> (get-items :crowberto (collection/trash-collection-id))
-                    (map :name)
-                    set)))))))
+               (set-of-item-names (collection/trash-collection-id))))))))
 
 (deftest collection-items-revision-history-and-ordering-test
   (testing "GET /api/collection/:id/items"
@@ -1215,6 +1204,7 @@
   (merge
    (mt/object-defaults Collection)
    {:slug                "lucky_pigeon_s_personal_collection"
+    :can_delete          false
     :can_write           true
     :name                "Lucky Pigeon's Personal Collection"
     :personal_owner_id   (mt/user->id :lucky)
@@ -1437,7 +1427,8 @@
               :effective_ancestors []
               :authority_level     nil
               :parent_id           nil
-              :is_personal         false}
+              :is_personal         false
+              :can_delete          false}
              (with-some-children-of-collection nil
                (mt/user-http-request :crowberto :get 200 "collection/root")))))))
 
@@ -2213,3 +2204,17 @@
       (t2.with-temp/with-temp [:model/Collection collection {:name "A"}]
         (mt/user-http-request :crowberto :put 200 (str "collection/" (u/the-id collection)) {:archived true})
         (is (true? (:can_restore (get-item-with-id-in-coll (collection/trash-collection-id) (u/the-id collection)))))))))
+
+(deftest nothing-can-be-moved-to-the-trash
+  (t2.with-temp/with-temp [:model/Dashboard dashboard {}
+                           :model/Collection collection {}
+                           :model/Card card {}]
+    (testing "Collections can't be moved to the trash"
+      (mt/user-http-request :crowberto :put 400 (str "collection/" (u/the-id collection)) {:parent_id (collection/trash-collection-id)})
+      (is (not (t2/exists? :model/Collection :location (collection/trash-path)))))
+    (testing "Dashboards can't be moved to the trash"
+      (mt/user-http-request :crowberto :put 400 (str "dashboard/" (u/the-id dashboard)) {:collection_id (collection/trash-collection-id)})
+      (is (not (t2/exists? :model/Dashboard :collection_id (collection/trash-collection-id)))))
+    (testing "Cards can't be moved to the trash"
+      (mt/user-http-request :crowberto :put 400 (str "card/" (u/the-id card)) {:collection_id (collection/trash-collection-id)})
+      (is (not (t2/exists? :model/Card :collection_id (collection/trash-collection-id)))))))
diff --git a/test/metabase/api/dashboard_test.clj b/test/metabase/api/dashboard_test.clj
index c412be1435b61898352f969a53ba7a3d63bf3ce0..6cf149a87e7225f03af46341aac3ae47c6b0bbff 100644
--- a/test/metabase/api/dashboard_test.clj
+++ b/test/metabase/api/dashboard_test.clj
@@ -200,29 +200,29 @@
                                                                    :parameters "abc"})))))
 
 (def ^:private dashboard-defaults
-  {:archived                   false
-   :caveats                    nil
-   :collection_id              nil
-   :collection_position        nil
-   :collection                 true
-   :created_at                 true ; assuming you call dashboard-response on the results
-   :description                nil
-   :embedding_params           nil
-   :enable_embedding           false
-   :initially_published_at     nil
-   :entity_id                  true
-   :made_public_by_id          nil
-   :parameters                 []
-   :points_of_interest         nil
-   :cache_ttl                  nil
-   :position                   nil
-   :width                      "fixed"
-   :public_uuid                nil
-   :auto_apply_filters         true
-   :show_in_getting_started    false
-   :updated_at                 true
-   :view_count                 0
-   :trashed_from_collection_id nil})
+  {:archived                false
+   :caveats                 nil
+   :collection_id           nil
+   :collection_position     nil
+   :collection              true
+   :created_at              true ; assuming you call dashboard-response on the results
+   :description             nil
+   :embedding_params        nil
+   :enable_embedding        false
+   :initially_published_at  nil
+   :entity_id               true
+   :made_public_by_id       nil
+   :parameters              []
+   :points_of_interest      nil
+   :cache_ttl               nil
+   :position                nil
+   :width                   "fixed"
+   :public_uuid             nil
+   :auto_apply_filters      true
+   :show_in_getting_started false
+   :updated_at              true
+   :view_count              0
+   :archived_directly        false})
 
 (deftest create-dashboard-test
   (testing "POST /api/dashboard"
@@ -288,9 +288,10 @@
      :model/Dashboard {crowberto-dash-id :id
                        :as               crowberto-dash}    {:creator_id    (mt/user->id :crowberto)
                                                              :collection_id (:id (collection/user->personal-collection (mt/user->id :crowberto)))}
-     :model/Dashboard {archived-dash-id :id} {:archived                   true
-                                              :trashed_from_collection_id (:id (collection/user->personal-collection (mt/user->id :crowberto)))
-                                              :creator_id                 (mt/user->id :crowberto)}]
+     :model/Dashboard {archived-dash-id :id} {:archived          true
+                                              :archived_directly true
+                                              :collection_id     (:id (collection/user->personal-collection (mt/user->id :crowberto)))
+                                              :creator_id        (mt/user->id :crowberto)}]
     (testing "should include creator info and last edited info"
       (revision/push-revision!
        {:entity       :model/Dashboard
@@ -4484,16 +4485,6 @@
         (mt/user-http-request :crowberto :put 200 (str "dashboard/" dash-id) {:archived true})
         (mt/user-http-request :crowberto :put 200 (str "collection/" coll-id) {:archived true})
         (is (false? (can-restore? dash-id :rasta)))))
-    (testing "I can't restore a trashed dashboard if the collection it was from was deleted"
-      (t2.with-temp/with-temp [:model/Collection {coll-id :id} {:name "A"}
-                               :model/Dashboard {dash-id :id} {:name          "My Dashboard"
-                                                               :collection_id coll-id}]
-        (mt/user-http-request :crowberto :put 200 (str "dashboard/" dash-id) {:archived true})
-        (t2/delete! :model/Collection :id coll-id)
-        ;; rasta can no longer view the dashboard at all, because we can't check perms on it
-        (is (= "You don't have permissions to do that." (mt/user-http-request :rasta :get 403 (str "dashboard/" dash-id))))
-        ;; even the mighty crowberto can't restore it!
-        (is (false? (can-restore? dash-id :crowberto)))))
     (testing "I can't restore a trashed dashboard if it isn't archived in the first place"
       (t2.with-temp/with-temp [:model/Collection {coll-id :id} {:name "A"}
                                :model/Dashboard {dash-id :id} {:name          "My Dashboard"
diff --git a/test/metabase/api/search_test.clj b/test/metabase/api/search_test.clj
index 9a5103535316144744f53eed2469900cb8cc84e7..028632ba28db7aacb6eb1fb641ec5f6960f9073e 100644
--- a/test/metabase/api/search_test.clj
+++ b/test/metabase/api/search_test.clj
@@ -743,14 +743,14 @@
 (defn- archived-collection [m]
   (assoc m
          :archived true
-         :trashed_from_location "/"
-         :location (collection/trash-path)))
+         :archived_directly true
+         :archive_operation_id (str (random-uuid))))
 
 (defn- archived-with-trashed-from-id [m]
   (assoc m
          :archived true
-         :trashed_from_collection_id (:collection_id m)
-         :collection_id (collection/trash-collection-id)))
+         :archived_directly true
+         :collection_id (:collection_id m)))
 
 (deftest archived-results-test
   (testing "Should return unarchived results by default"
diff --git a/test/metabase/db/schema_migrations_test.clj b/test/metabase/db/schema_migrations_test.clj
index fa0d25f3ab61ceb0056b961a36afff064b52ff2d..ddbc1a5b4895cea2b11f932da46582a4e04d38e8 100644
--- a/test/metabase/db/schema_migrations_test.clj
+++ b/test/metabase/db/schema_migrations_test.clj
@@ -11,9 +11,9 @@
   (:require
    [cheshire.core :as json]
    [clojure.java.jdbc :as jdbc]
-   [clojure.set :as set]
    [clojure.test :refer :all]
    [java-time.api :as t]
+   [medley.core :as m]
    [metabase.config :as config]
    [metabase.db :as mdb]
    [metabase.db.custom-migrations-test :as custom-migrations-test]
@@ -2025,67 +2025,96 @@
         (is (= 1 (t2/select-one-fn :view_count :report_card card-id)))
         (is (= 2 (t2/select-one-fn :view_count :report_dashboard dash-id)))))))
 
-(deftest move-to-trash-test
-  (testing "existing archived items should be moved to the Trash"
-    (impl/test-migrations ["v50.2024-05-14T12:42:44" "v50.2024-05-14T12:42:52"] [migrate!]
-      (let [user       (create-raw-user! (mt/random-email))
-            db         (t2/insert-returning-pk! :metabase_database (-> (mt/with-temp-defaults Database)
-                                                                       (update :details json/generate-string)
-                                                                       (update :engine str)))
-            dash       (t2/insert-returning-pk! (t2/table-name :model/Dashboard)
-                                                {:name       "A dashboard"
-                                                 :archived   true
-                                                 :creator_id (:id user)
-                                                 :created_at :%now
-                                                 :updated_at :%now
-                                                 :parameters ""})
-            card       (t2/insert-returning-pk! (t2/table-name Card)
-                                                {:name                   "Card"
-                                                 :archived               true
-                                                 :display                "table"
-                                                 :dataset_query          "{}"
-                                                 :visualization_settings "{}"
-                                                 :cache_ttl              30
-                                                 :creator_id             (:id user)
-                                                 :database_id            db
-                                                 :created_at             :%now
-                                                 :updated_at             :%now})
-            collection (t2/insert-returning-pk! (t2/table-name Collection)
-                                                {:name     "Silly Collection"
-                                                 :slug     "silly-collection"
-                                                 :archived true})]
-        (is (empty? (t2/select-fn-set :id :model/Dashboard :collection_id (collection/trash-collection-id))))
-        (is (empty? (t2/select-fn-set :id :model/Card :collection_id (collection/trash-collection-id))))
-        (is (empty? (t2/select-fn-set :id :model/Collection :location (collection/children-location (collection/trash-collection)))))
-        (migrate!)
-        (is (set/subset? #{dash} (t2/select-fn-set :id :model/Dashboard :collection_id (collection/trash-collection-id))))
-        (is (set/subset? #{card} (t2/select-fn-set :id :model/Card :collection_id (collection/trash-collection-id))))
-        (is (set/subset? #{collection} (t2/select-fn-set :id :model/Collection :location (collection/children-location (collection/trash-collection)))))))))
-
 (deftest trash-migrations-test
-  (impl/test-migrations ["v50.2024-05-14T12:13:22" "v50.2024-05-14T12:42:52"] [migrate!]
+  (impl/test-migrations ["v50.2024-05-29T14:04:47" "v50.2024-05-29T18:42:15"] [migrate!]
     (with-redefs [collection/is-trash? (constantly false)]
       (let [collection-id    (t2/insert-returning-pk! (t2/table-name :model/Collection)
-                                                      {:name "Silly Collection"
-                                                       :slug "silly-collection"})
+                                                      {:name     "Silly Collection"
+                                                       :archived true
+                                                       :slug     "silly-collection"})
             subcollection-id (t2/insert-returning-pk! (t2/table-name :model/Collection)
                                                       {:name     "Subcollection"
                                                        :slug     "subcollection"
+                                                       :archived true
                                                        :location (collection/children-location (t2/select-one :model/Collection :id collection-id))})]
         (migrate!)
-        (mt/user-http-request :crowberto :put 200 (str "/collection/" subcollection-id) {:archived true})
-        (mt/user-http-request :crowberto :put 200 (str "/collection/" collection-id) {:archived true})
-        (mt/user-http-request :crowberto :delete 200 (str "/collection/" collection-id))
-        (testing "sanity check: `collection` no longer exists"
-          (is (nil? (t2/select-one :model/Collection :id collection-id))))
+        (is (:archived_directly (t2/select-one :model/Collection :id collection-id)))
+        (is (not (:archived_directly (t2/select-one :model/Collection :id subcollection-id))))
+        (is (= (:archive_operation_id (t2/select-one :model/Collection :id collection-id))
+               (:archive_operation_id (t2/select-one :model/Collection :id subcollection-id))))
         (let [trash-collection-id (collection/trash-collection-id)]
-          (testing "After a down-migration, it stays in the trash"
+          (testing "After a down-migration, the trash is removed entirely."
             (migrate! :down 49)
-            (is (= (str "/" trash-collection-id "/") (t2/select-one-fn :location :model/Collection :id subcollection-id))))
-          (testing "but it's not really the trash anymore"
-            (is (nil? (:type (t2/select-one :model/Collection :id trash-collection-id)))))
+            (is (nil? (t2/select-one :model/Collection :name "Trash")))
+            (is (= "/" (t2/select-one-fn :location :model/Collection :id collection-id)))
+            (is (= (str "/" collection-id "/") (t2/select-one-fn :location :model/Collection :id subcollection-id))))
           (testing "we can migrate back up"
             (migrate!)
+            (is (:archived_directly (t2/select-one :model/Collection :id collection-id)))
+            (is (not (:archived_directly (t2/select-one :model/Collection :id subcollection-id))))
             (is (not= trash-collection-id (t2/select-one-pk :model/Collection :type "trash")))
-            (is (= (str "/" (t2/select-one-pk :model/Collection :type "trash") "/")
+            (is (= (str "/" collection-id "/")
                    (t2/select-one-fn :location :model/Collection :id subcollection-id)))))))))
+
+(deftest trash-migrations-make-archive-operation-ids-correctly
+  (impl/test-migrations ["v50.2024-05-29T14:04:47" "v50.2024-05-29T18:42:15"] [migrate!]
+    (with-redefs [collection/is-trash? (constantly false)]
+      (let [relevant-collection-ids (atom #{})
+            parent-id (fn [id]
+                        (:parent_id (t2/hydrate (t2/select-one :model/Collection :id id) :parent_id)))
+            make-collection! (fn [{:keys [archived? in]}]
+                               (let [result (t2/insert-returning-pk!
+                                             (t2/table-name :model/Collection) {:archived archived?
+                                                                                :name (str (gensym))
+                                                                                :slug (#'collection/slugify (str (gensym)))
+                                                                                :location (if in
+                                                                                            (collection/children-location (t2/select-one :model/Collection :id in))
+                                                                                            "/")})]
+                                 (swap! relevant-collection-ids conj result)
+                                 result))
+            a (make-collection! {:archived? true})
+            b (make-collection! {:archived? false :in a})
+            c (make-collection! {:archived? true :in b})
+            d (make-collection! {:archived? true :in c})
+            e (make-collection! {:archived? true :in d})
+            f (make-collection! {:archived? true :in e})
+            g (make-collection! {:archived? true :in e})
+            h (make-collection! {:archived? false :in g})
+            i (make-collection! {:archived? true :in h})]
+        (migrate!)
+        (let [archive-operation-id->collection-ids (m/map-vals #(into #{} (map :id %)) (group-by :archive_operation_id (t2/select :model/Collection :id [:in @relevant-collection-ids])))]
+          (is (= 4 (count archive-operation-id->collection-ids)))
+          (testing "Each contiguous subtree has its own archive_operation_id"
+            (is (= #{#{a} ;; => A is one subtree, none of its children are archived.
+                     #{c d e f g} ;; => C/D/E/[F,G] is a big ol' subtree
+                     #{i} ;; => I is the last archived subtree. It's a grandchild of G, but H isn't archived.
+                     #{b h}} ;; => not archived at all, `archive_operation_id` is nil
+                   (set (vals archive-operation-id->collection-ids)))))
+          (testing "Trashed directly is correctly set"
+            (is (= {true #{a c i}
+                    false #{d e f g}
+                    nil #{b h}}
+                   (m/map-vals #(into #{} (map :id %)) (group-by :archived_directly (t2/select :model/Collection :id [:in @relevant-collection-ids])))))))
+        ;; We can roll back. Nothing got moved around.
+        (migrate! :down 49)
+        (is (= nil (parent-id a)))
+        (is (= a (parent-id b)))
+        (is (= b (parent-id c)))
+        (is (= c (parent-id d)))
+        (is (= d (parent-id e)))
+        (is (= e (parent-id f)))
+        (is (= e (parent-id g)))
+        (is (= g (parent-id h)))
+        (is (= h (parent-id i)))
+        (migrate!)
+        (let [archive-operation-id->collection-ids (m/map-vals #(into #{} (map :id %)) (group-by :archive_operation_id (t2/select :model/Collection :id [:in @relevant-collection-ids])))]
+          (is (= 4 (count archive-operation-id->collection-ids)))
+          (doseq [id (keys archive-operation-id->collection-ids)]
+            (when-not (nil? id)
+              (is (uuid? (java.util.UUID/fromString id)))))
+          (testing "Run the same test as above just to make sure that it survives the round trip"
+            (is (= #{#{a} ;; => A is one subtree, none of its children are archived.
+                     #{c d e f g} ;; => C/D/E/[F,G] is a big ol' subtree
+                     #{i} ;; => I is the last archived subtree. It's a grandchild of G, but H isn't archived.
+                     #{b h}} ;; => not archived at all, `archive_operation_id` is nil
+                   (set (vals archive-operation-id->collection-ids))))))))))
diff --git a/test/metabase/events/revision_test.clj b/test/metabase/events/revision_test.clj
index c0ff2b925ec5289b0dfbb91b0bff667d8de229d7..c4930d8886833399dae414a7b220b37a192d41f6 100644
--- a/test/metabase/events/revision_test.clj
+++ b/test/metabase/events/revision_test.clj
@@ -21,41 +21,41 @@
    :creator_id             (mt/user->id :crowberto)})
 
 (defn- card->revision-object [card]
-  {:archived                   false
-   :collection_id              nil
-   :collection_position        nil
-   :collection_preview         true
-   :database_id                (mt/id)
-   :dataset_query              (:dataset_query card)
-   :type                       :question
-   :description                nil
-   :display                    :table
-   :enable_embedding           false
-   :embedding_params           nil
-   :name                       (:name card)
-   :parameters                 []
-   :parameter_mappings         []
-   :cache_ttl                  nil
-   :query_type                 :query
-   :table_id                   (mt/id :categories)
-   :visualization_settings     {}
-   :trashed_from_collection_id (:trashed_from_collection_id card)})
+  {:archived               false
+   :collection_id          nil
+   :collection_position    nil
+   :collection_preview     true
+   :database_id            (mt/id)
+   :dataset_query          (:dataset_query card)
+   :type                   :question
+   :description            nil
+   :display                :table
+   :enable_embedding       false
+   :embedding_params       nil
+   :name                   (:name card)
+   :parameters             []
+   :parameter_mappings     []
+   :cache_ttl              nil
+   :query_type             :query
+   :table_id               (mt/id :categories)
+   :visualization_settings {}
+   :archived_directly      (:archived_directly card)})
 
 (defn- dashboard->revision-object [dashboard]
-  {:collection_id              (:collection_id dashboard)
-   :description                nil
-   :cache_ttl                  nil
-   :auto_apply_filters         true
-   :name                       (:name dashboard)
-   :width                      (:width dashboard)
-   :tabs                       []
-   :cards                      []
-   :archived                   false
-   :collection_position        nil
-   :enable_embedding           false
-   :embedding_params           nil
-   :parameters                 []
-   :trashed_from_collection_id (:trashed_from_collection_id dashboard)})
+  {:collection_id       (:collection_id dashboard)
+   :description         nil
+   :cache_ttl           nil
+   :auto_apply_filters  true
+   :name                (:name dashboard)
+   :width               (:width dashboard)
+   :tabs                []
+   :cards               []
+   :archived            false
+   :collection_position nil
+   :enable_embedding    false
+   :embedding_params    nil
+   :parameters          []
+   :archived_directly   (:archived_directly dashboard)})
 
 (deftest card-create-test
   (testing :event/card-create
diff --git a/test/metabase/models/card_test.clj b/test/metabase/models/card_test.clj
index f0b7df1927dacd482a4d95f33826791660acbe30..c53a9afc6a7690eeb7b52a41c7ba57af36be85e3 100644
--- a/test/metabase/models/card_test.clj
+++ b/test/metabase/models/card_test.clj
@@ -844,9 +844,9 @@
                          ;; we don't need a description for made_public_by_id because whenever this field changes
                          ;; public_uuid will change and we have a description for it.
                          :made_public_by_id
-                         ;; similarly, we don't need a description for `trashed_from_collection_id` because whenever
+                         ;; similarly, we don't need a description for `archived_directly` because whenever
                          ;; this field changes `archived` will also change and we have a description for that.
-                         :trashed_from_collection_id
+                         :archived_directly
                          ;; we don't expect a description for this column because it should never change
                          ;; once created by the migration
                          :dataset_query_metrics_v2_migration_backup} col)
diff --git a/test/metabase/models/collection_test.clj b/test/metabase/models/collection_test.clj
index 412979283f764273783c2e277d2dded9ed389b0f..10e774c3e8bb77de9eaf025f28614149358eed09 100644
--- a/test/metabase/models/collection_test.clj
+++ b/test/metabase/models/collection_test.clj
@@ -280,82 +280,157 @@
              Exception
              (collection/children-location collection)))))))
 
-(deftest ^:parallel permissions-set->visible-collection-ids-test
-  (testing "Make sure we can look at the current user's permissions set and figure out which Collections they're allowed to see"
-    (is (= #{8 9}
-           (collection/permissions-set->visible-collection-ids
-            #{"/db/1/"
-              "/db/2/native/"
-              "/db/4/schema/"
-              "/db/5/schema/PUBLIC/"
-              "/db/6/schema/PUBLIC/table/7/"
-              "/collection/8/"
-              "/collection/9/read/"}))))
-
-  (testing "If the current user has root permissions then make sure the function returns `:all`, which signifies that they are able to see all Collections"
-    (is (= :all
-           (collection/permissions-set->visible-collection-ids
-            #{"/"
-              "/db/2/native/"
-              "/collection/9/read/"}))))
-
-  (testing "for the Root Collection we should return `root`"
-    (is (= #{8 9 "root"}
-           (collection/permissions-set->visible-collection-ids
-            #{"/collection/8/"
-              "/collection/9/read/"
-              "/collection/root/"})))
-
-    (is (= #{"root"}
-           (collection/permissions-set->visible-collection-ids
-            #{"/collection/root/read/"})))))
-
-(deftest ^:parallel effective-location-path-test
-  (testing "valid input"
-    (doseq [[args expected] {["/10/20/30/" #{10 20}]    "/10/20/"
-                             ["/10/20/30/" #{10 30}]    "/10/30/"
-                             ["/10/20/30/" #{}]         "/"
-                             ["/10/20/30/" #{10 20 30}] "/10/20/30/"
-                             ["/10/20/30/" :all]        "/10/20/30/"}]
-      (testing (pr-str (cons 'effective-location-path args))
-        (is (= expected
+(deftest permissions-set->visible-collection-ids-test
+  ;; let's just say all of the collections we're dealing with are:
+  ;; - NOT the trash
+  ;; - NOT archived
+  ;; - don't have a `archive_operation_id`
+  (with-redefs [collection/is-trash? (constantly false)
+                collection/collection-id->collection
+                (constantly
+                 (zipmap (next (range 10))
+                         (next (map (fn [id]
+                                      {:id id
+                                       :archived false
+                                       :archive_operation_id nil})
+                                    (range 10)))))]
+    (testing "Make sure we can look at the current user's permissions set and figure out which Collections they're allowed to see"
+      (is (= #{8 9}
+             (collection/permissions-set->visible-collection-ids
+              #{"/db/1/"
+                "/db/2/native/"
+                "/db/4/schema/"
+                "/db/5/schema/PUBLIC/"
+                "/db/6/schema/PUBLIC/table/7/"
+                "/collection/8/"
+                "/collection/9/read/"}))))
+
+    (testing "If the current user has root permissions then make sure the function returns `:all`, which signifies that they are able to see all Collections"
+      (is (= (into #{"root"} (range 1 10))
+             (collection/permissions-set->visible-collection-ids
+              #{"/"
+                "/db/2/native/"
+                "/collection/9/read/"}))))
+
+    (testing "for the Root Collection we should return `root`"
+      (is (= #{8 9 "root"}
+             (collection/permissions-set->visible-collection-ids
+              #{"/collection/8/"
+                "/collection/9/read/"
+                "/collection/root/"})))
+
+      (is (= #{"root"}
+             (collection/permissions-set->visible-collection-ids
+              #{"/collection/root/read/"}))))))
+
+;; testing the 2-arity form of `permissions-set->visible-collection-ids`
+(deftest permissions-set->visible-collection-ids-test-with-config
+  (with-redefs [collection/is-trash? #(= (:id %) 1)
+                collection/collection-id->collection
+                ;; These are the collections we get to play with
+                (constantly
+                 {1 {:id 1
+                     :archived false
+                     :archive_operation_id nil}
+                  2 {:id 2
+                     :archived true
+                     :archive_operation_id "1234"}
+                  3 {:id 3
+                     :archived true
+                     :archive_operation_id "1234"}
+                  4 {:id 4
+                     :archived true
+                     :archive_operation_id "5678"}
+                  5 {:id 5
+                     :archived false
+                     :archive_operation_id nil}})]
+    (let [permissions #{"/collection/1/" "/collection/2/"
+                        "/collection/3/" "/collection/4/"
+                        "/collection/5/"}]
+      (testing "Archived"
+        (testing "Default"
+          (is (= #{5}
+                 (collection/permissions-set->visible-collection-ids permissions {}))))
+        (testing "Only"
+          (is (= #{2 3 4}
+                 (collection/permissions-set->visible-collection-ids permissions {:include-archived-items :only}))))
+        (testing "Exclude"
+          (is (= #{5}
+                 (collection/permissions-set->visible-collection-ids permissions {:include-archived-items :exclude}))))
+        (testing "All"
+          (is (= #{2 3 4 5}
+                 (collection/permissions-set->visible-collection-ids permissions {:include-archived-items :all})))))
+      (testing "Include trash?"
+        (testing "true"
+          (is (= #{1 5}
+                 (collection/permissions-set->visible-collection-ids permissions {:include-trash-collection? true}))))
+        (testing "false"
+          (is (= #{5}
+                 (collection/permissions-set->visible-collection-ids permissions {:include-trash-collection? false}))))
+        (testing "default"
+          (is (= #{5}
+                 (collection/permissions-set->visible-collection-ids permissions {})))))
+      (testing "archive operation id"
+        (testing "can filter down to a particular archive operation id"
+          (is (= #{2 3}
+                 (collection/permissions-set->visible-collection-ids permissions {:archive-operation-id "1234"
+                                                                                  :include-archived-items :all})))
+          (is (= #{4}
+                 (collection/permissions-set->visible-collection-ids permissions {:archive-operation-id "5678"
+                                                                                  :include-archived-items :all}))))))))
+
+(deftest effective-location-path-test
+  (with-redefs [collection/collection-id->collection (constantly
+                                                      (zipmap (map * (next (range 10)) (repeat 10))
+                                                              (next (map (fn [id]
+                                                                           {:id id
+                                                                            :archived false
+                                                                            :archive_operation_id nil})
+                                                                         (map * (range 10) (repeat 10))))))]
+    (testing "valid input"
+      (doseq [[args expected] {["/10/20/30/" #{10 20}]    "/10/20/"
+                               ["/10/20/30/" #{10 30}]    "/10/30/"
+                               ["/10/20/30/" #{}]         "/"
+                               ["/10/20/30/" #{10 20 30}] "/10/20/30/"}]
+        (testing (pr-str (cons 'effective-location-path args))
+          (is (= expected
+                 (apply collection/effective-location-path args))))))
+
+    (testing "invalid input"
+      (doseq [args [["/10/20/30/" nil]
+                    ["/10/20/30/" [20]]
+                    [nil #{}]
+                    [[10 20] #{}]]]
+        (testing (pr-str (cons 'effective-location-path args))
+          (is (thrown?
+               Exception
                (apply collection/effective-location-path args))))))
 
-  (testing "invalid input"
-    (doseq [args [["/10/20/30/" nil]
-                  ["/10/20/30/" [20]]
-                  [nil #{}]
-                  [[10 20] #{}]]]
-      (testing (pr-str (cons 'effective-location-path args))
-        (is (thrown?
-             Exception
-             (apply collection/effective-location-path args))))))
-
-  (testing "Does the function also work if we call the single-arity version that powers hydration?"
-    (testing "mix of full and read perms"
-      (binding [*current-user-permissions-set* (atom #{"/collection/10/" "/collection/20/read/"})]
-        (is (= "/10/20/"
-               (collection/effective-location-path {:location "/10/20/30/"})))))
-
-    (testing "missing some perms"
-      (binding [*current-user-permissions-set* (atom #{"/collection/10/read/" "/collection/30/read/"})]
-        (is (= "/10/30/"
-               (collection/effective-location-path {:location "/10/20/30/"})))))
-
-    (testing "no perms"
-      (binding [*current-user-permissions-set* (atom #{})]
-        (is (= "/"
-               (collection/effective-location-path {:location "/10/20/30/"})))))
-
-    (testing "read perms for all"
-      (binding [*current-user-permissions-set* (atom #{"/collection/10/" "/collection/20/read/" "/collection/30/read/"})]
-        (is (= "/10/20/30/"
-               (collection/effective-location-path {:location "/10/20/30/"})))))
-
-    (testing "root perms"
-      (binding [*current-user-permissions-set* (atom #{"/"})]
-        (is (= "/10/20/30/"
-               (collection/effective-location-path {:location "/10/20/30/"})))))))
+    (testing "Does the function also work if we call the single-arity version that powers hydration?"
+      (testing "mix of full and read perms"
+        (binding [*current-user-permissions-set* (atom #{"/collection/10/" "/collection/20/read/"})]
+          (is (= "/10/20/"
+                 (collection/effective-location-path {:location "/10/20/30/"})))))
+
+      (testing "missing some perms"
+        (binding [*current-user-permissions-set* (atom #{"/collection/10/read/" "/collection/30/read/"})]
+          (is (= "/10/30/"
+                 (collection/effective-location-path {:location "/10/20/30/"})))))
+
+      (testing "no perms"
+        (binding [*current-user-permissions-set* (atom #{})]
+          (is (= "/"
+                 (collection/effective-location-path {:location "/10/20/30/"})))))
+
+      (testing "read perms for all"
+        (binding [*current-user-permissions-set* (atom #{"/collection/10/" "/collection/20/read/" "/collection/30/read/"})]
+          (is (= "/10/20/30/"
+                 (collection/effective-location-path {:location "/10/20/30/"})))))
+
+      (testing "root perms"
+        (binding [*current-user-permissions-set* (atom #{"/"})]
+          (is (= "/10/20/30/"
+                 (collection/effective-location-path {:location "/10/20/30/"}))))))))
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
 ;;; |                                Nested Collections: CRUD Constraints & Behavior                                 |
diff --git a/test/metabase/models/dashboard_test.clj b/test/metabase/models/dashboard_test.clj
index 6427373fdb93c948978d19950df5c5841e898f02..901e04d492617b1274be5e4dca45c958942e2a76 100644
--- a/test/metabase/models/dashboard_test.clj
+++ b/test/metabase/models/dashboard_test.clj
@@ -37,31 +37,31 @@
                              DashboardCard       {dashcard-id :id} {:dashboard_id dashboard-id, :card_id card-id}
                              DashboardCardSeries _                 {:dashboardcard_id dashcard-id, :card_id series-id-1, :position 0}
                              DashboardCardSeries _                 {:dashboardcard_id dashcard-id, :card_id series-id-2, :position 1}]
-      (is (= {:name                       "Test Dashboard"
-              :trashed_from_collection_id nil
-              :auto_apply_filters         true
-              :collection_id              nil
-              :description                nil
-              :cache_ttl                  nil
-              :cards                      [{:size_x                 4
-                                            :size_y                 4
-                                            :row                    0
-                                            :col                    0
-                                            :id                     true
-                                            :card_id                true
-                                            :series                 true
-                                            :dashboard_tab_id       nil
-                                            :action_id              nil
-                                            :parameter_mappings     []
-                                            :visualization_settings {}
-                                            :dashboard_id           dashboard-id}]
-              :tabs                       []
-              :archived                   false
-              :collection_position        nil
-              :enable_embedding           false
-              :embedding_params           nil
-              :parameters                 []
-              :width                      "fixed"}
+      (is (= {:name                "Test Dashboard"
+              :archived_directly   false
+              :auto_apply_filters  true
+              :collection_id       nil
+              :description         nil
+              :cache_ttl           nil
+              :cards               [{:size_x                 4
+                                     :size_y                 4
+                                     :row                    0
+                                     :col                    0
+                                     :id                     true
+                                     :card_id                true
+                                     :series                 true
+                                     :dashboard_tab_id       nil
+                                     :action_id              nil
+                                     :parameter_mappings     []
+                                     :visualization_settings {}
+                                     :dashboard_id           dashboard-id}]
+              :tabs                []
+              :archived            false
+              :collection_position nil
+              :enable_embedding    false
+              :embedding_params    nil
+              :parameters          []
+              :width               "fixed"}
              (update (revision/serialize-instance Dashboard (:id dashboard) dashboard)
                      :cards
                      (fn [[{:keys [id card_id series], :as card}]]
@@ -267,7 +267,6 @@
                                (= col :made_public_by_id)          (mt/user->id :crowberto)
                                (= col :embedding_params)           {:category_name "locked"}
                                (= col :public_uuid)                (str (random-uuid))
-                               (= col :trashed_from_collection_id) (:id coll)
                                (int? value)                        (inc value)
                                (boolean? value)                    (not value)
                                (string? value)                     (str value "_changed")))]
@@ -283,9 +282,9 @@
                   (is (= 2 (t2/count Revision :model "Dashboard" :model_id (:id dashboard)))))
 
                 ;; we don't need a description for made_public_by_id because whenever this field changes public_uuid
-                ;; will changes and we had a description for it. Same is true for `trashed_from_collection_id` -
+                ;; will changes and we had a description for it. Same is true for `archived_directly` -
                 ;; `archived` will always change with it.
-                (when-not (#{:made_public_by_id :trashed_from_collection_id} col)
+                (when-not (#{:made_public_by_id :archived_directly} col)
                   (testing (format "we should have a revision description for %s" col)
                     (is (some? (u/build-sentence
                                  (revision/diff-strings
@@ -356,47 +355,47 @@
                                          :id      (= dashcard-id id)
                                          :card_id (= card-id card_id)
                                          :series  (= [series-id-1 series-id-2] series))])
-          empty-dashboard      {:name                       "Revert Test"
-                                :trashed_from_collection_id nil
-                                :description                "something"
-                                :auto_apply_filters         true
-                                :collection_id              nil
-                                :cache_ttl                  nil
-                                :cards                      []
-                                :tabs                       []
-                                :archived                   false
-                                :collection_position        nil
-                                :enable_embedding           false
-                                :embedding_params           nil
-                                :parameters                 []
-                                :width                      "fixed"}
+          empty-dashboard      {:name                "Revert Test"
+                                :archived_directly   false
+                                :description         "something"
+                                :auto_apply_filters  true
+                                :collection_id       nil
+                                :cache_ttl           nil
+                                :cards               []
+                                :tabs                []
+                                :archived            false
+                                :collection_position nil
+                                :enable_embedding    false
+                                :embedding_params    nil
+                                :parameters          []
+                                :width               "fixed"}
           serialized-dashboard (revision/serialize-instance Dashboard (:id dashboard) dashboard)]
       (testing "original state"
-        (is (= {:name                       "Test Dashboard"
-                :trashed_from_collection_id nil
-                :description                nil
-                :cache_ttl                  nil
-                :auto_apply_filters         true
-                :collection_id              nil
-                :cards                      [{:size_x                 4
-                                              :size_y                 4
-                                              :row                    0
-                                              :col                    0
-                                              :id                     true
-                                              :card_id                true
-                                              :series                 true
-                                              :dashboard_tab_id       nil
-                                              :action_id              nil
-                                              :parameter_mappings     []
-                                              :visualization_settings {}
-                                              :dashboard_id           dashboard-id}]
-                :tabs                       []
-                :archived                   false
-                :collection_position        nil
-                :enable_embedding           false
-                :embedding_params           nil
-                :parameters                 []
-                :width                      "fixed"}
+        (is (= {:name                "Test Dashboard"
+                :archived_directly   false
+                :description         nil
+                :cache_ttl           nil
+                :auto_apply_filters  true
+                :collection_id       nil
+                :cards               [{:size_x                 4
+                                       :size_y                 4
+                                       :row                    0
+                                       :col                    0
+                                       :id                     true
+                                       :card_id                true
+                                       :series                 true
+                                       :dashboard_tab_id       nil
+                                       :action_id              nil
+                                       :parameter_mappings     []
+                                       :visualization_settings {}
+                                       :dashboard_id           dashboard-id}]
+                :tabs                []
+                :archived            false
+                :collection_position nil
+                :enable_embedding    false
+                :embedding_params    nil
+                :parameters          []
+                :width               "fixed"}
                (update serialized-dashboard :cards check-ids))))
       (testing "delete the dashcard and modify the dash attributes"
         (dashboard-card/delete-dashboard-cards! [(:id dashboard-card)])
@@ -410,31 +409,31 @@
                    (revision/serialize-instance Dashboard (:id dashboard) dashboard))))))
       (testing "now do the reversion; state should return to original"
         (revision/revert-to-revision! Dashboard dashboard-id (test.users/user->id :crowberto) serialized-dashboard)
-        (is (= {:name                       "Test Dashboard"
-                :trashed_from_collection_id nil
-                :description                nil
-                :cache_ttl                  nil
-                :auto_apply_filters         true
-                :collection_id              nil
-                :cards                      [{:size_x                 4
-                                              :size_y                 4
-                                              :row                    0
-                                              :col                    0
-                                              :id                     false
-                                              :card_id                true
-                                              :series                 true
-                                              :dashboard_tab_id       nil
-                                              :action_id              nil
-                                              :parameter_mappings     []
-                                              :visualization_settings {}
-                                              :dashboard_id           dashboard-id}]
-                :tabs                       []
-                :archived                   false
-                :collection_position        nil
-                :enable_embedding           false
-                :embedding_params           nil
-                :parameters                 []
-                :width                      "fixed"}
+        (is (= {:name                "Test Dashboard"
+                :archived_directly   false
+                :description         nil
+                :cache_ttl           nil
+                :auto_apply_filters  true
+                :collection_id       nil
+                :cards               [{:size_x                 4
+                                       :size_y                 4
+                                       :row                    0
+                                       :col                    0
+                                       :id                     false
+                                       :card_id                true
+                                       :series                 true
+                                       :dashboard_tab_id       nil
+                                       :action_id              nil
+                                       :parameter_mappings     []
+                                       :visualization_settings {}
+                                       :dashboard_id           dashboard-id}]
+                :tabs                []
+                :archived            false
+                :collection_position nil
+                :enable_embedding    false
+                :embedding_params    nil
+                :parameters          []
+                :width               "fixed"}
                (update (revision/serialize-instance Dashboard dashboard-id (t2/select-one Dashboard :id dashboard-id))
                        :cards check-ids))))
       (testing "revert back to the empty state"
diff --git a/test/metabase/search/impl_test.clj b/test/metabase/search/impl_test.clj
index 5e98e770b12e876c225706129164323fa380a418..88aff0475c0c3c522f4f4ae08d737895db48b1c0 100644
--- a/test/metabase/search/impl_test.clj
+++ b/test/metabase/search/impl_test.clj
@@ -7,6 +7,7 @@
    [clojure.test :refer :all]
    [java-time.api :as t]
    [metabase.api.common :as api]
+   [metabase.config :as config]
    [metabase.search.config :as search.config]
    [metabase.search.impl :as search.impl]
    [metabase.test :as mt]
@@ -56,14 +57,15 @@
        :model/Segment   _              {:table_id table-id
                                         :name     (str "segment 3 " search-string)}]
       (mt/with-current-user (mt/user->id :crowberto)
-        (let [do-search (fn []
-                          (search.impl/search {:search-string      search-string
-                                               :archived?          false
-                                               :models             search.config/all-models
-                                               :current-user-id    (mt/user->id :crowberto)
-                                               :current-user-perms #{"/"}
-                                               :model-ancestors?   false
-                                               :limit-int          100}))]
+        (binding [config/*request-id* (random-uuid)]
+          (let [do-search (fn []
+                            (search.impl/search {:search-string      search-string
+                                                 :archived?          false
+                                                 :models             search.config/all-models
+                                                 :current-user-id    (mt/user->id :crowberto)
+                                                 :current-user-perms #{"/"}
+                                                 :model-ancestors?   false
+                                                 :limit-int          100}))]
           ;; warm it up, in case the DB call depends on the order of test execution and it needs to
           ;; do some initialization
           (do-search)
@@ -72,7 +74,7 @@
             ;; the call count number here are expected to change if we change the search api
             ;; we have this test here just to keep tracks this number to remind us to put effort
             ;; into keep this number as low as we can
-            (is (= 6 (call-count)))))))))
+            (is (= 6 (call-count))))))))))
 
 (deftest created-at-correctness-test
   (let [search-term   "created-at-filtering"