Merge pull request #235 - Schema upgrade by skizzerz
Fix FK deferrable status, ensure that FKs are always enforced in the bot, and redo how fool wins are stored/tracked. Drop unneded table.
This commit is contained in:
parent
0caaba9152
commit
22aa7af5c6
1
.gitignore
vendored
1
.gitignore
vendored
@ -15,6 +15,7 @@ botconfig.py
|
|||||||
|
|
||||||
# Database files
|
# Database files
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
|
*.bak
|
||||||
|
|
||||||
# Log files
|
# Log files
|
||||||
*.log
|
*.log
|
||||||
|
131
src/db.py
131
src/db.py
@ -3,12 +3,15 @@ import src.settings as var
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
# increment this whenever making a schema change so that the schema upgrade functions run on start
|
# increment this whenever making a schema change so that the schema upgrade functions run on start
|
||||||
# they do not run by default for performance reasons
|
# they do not run by default for performance reasons
|
||||||
SCHEMA_VERSION = 1
|
SCHEMA_VERSION = 2
|
||||||
|
|
||||||
_ts = threading.local()
|
_ts = threading.local()
|
||||||
|
|
||||||
@ -27,10 +30,8 @@ def init_vars():
|
|||||||
pe.stasis_expires,
|
pe.stasis_expires,
|
||||||
COALESCE(at.flags, a.flags)
|
COALESCE(at.flags, a.flags)
|
||||||
FROM person pe
|
FROM person pe
|
||||||
JOIN person_player pp
|
|
||||||
ON pp.person = pe.id
|
|
||||||
JOIN player pl
|
JOIN player pl
|
||||||
ON pl.id = pp.player
|
ON pl.person = pe.id
|
||||||
LEFT JOIN access a
|
LEFT JOIN access a
|
||||||
ON a.person = pe.id
|
ON a.person = pe.id
|
||||||
LEFT JOIN access_template at
|
LEFT JOIN access_template at
|
||||||
@ -93,10 +94,8 @@ def init_vars():
|
|||||||
ON ws.warning = w.id
|
ON ws.warning = w.id
|
||||||
JOIN person pe
|
JOIN person pe
|
||||||
ON pe.id = w.target
|
ON pe.id = w.target
|
||||||
JOIN person_player pp
|
|
||||||
ON pp.person = pe.id
|
|
||||||
JOIN player pl
|
JOIN player pl
|
||||||
ON pl.id = pp.player
|
ON pl.person = pe.id
|
||||||
WHERE
|
WHERE
|
||||||
ws.sanction = 'deny command'
|
ws.sanction = 'deny command'
|
||||||
AND w.deleted = 0
|
AND w.deleted = 0
|
||||||
@ -258,17 +257,6 @@ def add_game(mode, size, started, finished, winner, players, options):
|
|||||||
p["personid"], p["playerid"] = _get_ids(p["account"], p["hostmask"], add=True)
|
p["personid"], p["playerid"] = _get_ids(p["account"], p["hostmask"], add=True)
|
||||||
with conn:
|
with conn:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
if winner.startswith("@"):
|
|
||||||
# fool won, convert the nick portion into a player id
|
|
||||||
for p in players:
|
|
||||||
if p["nick"] == winner[1:]:
|
|
||||||
winner = "@" + str(p["playerid"])
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# invalid winner? We can't find the fool's nick in the player list
|
|
||||||
# maybe raise an exception here instead of silently failing
|
|
||||||
return
|
|
||||||
|
|
||||||
c.execute("""INSERT INTO game (gamemode, options, started, finished, gamesize, winner)
|
c.execute("""INSERT INTO game (gamemode, options, started, finished, gamesize, winner)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)""", (mode, json.dumps(options), started, finished, size, winner))
|
VALUES (?, ?, ?, ?, ?, ?)""", (mode, json.dumps(options), started, finished, size, winner))
|
||||||
gameid = c.lastrowid
|
gameid = c.lastrowid
|
||||||
@ -297,10 +285,10 @@ def get_player_stats(acc, hostmask, role):
|
|||||||
SUM(gp.indiv_win) AS indiv,
|
SUM(gp.indiv_win) AS indiv,
|
||||||
COUNT(1) AS total
|
COUNT(1) AS total
|
||||||
FROM person pe
|
FROM person pe
|
||||||
JOIN person_player pmap
|
JOIN player pl
|
||||||
ON pmap.person = pe.id
|
ON pl.person = pe.id
|
||||||
JOIN game_player gp
|
JOIN game_player gp
|
||||||
ON gp.player = pmap.player
|
ON gp.player = pl.id
|
||||||
JOIN game_player_role gpr
|
JOIN game_player_role gpr
|
||||||
ON gpr.game_player = gp.id
|
ON gpr.game_player = gp.id
|
||||||
AND gpr.role = ?
|
AND gpr.role = ?
|
||||||
@ -324,10 +312,10 @@ def get_player_totals(acc, hostmask):
|
|||||||
gpr.role AS role,
|
gpr.role AS role,
|
||||||
COUNT(1) AS total
|
COUNT(1) AS total
|
||||||
FROM person pe
|
FROM person pe
|
||||||
JOIN person_player pmap
|
JOIN player pl
|
||||||
ON pmap.person = pe.id
|
ON pl.person = pe.id
|
||||||
JOIN game_player gp
|
JOIN game_player gp
|
||||||
ON gp.player = pmap.player
|
ON gp.player = pl.id
|
||||||
JOIN game_player_role gpr
|
JOIN game_player_role gpr
|
||||||
ON gpr.game_player = gp.id
|
ON gpr.game_player = gp.id
|
||||||
WHERE pe.id = ?
|
WHERE pe.id = ?
|
||||||
@ -352,9 +340,7 @@ def get_game_stats(mode, size):
|
|||||||
if not total_games:
|
if not total_games:
|
||||||
return "No stats for \u0002{0}\u0002 player games.".format(size)
|
return "No stats for \u0002{0}\u0002 player games.".format(size)
|
||||||
c.execute("""SELECT
|
c.execute("""SELECT
|
||||||
CASE substr(winner, 1, 1)
|
winner AS team,
|
||||||
WHEN '@' THEN 'fools'
|
|
||||||
ELSE winner END AS team,
|
|
||||||
COUNT(1) AS games,
|
COUNT(1) AS games,
|
||||||
CASE winner
|
CASE winner
|
||||||
WHEN 'villagers' THEN 0
|
WHEN 'villagers' THEN 0
|
||||||
@ -693,11 +679,47 @@ def acknowledge_warning(warning):
|
|||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute("UPDATE warning SET acknowledged = 1 WHERE id = ?", (warning,))
|
c.execute("UPDATE warning SET acknowledged = 1 WHERE id = ?", (warning,))
|
||||||
|
|
||||||
def _upgrade():
|
def _upgrade(oldversion):
|
||||||
# no upgrades yet, once there are some, add methods like _add_table(), _add_column(), etc.
|
# try to make a backup copy of the database
|
||||||
# that check for the existence of that table/column/whatever and adds/drops/whatevers them
|
print ("Performing schema upgrades, this may take a while.", file=sys.stderr)
|
||||||
# as needed. We can't do this purely in SQL because sqlite lacks a scripting-level IF statement.
|
have_backup = False
|
||||||
pass
|
try:
|
||||||
|
print ("Creating database backup...", file=sys.stderr)
|
||||||
|
shutil.copyfile("data.sqlite3", "data.sqlite3.bak")
|
||||||
|
have_backup = True
|
||||||
|
print ("Database backup created at data.sqlite3.bak...", file=sys.stderr)
|
||||||
|
except OSError:
|
||||||
|
print ("Database backup failed! Hit Ctrl+C to abort, otherwise upgrade will continue in 5 seconds...", file=sys.stderr)
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
dn = os.path.dirname(__file__)
|
||||||
|
conn = _conn()
|
||||||
|
try:
|
||||||
|
with conn:
|
||||||
|
c = conn.cursor()
|
||||||
|
if oldversion < 2:
|
||||||
|
print ("Upgrade from version 1 to 2...", file=sys.stderr)
|
||||||
|
# Update FKs to be deferrable, update collations to nocase where it makes sense,
|
||||||
|
# and clean up how fool wins are tracked (giving fools team wins instead of saving the winner's
|
||||||
|
# player id as a string). When nocasing players, this may cause some records to be merged.
|
||||||
|
with open(os.path.join(dn, "db", "upgrade2.sql"), "rt") as f:
|
||||||
|
c.executescript(f.read())
|
||||||
|
|
||||||
|
c.execute("PRAGMA user_version = " + str(SCHEMA_VERSION))
|
||||||
|
print ("Upgrades complete!", file=sys.stderr)
|
||||||
|
except sqlite3.Error:
|
||||||
|
print ("An error has occurred while upgrading the database schema.",
|
||||||
|
"Please report this issue to ##werewolf-dev on irc.freenode.net.",
|
||||||
|
"Include all of the following details in your report:",
|
||||||
|
sep="\n", file=sys.stderr)
|
||||||
|
if have_backup:
|
||||||
|
try:
|
||||||
|
shutil.copyfile("data.sqlite3.bak", "data.sqlite3")
|
||||||
|
except OSError:
|
||||||
|
print ("An error has occurred while restoring your database backup.",
|
||||||
|
"You can manually move data.sqlite3.bak to data.sqlite3 to restore the original database.",
|
||||||
|
sep="\n", file=sys.stderr)
|
||||||
|
raise
|
||||||
|
|
||||||
def _migrate():
|
def _migrate():
|
||||||
# try to make a backup copy of the database
|
# try to make a backup copy of the database
|
||||||
@ -708,7 +730,7 @@ def _migrate():
|
|||||||
pass
|
pass
|
||||||
dn = os.path.dirname(__file__)
|
dn = os.path.dirname(__file__)
|
||||||
conn = _conn()
|
conn = _conn()
|
||||||
with conn, open(os.path.join(dn, "db.sql"), "rt") as f1, open(os.path.join(dn, "migrate.sql"), "rt") as f2:
|
with conn, open(os.path.join(dn, "db", "db.sql"), "rt") as f1, open(os.path.join(dn, "db", "migrate.sql"), "rt") as f2:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
#######################################################
|
#######################################################
|
||||||
# Step 1: install the new schema (from db.sql script) #
|
# Step 1: install the new schema (from db.sql script) #
|
||||||
@ -728,7 +750,7 @@ def _migrate():
|
|||||||
def _install():
|
def _install():
|
||||||
dn = os.path.dirname(__file__)
|
dn = os.path.dirname(__file__)
|
||||||
conn = _conn()
|
conn = _conn()
|
||||||
with conn, open(os.path.join(dn, "db.sql"), "rt") as f1:
|
with conn, open(os.path.join(dn, "db", "db.sql"), "rt") as f1:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.executescript(f1.read())
|
c.executescript(f1.read())
|
||||||
c.execute("PRAGMA user_version = " + str(SCHEMA_VERSION))
|
c.execute("PRAGMA user_version = " + str(SCHEMA_VERSION))
|
||||||
@ -743,10 +765,8 @@ def _get_ids(acc, hostmask, add=False):
|
|||||||
elif acc is None:
|
elif acc is None:
|
||||||
c.execute("""SELECT pe.id, pl.id
|
c.execute("""SELECT pe.id, pl.id
|
||||||
FROM player pl
|
FROM player pl
|
||||||
JOIN person_player pp
|
|
||||||
ON pp.player = pl.id
|
|
||||||
JOIN person pe
|
JOIN person pe
|
||||||
ON pe.id = pp.person
|
ON pe.id = pl.person
|
||||||
WHERE
|
WHERE
|
||||||
pl.account IS NULL
|
pl.account IS NULL
|
||||||
AND pl.hostmask = ?
|
AND pl.hostmask = ?
|
||||||
@ -755,10 +775,8 @@ def _get_ids(acc, hostmask, add=False):
|
|||||||
hostmask = None
|
hostmask = None
|
||||||
c.execute("""SELECT pe.id, pl.id
|
c.execute("""SELECT pe.id, pl.id
|
||||||
FROM player pl
|
FROM player pl
|
||||||
JOIN person_player pp
|
|
||||||
ON pp.player = pl.id
|
|
||||||
JOIN person pe
|
JOIN person pe
|
||||||
ON pe.id = pp.person
|
ON pe.id = pl.person
|
||||||
WHERE
|
WHERE
|
||||||
pl.account = ?
|
pl.account = ?
|
||||||
AND pl.hostmask IS NULL
|
AND pl.hostmask IS NULL
|
||||||
@ -774,7 +792,7 @@ def _get_ids(acc, hostmask, add=False):
|
|||||||
plid = c.lastrowid
|
plid = c.lastrowid
|
||||||
c.execute("INSERT INTO person (primary_player) VALUES (?)", (plid,))
|
c.execute("INSERT INTO person (primary_player) VALUES (?)", (plid,))
|
||||||
peid = c.lastrowid
|
peid = c.lastrowid
|
||||||
c.execute("INSERT INTO person_player (person, player) VALUES (?, ?)", (peid, plid))
|
c.execute("UPDATE player SET person=? WHERE id=?" (peid, plid))
|
||||||
return (peid, plid)
|
return (peid, plid)
|
||||||
|
|
||||||
def _get_display_name(peid):
|
def _get_display_name(peid):
|
||||||
@ -796,10 +814,10 @@ def _total_games(peid):
|
|||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute("""SELECT COUNT(DISTINCT gp.game)
|
c.execute("""SELECT COUNT(DISTINCT gp.game)
|
||||||
FROM person pe
|
FROM person pe
|
||||||
JOIN person_player pmap
|
JOIN player pl
|
||||||
ON pmap.person = pe.id
|
ON pl.person = pe.id
|
||||||
JOIN game_player gp
|
JOIN game_player gp
|
||||||
ON gp.player = pmap.player
|
ON gp.player = pl.id
|
||||||
WHERE
|
WHERE
|
||||||
pe.id = ?""", (peid,))
|
pe.id = ?""", (peid,))
|
||||||
# aggregates without GROUP BY always have exactly one row,
|
# aggregates without GROUP BY always have exactly one row,
|
||||||
@ -826,6 +844,9 @@ def _conn():
|
|||||||
return _ts.conn
|
return _ts.conn
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
_ts.conn = sqlite3.connect("data.sqlite3")
|
_ts.conn = sqlite3.connect("data.sqlite3")
|
||||||
|
with _ts.conn:
|
||||||
|
c = _ts.conn.cursor()
|
||||||
|
c.execute("PRAGMA foreign_keys = ON")
|
||||||
return _ts.conn
|
return _ts.conn
|
||||||
|
|
||||||
need_install = not os.path.isfile("data.sqlite3")
|
need_install = not os.path.isfile("data.sqlite3")
|
||||||
@ -837,18 +858,20 @@ with conn:
|
|||||||
_install()
|
_install()
|
||||||
c.execute("PRAGMA user_version")
|
c.execute("PRAGMA user_version")
|
||||||
row = c.fetchone()
|
row = c.fetchone()
|
||||||
if row[0] == 0:
|
ver = row[0]
|
||||||
# new schema does not exist yet, migrate from old schema
|
|
||||||
# NOTE: game stats are NOT migrated to the new schema; the old gamestats table
|
|
||||||
# will continue to exist to allow queries against it, however given how horribly
|
|
||||||
# inaccurate the stats on it are, it would be a disservice to copy those inaccurate
|
|
||||||
# statistics over to the new schema which has the capability of actually being accurate.
|
|
||||||
_migrate()
|
|
||||||
elif row[0] < SCHEMA_VERSION:
|
|
||||||
_upgrade()
|
|
||||||
c.close()
|
c.close()
|
||||||
|
|
||||||
del need_install, conn, c
|
if ver == 0:
|
||||||
|
# new schema does not exist yet, migrate from old schema
|
||||||
|
# NOTE: game stats are NOT migrated to the new schema; the old gamestats table
|
||||||
|
# will continue to exist to allow queries against it, however given how horribly
|
||||||
|
# inaccurate the stats on it are, it would be a disservice to copy those inaccurate
|
||||||
|
# statistics over to the new schema which has the capability of actually being accurate.
|
||||||
|
_migrate()
|
||||||
|
elif ver < SCHEMA_VERSION:
|
||||||
|
_upgrade(ver)
|
||||||
|
|
||||||
|
del need_install, conn, c, ver
|
||||||
init_vars()
|
init_vars()
|
||||||
|
|
||||||
# vim: set expandtab:sw=4:ts=4:
|
# vim: set expandtab:sw=4:ts=4:
|
||||||
|
@ -6,16 +6,19 @@
|
|||||||
-- here may end up corresponding to the same actual person (see below).
|
-- here may end up corresponding to the same actual person (see below).
|
||||||
CREATE TABLE IF NOT EXISTS player (
|
CREATE TABLE IF NOT EXISTS player (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
|
-- What person this player record belongs to
|
||||||
|
person INTEGER REFERENCES person(id) DEFERRABLE INITIALLY DEFERRED,
|
||||||
-- NickServ account name, or NULL if this player is based on a hostmask
|
-- NickServ account name, or NULL if this player is based on a hostmask
|
||||||
account TEXT,
|
account TEXT COLLATE NOCASE,
|
||||||
-- Hostmask for the player, if not based on an account (NULL otherwise)
|
-- Hostmask for the player, if not based on an account (NULL otherwise)
|
||||||
hostmask TEXT,
|
hostmask TEXT COLLATE NOCASE,
|
||||||
-- If a player entry needs to be retired (for example, an account expired),
|
-- If a player entry needs to be retired (for example, an account expired),
|
||||||
-- setting this to 0 allows for that entry to be re-used without corrupting old stats/logs
|
-- setting this to 0 allows for that entry to be re-used without corrupting old stats/logs
|
||||||
active BOOLEAN NOT NULL DEFAULT 1
|
active BOOLEAN NOT NULL DEFAULT 1
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS player_idx ON player (account, hostmask, active);
|
CREATE INDEX IF NOT EXISTS player_idx ON player (account, hostmask, active);
|
||||||
|
CREATE INDEX IF NOT EXISTS person_idx ON player (person);
|
||||||
|
|
||||||
-- Person tracking; a person can consist of multiple players (for example, someone may have
|
-- Person tracking; a person can consist of multiple players (for example, someone may have
|
||||||
-- an account player for when they are logged in and 3 hostmask players for when they are
|
-- an account player for when they are logged in and 3 hostmask players for when they are
|
||||||
@ -23,7 +26,7 @@ CREATE INDEX IF NOT EXISTS player_idx ON player (account, hostmask, active);
|
|||||||
CREATE TABLE IF NOT EXISTS person (
|
CREATE TABLE IF NOT EXISTS person (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
-- Primary player for this person
|
-- Primary player for this person
|
||||||
primary_player INTEGER NOT NULL UNIQUE REFERENCES player(id),
|
primary_player INTEGER NOT NULL UNIQUE REFERENCES player(id) DEFERRABLE INITIALLY DEFERRED,
|
||||||
-- If 1, the bot will notice the player instead of sending privmsgs
|
-- If 1, the bot will notice the player instead of sending privmsgs
|
||||||
notice BOOLEAN NOT NULL DEFAULT 0,
|
notice BOOLEAN NOT NULL DEFAULT 0,
|
||||||
-- If 1, the bot will send simple role notifications to the player
|
-- If 1, the bot will send simple role notifications to the player
|
||||||
@ -39,24 +42,15 @@ CREATE TABLE IF NOT EXISTS person (
|
|||||||
stasis_expires DATETIME
|
stasis_expires DATETIME
|
||||||
);
|
);
|
||||||
|
|
||||||
-- A person can have multiple attached players, however each player can be attached
|
|
||||||
-- to only exactly one person
|
|
||||||
CREATE TABLE IF NOT EXISTS person_player (
|
|
||||||
person INTEGER NOT NULL REFERENCES person(id),
|
|
||||||
player INTEGER NOT NULL UNIQUE REFERENCES player(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS person_player_idx ON person_player (person);
|
|
||||||
|
|
||||||
-- Sometimes people are bad, this keeps track of that for the purpose of automatically applying
|
-- Sometimes people are bad, this keeps track of that for the purpose of automatically applying
|
||||||
-- various sanctions and viewing the past history of someone. Outside of specifically-marked
|
-- various sanctions and viewing the past history of someone. Outside of specifically-marked
|
||||||
-- fields, records are never modified or deleted from this table once inserted.
|
-- fields, records are never modified or deleted from this table once inserted.
|
||||||
CREATE TABLE IF NOT EXISTS warning (
|
CREATE TABLE IF NOT EXISTS warning (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
-- The target (recipient) of the warning
|
-- The target (recipient) of the warning
|
||||||
target INTEGER NOT NULL REFERENCES person(id),
|
target INTEGER NOT NULL REFERENCES person(id) DEFERRABLE INITIALLY DEFERRED,
|
||||||
-- The person who gave out the warning, or NULL if it was automatically generated
|
-- The person who gave out the warning, or NULL if it was automatically generated
|
||||||
sender INTEGER REFERENCES person(id),
|
sender INTEGER REFERENCES person(id) DEFERRABLE INITIALLY DEFERRED,
|
||||||
-- Number of warning points
|
-- Number of warning points
|
||||||
amount INTEGER NOT NULL,
|
amount INTEGER NOT NULL,
|
||||||
-- When the warning was issued ('YYYY-MM-DD HH:MM:SS')
|
-- When the warning was issued ('YYYY-MM-DD HH:MM:SS')
|
||||||
@ -74,21 +68,22 @@ CREATE TABLE IF NOT EXISTS warning (
|
|||||||
-- Set to 1 if the warning was rescinded by an admin before it expired
|
-- Set to 1 if the warning was rescinded by an admin before it expired
|
||||||
deleted BOOLEAN NOT NULL DEFAULT 0,
|
deleted BOOLEAN NOT NULL DEFAULT 0,
|
||||||
-- If the warning was rescinded, this tracks by whom
|
-- If the warning was rescinded, this tracks by whom
|
||||||
deleted_by INTEGER REFERENCES person(id),
|
deleted_by INTEGER REFERENCES person(id) DEFERRABLE INITIALLY DEFERRED,
|
||||||
-- If the warning was rescinded, this tracks when that happened ('YYYY-MM-DD HH:MM:SS')
|
-- If the warning was rescinded, this tracks when that happened ('YYYY-MM-DD HH:MM:SS')
|
||||||
deleted_on DATETIME
|
deleted_on DATETIME
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS warning_idx ON warning (target, deleted, issued);
|
CREATE INDEX IF NOT EXISTS warning_idx ON warning (target, deleted, issued);
|
||||||
|
CREATE INDEX IF NOT EXISTS warning_sender_idx ON warning (target, sender, deleted, issued);
|
||||||
|
|
||||||
-- In addition to giving warning points, a warning may have specific sanctions attached
|
-- In addition to giving warning points, a warning may have specific sanctions attached
|
||||||
-- that apply until the warning expires; for example preventing a user from joining deadchat
|
-- that apply until the warning expires; for example preventing a user from joining deadchat
|
||||||
-- or denying them access to a particular command (such as !goat).
|
-- or denying them access to a particular command (such as !goat).
|
||||||
CREATE TABLE IF NOT EXISTS warning_sanction (
|
CREATE TABLE IF NOT EXISTS warning_sanction (
|
||||||
-- The warning this sanction is attached to
|
-- The warning this sanction is attached to
|
||||||
warning INTEGER NOT NULL REFERENCES warning(id),
|
warning INTEGER NOT NULL REFERENCES warning(id) DEFERRABLE INITIALLY DEFERRED,
|
||||||
-- The type of sanction this is
|
-- The type of sanction this is
|
||||||
sanction TEXT NOT NULL,
|
sanction TEXT NOT NULL COLLATE NOCASE,
|
||||||
-- If the sanction type has additional data attached, it is listed here
|
-- If the sanction type has additional data attached, it is listed here
|
||||||
data TEXT
|
data TEXT
|
||||||
);
|
);
|
||||||
@ -100,7 +95,7 @@ CREATE TABLE IF NOT EXISTS warning_sanction (
|
|||||||
CREATE TABLE IF NOT EXISTS game (
|
CREATE TABLE IF NOT EXISTS game (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
-- The gamemode played
|
-- The gamemode played
|
||||||
gamemode TEXT NOT NULL,
|
gamemode TEXT NOT NULL COLLATE NOCASE,
|
||||||
-- Game options (role reveal, stats type, etc.), stored as JSON string
|
-- Game options (role reveal, stats type, etc.), stored as JSON string
|
||||||
-- The json1 extension can be loaded into sqlite to allow for easy querying of these values
|
-- The json1 extension can be loaded into sqlite to allow for easy querying of these values
|
||||||
-- lykos itself does not make use of this field when calculating stats at this time
|
-- lykos itself does not make use of this field when calculating stats at this time
|
||||||
@ -112,7 +107,7 @@ CREATE TABLE IF NOT EXISTS game (
|
|||||||
-- Game size (at game start)
|
-- Game size (at game start)
|
||||||
gamesize INTEGER NOT NULL,
|
gamesize INTEGER NOT NULL,
|
||||||
-- Winning team (NULL if no winner)
|
-- Winning team (NULL if no winner)
|
||||||
winner TEXT
|
winner TEXT COLLATE NOCASE
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS game_idx ON game (gamemode, gamesize);
|
CREATE INDEX IF NOT EXISTS game_idx ON game (gamemode, gamesize);
|
||||||
@ -120,8 +115,8 @@ CREATE INDEX IF NOT EXISTS game_idx ON game (gamemode, gamesize);
|
|||||||
-- List of people who played in each game
|
-- List of people who played in each game
|
||||||
CREATE TABLE IF NOT EXISTS game_player (
|
CREATE TABLE IF NOT EXISTS game_player (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
game INTEGER NOT NULL REFERENCES game(id),
|
game INTEGER NOT NULL REFERENCES game(id) DEFERRABLE INITIALLY DEFERRED,
|
||||||
player INTEGER NOT NULL REFERENCES player(id),
|
player INTEGER NOT NULL REFERENCES player(id) DEFERRABLE INITIALLY DEFERRED,
|
||||||
-- 1 if the player has a team win for this game
|
-- 1 if the player has a team win for this game
|
||||||
team_win BOOLEAN NOT NULL,
|
team_win BOOLEAN NOT NULL,
|
||||||
-- 1 if the player has an individual win for this game
|
-- 1 if the player has an individual win for this game
|
||||||
@ -135,9 +130,9 @@ CREATE INDEX IF NOT EXISTS game_player_player_idx ON game_player (player);
|
|||||||
|
|
||||||
-- List of all roles and other special qualities (e.g. lover, entranced, etc.) the player had in game
|
-- List of all roles and other special qualities (e.g. lover, entranced, etc.) the player had in game
|
||||||
CREATE TABLE IF NOT EXISTS game_player_role (
|
CREATE TABLE IF NOT EXISTS game_player_role (
|
||||||
game_player INTEGER NOT NULL REFERENCES game_player(id),
|
game_player INTEGER NOT NULL REFERENCES game_player(id) DEFERRABLE INITIALLY DEFERRED,
|
||||||
-- Name of the role or other quality recorded
|
-- Name of the role or other quality recorded
|
||||||
role TEXT NOT NULL,
|
role TEXT NOT NULL COLLATE NOCASE,
|
||||||
-- 1 if role is a special quality instead of an actual role/template name
|
-- 1 if role is a special quality instead of an actual role/template name
|
||||||
special BOOLEAN NOT NULL
|
special BOOLEAN NOT NULL
|
||||||
);
|
);
|
||||||
@ -156,9 +151,9 @@ CREATE TABLE IF NOT EXISTS access_template (
|
|||||||
|
|
||||||
-- Access control, owners still need to be specified in botconfig, but everyone else goes here
|
-- Access control, owners still need to be specified in botconfig, but everyone else goes here
|
||||||
CREATE TABLE IF NOT EXISTS access (
|
CREATE TABLE IF NOT EXISTS access (
|
||||||
person INTEGER NOT NULL PRIMARY KEY REFERENCES person(id),
|
person INTEGER NOT NULL PRIMARY KEY REFERENCES person(id) DEFERRABLE INITIALLY DEFERRED,
|
||||||
-- Template to base this person's access on, or NULL if it is not based on a template
|
-- Template to base this person's access on, or NULL if it is not based on a template
|
||||||
template INTEGER REFERENCES access_template(id),
|
template INTEGER REFERENCES access_template(id) DEFERRABLE INITIALLY DEFERRED,
|
||||||
-- If template is NULL, this is the list of flags that will be used
|
-- If template is NULL, this is the list of flags that will be used
|
||||||
-- Has no effect if template is not NULL
|
-- Has no effect if template is not NULL
|
||||||
flags TEXT
|
flags TEXT
|
266
src/db/upgrade2.sql
Normal file
266
src/db/upgrade2.sql
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
-- Upgrade script to migrate from schema version 1 to 2
|
||||||
|
PRAGMA foreign_keys = OFF;
|
||||||
|
BEGIN EXCLUSIVE TRANSACTION;
|
||||||
|
|
||||||
|
CREATE TEMPORARY TABLE mergeq (
|
||||||
|
oldperson INTEGER,
|
||||||
|
newperson INTEGER,
|
||||||
|
active BOOLEAN
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE player2 (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
-- must be nullable so that we can insert new player records
|
||||||
|
person INTEGER REFERENCES person(id) DEFERRABLE INITIALLY DEFERRED,
|
||||||
|
account TEXT COLLATE NOCASE,
|
||||||
|
hostmask TEXT COLLATE NOCASE,
|
||||||
|
active BOOLEAN NOT NULL DEFAULT 1
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO player2 (
|
||||||
|
id,
|
||||||
|
person,
|
||||||
|
account,
|
||||||
|
hostmask,
|
||||||
|
active
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
p.id,
|
||||||
|
pp.person,
|
||||||
|
p.account,
|
||||||
|
p.hostmask,
|
||||||
|
p.active
|
||||||
|
FROM player p
|
||||||
|
JOIN person_player pp
|
||||||
|
ON pp.player = p.id;
|
||||||
|
|
||||||
|
DROP TABLE player;
|
||||||
|
ALTER TABLE player2 RENAME TO player;
|
||||||
|
|
||||||
|
CREATE INDEX player_idx ON player (account, hostmask, active);
|
||||||
|
CREATE INDEX person_idx ON player (person);
|
||||||
|
|
||||||
|
-- Casefold the player table; we may have multiple records
|
||||||
|
-- with the same account/hostmask that are active
|
||||||
|
-- in that case, we need to keep track of them to merge
|
||||||
|
-- them together later on.
|
||||||
|
INSERT INTO mergeq (oldperson, newperson, active)
|
||||||
|
SELECT DISTINCT
|
||||||
|
pp1.person,
|
||||||
|
MAX(pp2.person),
|
||||||
|
0
|
||||||
|
FROM player p1
|
||||||
|
JOIN player p2
|
||||||
|
ON (p1.account IS NOT NULL AND p1.account = p2.account)
|
||||||
|
OR (p1.hostmask IS NOT NULL AND p1.hostmask = p2.hostmask)
|
||||||
|
JOIN person_player pp1
|
||||||
|
ON pp1.player = p1.id
|
||||||
|
JOIN person_player pp2
|
||||||
|
ON pp2.player = p2.id
|
||||||
|
WHERE
|
||||||
|
p1.active = 1
|
||||||
|
AND p2.active = 1
|
||||||
|
AND p1.id < p2.id
|
||||||
|
GROUP BY p1.id, pp1.person;
|
||||||
|
|
||||||
|
-- person_player no longer needs to exist; it was moved to a column on player
|
||||||
|
-- it was already set up as a one-to-many relationship, so a mapping table
|
||||||
|
-- was not needed (mapping tables are for many-to-many relationships)
|
||||||
|
DROP TABLE person_player;
|
||||||
|
|
||||||
|
-- set FKs on warning and warning_sanction to be deferrable, and make
|
||||||
|
-- sanction type case-insensitive.
|
||||||
|
CREATE TABLE warning2 (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
target INTEGER NOT NULL REFERENCES person(id) DEFERRABLE INITIALLY DEFERRED,
|
||||||
|
sender INTEGER REFERENCES person(id) DEFERRABLE INITIALLY DEFERRED,
|
||||||
|
amount INTEGER NOT NULL,
|
||||||
|
issued DATETIME NOT NULL,
|
||||||
|
expires DATETIME,
|
||||||
|
reason TEXT NOT NULL,
|
||||||
|
notes TEXT,
|
||||||
|
acknowledged BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
deleted BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
deleted_by INTEGER REFERENCES person(id) DEFERRABLE INITIALLY DEFERRED,
|
||||||
|
deleted_on DATETIME
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO warning2 (
|
||||||
|
id, target, sender, amount, issued, expires, reason, notes,
|
||||||
|
acknowledged, deleted, deleted_by, deleted_on
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
id, target, sender, amount, issued, expires, reason, notes,
|
||||||
|
acknowledged, deleted, deleted_by, deleted_on
|
||||||
|
FROM warning;
|
||||||
|
|
||||||
|
DROP TABLE warning;
|
||||||
|
ALTER TABLE warning2 RENAME TO warning;
|
||||||
|
|
||||||
|
CREATE INDEX warning_idx ON warning (target, deleted, issued);
|
||||||
|
CREATE INDEX warning_sender_idx ON warning (target, sender, deleted, issued);
|
||||||
|
|
||||||
|
CREATE TABLE warning_sanction2 (
|
||||||
|
warning INTEGER NOT NULL REFERENCES warning(id) DEFERRABLE INITIALLY DEFERRED,
|
||||||
|
sanction TEXT NOT NULL COLLATE NOCASE,
|
||||||
|
data TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO warning_sanction2 (warning, sanction, data)
|
||||||
|
SELECT warning, sanction, data
|
||||||
|
FROM warning_sanction;
|
||||||
|
|
||||||
|
DROP TABLE warning_sanction;
|
||||||
|
ALTER TABLE warning_sanction2 RENAME TO warning_sanction;
|
||||||
|
|
||||||
|
-- Make game caseless, also modify winner for fool
|
||||||
|
-- instead of @id, make winner 'fool' and then game_player
|
||||||
|
-- can be checked to see what fool won (the winning fool gets a team win)
|
||||||
|
CREATE TABLE game2 (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
gamemode TEXT NOT NULL COLLATE NOCASE,
|
||||||
|
options TEXT,
|
||||||
|
started DATETIME NOT NULL,
|
||||||
|
finished DATETIME NOT NULL,
|
||||||
|
gamesize INTEGER NOT NULL,
|
||||||
|
winner TEXT COLLATE NOCASE
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO game2 (id, gamemode, options, started, finished, gamesize, winner)
|
||||||
|
SELECT id, gamemode, options, started, finished, gamesize, winner
|
||||||
|
FROM game;
|
||||||
|
|
||||||
|
DROP TABLE game;
|
||||||
|
ALTER TABLE game2 RENAME TO game;
|
||||||
|
|
||||||
|
CREATE INDEX game_idx ON game (gamemode, gamesize);
|
||||||
|
|
||||||
|
CREATE TABLE game_player_role2 (
|
||||||
|
game_player INTEGER NOT NULL REFERENCES game_player(id) DEFERRABLE INITIALLY DEFERRED,
|
||||||
|
role TEXT NOT NULL COLLATE NOCASE,
|
||||||
|
special BOOLEAN NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO game_player_role2 (game_player, role, special)
|
||||||
|
SELECT game_player, role, special
|
||||||
|
FROM game_player_role;
|
||||||
|
|
||||||
|
DROP TABLE game_player_role;
|
||||||
|
ALTER TABLE game_player_role2 RENAME TO game_player_role;
|
||||||
|
|
||||||
|
CREATE INDEX game_player_role_idx ON game_player_role (game_player);
|
||||||
|
|
||||||
|
UPDATE game_player
|
||||||
|
SET team_win = 1
|
||||||
|
WHERE id IN (
|
||||||
|
SELECT gp.id
|
||||||
|
FROM game_player gp
|
||||||
|
JOIN game g
|
||||||
|
ON g.id = gp.game
|
||||||
|
JOIN game_player_role gpr
|
||||||
|
ON gpr.game_player = gp.id
|
||||||
|
WHERE
|
||||||
|
gpr.role = 'fool'
|
||||||
|
AND g.winner = '@' || gp.player
|
||||||
|
AND gp.indiv_win = 1
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE game
|
||||||
|
SET winner = 'fool'
|
||||||
|
WHERE SUBSTR(winner, 1, 1) = '@';
|
||||||
|
|
||||||
|
-- deferrable FK on access
|
||||||
|
CREATE TABLE access2 (
|
||||||
|
person INTEGER NOT NULL PRIMARY KEY REFERENCES person(id) DEFERRABLE INITIALLY DEFERRED,
|
||||||
|
template INTEGER REFERENCES access_template(id) DEFERRABLE INITIALLY DEFERRED,
|
||||||
|
flags TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO access2 (person, template, flags)
|
||||||
|
SELECT person, template, flags
|
||||||
|
FROM access;
|
||||||
|
|
||||||
|
DROP TABLE access;
|
||||||
|
ALTER TABLE access2 RENAME TO access;
|
||||||
|
|
||||||
|
-- Merge player/person records from mergeq
|
||||||
|
-- We merge into the newest record, only thing
|
||||||
|
-- to carry over from the old are warnings and stasis
|
||||||
|
-- access entries are NOT carried over if the new
|
||||||
|
-- person has access (if old is non-null and new is null,
|
||||||
|
-- then access is migrated). If the user has multiple old
|
||||||
|
-- access records, one is selected arbitrarily.
|
||||||
|
|
||||||
|
-- annoyingly, CTEs are only supported on sqlite 3.8.3+
|
||||||
|
-- and ubuntu 14.04 ships with 3.8.2. I *really* want to
|
||||||
|
-- just move over to postgres >_>
|
||||||
|
CREATE TEMPORARY TABLE u (
|
||||||
|
player INTEGER,
|
||||||
|
person INTEGER,
|
||||||
|
active BOOLEAN
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO u (player, person, active)
|
||||||
|
SELECT
|
||||||
|
p.id,
|
||||||
|
COALESCE(mergeq.newperson, p.person),
|
||||||
|
COALESCE(mergeq.active, p.active)
|
||||||
|
FROM player p
|
||||||
|
LEFT JOIN mergeq
|
||||||
|
ON mergeq.oldperson = p.person;
|
||||||
|
|
||||||
|
UPDATE player
|
||||||
|
SET
|
||||||
|
person = (SELECT u.person FROM u WHERE u.player = player.id),
|
||||||
|
active = (SELECT u.active FROM u WHERE u.player = player.id);
|
||||||
|
|
||||||
|
DROP TABLE u;
|
||||||
|
|
||||||
|
INSERT OR IGNORE INTO access (person, template, flags)
|
||||||
|
SELECT
|
||||||
|
m.newperson,
|
||||||
|
a.template,
|
||||||
|
a.flags
|
||||||
|
FROM mergeq m
|
||||||
|
JOIN access a
|
||||||
|
ON a.person = m.oldperson;
|
||||||
|
|
||||||
|
DELETE FROM access
|
||||||
|
WHERE person IN (SELECT oldperson FROM mergeq);
|
||||||
|
|
||||||
|
CREATE TEMPORARY TABLE u (
|
||||||
|
id INTEGER,
|
||||||
|
target INTEGER,
|
||||||
|
sender INTEGER,
|
||||||
|
deleted_by INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO u (id, target, sender, deleted_by)
|
||||||
|
SELECT
|
||||||
|
w.id,
|
||||||
|
COALESCE(m1.newperson, w.target),
|
||||||
|
COALESCE(m2.newperson, w.sender),
|
||||||
|
COALESCE(m3.newperson, w.deleted_by)
|
||||||
|
FROM warning w
|
||||||
|
LEFT JOIN mergeq m1
|
||||||
|
ON m1.oldperson = w.target
|
||||||
|
LEFT JOIN mergeq m2
|
||||||
|
ON m2.oldperson = w.sender
|
||||||
|
LEFT JOIN mergeq m3
|
||||||
|
ON m3.oldperson = w.deleted_by;
|
||||||
|
|
||||||
|
UPDATE warning
|
||||||
|
SET
|
||||||
|
target = (SELECT u.target FROM u WHERE u.id = warning.id),
|
||||||
|
sender = (SELECT u.sender FROM u WHERE u.id = warning.id),
|
||||||
|
deleted_by = (SELECT u.deleted_by FROM u WHERE u.id = warning.id);
|
||||||
|
|
||||||
|
DROP TABLE u;
|
||||||
|
|
||||||
|
-- finally, blow off our old person records
|
||||||
|
DELETE FROM person WHERE id IN (SELECT oldperson FROM mergeq);
|
||||||
|
|
||||||
|
DROP TABLE mergeq;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
PRAGMA foreign_keys = ON;
|
@ -2571,6 +2571,8 @@ def stop_game(cli, winner = "", abort = False, additional_winners = None):
|
|||||||
won = True
|
won = True
|
||||||
elif rol == "turncoat" and splr in var.TURNCOATS and var.TURNCOATS[splr][0] != "none":
|
elif rol == "turncoat" and splr in var.TURNCOATS and var.TURNCOATS[splr][0] != "none":
|
||||||
won = (winner == var.TURNCOATS[splr][0])
|
won = (winner == var.TURNCOATS[splr][0])
|
||||||
|
elif rol == "fool" and "@" + splr == winner:
|
||||||
|
won = True
|
||||||
elif winner != "succubi" and splr in var.ENTRANCED:
|
elif winner != "succubi" and splr in var.ENTRANCED:
|
||||||
# entranced players can't win with villager or wolf teams
|
# entranced players can't win with villager or wolf teams
|
||||||
won = False
|
won = False
|
||||||
@ -2677,6 +2679,11 @@ def stop_game(cli, winner = "", abort = False, additional_winners = None):
|
|||||||
if len(pl) > 0:
|
if len(pl) > 0:
|
||||||
game_options["roles"][role] = len(pl)
|
game_options["roles"][role] = len(pl)
|
||||||
|
|
||||||
|
# normalize fool wins; to determine which fool won look for who got a team win for the game
|
||||||
|
# not plural (unlike other winner values) since only a singular fool wins
|
||||||
|
if winner.startswith("@"):
|
||||||
|
winner = "fool"
|
||||||
|
|
||||||
db.add_game(var.CURRENT_GAMEMODE.name,
|
db.add_game(var.CURRENT_GAMEMODE.name,
|
||||||
len(survived) + len(var.DEAD),
|
len(survived) + len(var.DEAD),
|
||||||
time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(var.GAME_ID)),
|
time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(var.GAME_ID)),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user