merge: 최신 main을 Profile 포괄 권한 제거에 통합
This commit is contained in:
@@ -25,6 +25,7 @@ export * from './ranking/types.js';
|
||||
export * from './ranking/legacyColor.js';
|
||||
export * from './auth/accountIconProjection.js';
|
||||
export * from './logging/formatLegacyLogHtml.js';
|
||||
export * from './legacyArchive/ArchivedGeneralSnapshot.js';
|
||||
export * from './gateway/profileStatus.js';
|
||||
export * from './game/accessPenalty.js';
|
||||
export * from './http/trpcTransport.js';
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
export type ArchivedJsonValue =
|
||||
null | boolean | number | string | ArchivedJsonValue[] | { [key: string]: ArchivedJsonValue };
|
||||
|
||||
export const ARCHIVED_GENERAL_SCHEMA_VERSION = 1 as const;
|
||||
|
||||
export const LEGACY_ARCHIVE_PROFILES = ['che', 'kwe', 'pwe', 'twe', 'nya', 'pya', 'hwe'] as const;
|
||||
export type LegacyArchiveProfile = (typeof LEGACY_ARCHIVE_PROFILES)[number];
|
||||
|
||||
export const isLegacyArchiveProfile = (value: string): value is LegacyArchiveProfile =>
|
||||
(LEGACY_ARCHIVE_PROFILES as readonly string[]).includes(value);
|
||||
|
||||
export type ArchivedGeneralSourceFormat = 'legacy-flat-v0' | 'ref-flat-v1' | 'core-snapshot-v1' | 'unknown';
|
||||
|
||||
export interface ArchivedGeneralSnapshotV1 {
|
||||
schemaVersion: typeof ARCHIVED_GENERAL_SCHEMA_VERSION;
|
||||
identity: {
|
||||
name: string;
|
||||
picture: string | null;
|
||||
imageServer: number | null;
|
||||
npcState: number | null;
|
||||
nationId: number | null;
|
||||
cityId: number | null;
|
||||
officerLevel: number | null;
|
||||
officerCity: number | null;
|
||||
};
|
||||
stats: {
|
||||
leadership: number | null;
|
||||
strength: number | null;
|
||||
intelligence: number | null;
|
||||
leadershipExperience: number | null;
|
||||
strengthExperience: number | null;
|
||||
intelligenceExperience: number | null;
|
||||
};
|
||||
progression: {
|
||||
experience: number | null;
|
||||
experienceLevel: number | null;
|
||||
dedication: number | null;
|
||||
dedicationLevel: number | null;
|
||||
age: number | null;
|
||||
startAge: number | null;
|
||||
bornYear: number | null;
|
||||
deadYear: number | null;
|
||||
};
|
||||
traits: {
|
||||
personality: string | null;
|
||||
specialDomestic: string | null;
|
||||
specialWar: string | null;
|
||||
};
|
||||
resources: {
|
||||
gold: number | null;
|
||||
rice: number | null;
|
||||
crew: number | null;
|
||||
crewType: string | null;
|
||||
train: number | null;
|
||||
morale: number | null;
|
||||
injury: number | null;
|
||||
};
|
||||
items: {
|
||||
horse: string | null;
|
||||
weapon: string | null;
|
||||
book: string | null;
|
||||
item: string | null;
|
||||
};
|
||||
mastery: {
|
||||
infantry: number | null;
|
||||
archery: number | null;
|
||||
cavalry: number | null;
|
||||
special: number | null;
|
||||
siege: number | null;
|
||||
};
|
||||
battle: {
|
||||
battles: number | null;
|
||||
wins: number | null;
|
||||
losses: number | null;
|
||||
fireSuccesses: number | null;
|
||||
kills: number | null;
|
||||
deaths: number | null;
|
||||
killedCrew: number | null;
|
||||
lostCrew: number | null;
|
||||
winRate: number | null;
|
||||
killRate: number | null;
|
||||
recentWar: string | null;
|
||||
tactics: {
|
||||
total: { wins: number | null; draws: number | null; losses: number | null };
|
||||
leadership: { wins: number | null; draws: number | null; losses: number | null };
|
||||
intelligence: { wins: number | null; draws: number | null; losses: number | null };
|
||||
};
|
||||
};
|
||||
history: string[];
|
||||
availability: {
|
||||
mastery: boolean;
|
||||
battleAggregates: boolean;
|
||||
tactics: boolean;
|
||||
history: boolean;
|
||||
battleDetailLogs: false;
|
||||
battleResultLogs: false;
|
||||
};
|
||||
}
|
||||
|
||||
type JsonRecord = Record<string, ArchivedJsonValue | undefined>;
|
||||
|
||||
const asRecord = (value: ArchivedJsonValue | undefined): JsonRecord =>
|
||||
value !== null && typeof value === 'object' && !Array.isArray(value) ? (value as JsonRecord) : {};
|
||||
|
||||
const finiteNumber = (value: ArchivedJsonValue | undefined): number | null => {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
||||
if (typeof value === 'string' && value.trim() !== '') {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const firstNumber = (...values: Array<ArchivedJsonValue | undefined>): number | null => {
|
||||
for (const value of values) {
|
||||
const parsed = finiteNumber(value);
|
||||
if (parsed !== null) return parsed;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const text = (value: ArchivedJsonValue | undefined): string | null => {
|
||||
if (typeof value === 'string') return value.trim() === '' ? null : value;
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return String(value);
|
||||
return null;
|
||||
};
|
||||
|
||||
const firstText = (...values: Array<ArchivedJsonValue | undefined>): string | null => {
|
||||
for (const value of values) {
|
||||
const parsed = text(value);
|
||||
if (parsed !== null) return parsed;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const historyLines = (value: ArchivedJsonValue | undefined): string[] => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0);
|
||||
}
|
||||
if (typeof value !== 'string') return [];
|
||||
return value
|
||||
.split(/<br\s*\/?>/iu)
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
const rate = (numerator: number | null, denominator: number | null): number | null =>
|
||||
numerator === null || denominator === null || denominator <= 0
|
||||
? null
|
||||
: Math.round((numerator / denominator) * 10_000) / 100;
|
||||
|
||||
export const detectArchivedGeneralSourceFormat = (raw: ArchivedJsonValue): ArchivedGeneralSourceFormat => {
|
||||
const data = asRecord(raw);
|
||||
if (Object.keys(asRecord(data.stats)).length > 0 || data.schemaVersion === 1) return 'core-snapshot-v1';
|
||||
if ('leadership' in data || 'strength' in data || 'dex1' in data) return 'ref-flat-v1';
|
||||
if ('leader' in data || 'power' in data || 'dex0' in data) return 'legacy-flat-v0';
|
||||
return 'unknown';
|
||||
};
|
||||
|
||||
export const normalizeArchivedGeneral = (
|
||||
raw: ArchivedJsonValue,
|
||||
fallbackName: string
|
||||
): { sourceFormat: ArchivedGeneralSourceFormat; snapshot: ArchivedGeneralSnapshotV1 } => {
|
||||
const data = asRecord(raw);
|
||||
const identity = asRecord(data.identity);
|
||||
const stats = asRecord(data.stats);
|
||||
const role = asRecord(data.role);
|
||||
const items = asRecord(data.items);
|
||||
const progression = asRecord(data.progression);
|
||||
const traits = asRecord(data.traits);
|
||||
const resources = asRecord(data.resources);
|
||||
const nestedMastery = asRecord(data.mastery);
|
||||
const nestedBattle = asRecord(data.battle);
|
||||
const nestedTactics = asRecord(nestedBattle.tactics);
|
||||
const totalTactics = asRecord(nestedTactics.total);
|
||||
const leadershipTactics = asRecord(nestedTactics.leadership);
|
||||
const intelligenceTactics = asRecord(nestedTactics.intelligence);
|
||||
const oldMastery = 'dex0' in data;
|
||||
const masteryValues = oldMastery
|
||||
? [data.dex0, data.dex10, data.dex20, data.dex30, data.dex40]
|
||||
: [
|
||||
data.dex1 ?? nestedMastery.infantry,
|
||||
data.dex2 ?? nestedMastery.archery,
|
||||
data.dex3 ?? nestedMastery.cavalry,
|
||||
data.dex4 ?? nestedMastery.special,
|
||||
data.dex5 ?? nestedMastery.siege,
|
||||
];
|
||||
const mastery = masteryValues.map(finiteNumber);
|
||||
const battles = firstNumber(data.warnum, nestedBattle.battles);
|
||||
const wins = firstNumber(data.killnum, nestedBattle.wins);
|
||||
const losses = firstNumber(data.deathnum, nestedBattle.losses);
|
||||
const killedCrew = firstNumber(data.killcrew, nestedBattle.killedCrew);
|
||||
const lostCrew = firstNumber(data.deathcrew, nestedBattle.lostCrew);
|
||||
const history = historyLines(data.history);
|
||||
const tacticValues = [
|
||||
data.ttw ?? totalTactics.wins,
|
||||
data.ttd ?? totalTactics.draws,
|
||||
data.ttl ?? totalTactics.losses,
|
||||
data.tlw ?? leadershipTactics.wins,
|
||||
data.tld ?? leadershipTactics.draws,
|
||||
data.tll ?? leadershipTactics.losses,
|
||||
data.tiw ?? intelligenceTactics.wins,
|
||||
data.tid ?? intelligenceTactics.draws,
|
||||
data.til ?? intelligenceTactics.losses,
|
||||
];
|
||||
const tacticsAvailable = tacticValues.some((value) => finiteNumber(value) !== null);
|
||||
|
||||
return {
|
||||
sourceFormat: detectArchivedGeneralSourceFormat(raw),
|
||||
snapshot: {
|
||||
schemaVersion: ARCHIVED_GENERAL_SCHEMA_VERSION,
|
||||
identity: {
|
||||
name: firstText(data.name, identity.name) ?? fallbackName,
|
||||
picture: firstText(data.picture, identity.picture),
|
||||
imageServer: firstNumber(data.imageServer, data.imgsvr, identity.imageServer),
|
||||
npcState: firstNumber(data.npcState, data.npc, identity.npcState),
|
||||
nationId: firstNumber(data.nationId, data.nation, identity.nationId),
|
||||
cityId: firstNumber(data.cityId, data.city, identity.cityId),
|
||||
officerLevel: firstNumber(data.officerLevel, data.officer_level, data.level, identity.officerLevel),
|
||||
officerCity: firstNumber(data.officerCity, data.officer_city, identity.officerCity),
|
||||
},
|
||||
stats: {
|
||||
leadership: firstNumber(data.leadership, data.leader, stats.leadership),
|
||||
strength: firstNumber(data.strength, data.power, stats.strength),
|
||||
intelligence: firstNumber(data.intelligence, data.intel, stats.intelligence),
|
||||
leadershipExperience: firstNumber(
|
||||
data.leadershipExperience,
|
||||
data.leadership_exp,
|
||||
stats.leadershipExperience
|
||||
),
|
||||
strengthExperience: firstNumber(data.strengthExperience, data.strength_exp, stats.strengthExperience),
|
||||
intelligenceExperience: firstNumber(
|
||||
data.intelligenceExperience,
|
||||
data.intel_exp,
|
||||
stats.intelligenceExperience
|
||||
),
|
||||
},
|
||||
progression: {
|
||||
experience: firstNumber(data.experience, progression.experience),
|
||||
experienceLevel: firstNumber(data.experienceLevel, data.explevel, progression.experienceLevel),
|
||||
dedication: firstNumber(data.dedication, progression.dedication),
|
||||
dedicationLevel: firstNumber(data.dedicationLevel, data.dedlevel, progression.dedicationLevel),
|
||||
age: firstNumber(data.age, progression.age),
|
||||
startAge: firstNumber(data.startAge, data.startage, progression.startAge),
|
||||
bornYear: firstNumber(data.bornYear, data.bornyear, progression.bornYear),
|
||||
deadYear: firstNumber(data.deadYear, data.deadyear, progression.deadYear),
|
||||
},
|
||||
traits: {
|
||||
personality: firstText(data.personalCode, data.personal, role.personality, traits.personality),
|
||||
specialDomestic: firstText(
|
||||
data.specialCode,
|
||||
data.special,
|
||||
role.specialDomestic,
|
||||
traits.specialDomestic
|
||||
),
|
||||
specialWar: firstText(data.special2Code, data.special2, role.specialWar, traits.specialWar),
|
||||
},
|
||||
resources: {
|
||||
gold: firstNumber(data.gold, resources.gold),
|
||||
rice: firstNumber(data.rice, resources.rice),
|
||||
crew: firstNumber(data.crew, resources.crew),
|
||||
crewType: firstText(data.crewType, data.crewtype, resources.crewType),
|
||||
train: firstNumber(data.train, resources.train),
|
||||
morale: firstNumber(data.morale, data.atmos, resources.morale),
|
||||
injury: firstNumber(data.injury, resources.injury),
|
||||
},
|
||||
items: {
|
||||
horse: firstText(items.horse, data.horse),
|
||||
weapon: firstText(items.weapon, data.weapon, data.weap),
|
||||
book: firstText(items.book, data.book),
|
||||
item: firstText(items.item, data.item),
|
||||
},
|
||||
mastery: {
|
||||
infantry: mastery[0] ?? null,
|
||||
archery: mastery[1] ?? null,
|
||||
cavalry: mastery[2] ?? null,
|
||||
special: mastery[3] ?? null,
|
||||
siege: mastery[4] ?? null,
|
||||
},
|
||||
battle: {
|
||||
battles,
|
||||
wins,
|
||||
losses,
|
||||
fireSuccesses: firstNumber(data.firenum, nestedBattle.fireSuccesses),
|
||||
kills: firstNumber(nestedBattle.kills, wins),
|
||||
deaths: firstNumber(nestedBattle.deaths, losses),
|
||||
killedCrew,
|
||||
lostCrew,
|
||||
winRate: firstNumber(nestedBattle.winRate) ?? rate(wins, battles),
|
||||
killRate: firstNumber(nestedBattle.killRate) ?? rate(killedCrew, lostCrew),
|
||||
recentWar: firstText(data.recentWar, data.recent_war, nestedBattle.recentWar),
|
||||
tactics: {
|
||||
total: {
|
||||
wins: firstNumber(data.ttw, totalTactics.wins),
|
||||
draws: firstNumber(data.ttd, totalTactics.draws),
|
||||
losses: firstNumber(data.ttl, totalTactics.losses),
|
||||
},
|
||||
leadership: {
|
||||
wins: firstNumber(data.tlw, leadershipTactics.wins),
|
||||
draws: firstNumber(data.tld, leadershipTactics.draws),
|
||||
losses: firstNumber(data.tll, leadershipTactics.losses),
|
||||
},
|
||||
intelligence: {
|
||||
wins: firstNumber(data.tiw, intelligenceTactics.wins),
|
||||
draws: firstNumber(data.tid, intelligenceTactics.draws),
|
||||
losses: firstNumber(data.til, intelligenceTactics.losses),
|
||||
},
|
||||
},
|
||||
},
|
||||
history,
|
||||
availability: {
|
||||
mastery: mastery.some((value) => value !== null),
|
||||
battleAggregates: [battles, wins, losses, killedCrew, lostCrew].some((value) => value !== null),
|
||||
tactics: tacticsAvailable,
|
||||
history: history.length > 0,
|
||||
battleDetailLogs: false,
|
||||
battleResultLogs: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE "app_user"
|
||||
ADD COLUMN "password_reset_required" BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
UPDATE "app_user"
|
||||
SET "password_reset_required" = TRUE
|
||||
WHERE "password_hash" ~ '^[[:xdigit:]]{128}$';
|
||||
@@ -82,6 +82,7 @@ model AppUser {
|
||||
displayName String @unique @map("display_name")
|
||||
passwordHash String @map("password_hash")
|
||||
passwordSalt String @map("password_salt")
|
||||
passwordResetRequired Boolean @default(false) @map("password_reset_required")
|
||||
roles Json @default(dbgenerated("'[]'::jsonb"))
|
||||
sanctions Json @default(dbgenerated("'{}'::jsonb"))
|
||||
oauthType OAuthType @default(NONE) @map("oauth_type")
|
||||
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
CREATE SCHEMA IF NOT EXISTS "legacy_archive";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "legacy_archive"."import_run" (
|
||||
"id" BIGSERIAL PRIMARY KEY,
|
||||
"source_profile" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL,
|
||||
"started_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"finished_at" TIMESTAMP(3),
|
||||
"counts" JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
"source_format_summary" JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
"error" TEXT,
|
||||
CONSTRAINT "legacy_archive_import_run_profile_check"
|
||||
CHECK ("source_profile" IN ('che', 'kwe', 'pwe', 'twe', 'nya', 'pya', 'hwe')),
|
||||
CONSTRAINT "legacy_archive_import_run_status_check"
|
||||
CHECK ("status" IN ('RUNNING', 'COMPLETED', 'FAILED'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "legacy_archive_import_run_profile_started"
|
||||
ON "legacy_archive"."import_run" ("source_profile", "started_at" DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "legacy_archive"."game_history" (
|
||||
"source_profile" TEXT NOT NULL,
|
||||
"server_id" TEXT NOT NULL,
|
||||
"legacy_id" INTEGER NOT NULL,
|
||||
"opened_at" TIMESTAMP(3) NOT NULL,
|
||||
"completed_at" TIMESTAMP(3),
|
||||
"legacy_date" TIMESTAMP(3) NOT NULL,
|
||||
"winner_nation" INTEGER,
|
||||
"map" TEXT,
|
||||
"season" INTEGER NOT NULL,
|
||||
"scenario" INTEGER NOT NULL,
|
||||
"scenario_name" TEXT NOT NULL,
|
||||
"raw_env" JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
"import_run_id" BIGINT NOT NULL REFERENCES "legacy_archive"."import_run" ("id"),
|
||||
PRIMARY KEY ("source_profile", "server_id")
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "legacy_archive_game_history_opened"
|
||||
ON "legacy_archive"."game_history" ("source_profile", "opened_at" DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "legacy_archive"."general" (
|
||||
"source_profile" TEXT NOT NULL,
|
||||
"server_id" TEXT NOT NULL,
|
||||
"general_no" INTEGER NOT NULL,
|
||||
"legacy_id" INTEGER NOT NULL,
|
||||
"owner" TEXT,
|
||||
"name" TEXT NOT NULL,
|
||||
"last_yearmonth" INTEGER NOT NULL,
|
||||
"turntime" TIMESTAMP(3) NOT NULL,
|
||||
"schema_version" INTEGER NOT NULL DEFAULT 1,
|
||||
"source_format" TEXT NOT NULL,
|
||||
"data" JSONB NOT NULL,
|
||||
"raw_data" JSONB NOT NULL,
|
||||
"import_run_id" BIGINT NOT NULL REFERENCES "legacy_archive"."import_run" ("id"),
|
||||
PRIMARY KEY ("source_profile", "server_id", "general_no"),
|
||||
CONSTRAINT "legacy_archive_general_schema_version_check" CHECK ("schema_version" = 1)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "legacy_archive_general_owner_opened"
|
||||
ON "legacy_archive"."general" ("owner", "source_profile", "server_id");
|
||||
CREATE INDEX IF NOT EXISTS "legacy_archive_general_name"
|
||||
ON "legacy_archive"."general" ("source_profile", "server_id", "name");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "legacy_archive"."nation" (
|
||||
"source_profile" TEXT NOT NULL,
|
||||
"legacy_id" INTEGER NOT NULL,
|
||||
"server_id" TEXT NOT NULL,
|
||||
"nation" INTEGER NOT NULL,
|
||||
"data" JSONB NOT NULL,
|
||||
"archived_at" TIMESTAMP(3) NOT NULL,
|
||||
"import_run_id" BIGINT NOT NULL REFERENCES "legacy_archive"."import_run" ("id"),
|
||||
PRIMARY KEY ("source_profile", "legacy_id")
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "legacy_archive_nation_server"
|
||||
ON "legacy_archive"."nation" ("source_profile", "server_id", "nation", "archived_at" DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "legacy_archive"."hall" (
|
||||
"source_profile" TEXT NOT NULL,
|
||||
"legacy_id" INTEGER NOT NULL,
|
||||
"server_id" TEXT NOT NULL,
|
||||
"season" INTEGER NOT NULL,
|
||||
"scenario" INTEGER NOT NULL,
|
||||
"general_no" INTEGER NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"value" DOUBLE PRECISION NOT NULL,
|
||||
"owner" TEXT,
|
||||
"aux" JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
"import_run_id" BIGINT NOT NULL REFERENCES "legacy_archive"."import_run" ("id"),
|
||||
PRIMARY KEY ("source_profile", "server_id", "type", "general_no")
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "legacy_archive_hall_scenario"
|
||||
ON "legacy_archive"."hall" ("source_profile", "season", "scenario", "type", "value" DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "legacy_archive"."emperor" (
|
||||
"id" BIGSERIAL PRIMARY KEY,
|
||||
"source_profile" TEXT NOT NULL,
|
||||
"legacy_id" INTEGER NOT NULL,
|
||||
"server_id" TEXT,
|
||||
"data" JSONB NOT NULL,
|
||||
"import_run_id" BIGINT NOT NULL REFERENCES "legacy_archive"."import_run" ("id"),
|
||||
CONSTRAINT "legacy_archive_emperor_source_key" UNIQUE ("source_profile", "legacy_id")
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "legacy_archive_emperor_server"
|
||||
ON "legacy_archive"."emperor" ("source_profile", "server_id", "id" DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "legacy_archive"."yearbook" (
|
||||
"source_profile" TEXT NOT NULL,
|
||||
"legacy_id" INTEGER NOT NULL,
|
||||
"profile_name" TEXT NOT NULL,
|
||||
"year" INTEGER NOT NULL,
|
||||
"month" INTEGER NOT NULL,
|
||||
"map" JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
"nations" JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
"global_history" JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
"global_action" JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
"content_hash" TEXT NOT NULL,
|
||||
"import_run_id" BIGINT NOT NULL REFERENCES "legacy_archive"."import_run" ("id"),
|
||||
PRIMARY KEY ("source_profile", "legacy_id")
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "legacy_archive_yearbook_month"
|
||||
ON "legacy_archive"."yearbook" ("source_profile", "profile_name", "year", "month", "legacy_id");
|
||||
Reference in New Issue
Block a user