db: split schema into separate files
This commit is contained in:
@@ -1,502 +1,21 @@
|
|||||||
import { relations } from "drizzle-orm";
|
// Re-export all schema tables + relations from their dedicated modules.
|
||||||
import {
|
//
|
||||||
index,
|
// This replaces the previous monolithic schema file while keeping the
|
||||||
integer,
|
// public import surface stable (e.g. `import { animeTable } from "$lib/db/schema"`).
|
||||||
primaryKey,
|
|
||||||
real,
|
|
||||||
sqliteTable,
|
|
||||||
text,
|
|
||||||
uniqueIndex,
|
|
||||||
} from "drizzle-orm/sqlite-core";
|
|
||||||
|
|
||||||
/**
|
export * from "./relations";
|
||||||
* Normalized schema for AMQ source data (imported from JSON).
|
export * from "./tables/anime";
|
||||||
*
|
export * from "./tables/anime-genres";
|
||||||
* Source Zod types (src/lib/types/amq):
|
export * from "./tables/anime-names";
|
||||||
* - AmqAnimeSchema (anime + names + genres + tags + songLinks)
|
export * from "./tables/anime-song-links";
|
||||||
* - AmqSongSchema (song + category + artist/group ids for roles)
|
export * from "./tables/anime-tags";
|
||||||
* - AmqArtistSchema (artist + memberships + altNames)
|
export * from "./tables/artist-alt-names";
|
||||||
* - AmqGroupSchema (group + members + altNames)
|
export * from "./tables/artist-groups";
|
||||||
*
|
export * from "./tables/artists";
|
||||||
* Notes:
|
export * from "./tables/genres";
|
||||||
* - We keep original AMQ integer identifiers where present (annId, songId, annSongId, songArtistId, songGroupId, etc.)
|
export * from "./tables/group-alt-names";
|
||||||
* - Arrays (genres, tags, names, songLinks) are normalized to separate tables.
|
export * from "./tables/group-artist-members";
|
||||||
* - Artists and groups are stored in separate tables: `artists` and `groups`.
|
export * from "./tables/group-group-members";
|
||||||
* - Membership and alt-names are normalized to join tables.
|
export * from "./tables/groups";
|
||||||
* - Anime-song linkage from `songLinks` is stored in `anime_song_links` (unique per anime+song).
|
export * from "./tables/songs";
|
||||||
*/
|
export * from "./tables/tags";
|
||||||
|
|
||||||
// ----------------------
|
|
||||||
// Core tables
|
|
||||||
// ----------------------
|
|
||||||
|
|
||||||
export const animeTable = sqliteTable(
|
|
||||||
"anime",
|
|
||||||
{
|
|
||||||
/** AMQ anime ID */
|
|
||||||
annId: integer("ann_id").notNull().primaryKey(),
|
|
||||||
|
|
||||||
// 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: index("anime_anilist_id_uq").on(t.aniListId),
|
|
||||||
malIndex: index("anime_mal_id_uq").on(t.malId),
|
|
||||||
kitsuIndex: index("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(),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Song audio filename used by the AMQ CDN:
|
|
||||||
* https://nawdist.animemusicquiz.com/{fileName}
|
|
||||||
*/
|
|
||||||
fileName: text("file_name"),
|
|
||||||
fileName480: text("file_name_480"),
|
|
||||||
fileName720: text("file_name_720"),
|
|
||||||
meanVolume: real("mean_volume"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Primary artist/group ids for this song (nullable in source).
|
|
||||||
* These reference existing `artists` / `groups` rows.
|
|
||||||
*/
|
|
||||||
songArtistId: integer("song_artist_id").references(
|
|
||||||
() => artistsTable.songArtistId,
|
|
||||||
{ onDelete: "set null" },
|
|
||||||
),
|
|
||||||
songGroupId: integer("song_group_id").references(
|
|
||||||
() => groupsTable.songGroupId,
|
|
||||||
{ onDelete: "set null" },
|
|
||||||
),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Additional contributor ids (nullable in source)
|
|
||||||
*/
|
|
||||||
composerArtistId: integer("composer_artist_id").references(
|
|
||||||
() => artistsTable.songArtistId,
|
|
||||||
{ onDelete: "set null" },
|
|
||||||
),
|
|
||||||
composerGroupId: integer("composer_group_id").references(
|
|
||||||
() => groupsTable.songGroupId,
|
|
||||||
{ onDelete: "set null" },
|
|
||||||
),
|
|
||||||
arrangerArtistId: integer("arranger_artist_id").references(
|
|
||||||
() => artistsTable.songArtistId,
|
|
||||||
{ onDelete: "set null" },
|
|
||||||
),
|
|
||||||
arrangerGroupId: integer("arranger_group_id").references(
|
|
||||||
() => groupsTable.songGroupId,
|
|
||||||
{ onDelete: "set null" },
|
|
||||||
),
|
|
||||||
},
|
|
||||||
(t) => ({
|
|
||||||
songIdIndex: index("songs_song_id_idx").on(t.songId),
|
|
||||||
songArtistIdIndex: index("songs_song_artist_id_idx").on(t.songArtistId),
|
|
||||||
songGroupIdIndex: index("songs_song_group_id_idx").on(t.songGroupId),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
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 genresTable = sqliteTable(
|
|
||||||
"genres",
|
|
||||||
{
|
|
||||||
/** Primary key is the genre string itself */
|
|
||||||
name: text("name").notNull().primaryKey(),
|
|
||||||
},
|
|
||||||
(t) => ({
|
|
||||||
nameIndex: index("genres_name_idx").on(t.name),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const animeGenresTable = sqliteTable(
|
|
||||||
"anime_genres",
|
|
||||||
{
|
|
||||||
annId: integer("ann_id")
|
|
||||||
.notNull()
|
|
||||||
.references(() => animeTable.annId, { onDelete: "cascade" }),
|
|
||||||
genreName: text("genre_name")
|
|
||||||
.notNull()
|
|
||||||
.references(() => genresTable.name, { onDelete: "cascade" }),
|
|
||||||
},
|
|
||||||
(t) => ({
|
|
||||||
pk: primaryKey({
|
|
||||||
name: "anime_genres_pk",
|
|
||||||
columns: [t.annId, t.genreName],
|
|
||||||
}),
|
|
||||||
animeIndex: index("anime_genres_ann_id_idx").on(t.annId),
|
|
||||||
genreIndex: index("anime_genres_genre_name_idx").on(t.genreName),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const tagsTable = sqliteTable(
|
|
||||||
"tags",
|
|
||||||
{
|
|
||||||
/** Primary key is the tag string itself */
|
|
||||||
name: text("name").notNull().primaryKey(),
|
|
||||||
},
|
|
||||||
(t) => ({
|
|
||||||
nameIndex: index("tags_name_idx").on(t.name),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const animeTagsTable = sqliteTable(
|
|
||||||
"anime_tags",
|
|
||||||
{
|
|
||||||
annId: integer("ann_id")
|
|
||||||
.notNull()
|
|
||||||
.references(() => animeTable.annId, { onDelete: "cascade" }),
|
|
||||||
tagName: text("tag_name")
|
|
||||||
.notNull()
|
|
||||||
.references(() => tagsTable.name, { onDelete: "cascade" }),
|
|
||||||
},
|
|
||||||
(t) => ({
|
|
||||||
pk: primaryKey({
|
|
||||||
name: "anime_tags_pk",
|
|
||||||
columns: [t.annId, t.tagName],
|
|
||||||
}),
|
|
||||||
animeIndex: index("anime_tags_ann_id_idx").on(t.annId),
|
|
||||||
tagIndex: index("anime_tags_tag_name_idx").on(t.tagName),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// ----------------------
|
|
||||||
// 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],
|
|
||||||
}),
|
|
||||||
genre: one(genresTable, {
|
|
||||||
fields: [animeGenresTable.genreName],
|
|
||||||
references: [genresTable.name],
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const animeTagsRelations = relations(animeTagsTable, ({ one }) => ({
|
|
||||||
anime: one(animeTable, {
|
|
||||||
fields: [animeTagsTable.annId],
|
|
||||||
references: [animeTable.annId],
|
|
||||||
}),
|
|
||||||
tag: one(tagsTable, {
|
|
||||||
fields: [animeTagsTable.tagName],
|
|
||||||
references: [tagsTable.name],
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const animeSongLinksRelations = relations(
|
|
||||||
animeSongLinksTable,
|
|
||||||
({ one }) => ({
|
|
||||||
anime: one(animeTable, {
|
|
||||||
fields: [animeSongLinksTable.annId],
|
|
||||||
references: [animeTable.annId],
|
|
||||||
}),
|
|
||||||
song: one(songsTable, {
|
|
||||||
fields: [animeSongLinksTable.annSongId],
|
|
||||||
references: [songsTable.annSongId],
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|||||||
87
src/lib/db/schema/relations.ts
Normal file
87
src/lib/db/schema/relations.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { relations } from "drizzle-orm";
|
||||||
|
import { animeTable } from "./tables/anime";
|
||||||
|
import { animeGenresTable } from "./tables/anime-genres";
|
||||||
|
import { animeNamesTable } from "./tables/anime-names";
|
||||||
|
import { animeSongLinksTable } from "./tables/anime-song-links";
|
||||||
|
import { animeTagsTable } from "./tables/anime-tags";
|
||||||
|
import { artistAltNamesTable } from "./tables/artist-alt-names";
|
||||||
|
import { artistGroupsTable } from "./tables/artist-groups";
|
||||||
|
import { artistsTable } from "./tables/artists";
|
||||||
|
import { genresTable } from "./tables/genres";
|
||||||
|
import { groupAltNamesTable } from "./tables/group-alt-names";
|
||||||
|
import { groupArtistMembersTable } from "./tables/group-artist-members";
|
||||||
|
import { groupGroupMembersTable } from "./tables/group-group-members";
|
||||||
|
import { groupsTable } from "./tables/groups";
|
||||||
|
import { songsTable } from "./tables/songs";
|
||||||
|
import { tagsTable } from "./tables/tags";
|
||||||
|
|
||||||
|
// ----------------------
|
||||||
|
// 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],
|
||||||
|
}),
|
||||||
|
genre: one(genresTable, {
|
||||||
|
fields: [animeGenresTable.genreName],
|
||||||
|
references: [genresTable.name],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const animeTagsRelations = relations(animeTagsTable, ({ one }) => ({
|
||||||
|
anime: one(animeTable, {
|
||||||
|
fields: [animeTagsTable.annId],
|
||||||
|
references: [animeTable.annId],
|
||||||
|
}),
|
||||||
|
tag: one(tagsTable, {
|
||||||
|
fields: [animeTagsTable.tagName],
|
||||||
|
references: [tagsTable.name],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const animeSongLinksRelations = relations(
|
||||||
|
animeSongLinksTable,
|
||||||
|
({ one }) => ({
|
||||||
|
anime: one(animeTable, {
|
||||||
|
fields: [animeSongLinksTable.annId],
|
||||||
|
references: [animeTable.annId],
|
||||||
|
}),
|
||||||
|
song: one(songsTable, {
|
||||||
|
fields: [animeSongLinksTable.annSongId],
|
||||||
|
references: [songsTable.annSongId],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
34
src/lib/db/schema/tables/anime-genres.ts
Normal file
34
src/lib/db/schema/tables/anime-genres.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import {
|
||||||
|
index,
|
||||||
|
integer,
|
||||||
|
primaryKey,
|
||||||
|
sqliteTable,
|
||||||
|
text,
|
||||||
|
} from "drizzle-orm/sqlite-core";
|
||||||
|
import { animeTable } from "./anime";
|
||||||
|
import { genresTable } from "./genres";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Join table: Anime -> Genres
|
||||||
|
*
|
||||||
|
* Source: AmqAnimeSchema.genres (string[])
|
||||||
|
*/
|
||||||
|
export const animeGenresTable = sqliteTable(
|
||||||
|
"anime_genres",
|
||||||
|
{
|
||||||
|
annId: integer("ann_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => animeTable.annId, { onDelete: "cascade" }),
|
||||||
|
genreName: text("genre_name")
|
||||||
|
.notNull()
|
||||||
|
.references(() => genresTable.name, { onDelete: "cascade" }),
|
||||||
|
},
|
||||||
|
(t) => ({
|
||||||
|
pk: primaryKey({
|
||||||
|
name: "anime_genres_pk",
|
||||||
|
columns: [t.annId, t.genreName],
|
||||||
|
}),
|
||||||
|
animeIndex: index("anime_genres_ann_id_idx").on(t.annId),
|
||||||
|
genreIndex: index("anime_genres_genre_name_idx").on(t.genreName),
|
||||||
|
}),
|
||||||
|
);
|
||||||
40
src/lib/db/schema/tables/anime-names.ts
Normal file
40
src/lib/db/schema/tables/anime-names.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import {
|
||||||
|
index,
|
||||||
|
integer,
|
||||||
|
sqliteTable,
|
||||||
|
text,
|
||||||
|
uniqueIndex,
|
||||||
|
} from "drizzle-orm/sqlite-core";
|
||||||
|
import { animeTable } from "./anime";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional localized/alternative names for an anime.
|
||||||
|
*
|
||||||
|
* Source: AmqAnimeSchema.names
|
||||||
|
* - language: "EN" | "JA" (per source)
|
||||||
|
* - name: string
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
45
src/lib/db/schema/tables/anime-song-links.ts
Normal file
45
src/lib/db/schema/tables/anime-song-links.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
index,
|
||||||
|
integer,
|
||||||
|
primaryKey,
|
||||||
|
sqliteTable,
|
||||||
|
} from "drizzle-orm/sqlite-core";
|
||||||
|
import { animeTable } from "./anime";
|
||||||
|
import { songsTable } from "./songs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Join table: Anime <-> Songs links
|
||||||
|
*
|
||||||
|
* Source: AmqAnimeSchema.songLinks (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),
|
||||||
|
annSongIdIndex: index("anime_songs_song_id_idx").on(t.annSongId),
|
||||||
|
}),
|
||||||
|
);
|
||||||
34
src/lib/db/schema/tables/anime-tags.ts
Normal file
34
src/lib/db/schema/tables/anime-tags.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import {
|
||||||
|
index,
|
||||||
|
integer,
|
||||||
|
primaryKey,
|
||||||
|
sqliteTable,
|
||||||
|
text,
|
||||||
|
} from "drizzle-orm/sqlite-core";
|
||||||
|
import { animeTable } from "./anime";
|
||||||
|
import { tagsTable } from "./tags";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Join table: Anime -> Tags
|
||||||
|
*
|
||||||
|
* Source: AmqAnimeSchema.tags (string[])
|
||||||
|
*/
|
||||||
|
export const animeTagsTable = sqliteTable(
|
||||||
|
"anime_tags",
|
||||||
|
{
|
||||||
|
annId: integer("ann_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => animeTable.annId, { onDelete: "cascade" }),
|
||||||
|
tagName: text("tag_name")
|
||||||
|
.notNull()
|
||||||
|
.references(() => tagsTable.name, { onDelete: "cascade" }),
|
||||||
|
},
|
||||||
|
(t) => ({
|
||||||
|
pk: primaryKey({
|
||||||
|
name: "anime_tags_pk",
|
||||||
|
columns: [t.annId, t.tagName],
|
||||||
|
}),
|
||||||
|
animeIndex: index("anime_tags_ann_id_idx").on(t.annId),
|
||||||
|
tagIndex: index("anime_tags_tag_name_idx").on(t.tagName),
|
||||||
|
}),
|
||||||
|
);
|
||||||
45
src/lib/db/schema/tables/anime.ts
Normal file
45
src/lib/db/schema/tables/anime.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core `anime` table.
|
||||||
|
*
|
||||||
|
* Normalized schema for AMQ source data (imported from JSON).
|
||||||
|
* Keeps original AMQ integer identifiers where present (annId, etc.).
|
||||||
|
*
|
||||||
|
* Source: AmqAnimeSchema
|
||||||
|
*/
|
||||||
|
export const animeTable = sqliteTable(
|
||||||
|
"anime",
|
||||||
|
{
|
||||||
|
/** AMQ anime ID */
|
||||||
|
annId: integer("ann_id").notNull().primaryKey(),
|
||||||
|
|
||||||
|
// 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: index("anime_anilist_id_uq").on(t.aniListId),
|
||||||
|
malIndex: index("anime_mal_id_uq").on(t.malId),
|
||||||
|
kitsuIndex: index("anime_kitsu_id_uq").on(t.kitsuId),
|
||||||
|
}),
|
||||||
|
);
|
||||||
55
src/lib/db/schema/tables/artist-alt-names.ts
Normal file
55
src/lib/db/schema/tables/artist-alt-names.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import {
|
||||||
|
index,
|
||||||
|
integer,
|
||||||
|
sqliteTable,
|
||||||
|
text,
|
||||||
|
uniqueIndex,
|
||||||
|
} from "drizzle-orm/sqlite-core";
|
||||||
|
import { artistsTable } from "./artists";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alternative names for artists.
|
||||||
|
*
|
||||||
|
* Source: AmqArtistSchema.altNames
|
||||||
|
*
|
||||||
|
* Interpreted as: artist `songArtistId` is also known as `name` (optionally via altSongArtistId link).
|
||||||
|
*/
|
||||||
|
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 }
|
||||||
|
*
|
||||||
|
* Store as a linked artist row for referential integrity.
|
||||||
|
*/
|
||||||
|
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),
|
||||||
|
}),
|
||||||
|
);
|
||||||
35
src/lib/db/schema/tables/artist-groups.ts
Normal file
35
src/lib/db/schema/tables/artist-groups.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import {
|
||||||
|
index,
|
||||||
|
integer,
|
||||||
|
primaryKey,
|
||||||
|
sqliteTable,
|
||||||
|
} from "drizzle-orm/sqlite-core";
|
||||||
|
import { artistsTable } from "./artists";
|
||||||
|
import { groupsTable } from "./groups";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Join table: Artist -> Groups membership
|
||||||
|
*
|
||||||
|
* Source: 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),
|
||||||
|
}),
|
||||||
|
);
|
||||||
18
src/lib/db/schema/tables/artists.ts
Normal file
18
src/lib/db/schema/tables/artists.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core `artists` table.
|
||||||
|
*
|
||||||
|
* Source: AmqArtistSchema
|
||||||
|
*/
|
||||||
|
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),
|
||||||
|
}),
|
||||||
|
);
|
||||||
17
src/lib/db/schema/tables/genres.ts
Normal file
17
src/lib/db/schema/tables/genres.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { index, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core `genres` lookup table.
|
||||||
|
*
|
||||||
|
* Source: AmqAnimeSchema.genres (string[])
|
||||||
|
*/
|
||||||
|
export const genresTable = sqliteTable(
|
||||||
|
"genres",
|
||||||
|
{
|
||||||
|
/** Primary key is the genre string itself */
|
||||||
|
name: text("name").notNull().primaryKey(),
|
||||||
|
},
|
||||||
|
(t) => ({
|
||||||
|
nameIndex: index("genres_name_idx").on(t.name),
|
||||||
|
}),
|
||||||
|
);
|
||||||
51
src/lib/db/schema/tables/group-alt-names.ts
Normal file
51
src/lib/db/schema/tables/group-alt-names.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
index,
|
||||||
|
integer,
|
||||||
|
sqliteTable,
|
||||||
|
text,
|
||||||
|
uniqueIndex,
|
||||||
|
} from "drizzle-orm/sqlite-core";
|
||||||
|
import { groupsTable } from "./groups";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alternative names for groups.
|
||||||
|
*
|
||||||
|
* Source: AmqGroupSchema.altNames
|
||||||
|
*
|
||||||
|
* The source uses objects shaped like `{ songGroupId, name }`, but that nested
|
||||||
|
* `songGroupId` is effectively the *context group* the alias is associated with.
|
||||||
|
* We persist it as `contextSongGroupId` to make the meaning explicit.
|
||||||
|
*/
|
||||||
|
export const groupAltNamesTable = sqliteTable(
|
||||||
|
"group_alt_names",
|
||||||
|
{
|
||||||
|
id: integer("id").notNull().primaryKey({ autoIncrement: true }),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The "owning" group.
|
||||||
|
*/
|
||||||
|
songGroupId: integer("song_group_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => groupsTable.songGroupId, { onDelete: "cascade" }),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context group the alias is associated with.
|
||||||
|
* (This is the nested `songGroupId` value in the source.)
|
||||||
|
*/
|
||||||
|
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),
|
||||||
|
}),
|
||||||
|
);
|
||||||
35
src/lib/db/schema/tables/group-artist-members.ts
Normal file
35
src/lib/db/schema/tables/group-artist-members.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import {
|
||||||
|
index,
|
||||||
|
integer,
|
||||||
|
primaryKey,
|
||||||
|
sqliteTable,
|
||||||
|
} from "drizzle-orm/sqlite-core";
|
||||||
|
import { artistsTable } from "./artists";
|
||||||
|
import { groupsTable } from "./groups";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Join table: Group -> Artist members
|
||||||
|
*
|
||||||
|
* Source: 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),
|
||||||
|
}),
|
||||||
|
);
|
||||||
34
src/lib/db/schema/tables/group-group-members.ts
Normal file
34
src/lib/db/schema/tables/group-group-members.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import {
|
||||||
|
index,
|
||||||
|
integer,
|
||||||
|
primaryKey,
|
||||||
|
sqliteTable,
|
||||||
|
} from "drizzle-orm/sqlite-core";
|
||||||
|
import { groupsTable } from "./groups";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Join table: Group -> Group members
|
||||||
|
*
|
||||||
|
* Source: 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,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
18
src/lib/db/schema/tables/groups.ts
Normal file
18
src/lib/db/schema/tables/groups.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core `groups` table.
|
||||||
|
*
|
||||||
|
* Source: AmqGroupSchema
|
||||||
|
*/
|
||||||
|
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),
|
||||||
|
}),
|
||||||
|
);
|
||||||
80
src/lib/db/schema/tables/songs.ts
Normal file
80
src/lib/db/schema/tables/songs.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import {
|
||||||
|
index,
|
||||||
|
integer,
|
||||||
|
real,
|
||||||
|
sqliteTable,
|
||||||
|
text,
|
||||||
|
} from "drizzle-orm/sqlite-core";
|
||||||
|
import { artistsTable } from "./artists";
|
||||||
|
import { groupsTable } from "./groups";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core `songs` table.
|
||||||
|
*
|
||||||
|
* Source: AmqSongSchema
|
||||||
|
*/
|
||||||
|
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(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Song audio filename used by the AMQ CDN:
|
||||||
|
* https://nawdist.animemusicquiz.com/{fileName}
|
||||||
|
*/
|
||||||
|
fileName: text("file_name"),
|
||||||
|
fileName480: text("file_name_480"),
|
||||||
|
fileName720: text("file_name_720"),
|
||||||
|
meanVolume: real("mean_volume"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primary artist/group ids for this song (nullable in source).
|
||||||
|
* These reference existing `artists` / `groups` rows.
|
||||||
|
*/
|
||||||
|
songArtistId: integer("song_artist_id").references(
|
||||||
|
() => artistsTable.songArtistId,
|
||||||
|
{ onDelete: "set null" },
|
||||||
|
),
|
||||||
|
songGroupId: integer("song_group_id").references(
|
||||||
|
() => groupsTable.songGroupId,
|
||||||
|
{ onDelete: "set null" },
|
||||||
|
),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional contributor ids (nullable in source)
|
||||||
|
*/
|
||||||
|
composerArtistId: integer("composer_artist_id").references(
|
||||||
|
() => artistsTable.songArtistId,
|
||||||
|
{ onDelete: "set null" },
|
||||||
|
),
|
||||||
|
composerGroupId: integer("composer_group_id").references(
|
||||||
|
() => groupsTable.songGroupId,
|
||||||
|
{ onDelete: "set null" },
|
||||||
|
),
|
||||||
|
arrangerArtistId: integer("arranger_artist_id").references(
|
||||||
|
() => artistsTable.songArtistId,
|
||||||
|
{ onDelete: "set null" },
|
||||||
|
),
|
||||||
|
arrangerGroupId: integer("arranger_group_id").references(
|
||||||
|
() => groupsTable.songGroupId,
|
||||||
|
{ onDelete: "set null" },
|
||||||
|
),
|
||||||
|
},
|
||||||
|
(t) => ({
|
||||||
|
songIdIndex: index("songs_song_id_idx").on(t.songId),
|
||||||
|
songArtistIdIndex: index("songs_song_artist_id_idx").on(t.songArtistId),
|
||||||
|
songGroupIdIndex: index("songs_song_group_id_idx").on(t.songGroupId),
|
||||||
|
}),
|
||||||
|
);
|
||||||
17
src/lib/db/schema/tables/tags.ts
Normal file
17
src/lib/db/schema/tables/tags.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { index, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core `tags` lookup table.
|
||||||
|
*
|
||||||
|
* Source: AmqAnimeSchema.tags (string[])
|
||||||
|
*/
|
||||||
|
export const tagsTable = sqliteTable(
|
||||||
|
"tags",
|
||||||
|
{
|
||||||
|
/** Primary key is the tag string itself */
|
||||||
|
name: text("name").notNull().primaryKey(),
|
||||||
|
},
|
||||||
|
(t) => ({
|
||||||
|
nameIndex: index("tags_name_idx").on(t.name),
|
||||||
|
}),
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user