Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔨 migrate excludedEntities and includedEntities to use entity names instead of ids #4582

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 34 additions & 55 deletions adminSiteClient/EditorMarimekkoTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { faMinus, faTrash } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome/index.js"
import { EntityName } from "@ourworldindata/types"
import { Grapher } from "@ourworldindata/grapher"
import { excludeUndefined } from "@ourworldindata/utils"
import lodash from "lodash"
import { action, computed, IReactionDisposer, observable, reaction } from "mobx"
import { observer } from "mobx-react"
Expand All @@ -18,91 +17,71 @@ export class EditorMarimekkoTab extends Component<{ grapher: Grapher }> {
}

@computed private get includedEntityNames(): EntityName[] {
const { includedEntities, inputTable } = this.props.grapher
const { entityIdToNameMap } = inputTable
const includedEntityIds = includedEntities ?? []
return excludeUndefined(
includedEntityIds.map((entityId) => entityIdToNameMap.get(entityId))
)
return this.props.grapher.includedEntityNames ?? []
}

@computed private get excludedEntityNames(): EntityName[] {
return this.props.grapher.excludedEntityNames ?? []
}

@computed private get includedEntityChoices() {
const { inputTable } = this.props.grapher
const { inputTable, includedEntityNames = [] } = this.props.grapher
return inputTable.availableEntityNames
.filter(
(entityName) => !this.includedEntityNames.includes(entityName)
)
.filter((entityName) => !includedEntityNames.includes(entityName))
.sort()
}

@action.bound onIncludeEntity(entity: string) {
const { grapher } = this.props
if (grapher.includedEntities === undefined) {
grapher.includedEntities = []
}

const entityId = grapher.table.entityNameToIdMap.get(entity)!
if (grapher.includedEntities.indexOf(entityId) === -1)
grapher.includedEntities.push(entityId)
@computed private get excludedEntityChoices() {
const { inputTable, excludedEntityNames = [] } = this.props.grapher
return inputTable.availableEntityNames
.filter((entityName) => !excludedEntityNames.includes(entityName))
.sort()
}

@action.bound onUnincludeEntity(entity: string) {
@action.bound onExcludeEntity(entityName: string) {
const { grapher } = this.props
if (!grapher.includedEntities) return
if (grapher.excludedEntityNames === undefined) {
grapher.excludedEntityNames = []
}

const entityId = grapher.table.entityNameToIdMap.get(entity)
grapher.includedEntities = grapher.includedEntities.filter(
(e) => e !== entityId
)
if (!grapher.excludedEntityNames.includes(entityName))
grapher.excludedEntityNames.push(entityName)
}

@computed private get excludedEntityNames(): EntityName[] {
const { excludedEntities, inputTable } = this.props.grapher
const { entityIdToNameMap } = inputTable
const excludedEntityIds = excludedEntities ?? []
return excludeUndefined(
excludedEntityIds.map((entityId) => entityIdToNameMap.get(entityId))
@action.bound onUnexcludeEntity(entityName: string) {
const { grapher } = this.props
if (!grapher.excludedEntityNames) return
grapher.excludedEntityNames = grapher.excludedEntityNames.filter(
(e) => e !== entityName
)
}

@computed private get excludedEntityChoices() {
const { inputTable } = this.props.grapher
return inputTable.availableEntityNames
.filter(
(entityName) => !this.excludedEntityNames.includes(entityName)
)
.sort()
}

@action.bound onExcludeEntity(entity: string) {
@action.bound onIncludeEntity(entityName: string) {
const { grapher } = this.props
if (grapher.excludedEntities === undefined) {
grapher.excludedEntities = []
if (grapher.includedEntityNames === undefined) {
grapher.includedEntityNames = []
}

const entityId = grapher.table.entityNameToIdMap.get(entity)!
if (grapher.excludedEntities.indexOf(entityId) === -1)
grapher.excludedEntities.push(entityId)
if (!grapher.includedEntityNames.includes(entityName))
grapher.includedEntityNames.push(entityName)
}

@action.bound onUnexcludeEntity(entity: string) {
@action.bound onUnincludeEntity(entityName: string) {
const { grapher } = this.props
if (!grapher.excludedEntities) return

const entityId = grapher.table.entityNameToIdMap.get(entity)
grapher.excludedEntities = grapher.excludedEntities.filter(
(e) => e !== entityId
if (!grapher.includedEntityNames) return
grapher.includedEntityNames = grapher.includedEntityNames.filter(
(e) => e !== entityName
)
}

@action.bound onClearExcludedEntities() {
const { grapher } = this.props
grapher.excludedEntities = []
grapher.excludedEntityNames = []
}

@action.bound onClearIncludedEntities() {
const { grapher } = this.props
grapher.includedEntities = []
grapher.includedEntityNames = []
}

@action.bound onXOverrideYear(value: number | undefined) {
Expand Down
74 changes: 27 additions & 47 deletions adminSiteClient/EditorScatterTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
ScatterPointLabelStrategy,
} from "@ourworldindata/types"
import { Grapher } from "@ourworldindata/grapher"
import { debounce, excludeUndefined } from "@ourworldindata/utils"
import { debounce } from "@ourworldindata/utils"
import { action, computed, observable } from "mobx"
import { observer } from "mobx-react"
import { Component } from "react"
Expand All @@ -33,91 +33,71 @@ export class EditorScatterTab extends Component<{ grapher: Grapher }> {
}

@computed private get includedEntityNames(): EntityName[] {
const { includedEntities, inputTable } = this.props.grapher
const { entityIdToNameMap } = inputTable
const includedEntityIds = includedEntities ?? []
return excludeUndefined(
includedEntityIds.map((entityId) => entityIdToNameMap.get(entityId))
)
return this.props.grapher.includedEntityNames ?? []
}

@computed private get excludedEntityNames(): EntityName[] {
const { excludedEntities, inputTable } = this.props.grapher
const { entityIdToNameMap } = inputTable
const excludedEntityIds = excludedEntities ?? []
return excludeUndefined(
excludedEntityIds.map((entityId) => entityIdToNameMap.get(entityId))
)
return this.props.grapher.excludedEntityNames ?? []
}

@computed private get includedEntityChoices() {
const { inputTable } = this.props.grapher
const { inputTable, includedEntityNames = [] } = this.props.grapher
return inputTable.availableEntityNames
.filter(
(entityName) => !this.includedEntityNames.includes(entityName)
)
.filter((entityName) => !includedEntityNames.includes(entityName))
.sort()
}

@computed private get excludedEntityChoices() {
const { inputTable } = this.props.grapher
const { inputTable, excludedEntityNames = [] } = this.props.grapher
return inputTable.availableEntityNames
.filter(
(entityName) => !this.excludedEntityNames.includes(entityName)
)
.filter((entityName) => !excludedEntityNames.includes(entityName))
.sort()
}

@action.bound onExcludeEntity(entity: string) {
@action.bound onExcludeEntity(entityName: string) {
const { grapher } = this.props
if (grapher.excludedEntities === undefined) {
grapher.excludedEntities = []
if (grapher.excludedEntityNames === undefined) {
grapher.excludedEntityNames = []
}

const entityId = grapher.table.entityNameToIdMap.get(entity)!
if (grapher.excludedEntities.indexOf(entityId) === -1)
grapher.excludedEntities.push(entityId)
if (!grapher.excludedEntityNames.includes(entityName))
grapher.excludedEntityNames.push(entityName)
}

@action.bound onUnexcludeEntity(entity: string) {
@action.bound onUnexcludeEntity(entityName: string) {
const { grapher } = this.props
if (!grapher.excludedEntities) return

const entityId = grapher.table.entityNameToIdMap.get(entity)
grapher.excludedEntities = grapher.excludedEntities.filter(
(e) => e !== entityId
if (!grapher.excludedEntityNames) return
grapher.excludedEntityNames = grapher.excludedEntityNames.filter(
(e) => e !== entityName
)
}

@action.bound onIncludeEntity(entity: string) {
@action.bound onIncludeEntity(entityName: string) {
const { grapher } = this.props
if (grapher.includedEntities === undefined) {
grapher.includedEntities = []
if (grapher.includedEntityNames === undefined) {
grapher.includedEntityNames = []
}

const entityId = grapher.table.entityNameToIdMap.get(entity)!
if (grapher.includedEntities.indexOf(entityId) === -1)
grapher.includedEntities.push(entityId)
if (!grapher.includedEntityNames.includes(entityName))
grapher.includedEntityNames.push(entityName)
}

@action.bound onUnincludeEntity(entity: string) {
@action.bound onUnincludeEntity(entityName: string) {
const { grapher } = this.props
if (!grapher.includedEntities) return

const entityId = grapher.table.entityNameToIdMap.get(entity)
grapher.includedEntities = grapher.includedEntities.filter(
(e) => e !== entityId
if (!grapher.includedEntityNames) return
grapher.includedEntityNames = grapher.includedEntityNames.filter(
(e) => e !== entityName
)
}

@action.bound onClearExcludedEntities() {
const { grapher } = this.props
grapher.excludedEntities = []
grapher.excludedEntityNames = []
}

@action.bound onClearIncludedEntities() {
const { grapher } = this.props
grapher.includedEntities = []
grapher.includedEntityNames = []
}

@action.bound onToggleConnection(value: boolean) {
Expand Down
2 changes: 1 addition & 1 deletion adminSiteClient/GrapherConfigGridEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ export class GrapherConfigGridEditor extends React.Component<GrapherConfigGridEd
EditorOption.primitiveListEditor,
() =>
// TODO: handle different kinds of arrays here. In effect, the ones to handle are
// includedEntities, excludedEntities (both with a yet to extract control)
// includedEntityNames, excludedEntityNames (both with a yet to extract control)
// selection. The seleciton should be refactored because ATM it's 3 arrays and it
// is annoying to keep those in sync and target more than one field with a column.
undefined
Expand Down
65 changes: 65 additions & 0 deletions db/migration/1740064108477-MigrateExcludeIncludeEntities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { MigrationInterface, QueryRunner } from "typeorm"

export class MigrateExcludeIncludeEntities1740064108477
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
// This query touches all configs where `excludedEntities` or `includedEntities`
// are present and converts the existing array of ids to an array of names.
// Example:
// - `excludedEntities`: [23, 31416, 8, 6, 3, 1, 13]
// => `excludedEntityNames`: ["Australia", "Korea", "Italy", "Germany", "France", "United Kingdom", "United States"]

const fields = [
{ oldName: "excludedEntities", newName: "excludedEntityNames" },
{ oldName: "includedEntities", newName: "includedEntityNames" },
]

const tables = [
{ table: "chart_configs", column: "patch" },
{ table: "chart_configs", column: "full" },
{ table: "chart_revisions", column: "config" },
{ table: "suggested_chart_revisions", column: "suggestedConfig" },
]

for (const { oldName, newName } of fields) {
for (const { table, column } of tables) {
await queryRunner.query(
`-- sql
WITH migrated AS (
SELECT
t.id,
${column} -> "$.${oldName}" AS ${oldName},
JSON_ARRAYAGG(entities.name) AS ${newName}
FROM ${table} t,
JSON_TABLE(
t.${column},
'$.${oldName}[*]' COLUMNS (id INT PATH '$')
) AS json_ids
JOIN entities ON json_ids.id = entities.id
WHERE ${column} -> "$.${oldName}" IS NOT NULL
GROUP BY t.id
)
UPDATE ${table} t
INNER JOIN migrated ON t.id = migrated.id
SET ${column} = JSON_SET(${column}, '$.${newName}', migrated.${newName})
WHERE ${column} ->> '$.${oldName}' IS NOT NULL;
`
)

// Now that the new field is set, drop the old one
await queryRunner.query(
`-- sql
UPDATE ${table}
SET ${column} = JSON_REMOVE(${column}, "$.${oldName}")
WHERE ${column} -> "$.${oldName}" IS NOT NULL
`
)
}
}
}

public async down(): Promise<void> {
// intentially empty
}
}
14 changes: 0 additions & 14 deletions db/model/Entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,3 @@ export async function mapEntityNamesToEntityIds(

return entityNameToIdMap
}

export async function mapEntityIdsToEntityNames(
knex: db.KnexReadonlyTransaction
): Promise<Map<number, string>> {
const entities = (await knex(EntitiesTableName).select(
"id",
"name"
)) as Pick<DbPlainEntity, "id" | "name">[]
const entityIdToNameMap = new Map<number, string>(
entities.map((entity) => [entity.id, entity.name])
)

return entityIdToNameMap
}
4 changes: 2 additions & 2 deletions devTools/schemaProcessor/columns.json
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,7 @@
},
{
"type": "array",
"pointer": "/excludedEntities",
"pointer": "/excludedEntityNames",
"editor": "primitiveListEditor"
},
{
Expand Down Expand Up @@ -759,7 +759,7 @@
},
{
"type": "array",
"pointer": "/includedEntities",
"pointer": "/includedEntityNames",
"editor": "primitiveListEditor"
},
{
Expand Down
Loading
Loading