Debugging db migration issues

This commit is contained in:
Collin R
2021-02-22 00:26:34 -08:00
parent 284fff7e36
commit 4305e3f619
4 changed files with 125 additions and 70 deletions

View File

@@ -4,7 +4,9 @@ File-specific lists are in loose descending order of priority.
## Current ## Current
n/a * [ ] Change commands from !solved and /solved to !helped and /helped
* [ ] Allow multiple users to be awarded points on a single post
* So just check whether each user is already awarded a point for a given post
## Bugs ## Bugs

View File

@@ -1,6 +1,6 @@
################################################################################ ################################################################################
# Core # Core
# #
# These are the primary fields needed to run the bot. # These are the primary fields needed to run the bot.
################################################################################ ################################################################################
@@ -10,26 +10,13 @@ subreddit = ""
valid_tags = "Java,Bedrock,Dungeons,Earth,Education,Legacy" valid_tags = "Java,Bedrock,Dungeons,Earth,Education,Legacy"
################################################################################
# Filepaths
#
# Any filepaths needed by the bot. Can be relative or absolute paths, or just
# filenames.
################################################################################
[filepaths]
# The name of the SQLite database file to use. This value is optional; if not
# specified, a default filepath will be used.
database = ""
################################################################################ ################################################################################
# Reddit Credentials # Reddit Credentials
# #
# See the bot documentation for more information about these fields. # See the bot documentation for more information about these fields.
# Some of these fields are sensitive, so once these fields are provided, take # Some of these fields are sensitive, so once these fields are provided, take
# care not to upload or distribute this file through unsecure channels, or to # care not to upload or distribute this file through insecure channels, or to
# public sites. # public sites.
################################################################################ ################################################################################
@@ -40,6 +27,34 @@ username = "REDACTED"
password = "REDACTED" password = "REDACTED"
################################################################################
# Filepaths
#
# Any filepaths needed by the bot. Can be relative or absolute paths, or just
# filenames.
################################################################################
[filepaths]
# The name/path of the SQLite database file to use. This value is optional; if not
# specified, a default filepath will be used.
database = ""
# The name/path of the log file to use. This value is optional; if not specified,
# a default filepath will be used.
log = ""
################################################################################
# Links
#
# Any links used in the bot's reply. If you do not need to use these links,
# leave these fields blank.
################################################################################
[links]
feedback = ""
scoreboard = ""
################################################################################ ################################################################################
# User Levels # User Levels
# #
@@ -47,9 +62,9 @@ password = "REDACTED"
# attain. To add a level, add another section with the format: # attain. To add a level, add another section with the format:
# #
# [[levels]] # [[levels]]
# name = "<string>" # name = "<name inside quotes>"
# points = <integer> # points = <integer>
# flair_template_id = "<string>" # flair_template_id = "<id inside quotes>"
# #
# These fields are case-sensitive, so enter the level name exactly as you want # These fields are case-sensitive, so enter the level name exactly as you want
# it to appear. # it to appear.

View File

@@ -119,7 +119,7 @@ class Config:
obj['levels'].append({ obj['levels'].append({
'name': level.name, 'name': level.name,
'points': level.points, 'points': level.points,
'flair_template_id': level.flair_template_id, 'flair_template_id': level.flair_template_id
}) })
obj['tags'] = ','.join(obj['tags']) obj['tags'] = ','.join(obj['tags'])
@@ -142,10 +142,8 @@ def interactive_config(dest):
print('\n' + ('#' * 80) + '\nCONFIGURING THE BOT\n' + ('#' * 80) + '\n') print('\n' + ('#' * 80) + '\nCONFIGURING THE BOT\n' + ('#' * 80) + '\n')
print('Type a value for each field, then press enter.') print('Type a value for each field, then press enter.')
print('\nIf a field is specified as optional, then you can skip it by just ' print('\nIf a field is specified as optional, then you can skip it by just pressing enter.')
'pressing enter.') print("\nIt is recommended that you skip any fields that you aren't sure about")
print("\nIt is recommended that you skip any fields that you aren't sure "
'about')
print('\n*** Core Configuration ***\n') print('\n*** Core Configuration ***\n')
configvals['core']['subreddit'] = input('name of subreddit to monitor? ') configvals['core']['subreddit'] = input('name of subreddit to monitor? ')
@@ -154,8 +152,8 @@ def interactive_config(dest):
configvals['filepaths']['log'] = input('log filepath? (optional) ') configvals['filepaths']['log'] = input('log filepath? (optional) ')
print('\n*** Website Links ***\n') print('\n*** Website Links ***\n')
print('These values should only be provided if you have valid URLs for ' print('These values should only be provided if you have valid URLs for websites that provide '
'websites that provide these services for your subreddit.\n') 'these services for your subreddit.\n')
configvals['links']['feedback'] = input('feedback webpage URL? (optional) ') configvals['links']['feedback'] = input('feedback webpage URL? (optional) ')
configvals['links']['scoreboard'] = input('scoreboard webpage URL? (optional) ') configvals['links']['scoreboard'] = input('scoreboard webpage URL? (optional) ')
@@ -166,8 +164,8 @@ def interactive_config(dest):
configvals['credentials']['password'] = input('bot password? ') configvals['credentials']['password'] = input('bot password? ')
print('\n*** Flair Levels ***\n') print('\n*** Flair Levels ***\n')
print('These fields will determine the different levels that your ' print('These fields will determine the different levels that your subreddit users can achieve '
'subreddit users can achieve by earning points.') 'by earning points.')
print('\nFor each level, you should provide...') print('\nFor each level, you should provide...')
print("\t- Level name: the text that appears in the user's flair") print("\t- Level name: the text that appears in the user's flair")
print('\t- Level points: the number of points needed to reach the level') print('\t- Level points: the number of points needed to reach the level')
@@ -175,12 +173,10 @@ def interactive_config(dest):
print('\t subreddit to be used for this level flair') print('\t subreddit to be used for this level flair')
print('\nThese may be provided in any order; the bot will sort them later.') print('\nThese may be provided in any order; the bot will sort them later.')
print('\nDo not provide more than one level with the same number of points.') print('\nDo not provide more than one level with the same number of points.')
print('\nNote that at the moment, providing a level points value of zero ' print('\nNote that at the moment, providing a level points value of zero will not set a '
'will not set a default flair, because users must solve at least one ' 'default flair, because users must solve at least one issue before the bot will keep '
'issue before the bot will keep track of their points and set their ' 'track of their points and set their flair for the first time.')
'flair for the first time.') print('\nFor any more questions, please refer to the README on the Github page.')
print('\nFor any more questions, please refer to the README on the Github '
'page.')
response = 'y' response = 'y'
while response.lower().startswith('y'): while response.lower().startswith('y'):

View File

@@ -22,16 +22,17 @@ def transaction(func):
self.cursor = self.conn.cursor() self.cursor = self.conn.cursor()
created_conn = True created_conn = True
try: return_value = func(self, *args, **kwargs)
return_value = func(self, *args, **kwargs) # try:
except Exception as e: # return_value = func(self, *args, **kwargs)
if self.conn.in_transaction: # except Exception as e:
self.conn.rollback() # if self.conn.in_transaction:
if created_conn: # self.conn.rollback()
self.cursor.close() # if created_conn:
self.conn.close() # self.cursor.close()
self.cursor = self.conn = None # self.conn.close()
raise e # self.cursor = self.conn = None
# raise e
if self.conn.in_transaction: if self.conn.in_transaction:
self.conn.commit() self.conn.commit()
@@ -51,10 +52,10 @@ def transaction(func):
class DatabaseVersion: class DatabaseVersion:
PRE_RELEASE_NAME_ORDER_NUMBER = { PRE_RELEASE_NAME_ORDER_NUMBER = {
None: None, None: 0,
'alpha': 0, 'alpha': 1,
'beta': 1, 'beta': 2,
'rc': 2 'rc': 3
} }
def __init__(self, major, minor, patch, pre_release_name=None, pre_release_number=None): def __init__(self, major, minor, patch, pre_release_name=None, pre_release_number=None):
@@ -88,9 +89,10 @@ class DatabaseVersion:
class Database: class Database:
# LATEST_VERSION = DatabaseVersion(0, 2, 0, None, None)
LATEST_VERSION = DatabaseVersion(0, 2, 0) LATEST_VERSION = DatabaseVersion(0, 2, 0)
# TODO now that I'm separating these statements by version, I could probably make these
# scripts instead of lists of individual statements...
SCHEMA_VERSION_STATEMENTS = { SCHEMA_VERSION_STATEMENTS = {
DatabaseVersion(0, 1, 0): [ DatabaseVersion(0, 1, 0): [
''' '''
@@ -112,9 +114,6 @@ class Database:
) )
''', ''',
''' '''
INSERT OR IGNORE INTO bot_version (major, minor, patch) VALUES (0, 2, 0)
''',
'''
ALTER TABLE redditor_points RENAME TO redditor ALTER TABLE redditor_points RENAME TO redditor
''', ''',
''' '''
@@ -160,6 +159,7 @@ class Database:
self._run_migrations() self._run_migrations()
logging.info('Successfully created database') logging.info('Successfully created database')
else: else:
logging.info(f'Using existing database: {self.path}')
current_version = self._get_current_version() current_version = self._get_current_version()
if current_version != self.LATEST_VERSION: if current_version != self.LATEST_VERSION:
logging.info('Newer database version exists; migrating...') logging.info('Newer database version exists; migrating...')
@@ -171,15 +171,34 @@ class Database:
if not current_version: if not current_version:
current_version = DatabaseVersion(0, 0, 0) current_version = DatabaseVersion(0, 0, 0)
logging.info(f'Current database version: {current_version}') logging.info(f'Current database version: {current_version}')
versions = sorted(v for v in self.SCHEMA_VERSION_STATEMENTS if current_version < v) versions = sorted(v for v in self.SCHEMA_VERSION_STATEMENTS if current_version < v)
for v in versions: for v in versions:
logging.info(f'Beginning migration to version: {v}...') logging.info(f'Beginning migration to version: {v}...')
for sql_stmt in self.SCHEMA_VERSION_STATEMENTS[v]: for sql_stmt in self.SCHEMA_VERSION_STATEMENTS[v]:
self.cursor.execute(sql_stmt) self.cursor.execute(sql_stmt)
if DatabaseVersion(0, 1, 0) < v:
# Only update bot_version table starting at version 0.2.0
self.cursor.execute('DELETE FROM bot_version')
params = {
'major': v.major,
'minor': v.minor,
'patch': v.patch,
'pre_release_name': v.pre_release_name,
'pre_release_number': v.pre_release_number
}
insert_stmt = '''
INSERT INTO bot_version (major, minor, patch, pre_release_name, pre_release_number)
VALUES (:major, :minor, :patch, :pre_release_name, :pre_release_number)
'''
self.cursor.execute(insert_stmt, params)
logging.info(f'Successfully completed migration') logging.info(f'Successfully completed migration')
@transaction @transaction
def _get_current_version(self): def _get_current_version(self):
# self.cursor.execute('select * from sqlite_master')
# for row in self.cursor.fetchmany():
# logging.info(tuple(row))
self.cursor.execute("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'bot_version'") self.cursor.execute("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'bot_version'")
has_version_table = (self.cursor.rowcount == 1) has_version_table = (self.cursor.rowcount == 1)
if not has_version_table: if not has_version_table:
@@ -226,7 +245,7 @@ class Database:
self.cursor.execute(select_stmt, {'submission_id': submission.id, 'author_id': solver.id}) self.cursor.execute(select_stmt, {'submission_id': submission.id, 'author_id': solver.id})
row = self.cursor.fetchone() row = self.cursor.fetchone()
return row and row['num_solutions'] > 0 return row and row['num_solutions'] > 0
def add_point_for_solution(self, submission, solver, solution_comment, chooser, chosen_by_comment): def add_point_for_solution(self, submission, solver, solution_comment, chooser, chosen_by_comment):
self._add_submission(submission) self._add_submission(submission)
self._add_comment(solution_comment, solver) self._add_comment(solution_comment, solver)
@@ -251,8 +270,10 @@ class Database:
@transaction @transaction
def add_back_point_for_solution(self, submission, solver): def add_back_point_for_solution(self, submission, solver):
self._update_points(solver, 1) self._update_points(solver, 1)
submission_rowid = self._get_rowid_from_reddit_id('submission', submission) # submission_rowid = self._get_rowid_from_reddit_id('submission', submission)
author_rowid = self._get_rowid_from_reddit_id('redditor', solver) # author_rowid = self._get_rowid_from_reddit_id('redditor', solver)
submission_rowid = self._get_submission_rowid(submission)
author_rowid = self._get_redditor_rowid(solver)
params = {'submission_rowid': submission_rowid, 'author_rowid': author_rowid} params = {'submission_rowid': submission_rowid, 'author_rowid': author_rowid}
update_stmt = ''' update_stmt = '''
UPDATE solution UPDATE solution
@@ -261,12 +282,14 @@ class Database:
AND author_rowid = :author_rowid AND author_rowid = :author_rowid
''' '''
return self.cursor.execute(update_stmt, params) return self.cursor.execute(update_stmt, params)
@transaction @transaction
def remove_point_and_delete_solution(self, submission, solver): def remove_point_and_delete_solution(self, submission, solver):
params = { params = {
'submission_rowid': self._get_rowid_from_reddit_id('submission', submission), # 'submission_rowid': self._get_rowid_from_reddit_id('submission', submission),
'author_rowid': self._get_rowid_from_reddit_id('redditor', solver) # 'author_rowid': self._get_rowid_from_reddit_id('redditor', solver)
'submission_rowid': self._get_submission_rowid(submission),
'author_rowid': self._get_redditor_rowid(solver)
} }
delete_stmt = ''' delete_stmt = '''
DELETE FROM solution DELETE FROM solution
@@ -296,10 +319,29 @@ class Database:
### Private Methods ### ### Private Methods ###
# @transaction
# def _get_rowid_from_reddit_id(self, table_name, reddit_object):
# params = {'table_name': table_name, 'reddit_id': reddit_object.id}
# self.cursor.execute('SELECT rowid FROM :table_name WHERE id = :reddit_id', params)
# row = self.cursor.fetchone()
# return row['rowid'] if row else None
# @transaction
def _get_submission_rowid(self, submission):
return self._get_rowid_from_reddit_id('SELECT rowid FROM submission WHERE id = :reddit_id', {'reddit_id': submission.id})
# self.cursor.execute('SELECT rowid FROM submission WHERE id = :reddit_id', {'reddit_id': submission.id})
# row = self.cursor.fetchone()
# return row['rowid'] if row else None
def _get_comment_rowid(self, comment):
return self._get_rowid_from_reddit_id('SELECT rowid FROM comment WHERE id = :reddit_id', {'reddit_id': comment.id})
def _get_redditor_rowid(self, redditor):
return self._get_rowid_from_reddit_id('SELECT rowid FROM redditor WHERE id = :reddit_id', {'reddit_id': redditor.id})
@transaction @transaction
def _get_rowid_from_reddit_id(self, table_name, reddit_object): def _get_rowid_from_reddit_id(self, stmt, params):
params = {'table_name': table_name, 'reddit_id': reddit_object.id} self.cursor.execute(stmt, params)
self.cursor.execute('SELECT rowid FROM :table_name WHERE id = :reddit_id', params)
row = self.cursor.fetchone() row = self.cursor.fetchone()
return row['rowid'] if row else None return row['rowid'] if row else None
@@ -328,10 +370,10 @@ class Database:
@transaction @transaction
def _add_solution(self, submission, solver, comment, chosen_by_comment): def _add_solution(self, submission, solver, comment, chosen_by_comment):
submission_rowid = self._get_rowid_from_reddit_id('submission', submission) submission_rowid = self._get_submission_rowid(submission)
author_rowid = self._get_rowid_from_reddit_id('redditor', solver) author_rowid = self._get_redditor_rowid(solver)
comment_rowid = self._get_rowid_from_reddit_id('comment', comment) comment_rowid = self._get_comment_rowid(comment)
chosen_by_comment_rowid = self._get_rowid_from_reddit_id('comment', chosen_by_comment) chosen_by_comment_rowid = self._get_comment_rowid(chosen_by_comment)
params = { params = {
'submission_rowid': submission_rowid, 'submission_rowid': submission_rowid,
'author_rowid': author_rowid, 'author_rowid': author_rowid,
@@ -347,9 +389,9 @@ class Database:
@transaction @transaction
def _soft_remove_solution(self, submission, solver, removed_by_comment): def _soft_remove_solution(self, submission, solver, removed_by_comment):
submission_rowid = self._get_rowid_from_reddit_id('submission', submission) submission_rowid = self._get_submission_rowid(submission)
author_rowid = self._get_rowid_from_reddit_id('redditor', solver) author_rowid = self._get_redditor_rowid(solver)
removed_by_comment_rowid = self._get_rowid_from_reddit_id('comment', removed_by_comment) removed_by_comment_rowid = self._get_comment_rowid(removed_by_comment)
params = { params = {
'submission_rowid': submission_rowid, 'submission_rowid': submission_rowid,
'author_rowid': author_rowid, 'author_rowid': author_rowid,
@@ -387,6 +429,6 @@ class Database:
### Utility ### ### Utility ###
def reddit_datetime_to_iso(datetime): def reddit_datetime_to_iso(timestamp):
return datetime.datetime.utcfromtimestamp(datetime).isoformat() return datetime.datetime.utcfromtimestamp(timestamp).isoformat()