db: init amq db schema
This commit is contained in:
@@ -1,17 +1,411 @@
|
||||
import { sqliteTable } from "drizzle-orm/sqlite-core";
|
||||
import { relations } from "drizzle-orm";
|
||||
import {
|
||||
index,
|
||||
integer,
|
||||
primaryKey,
|
||||
sqliteTable,
|
||||
text,
|
||||
uniqueIndex,
|
||||
} from "drizzle-orm/sqlite-core";
|
||||
|
||||
export const animeTable = sqliteTable("anime", {
|
||||
// TODO
|
||||
});
|
||||
/**
|
||||
* Normalized schema for AMQ source data (imported from JSON).
|
||||
*
|
||||
* Source Zod types (src/lib/types/amq):
|
||||
* - AmqAnimeSchema (anime + names + genres + tags + songLinks)
|
||||
* - AmqSongSchema (song + category + artist/group ids for roles)
|
||||
* - AmqArtistSchema (artist + memberships + altNames)
|
||||
* - AmqGroupSchema (group + members + altNames)
|
||||
*
|
||||
* Notes:
|
||||
* - We keep original AMQ integer identifiers where present (annId, songId, annSongId, songArtistId, songGroupId, etc.)
|
||||
* - Arrays (genres, tags, names, songLinks) are normalized to separate tables.
|
||||
* - Artists and groups are stored in separate tables: `artists` and `groups`.
|
||||
* - Membership and alt-names are normalized to join tables.
|
||||
* - Anime-song linkage from `songLinks` is stored in `anime_song_links` (unique per anime+song).
|
||||
*/
|
||||
|
||||
export const songsTable = sqliteTable("songs", {
|
||||
// TODO
|
||||
});
|
||||
// ----------------------
|
||||
// Core tables
|
||||
// ----------------------
|
||||
|
||||
export const animeSongsTable = sqliteTable("anime_songs", {
|
||||
// TODO
|
||||
});
|
||||
export const animeTable = sqliteTable(
|
||||
"anime",
|
||||
{
|
||||
/** AMQ anime ID */
|
||||
annId: integer("ann_id").notNull().primaryKey(),
|
||||
|
||||
export const artistsTable = sqliteTable("artists", {
|
||||
// TODO
|
||||
});
|
||||
// External IDs from the source
|
||||
aniListId: integer("anilist_id"),
|
||||
malId: integer("mal_id").notNull(),
|
||||
kitsuId: integer("kitsu_id"),
|
||||
|
||||
// Category object (name + number that can be number|string|null in source)
|
||||
categoryName: text("category_name").notNull(),
|
||||
categoryNumber: text("category_number"),
|
||||
|
||||
// Names
|
||||
mainName: text("main_name").notNull(),
|
||||
mainNameEn: text("main_name_en"),
|
||||
mainNameJa: text("main_name_ja"),
|
||||
|
||||
// Season/year
|
||||
year: integer("year").notNull(),
|
||||
seasonId: integer("season_id").notNull(), // 0..3 from Season enum
|
||||
|
||||
// Counts
|
||||
opCount: integer("op_count").notNull(),
|
||||
edCount: integer("ed_count").notNull(),
|
||||
insertCount: integer("insert_count").notNull(),
|
||||
},
|
||||
(t) => ({
|
||||
aniListIndex: uniqueIndex("anime_anilist_id_uq").on(t.aniListId),
|
||||
malIndex: uniqueIndex("anime_mal_id_uq").on(t.malId),
|
||||
kitsuIndex: uniqueIndex("anime_kitsu_id_uq").on(t.kitsuId),
|
||||
}),
|
||||
);
|
||||
|
||||
export const songsTable = sqliteTable(
|
||||
"songs",
|
||||
{
|
||||
/** AMQ annSongId (ANN song id) */
|
||||
annSongId: integer("ann_song_id").notNull().primaryKey(),
|
||||
|
||||
/** AMQ songId */
|
||||
songId: integer("song_id").notNull(),
|
||||
|
||||
name: text("name").notNull(),
|
||||
|
||||
/**
|
||||
* AmqSongCategory enum from the source:
|
||||
* none(0), instrumental(1), chanting(2), character(3), standard(4)
|
||||
*/
|
||||
category: integer("category").notNull(),
|
||||
},
|
||||
(t) => ({
|
||||
songIdIndex: index("songs_song_id_idx").on(t.songId),
|
||||
}),
|
||||
);
|
||||
|
||||
export const artistsTable = sqliteTable(
|
||||
"artists",
|
||||
{
|
||||
/** AMQ songArtistId */
|
||||
songArtistId: integer("song_artist_id").notNull().primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
},
|
||||
(t) => ({
|
||||
nameIndex: index("artists_name_idx").on(t.name),
|
||||
}),
|
||||
);
|
||||
|
||||
export const groupsTable = sqliteTable(
|
||||
"groups",
|
||||
{
|
||||
/** AMQ songGroupId */
|
||||
songGroupId: integer("song_group_id").notNull().primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
},
|
||||
(t) => ({
|
||||
nameIndex: index("groups_name_idx").on(t.name),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Artist -> Groups membership (from AmqArtistSchema.inGroups)
|
||||
*/
|
||||
export const artistGroupsTable = sqliteTable(
|
||||
"artist_groups",
|
||||
{
|
||||
songArtistId: integer("song_artist_id")
|
||||
.notNull()
|
||||
.references(() => artistsTable.songArtistId, { onDelete: "cascade" }),
|
||||
songGroupId: integer("song_group_id")
|
||||
.notNull()
|
||||
.references(() => groupsTable.songGroupId, { onDelete: "cascade" }),
|
||||
},
|
||||
(t) => ({
|
||||
pk: primaryKey({
|
||||
name: "artist_groups_pk",
|
||||
columns: [t.songArtistId, t.songGroupId],
|
||||
}),
|
||||
artistIndex: index("artist_groups_artist_id_idx").on(t.songArtistId),
|
||||
groupIndex: index("artist_groups_group_id_idx").on(t.songGroupId),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Group -> Artist members (from AmqGroupSchema.artistMembers)
|
||||
*/
|
||||
export const groupArtistMembersTable = sqliteTable(
|
||||
"group_artist_members",
|
||||
{
|
||||
songGroupId: integer("song_group_id")
|
||||
.notNull()
|
||||
.references(() => groupsTable.songGroupId, { onDelete: "cascade" }),
|
||||
songArtistId: integer("song_artist_id")
|
||||
.notNull()
|
||||
.references(() => artistsTable.songArtistId, { onDelete: "cascade" }),
|
||||
},
|
||||
(t) => ({
|
||||
pk: primaryKey({
|
||||
name: "group_artist_members_pk",
|
||||
columns: [t.songGroupId, t.songArtistId],
|
||||
}),
|
||||
groupIndex: index("group_artist_members_group_id_idx").on(t.songGroupId),
|
||||
artistIndex: index("group_artist_members_artist_id_idx").on(t.songArtistId),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Group -> Group members (from AmqGroupSchema.groupMembers)
|
||||
*/
|
||||
export const groupGroupMembersTable = sqliteTable(
|
||||
"group_group_members",
|
||||
{
|
||||
songGroupId: integer("song_group_id")
|
||||
.notNull()
|
||||
.references(() => groupsTable.songGroupId, { onDelete: "cascade" }),
|
||||
memberSongGroupId: integer("member_song_group_id")
|
||||
.notNull()
|
||||
.references(() => groupsTable.songGroupId, { onDelete: "cascade" }),
|
||||
},
|
||||
(t) => ({
|
||||
pk: primaryKey({
|
||||
name: "group_group_members_pk",
|
||||
columns: [t.songGroupId, t.memberSongGroupId],
|
||||
}),
|
||||
groupIndex: index("group_group_members_group_id_idx").on(t.songGroupId),
|
||||
memberIndex: index("group_group_members_member_group_id_idx").on(
|
||||
t.memberSongGroupId,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Alternative names for artists and groups (both schemas use:
|
||||
* { songGroupId, name }
|
||||
*
|
||||
* Interpreted as: "the alias `name` used in/for group `songGroupId`".
|
||||
*/
|
||||
export const artistAltNamesTable = sqliteTable(
|
||||
"artist_alt_names",
|
||||
{
|
||||
id: integer("id").notNull().primaryKey({ autoIncrement: true }),
|
||||
/**
|
||||
* The "owning" artist.
|
||||
*/
|
||||
songArtistId: integer("song_artist_id")
|
||||
.notNull()
|
||||
.references(() => artistsTable.songArtistId, { onDelete: "cascade" }),
|
||||
|
||||
/**
|
||||
* The alternate-name entry is keyed by an artist id in the source:
|
||||
* { songArtistId, name }
|
||||
*
|
||||
* Interpreted as: artist `songArtistId` is also known as `name`.
|
||||
*/
|
||||
altSongArtistId: integer("alt_song_artist_id")
|
||||
.notNull()
|
||||
.references(() => artistsTable.songArtistId, { onDelete: "cascade" }),
|
||||
|
||||
name: text("name").notNull(),
|
||||
},
|
||||
(t) => ({
|
||||
artistIndex: index("artist_alt_names_artist_id_idx").on(t.songArtistId),
|
||||
altArtistIndex: index("artist_alt_names_alt_artist_id_idx").on(
|
||||
t.altSongArtistId,
|
||||
),
|
||||
nameIndex: index("artist_alt_names_name_idx").on(t.name),
|
||||
uniquePerArtistAltArtistName: uniqueIndex(
|
||||
"artist_alt_names_artist_alt_artist_name_uq",
|
||||
).on(t.songArtistId, t.altSongArtistId, t.name),
|
||||
}),
|
||||
);
|
||||
|
||||
export const groupAltNamesTable = sqliteTable(
|
||||
"group_alt_names",
|
||||
{
|
||||
id: integer("id").notNull().primaryKey({ autoIncrement: true }),
|
||||
songGroupId: integer("song_group_id")
|
||||
.notNull()
|
||||
.references(() => groupsTable.songGroupId, { onDelete: "cascade" }),
|
||||
/**
|
||||
* In the source shape, this is also called `songGroupId` inside the altName object.
|
||||
* Here we store it as the "context group" the alias is associated with.
|
||||
*/
|
||||
contextSongGroupId: integer("context_song_group_id")
|
||||
.notNull()
|
||||
.references(() => groupsTable.songGroupId, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
},
|
||||
(t) => ({
|
||||
groupIndex: index("group_alt_names_group_id_idx").on(t.songGroupId),
|
||||
contextGroupIndex: index("group_alt_names_context_group_id_idx").on(
|
||||
t.contextSongGroupId,
|
||||
),
|
||||
nameIndex: index("group_alt_names_name_idx").on(t.name),
|
||||
uniquePerGroupContextName: uniqueIndex(
|
||||
"group_alt_names_group_context_name_uq",
|
||||
).on(t.songGroupId, t.contextSongGroupId, t.name),
|
||||
}),
|
||||
);
|
||||
|
||||
// ----------------------
|
||||
// Anime: names / genres / tags
|
||||
// ----------------------
|
||||
|
||||
export const animeNamesTable = sqliteTable(
|
||||
"anime_names",
|
||||
{
|
||||
id: integer("id").notNull().primaryKey({ autoIncrement: true }),
|
||||
annId: integer("ann_id")
|
||||
.notNull()
|
||||
.references(() => animeTable.annId, { onDelete: "cascade" }),
|
||||
|
||||
/** "EN" | "JA" per source */
|
||||
language: text("language").notNull(),
|
||||
|
||||
name: text("name").notNull(),
|
||||
},
|
||||
(t) => ({
|
||||
animeIndex: index("anime_names_ann_id_idx").on(t.annId),
|
||||
nameIndex: index("anime_names_name_idx").on(t.name),
|
||||
uniquePerAnime: uniqueIndex("anime_names_ann_lang_name_uq").on(
|
||||
t.annId,
|
||||
t.language,
|
||||
t.name,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
export const animeGenresTable = sqliteTable(
|
||||
"anime_genres",
|
||||
{
|
||||
id: integer("id").notNull().primaryKey({ autoIncrement: true }),
|
||||
annId: integer("ann_id")
|
||||
.notNull()
|
||||
.references(() => animeTable.annId, { onDelete: "cascade" }),
|
||||
genre: text("genre").notNull(),
|
||||
},
|
||||
(t) => ({
|
||||
animeIndex: index("anime_genres_ann_id_idx").on(t.annId),
|
||||
genreIndex: index("anime_genres_genre_idx").on(t.genre),
|
||||
uniquePerAnime: uniqueIndex("anime_genres_ann_genre_uq").on(
|
||||
t.annId,
|
||||
t.genre,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
export const animeTagsTable = sqliteTable(
|
||||
"anime_tags",
|
||||
{
|
||||
id: integer("id").notNull().primaryKey({ autoIncrement: true }),
|
||||
annId: integer("ann_id")
|
||||
.notNull()
|
||||
.references(() => animeTable.annId, { onDelete: "cascade" }),
|
||||
tag: text("tag").notNull(),
|
||||
},
|
||||
(t) => ({
|
||||
animeIndex: index("anime_tags_ann_id_idx").on(t.annId),
|
||||
tagIndex: index("anime_tags_tag_idx").on(t.tag),
|
||||
uniquePerAnime: uniqueIndex("anime_tags_ann_tag_uq").on(t.annId, t.tag),
|
||||
}),
|
||||
);
|
||||
|
||||
// ----------------------
|
||||
// Anime <-> Songs links (from AmqSongLink)
|
||||
// ----------------------
|
||||
|
||||
export const animeSongLinksTable = sqliteTable(
|
||||
"anime_song_links",
|
||||
{
|
||||
annId: integer("ann_id")
|
||||
.notNull()
|
||||
.references(() => animeTable.annId, { onDelete: "cascade" }),
|
||||
|
||||
annSongId: integer("ann_song_id")
|
||||
.notNull()
|
||||
.references(() => songsTable.annSongId, { onDelete: "cascade" }),
|
||||
|
||||
/** 1(OP) | 2(ED) | 3(INS) per SongLinkType */
|
||||
type: integer("type").notNull(),
|
||||
|
||||
/** link number within type (1-based in source) */
|
||||
number: integer("number").notNull(),
|
||||
|
||||
/** boolean ints 0/1 in source */
|
||||
uploaded: integer("uploaded").notNull(),
|
||||
rebroadcast: integer("rebroadcast").notNull(),
|
||||
dub: integer("dub").notNull(),
|
||||
},
|
||||
(t) => ({
|
||||
pk: primaryKey({ name: "anime_songs_pk", columns: [t.annId, t.annSongId] }),
|
||||
annIdIndex: index("anime_songs_ann_id_idx").on(t.annId),
|
||||
songIdIndex: index("anime_songs_song_id_idx").on(t.annSongId),
|
||||
}),
|
||||
);
|
||||
|
||||
// ----------------------
|
||||
// Relations (optional but helpful for Drizzle queries)
|
||||
// ----------------------
|
||||
|
||||
export const animeRelations = relations(animeTable, ({ many }) => ({
|
||||
names: many(animeNamesTable),
|
||||
genres: many(animeGenresTable),
|
||||
tags: many(animeTagsTable),
|
||||
songLinks: many(animeSongLinksTable),
|
||||
}));
|
||||
|
||||
export const songRelations = relations(songsTable, ({ many }) => ({
|
||||
animeLinks: many(animeSongLinksTable),
|
||||
}));
|
||||
|
||||
export const artistRelations = relations(artistsTable, ({ many }) => ({
|
||||
inGroups: many(artistGroupsTable),
|
||||
altNames: many(artistAltNamesTable),
|
||||
groupMemberships: many(groupArtistMembersTable),
|
||||
}));
|
||||
|
||||
export const groupRelations = relations(groupsTable, ({ many }) => ({
|
||||
artists: many(artistGroupsTable),
|
||||
artistMembers: many(groupArtistMembersTable),
|
||||
groupMembers: many(groupGroupMembersTable),
|
||||
altNames: many(groupAltNamesTable),
|
||||
}));
|
||||
|
||||
export const animeNamesRelations = relations(animeNamesTable, ({ one }) => ({
|
||||
anime: one(animeTable, {
|
||||
fields: [animeNamesTable.annId],
|
||||
references: [animeTable.annId],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const animeGenresRelations = relations(animeGenresTable, ({ one }) => ({
|
||||
anime: one(animeTable, {
|
||||
fields: [animeGenresTable.annId],
|
||||
references: [animeTable.annId],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const animeTagsRelations = relations(animeTagsTable, ({ one }) => ({
|
||||
anime: one(animeTable, {
|
||||
fields: [animeTagsTable.annId],
|
||||
references: [animeTable.annId],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const animeSongLinksRelations = relations(
|
||||
animeSongLinksTable,
|
||||
({ one }) => ({
|
||||
anime: one(animeTable, {
|
||||
fields: [animeSongLinksTable.annId],
|
||||
references: [animeTable.annId],
|
||||
}),
|
||||
song: one(songsTable, {
|
||||
fields: [animeSongLinksTable.annSongId],
|
||||
references: [songsTable.annSongId],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user