diff --git a/cmd/ismdb/db.go b/cmd/ismdb/db.go new file mode 100644 index 0000000..1325c47 --- /dev/null +++ b/cmd/ismdb/db.go @@ -0,0 +1,188 @@ +package main + +import ( + "context" + _ "embed" + "errors" + "fmt" + "log/slog" + + "zombiezen.com/go/sqlite" + "zombiezen.com/go/sqlite/sqlitemigration" + "zombiezen.com/go/sqlite/sqlitex" + + "git.sunturtle.xyz/zephyr/horse/umadump/ism" +) + +const appID int32 = 0x4acedb + +//go:embed schema-00.sql +var schema0 string + +//go:embed schema-final.sql +var schemaFinal string + +func opendb(uri, journalMode string) *sqlitemigration.Pool { + schema := sqlitemigration.Schema{ + Migrations: []string{ + schema0, + }, + AppID: appID, + RepeatableMigration: schemaFinal, + } + j := "PRAGMA journal_mode=" + journalMode + opts := sqlitemigration.Options{ + Flags: sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenURI, + PrepareConn: func(conn *sqlite.Conn) error { + if journalMode == "" { + return nil + } + stmt, _, err := conn.PrepareTransient(j) + if err != nil { + return err + } + _, err = stmt.Step() + return errors.Join(err, stmt.Finalize()) + }, + } + db := sqlitemigration.NewPool(uri, schema, opts) + return db +} + +func insert(ctx context.Context, db *sqlitemigration.Pool, log *slog.Logger, career *ism.WorkIdleSingleMode, trainerID int64) (err error) { + conn, err := db.Take(ctx) + if err != nil { + return err + } + defer db.Put(conn) + + defer sqlitex.Transaction(conn)(&err) +} + +//go:embed insert-career.sql +var insertCareerSQL string + +func insertCareer(conn *sqlite.Conn, career *ism.WorkIdleSingleMode, trainerID int64) error { + stmt := conn.Prep(insertCareerSQL) + stmt.SetInt64("$trainer_id", trainerID) + stmt.SetInt64("$trained_chara_id", int64(career.End.Chara.SingleModeCharaID)) + stmt.SetInt64("$card_id", int64(career.End.Chara.CardID)) + stmt.SetInt64("$scenario_id", int64(career.End.Chara.ScenarioID)) + stmt.SetInt64("$route_id", int64(career.End.Chara.RouteID)) + stmt.SetText("$start_time", career.ProgressInfo.StartTime) + stmt.SetText("$end_time", career.ProgressInfo.EndTime) + stmt.SetInt64("$chara_grade", int64(career.End.Chara.CharaGrade)) + stmt.SetInt64("$star_level", int64(career.End.Chara.Rarity)) + stmt.SetInt64("$potential_level", int64(career.End.Chara.TalentLevel)) + stmt.SetInt64("$speed", int64(career.End.Chara.Speed)) + stmt.SetInt64("$stamina", int64(career.End.Chara.Stamina)) + stmt.SetInt64("$power", int64(career.End.Chara.Power)) + stmt.SetInt64("$guts", int64(career.End.Chara.Guts)) + stmt.SetInt64("$wit", int64(career.End.Chara.Wiz)) + stmt.SetInt64("$energy", int64(career.End.Chara.Vital)) + stmt.SetInt64("$max_speed", int64(career.End.Chara.MaxSpeed)) + stmt.SetInt64("$max_stamina", int64(career.End.Chara.MaxStamina)) + stmt.SetInt64("$max_power", int64(career.End.Chara.MaxPower)) + stmt.SetInt64("$max_guts", int64(career.End.Chara.MaxGuts)) + stmt.SetInt64("$max_wit", int64(career.End.Chara.MaxWiz)) + stmt.SetInt64("$max_energy", int64(career.End.Chara.MaxVital)) + stmt.SetInt64("$skill_point", int64(career.ProgressLog.TotalSkillPoint)) + stmt.SetInt64("$mood", int64(career.End.Chara.Motivation)) + stmt.SetInt64("$turf", int64(career.End.Chara.ProperGroundTurf)) + stmt.SetInt64("$dirt", int64(career.End.Chara.ProperGroundDirt)) + stmt.SetInt64("$sprint", int64(career.End.Chara.ProperDistanceShort)) + stmt.SetInt64("$mile", int64(career.End.Chara.ProperDistanceMile)) + stmt.SetInt64("$medium", int64(career.End.Chara.ProperDistanceMiddle)) + stmt.SetInt64("$long", int64(career.End.Chara.ProperDistanceLong)) + stmt.SetInt64("$front_runner", int64(career.End.Chara.ProperRunningStyleNige)) + stmt.SetInt64("$pace_chaser", int64(career.End.Chara.ProperRunningStyleSenko)) + stmt.SetInt64("$late_surger", int64(career.End.Chara.ProperRunningStyleSashi)) + stmt.SetInt64("$end_closer", int64(career.End.Chara.ProperRunningStyleOikomi)) + stmt.SetInt64("$start_speed", int64(career.ProgressInfo.Chara.Speed)) + stmt.SetInt64("$start_stamina", int64(career.ProgressInfo.Chara.Stamina)) + stmt.SetInt64("$start_power", int64(career.ProgressInfo.Chara.Power)) + stmt.SetInt64("$start_guts", int64(career.ProgressInfo.Chara.Guts)) + stmt.SetInt64("$start_wit", int64(career.ProgressInfo.Chara.Wiz)) + stmt.SetInt64("$start_max_speed", int64(career.ProgressInfo.Chara.MaxSpeed)) + stmt.SetInt64("$start_max_stamina", int64(career.ProgressInfo.Chara.MaxStamina)) + stmt.SetInt64("$start_max_power", int64(career.ProgressInfo.Chara.MaxPower)) + stmt.SetInt64("$start_max_guts", int64(career.ProgressInfo.Chara.MaxGuts)) + stmt.SetInt64("$start_max_wit", int64(career.ProgressInfo.Chara.MaxWiz)) + stmt.SetInt64("$start_turf", int64(career.ProgressInfo.Chara.ProperGroundTurf)) + stmt.SetInt64("$start_dirt", int64(career.ProgressInfo.Chara.ProperGroundDirt)) + stmt.SetInt64("$start_sprint", int64(career.ProgressInfo.Chara.ProperDistanceShort)) + stmt.SetInt64("$start_mile", int64(career.ProgressInfo.Chara.ProperDistanceMile)) + stmt.SetInt64("$start_medium", int64(career.ProgressInfo.Chara.ProperDistanceMiddle)) + stmt.SetInt64("$start_long", int64(career.ProgressInfo.Chara.ProperDistanceLong)) + stmt.SetInt64("$start_front_runner", int64(career.ProgressInfo.Chara.ProperRunningStyleNige)) + stmt.SetInt64("$start_pace_chaser", int64(career.ProgressInfo.Chara.ProperRunningStyleSenko)) + stmt.SetInt64("$start_late_surger", int64(career.ProgressInfo.Chara.ProperRunningStyleSashi)) + stmt.SetInt64("$start_end_closer", int64(career.ProgressInfo.Chara.ProperRunningStyleOikomi)) + stmt.SetInt64("$fans", int64(career.End.Chara.Fans)) + stmt.SetInt64("$race_program_id", int64(career.End.Chara.RaceProgramID)) + stmt.SetInt64("$reserve_race_program_id", int64(career.End.Chara.ReserveRaceProgramID)) + stmt.SetInt64("$race_running_style", int64(career.End.Chara.RaceRunningStyle)) + stmt.SetInt64("$is_short_race", int64(career.End.Chara.IsShortRace)) + stmt.SetInt64("$succession_trained_chara_id_1", int64(career.OldChara.SuccessionTrainedCharaID1)) + stmt.SetInt64("$succession_trained_chara_id_2", int64(career.OldChara.SuccessionTrainedCharaID2)) + _, err := stmt.Step() + err = errors.Join(err, stmt.Reset()) + if err != nil { + return fmt.Errorf("inserting race course: %w", err) + } + return nil +} + +func insertConditions(conn *sqlite.Conn, cs []ism.CharaEffectLog, trainerID int64, trainedChara int32) error { + const sql = `INSERT INTO condition VALUES (?, ?, ?, ?)` + stmt := conn.Prep(sql) + stmt.BindInt64(1, trainerID) + stmt.BindInt64(2, int64(trainedChara)) + for _, c := range cs { + stmt.BindInt64(3, int64(c.CharaEffectID)) + stmt.BindBool(4, c.IsActive) + _, err := stmt.Step() + err = errors.Join(err, stmt.Reset()) + if err != nil { + return fmt.Errorf("inserting condition: %w", err) + } + } + return nil +} + +//go:embed insert-gain.sql +var insertGainSQL string + +func insertGain(conn *sqlite.Conn, gain *ism.IdleSingleModeGainInfo, trainerID int64, trainedChara int32, source int32) error { + stmt := conn.Prep(insertGainSQL) + stmt.SetInt64("$trainer_id", trainerID) + stmt.SetInt64("$trained_chara_id", int64(trainedChara)) + stmt.SetInt64("$source", int64(source)) + stmt.SetInt64("$gain_speed", int64(gain.Speed.Value)) + stmt.SetInt64("$gain_stamina", int64(gain.Stamina.Value)) + stmt.SetInt64("$gain_power", int64(gain.Power.Value)) + stmt.SetInt64("$gain_guts", int64(gain.Guts.Value)) + stmt.SetInt64("$gain_wit", int64(gain.Wiz.Value)) + stmt.SetInt64("$gain_skill_point", int64(gain.SkillPoint)) + stmt.SetInt64("$gain_max_speed", int64(gain.MaxSpeed)) + stmt.SetInt64("$gain_max_stamina", int64(gain.MaxStamina)) + stmt.SetInt64("$gain_max_power", int64(gain.MaxPower)) + stmt.SetInt64("$gain_max_guts", int64(gain.MaxGuts)) + stmt.SetInt64("$gain_max_wit", int64(gain.MaxWiz)) + stmt.SetInt64("$gain_turf", int64(gain.ProperGroundTurf)) + stmt.SetInt64("$gain_dirt", int64(gain.ProperGroundDirt)) + stmt.SetInt64("$gain_sprint", int64(gain.ProperDistanceShort)) + stmt.SetInt64("$gain_mile", int64(gain.ProperDistanceMile)) + stmt.SetInt64("$gain_medium", int64(gain.ProperDistanceMiddle)) + stmt.SetInt64("$gain_long", int64(gain.ProperDistanceLong)) + stmt.SetInt64("$gain_front_runner", int64(gain.ProperRunningStyleNige)) + stmt.SetInt64("$gain_pace_chaser", int64(gain.ProperRunningStyleSenko)) + stmt.SetInt64("$gain_late_surger", int64(gain.ProperRunningStyleSashi)) + stmt.SetInt64("$gain_end_closer", int64(gain.ProperRunningStyleOikomi)) + _, err := stmt.Step() + err = errors.Join(err, stmt.Reset()) + if err != nil { + return fmt.Errorf("inserting gain info: %w", err) + } + return nil +} diff --git a/cmd/ismdb/insert-career.sql b/cmd/ismdb/insert-career.sql new file mode 100644 index 0000000..e08bb24 --- /dev/null +++ b/cmd/ismdb/insert-career.sql @@ -0,0 +1,69 @@ +INSERT INTO career VALUES ( + $trainer_id, + $trained_chara_id, + $card_id, + $scenario_id, + $route_id, + $start_time, + $end_time, + + $chara_grade, + $star_level, + $potential_level, + + $speed, + $stamina, + $power, + $guts, + $wit, + $energy, + $max_speed, + $max_stamina, + $max_power, + $max_guts, + $max_wit, + $max_energy, + $skill_point, + $mood, + + $turf, + $dirt, + $sprint, + $mile, + $medium, + $long, + $front_runner, + $pace_chaser, + $late_surger, + $end_closer, + + $start_speed, + $start_stamina, + $start_power, + $start_guts, + $start_wit, + $start_max_speed, + $start_max_stamina, + $start_max_power, + $start_max_guts, + $start_max_wit, + $start_turf, + $start_dirt, + $start_sprint, + $start_mile, + $start_medium, + $start_long, + $start_front_runner, + $start_pace_chaser, + $start_late_surger, + $start_end_closer, + + $fans, + $race_program_id, + $reserve_race_program_id, + $race_running_style, + $is_short_race, + + $succession_trained_chara_id_1, + $succession_trained_chara_id_2 +) \ No newline at end of file diff --git a/cmd/ismdb/insert-gain.sql b/cmd/ismdb/insert-gain.sql new file mode 100644 index 0000000..2703049 --- /dev/null +++ b/cmd/ismdb/insert-gain.sql @@ -0,0 +1,31 @@ +INSERT INTO gain VALUES ( + $trainer_id, + $trained_chara_id, + $source, + + $gain_speed, + $gain_stamina, + $gain_power, + $gain_guts, + $gain_wit, + $gain_skill_point, + + $gain_max_speed, + $gain_max_stamina, + $gain_max_power, + $gain_max_guts, + $gain_max_wit, + + $gain_turf, + $gain_dirt, + + $gain_sprint, + $gain_mile, + $gain_medium, + $gain_long, + + $gain_front_runner, + $gain_pace_chaser, + $gain_late_surger, + $gain_end_closer +) \ No newline at end of file diff --git a/cmd/ismdb/schema-00.sql b/cmd/ismdb/schema-00.sql new file mode 100644 index 0000000..a38fe13 --- /dev/null +++ b/cmd/ismdb/schema-00.sql @@ -0,0 +1,251 @@ +CREATE TABLE IF NOT EXISTS career ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + card_id INTEGER NOT NULL, + scenario_id INTEGER NOT NULL, + route_id INTEGER NOT NULL, + start_time TEXT NOT NULL, + end_time TEXT NOT NULL, + + chara_grade INTEGER NOT NULL, + star_level INTEGER NOT NULL, + potential_level INTEGER NOT NULL, + + speed INTEGER NOT NULL, + stamina INTEGER NOT NULL, + power INTEGER NOT NULL, + guts INTEGER NOT NULL, + wit INTEGER NOT NULL, + energy INTEGER NOT NULL, + max_speed INTEGER NOT NULL, + max_stamina INTEGER NOT NULL, + max_power INTEGER NOT NULL, + max_guts INTEGER NOT NULL, + max_wit INTEGER NOT NULL, + max_energy INTEGER NOT NULL, + skill_point INTEGER NOT NULL, + mood INTEGER NOT NULL, + + turf INTEGER NOT NULL, + dirt INTEGER NOT NULL, + sprint INTEGER NOT NULL, + mile INTEGER NOT NULL, + medium INTEGER NOT NULL, + long INTEGER NOT NULL, + front_runner INTEGER NOT NULL, + pace_chaser INTEGER NOT NULL, + late_surger INTEGER NOT NULL, + end_closer INTEGER NOT NULL, + + -- These fields are 0 if the career finished while it was not actively + -- displayed on the client used to dump it. + start_speed INTEGER NOT NULL, + start_stamina INTEGER NOT NULL, + start_power INTEGER NOT NULL, + start_guts INTEGER NOT NULL, + start_wit INTEGER NOT NULL, + start_max_speed INTEGER NOT NULL, + start_max_stamina INTEGER NOT NULL, + start_max_power INTEGER NOT NULL, + start_max_guts INTEGER NOT NULL, + start_max_wit INTEGER NOT NULL, + start_turf INTEGER NOT NULL, + start_dirt INTEGER NOT NULL, + start_sprint INTEGER NOT NULL, + start_mile INTEGER NOT NULL, + start_medium INTEGER NOT NULL, + start_long INTEGER NOT NULL, + start_front_runner INTEGER NOT NULL, + start_pace_chaser INTEGER NOT NULL, + start_late_surger INTEGER NOT NULL, + start_end_closer INTEGER NOT NULL, + + fans INTEGER NOT NULL, + race_program_id INTEGER NOT NULL, + reserve_race_program_id INTEGER NOT NULL, + race_running_style INTEGER NOT NULL, + is_short_race INTEGER NOT NULL, + + succession_trained_chara_id_1 INTEGER NOT NULL, + succession_trained_chara_id_2 INTEGER NOT NULL, + + PRIMARY KEY (trainer_id, trained_chara_id) +) STRICT; +CREATE INDEX IF NOT EXISTS career_by_scenario ON career (scenario_id); +CREATE INDEX IF NOT EXISTS career_by_start_time ON career (start_time); + +CREATE TABLE IF NOT EXISTS condition ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + condition INTEGER NOT NULL, + is_active INTEGER NOT NULL, + + PRIMARY KEY (trainer_id, trained_chara_id, condition), + FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE +) STRICT; + +CREATE TABLE IF NOT EXISTS gain ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + source INTEGER NOT NULL, -- 0 for events, 1 for inspiration, otherwise support card id + + gain_speed INTEGER NOT NULL, + gain_stamina INTEGER NOT NULL, + gain_power INTEGER NOT NULL, + gain_guts INTEGER NOT NULL, + gain_wit INTEGER NOT NULL, + gain_skill_point INTEGER NOT NULL, + + gain_max_speed INTEGER NOT NULL, + gain_max_stamina INTEGER NOT NULL, + gain_max_power INTEGER NOT NULL, + gain_max_guts INTEGER NOT NULL, + gain_max_wit INTEGER NOT NULL, + + gain_turf INTEGER NOT NULL, + gain_dirt INTEGER NOT NULL, + + gain_sprint INTEGER NOT NULL, + gain_mile INTEGER NOT NULL, + gain_medium INTEGER NOT NULL, + gain_long INTEGER NOT NULL, + + gain_front_runner INTEGER NOT NULL, + gain_pace_chaser INTEGER NOT NULL, + gain_late_surger INTEGER NOT NULL, + gain_end_closer INTEGER NOT NULL, + + PRIMARY KEY (trainer_id, trained_chara_id, source), + FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE +) STRICT; +CREATE INDEX IF NOT EXISTS gain_by_source ON gain (source); + +CREATE TABLE IF NOT EXISTS gain_skill_hint ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + source INTEGER NOT NULL, + + group_id INTEGER NOT NULL, + rarity INTEGER NOT NULL, + level INTEGER NOT NULL, + + PRIMARY KEY (trainer_id, trained_chara_id, source, group_id, rarity), + FOREIGN KEY (trainer_id, trained_chara_id, source) REFERENCES gain ON DELETE CASCADE +) STRICT; + +CREATE TABLE IF NOT EXISTS inspiration ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + year INTEGER NOT NULL, + factor_id INTEGER NOT NULL, + level INTEGER NOT NULL, -- always 0 but just in case we get it in the future + + FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE +) STRICT; +CREATE INDEX IF NOT EXISTS inspiration_by_career ON inspiration (trainer_id, trained_chara_id); +CREATE INDEX IF NOT EXISTS inspiration_by_factor_id ON inspiration (factor_id); + +CREATE TABLE IF NOT EXISTS race_history ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + + turn INTEGER NOT NULL, + program_id INTEGER NOT NULL, + weather INTEGER NOT NULL, + ground_condition INTEGER NOT NULL, + running_style INTEGER NOT NULL, + result_rank INTEGER NOT NULL, + frame_order INTEGER NOT NULL, + gained_fans INTEGER NOT NULL, + lose_tips_id INTEGER NOT NULL, + + PRIMARY KEY (trainer_id, trained_chara_id, turn), + FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE +) STRICT; + +CREATE TABLE IF NOT EXISTS race_reward ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + turn INTEGER NOT NULL, + + item_type INTEGER NOT NULL, + item_id INTEGER NOT NULL, + item_num INTEGER NOT NULL, + + is_bonus INTEGER NOT NULL, + is_plus_bonus INTEGER NOT NULL, + is_bonus_win INTEGER NOT NULL, + + FOREIGN KEY (trainer_id, trained_chara_id, turn) REFERENCES race_history ON DELETE CASCADE +) STRICT; +CREATE INDEX IF NOT EXISTS race_reward_by_career_turn ON race_reward (trainer_id, trained_chara_id, turn); +CREATE INDEX IF NOT EXISTS race_reward_by_item ON race_reward (item_id); + +CREATE TABLE IF NOT EXISTS gain_skill ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + source INTEGER NOT NULL, + skill_id INTEGER NOT NULL, + + PRIMARY KEY (trainer_id, trained_chara_id, source, skill_id), + FOREIGN KEY (trainer_id, trained_chara_id, source) REFERENCES gain ON DELETE CASCADE +) STRICT; + +CREATE TABLE IF NOT EXISTS career_skill ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + skill_id INTEGER NOT NULL, + level INTEGER NOT NULL, + + PRIMARY KEY (trainer_id, trained_chara_id, skill_id), + FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE +) STRICT; + +CREATE TABLE IF NOT EXISTS career_skill_hint ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + + group_id INTEGER NOT NULL, + rarity INTEGER NOT NULL, + level INTEGER NOT NULL, + + PRIMARY KEY (trainer_id, trained_chara_id, group_id, rarity), + FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE +) STRICT; + +CREATE TABLE IF NOT EXISTS career_support_card ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + position INTEGER NOT NULL, + support_card_id INTEGER NOT NULL, + limit_break_count INTEGER NOT NULL, + exp INTEGER NOT NULL, + owner_viewer_id INTEGER NOT NULL, + + PRIMARY KEY (trainer_id, trained_chara_id, position), + FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE, + UNIQUE (trainer_id, trained_chara_id, support_card_id) +) STRICT; +CREATE INDEX IF NOT EXISTS career_support_card_by_card ON career_support_card (support_card_id, exp); + +CREATE TABLE IF NOT EXISTS start_skill ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + skill_id INTEGER NOT NULL, + level INTEGER NOT NULL, + + PRIMARY KEY (trainer_id, trained_chara_id, skill_id), + FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE +) STRICT; + +CREATE TABLE IF NOT EXISTS start_skill_hint ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + + group_id INTEGER NOT NULL, + rarity INTEGER NOT NULL, + level INTEGER NOT NULL, + + PRIMARY KEY (trainer_id, trained_chara_id, group_id, rarity), + FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE +) STRICT; diff --git a/cmd/ismdb/schema-final.sql b/cmd/ismdb/schema-final.sql new file mode 100644 index 0000000..9c55cbf --- /dev/null +++ b/cmd/ismdb/schema-final.sql @@ -0,0 +1,88 @@ +DROP VIEW IF EXISTS gain_event; +DROP VIEW IF EXISTS gain_inspiration; +DROP VIEW IF EXISTS gain_support_card; + +CREATE VIEW gain_event AS +SELECT + trainer_id, + trained_chara_id, + gain_speed, + gain_stamina, + gain_power, + gain_guts, + gain_wit, + gain_skill_point, + gain_max_speed, + gain_max_stamina, + gain_max_power, + gain_max_guts, + gain_max_wit, + gain_turf, + gain_dirt, + gain_sprint, + gain_mile, + gain_medium, + gain_long, + gain_front_runner, + gain_pace_chaser, + gain_late_surger, + gain_end_closer +FROM gain +WHERE source = 0; + +CREATE VIEW gain_inspiration AS +SELECT + trainer_id, + trained_chara_id, + gain_speed, + gain_stamina, + gain_power, + gain_guts, + gain_wit, + gain_skill_point, + gain_max_speed, + gain_max_stamina, + gain_max_power, + gain_max_guts, + gain_max_wit, + gain_turf, + gain_dirt, + gain_sprint, + gain_mile, + gain_medium, + gain_long, + gain_front_runner, + gain_pace_chaser, + gain_late_surger, + gain_end_closer +FROM gain +WHERE source = 1; + +CREATE VIEW gain_support_card AS +SELECT + trainer_id, + trained_chara_id, + source AS support_card_id, + gain_speed, + gain_stamina, + gain_power, + gain_guts, + gain_wit, + gain_skill_point, + gain_max_speed, + gain_max_stamina, + gain_max_power, + gain_max_guts, + gain_max_wit, + gain_turf, + gain_dirt, + gain_sprint, + gain_mile, + gain_medium, + gain_long, + gain_front_runner, + gain_pace_chaser, + gain_late_surger, + gain_end_closer +FROM gain +WHERE source > 1;