ezdb - A one click script to quickly setting up a development database (#75053) (#21458)

* ezdb - A one click script to quickly setting up a development database (#75053)

https://user-images.githubusercontent.com/35135081/235344815-8e825ba9-52cf-44e8-b8e2-a2aeb5d47276.mp4

- Downloads a portable MariaDB (doesn't pollute your main system)
- Sets up a database with a random password on port 1338 (configurable)
- Installs the initial schema
- Every time after, will run updates

Major versions right now explicitly escape hatch, because those
historically come with something like a Python script, and I do not want
it to pretend to work.

---------

Co-authored-by: san7890 <the@san7890.com>

* touchups

* oh well

---------

Co-authored-by: Mothblocks <35135081+Mothblocks@users.noreply.github.com>
Co-authored-by: san7890 <the@san7890.com>
Co-authored-by: Useroth <37159550+Useroth@users.noreply.github.com>
This commit is contained in:
Tom
2023-06-03 23:15:41 +02:00
committed by GitHub
co-authored by san7890 Mothblocks Useroth
parent 8f9fd2893a
commit d91555b79e
22 changed files with 517 additions and 36 deletions
-1
View File
@@ -52,4 +52,3 @@
# Maptainers
/_maps/ @Jolly-66 @KathrinBailey
+2 -1
View File
@@ -110,8 +110,9 @@ Things you **CAN'T** do:
- [Hard Deletes](./guides/HARDDELETES.md)
- [MC Tab Guide](./guides/MC_tab.md)
- [Policy Configuration System](./guides/POLICYCONFIG.md)
- [Splitting up pull requests, aka atomization](./guides/ATOMIZATION.md)
- [Quickly setting up a development database with ezdb](./guides/EZDB.md)
- [Required Tests (Continuous Integration)](./guides/CI.md)
- [Splitting up pull requests, aka atomization](./guides/ATOMIZATION.md)
- [UI Development](../tgui/README.md)
- [Visual Effects and Systems](./guides/VISUALS.md)
+12
View File
@@ -0,0 +1,12 @@
# Quickly setting up a development database with ezdb
While you do not need a database to code for tgstation, it is a prerequisite to many important features, especially on the admin side. Thus, if you are working in any code that benefits from it, it can be helpful to have one handy.
**ezdb** is a tool for quickly setting up an isolated development database. It will manage downloading MariaDB, creating the database, setting it up, and updating it when the code evolves. It is not recommended for use in production servers, but is perfect for quick development.
To run ezdb, go to `tools/ezdb`, and double-click on ezdb.bat. This will set up the database on port 1338, but you can configure this with `--port`. When it is done, you should be able to launch tgstation as normal and have database access. This runs on the same Python bootstrapper as things like the map merge tool, which can sometimes be flaky.
If you wish to delete the ezdb database, delete the `db` folder as well as `config/ezdb.txt`.
To update ezdb, run the script again. This will both look for any updates in the database changelog, as well as update your schema revision.
Contact Mothblocks if you face any issues in this process.
+4
View File
@@ -239,3 +239,7 @@ Tracy.exe
# From /tools/define_sanity/check.py - potential output file that we load onto the user's machine that we don't want to have committed.
define_sanity_output.txt
# ezdb
/db/
/config/ezdb.txt
@@ -2,19 +2,21 @@ Any time you make a change to the schema files, remember to increment the databa
Make sure to also update `DB_MAJOR_VERSION` and `DB_MINOR_VERSION`, which can be found in `code/__DEFINES/subsystem.dm`.
The latest database version is 5.25 (5.23 for /tg/); The query to update the schema revision table is:
The latest database version is 5.26 (5.24 for /tg/); The query to update the schema revision table is:
INSERT INTO `schema_revision` (`major`, `minor`) VALUES (5, 25);
```sql
INSERT INTO `schema_revision` (`major`, `minor`) VALUES (5, 26);
```
or
INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (5, 25);
```sql
INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (5, 26);
```
In any query remember to add a prefix to the table names if you use one.
-----------------------------------------------------
<<<<<<< HEAD:SQL/database_changelog.txt
Version 5.25, 28 December 2022, by Mothblocks
=======
Version 5.24, 17 May 2023, by LemonInTheDark
Version 5.26, 17 May 2023, by LemonInTheDark
Modified the library action table to fit ckeys properly, and to properly store ips.
```sql
ALTER TABLE `library_action` MODIFY COLUMN `ckey` varchar(32) NOT NULL;
@@ -22,11 +24,10 @@ Modified the library action table to fit ckeys properly, and to properly store i
```
-----------------------------------------------------
Version 5.23, 28 December 2022, by Mothblocks
>>>>>>> 0065f020ed2 (Removes line about adding a new index to the library_action table (#75808)):SQL/database_changelog.md
Version 5.25, 28 December 2022, by Mothblocks
Added `tutorial_completions` to mark what ckeys have completed contextual tutorials.
```
```sql
CREATE TABLE `tutorial_completions` (
`id` INT NOT NULL AUTO_INCREMENT,
`ckey` VARCHAR(32) NOT NULL,
@@ -39,15 +40,15 @@ CREATE TABLE `tutorial_completions` (
Version 5.24, 22 December 2021, by Mothblocks
Fixes a bug in `telemetry_connections` that limited the range of IPs.
```
```sql
ALTER TABLE `telemetry_connections` MODIFY COLUMN `address` INT(10) UNSIGNED NOT NULL;
```
-----------------------------------------------------
Version 5.23, 15 December 2021, by Mothblocks
Adds `telemetry_connections` table for tracking tgui telemetry.
```
```sql
CREATE TABLE `telemetry_connections` (
`id` INT NOT NULL AUTO_INCREMENT,
`ckey` VARCHAR(32) NOT NULL,
@@ -65,17 +66,16 @@ CREATE TABLE `telemetry_connections` (
Version 5.22, 11 November 2021, by Mothblocks
Adds `admin_ckey` field to the `known_alts` table to track who added what.
```
```sql
ALTER TABLE `known_alts`
ADD COLUMN `admin_ckey` VARCHAR(32) NOT NULL DEFAULT '*no key*' AFTER `ckey2`;
```
-----------------------------------------------------
Version 5.21, 10 November 2021, by WalterMeldron
Adds an urgent column to tickets for ahelps marked as urgent.
```
```sql
ALTER TABLE `ticket` ADD COLUMN `urgent` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `sender`;
```
@@ -83,7 +83,7 @@ ALTER TABLE `ticket` ADD COLUMN `urgent` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0
Version 5.20, 1 November 2021, by Mothblocks
Added `known_alts` table for tracking who not to create suspicious logins for.
```
```sql
CREATE TABLE `known_alts` (
`id` INT NOT NULL AUTO_INCREMENT,
`ckey1` VARCHAR(32) NOT NULL,
@@ -94,18 +94,10 @@ CREATE TABLE `known_alts` (
```
-----------------------------------------------------
Version 5.19, 23 August 2021, by GoldenAlpharex
Added `discord_report` column to the `ban table`
```
`discord_reported` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0', /* SKYRAT EDIT - Labelling bans for ease of reporting them over Discord. */
```
-----------------------------------------------------
Version 5.18, 8 October 2021, by MrStonedOne + Mothblocks
Version 5.19, 8 October 2021, by MrStonedOne + Mothblocks
Changes any table that requrired a NOT NULL round ID to now accept NULL. In the BSQL past, these were handled as 0, but in the move to rust-g this behavior was lost.
```
```sql
ALTER TABLE `admin_log` CHANGE `round_id` `round_id` INT(11) UNSIGNED NULL;
ALTER TABLE `ban` CHANGE `round_id` `round_id` INT(11) UNSIGNED NULL;
ALTER TABLE `citation` CHANGE `round_id` `round_id` INT(11) UNSIGNED NULL;
@@ -120,11 +112,19 @@ ALTER TABLE `player` CHANGE `lastseen_round_id` `lastseen_round_id` INT(11) UNSI
ALTER TABLE `ticket` CHANGE `round_id` `round_id` INT(11) UNSIGNED NULL;
```
-----------------------------------------------------
Version 5.18, 23 August 2021, by GoldenAlpharex
Added `discord_report` column to the `ban table`
```sql
`discord_reported` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0', /* SKYRAT EDIT - Labelling bans for ease of reporting them over Discord. */
```
-----------------------------------------------------
Version 5.17, 31 July 2021, by Atlanta-Ned
Added `library_action` table for tracking reported library books and actions taken on them.
```
```sql
DROP TABLE IF EXISTS `library_action`;
CREATE TABLE `library_action` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
@@ -143,7 +143,7 @@ CREATE TABLE `library_action` (
Version 5.16, 2 June 2021, by Mothblocks
Added verified admin connection log used for 2FA
```
```sql
DROP TABLE IF EXISTS `admin_connections`;
CREATE TABLE `admin_connections` (
`id` INT NOT NULL AUTO_INCREMENT,
@@ -157,9 +157,10 @@ CREATE TABLE `admin_connections` (
-----------------------------------------------------
Version 5.15, 24 May 2021, by Anturke
Version 5.15, xx May 2021, by Anturke
Added exploration drone adventure table
```sql
DROP TABLE IF EXISTS `text_adventures`;
CREATE TABLE `text_adventures` (
`id` int(11) NOT NULL AUTO_INCREMENT,
@@ -169,12 +170,14 @@ CREATE TABLE `text_adventures` (
`approved` TINYINT(1) NOT NULL DEFAULT FALSE,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
```
-----------------------------------------------------
Version 5.14, 30 April 2021, by Atlanta Ned
Added the `citation` table for tracking security citations in the database.
```sql
CREATE TABLE `citation` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`round_id` INT(11) UNSIGNED NOT NULL,
@@ -196,6 +199,7 @@ COLLATE='utf8mb4_general_ci'
ENGINE=InnoDB
AUTO_INCREMENT=1
;
```
-----------------------------------------------------
@@ -206,6 +210,7 @@ between servers when it comes to bans, statistical data, etc.
You might also want to update the server_name column in older records in the tables, however it is not absolutely necessary.
```sql
ALTER TABLE `ban`
ADD COLUMN `server_name` VARCHAR(32) DEFAULT NULL AFTER `bantime`,
ADD COLUMN `global_ban` TINYINT(1) UNSIGNED NOT NULL DEFAULT '1' AFTER `role`;
@@ -227,6 +232,7 @@ ALTER TABLE `messages`
ALTER TABLE `round`
ADD COLUMN `server_name` VARCHAR(32) DEFAULT NULL AFTER `end_datetime`;
```
-----------------------------------------------------
@@ -234,7 +240,9 @@ ALTER TABLE `round`
Version 5.12, 29 December 2020, by Missfox
Modified table `messages`, adding column `playtime` to show the user's playtime when the note was created.
```sql
ALTER TABLE `messages` ADD `playtime` INT(11) NULL DEFAULT(NULL) AFTER `severity`
```
-----------------------------------------------------
@@ -242,11 +250,13 @@ Version 5.11, 7 September 2020, by bobbahbrown, MrStonedOne, and Jordie0608 (Upd
Adds indices to support search operations on the adminhelp ticket tables. This is to support improved performance on Atlanta Ned's Statbus.
```sql
ALTER TABLE `ticket`
ADD INDEX `idx_ticket_act_recip` (`action`, `recipient`),
ADD INDEX `idx_ticket_act_send` (`action`, `sender`),
ADD INDEX `idx_ticket_tic_rid` (`ticket`, `round_id`),
ADD INDEX `idx_ticket_act_time_rid` (`action`, `timestamp`, `round_id`);
```
-----------------------------------------------------
@@ -256,6 +266,7 @@ Changes how the discord verification process works.
Adds the discord_links table, and migrates discord id entries from player table to the discord links table in a once off operation and then removes the discord id
on the player table
```sql
START TRANSACTION;
DROP TABLE IF EXISTS `discord_links`;
@@ -274,6 +285,7 @@ INSERT INTO `discord_links` (`ckey`, `discord_id`, `one_time_token`, `valid`) SE
ALTER TABLE `player` DROP COLUMN `discord_id`;
COMMIT;
```
-----------------------------------------------------
@@ -283,6 +295,7 @@ Added the `deleted` column to tables 'poll_option', 'poll_textreply' and 'poll_v
Changes table 'poll_question' column `createdby_ckey` to be NOT NULL and index `idx_pquest_time_admin` to be `idx_pquest_time_deleted_id` and 'poll_textreply' column `adminrank` to have no default.
Added procedure `set_poll_deleted` that's called when deleting a poll to set deleted to true on each poll table where rows matching a poll_id argument.
```sql
ALTER TABLE `poll_option`
ADD COLUMN `deleted` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `default_percentage_calc`;
@@ -315,19 +328,23 @@ UPDATE `poll_textreply` SET deleted = 1 WHERE pollid = poll_id;
END
$$
DELIMITER ;
```
-----------------------------------------------------
Version 5.8, 7 April 2020, by Jordie0608
Modified table `messages`, adding column `deleted_ckey` to record who deleted a message.
```sql
ALTER TABLE `messages` ADD COLUMN `deleted_ckey` VARCHAR(32) NULL DEFAULT NULL AFTER `deleted`;
```
-----------------------------------------------------
Version 5.7, 10 January 2020 by Atlanta-Ned
Added ticket table for tracking ahelp tickets in the database.
```sql
DROP TABLE IF EXISTS `ticket`;
CREATE TABLE `ticket` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
@@ -342,20 +359,23 @@ CREATE TABLE `ticket` (
`sender` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
-----------------------------------------------------
Version 5.6, 6 December 2019 by Anturke
Added achievement_name and achievement_description columns to achievement_metadata table.
```sql
ALTER TABLE `achievement_metadata` ADD COLUMN (`achievement_name` VARCHAR(64) NULL DEFAULT NULL, `achievement_description` VARCHAR(512) NULL DEFAULT NULL);
```
-----------------------------------------------------
Version 5.5, 26 October 2019 by Anturke
Added achievement_metadata table.
```sql
DROP TABLE IF EXISTS `achievement_metadata`;
CREATE TABLE `achievement_metadata` (
`achievement_key` VARCHAR(32) NOT NULL,
@@ -363,7 +383,7 @@ CREATE TABLE `achievement_metadata` (
`achievement_type` enum('achievement','score','award') NULL DEFAULT NULL,
PRIMARY KEY (`achievement_key`)
) ENGINE=InnoDB;
```
-----------------------------------------------------
@@ -371,6 +391,7 @@ Version 5.4, 5 October 2019 by Anturke
Added achievements table.
See hub migration verb in _achievement_data.dm for details on migrating.
```sql
CREATE TABLE `achievements` (
`ckey` VARCHAR(32) NOT NULL,
`achievement_key` VARCHAR(32) NOT NULL,
@@ -378,26 +399,32 @@ CREATE TABLE `achievements` (
`last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`ckey`,`achievement_key`)
) ENGINE=InnoDB;
```
----------------------------------------------------
Version 5.3, 6 July 2019, by Atlanta-Ned
Added a `feedback` column to the admin table, used for linking to individual admin feedback threads. Currently this is only used for statistics tracking tools such as Statbus and isn't used by the game.
```sql
ALTER TABLE `admin` ADD `feedback` VARCHAR(255) NULL DEFAULT NULL AFTER `rank`;
```
----------------------------------------------------
Version 5.2, 30 May 2019, by AffectedArc07
Added a field to the `player` table to track ckey and discord ID relationships
```sql
ALTER TABLE `player`
ADD COLUMN `discord_id` BIGINT NULL DEFAULT NULL AFTER `flags`;
```
----------------------------------------------------
Version 5.1, 25 Feb 2018, by MrStonedOne
Added four tables to enable storing of stickybans in the database since byond can lose them, and to enable disabling stickybans for a round without depending on a crash free round. Existing stickybans are automagically imported to the tables.
```sql
CREATE TABLE `stickyban` (
`ckey` VARCHAR(32) NOT NULL,
`reason` VARCHAR(2048) NOT NULL,
@@ -430,6 +457,7 @@ CREATE TABLE `stickyban_matched_cid` (
`last_matched` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`stickyban`, `matched_cid`)
) ENGINE=InnoDB;
```
----------------------------------------------------
@@ -439,6 +467,8 @@ Modified ban table to remove the need for the `bantype` column, a python script
See the file 'ban_conversion_2018-10-28.py' for instructions on how to use the script.
A new ban table can be created with the query:
```sql
CREATE TABLE `ban` (
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`bantime` DATETIME NOT NULL,
@@ -468,6 +498,7 @@ CREATE TABLE `ban` (
KEY `idx_ban_isbanned_details` (`ckey`,`ip`,`computerid`,`role`,`unbanned_datetime`,`expiration_time`),
KEY `idx_ban_count` (`bantime`,`a_ckey`,`applies_to_admins`,`unbanned_datetime`,`expiration_time`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
```
----------------------------------------------------
@@ -90,6 +90,8 @@
break
if (fexists("[directory]/dev_overrides.txt"))
LoadEntries("dev_overrides.txt")
if (fexists("[directory]/ezdb.txt"))
LoadEntries("ezdb.txt")
loadmaplist(CONFIG_MAPS_FILE)
LoadMOTD()
LoadPolicy()
@@ -68,3 +68,8 @@
. = ..()
if (.)
SSdbcore.max_concurrent_queries = config_entry_value
/// The exe for mariadbd.exe.
/// Shouldn't really be set on production servers, primarily for EZDB.
/datum/config_entry/string/db_daemon
protection = CONFIG_ENTRY_LOCKED | CONFIG_ENTRY_HIDDEN
+46 -2
View File
@@ -37,6 +37,8 @@ SUBSYSTEM_DEF(dbcore)
var/connection // Arbitrary handle returned from rust_g.
var/db_daemon_started = FALSE
/datum/controller/subsystem/dbcore/Initialize()
//We send warnings to the admins during subsystem init, as the clients will be New'd and messages
//will queue properly with goonchat
@@ -74,8 +76,6 @@ SUBSYSTEM_DEF(dbcore)
CONFIG_SET(number/pooling_max_sql_connections, max(min_sql_connections, max_sql_connections))
log_config("ERROR: POOLING_MAX_SQL_CONNECTIONS ([max_sql_connections]) is set lower than POOLING_MIN_SQL_CONNECTIONS ([min_sql_connections]). Please check your config or the code defaults for sanity")
/datum/controller/subsystem/dbcore/stat_entry(msg)
msg = "P:[length(all_queries)]|Active:[length(queries_active)]|Standby:[length(queries_standby)]"
return ..()
@@ -188,6 +188,7 @@ SUBSYSTEM_DEF(dbcore)
qdel(query_round_shutdown)
if(IsConnected())
Disconnect()
stop_db_daemon()
//nu
/datum/controller/subsystem/dbcore/can_vv_get(var_name)
@@ -231,6 +232,8 @@ SUBSYSTEM_DEF(dbcore)
if(!CONFIG_GET(flag/sql_enabled))
return FALSE
start_db_daemon()
var/user = CONFIG_GET(string/feedback_login)
var/pass = CONFIG_GET(string/feedback_password)
var/db = CONFIG_GET(string/feedback_database)
@@ -447,6 +450,47 @@ Delayed insert mode was removed in mysql 7 and only works with MyISAM type table
. = Query.Execute(async)
qdel(Query)
/datum/controller/subsystem/dbcore/proc/start_db_daemon()
set waitfor = FALSE
if (db_daemon_started)
return
db_daemon_started = TRUE
var/daemon = CONFIG_GET(string/db_daemon)
if (!daemon)
return
ASSERT(fexists(daemon), "Configured db_daemon doesn't exist")
var/list/result = world.shelleo("echo \"Starting ezdb daemon, do not close this window\" && [daemon]")
var/error_code = result[1]
if (!error_code)
return
stack_trace("Failed to start DB daemon: [error_code]\n[result[3]]")
/datum/controller/subsystem/dbcore/proc/stop_db_daemon()
set waitfor = FALSE
if (!db_daemon_started)
return
db_daemon_started = FALSE
var/daemon = CONFIG_GET(string/db_daemon)
if (!daemon)
return
switch (world.system_type)
if (MS_WINDOWS)
var/list/result = world.shelleo("Get-Process | ? { $_.Path -eq '[daemon]' } | Stop-Process")
ASSERT(result[1], "Failed to stop DB daemon: [result[3]]")
if (UNIX)
var/list/result = world.shelleo("kill $(pgrep -f '[daemon]')")
ASSERT(result[1], "Failed to stop DB daemon: [result[3]]")
/datum/db_query
// Inputs
var/connection
+15
View File
@@ -0,0 +1,15 @@
import argparse
from .steps import STEPS
parser = argparse.ArgumentParser()
parser.add_argument("--port", type = int, default = 1338)
args = parser.parse_args()
for step in STEPS:
if not step.should_run():
continue
step.run(args)
print("Done!")
+2
View File
@@ -0,0 +1,2 @@
@call "%~dp0\..\bootstrap\python" -m ezdb %*
@pause
View File
+31
View File
@@ -0,0 +1,31 @@
import re
from dataclasses import dataclass
from .paths import get_changelog_path
REGEX_CHANGE = r"-+\s*Version (?P<major>[0-9]+)\.(?P<minor>[0-9]+), .+?\`\`\`sql\s*(?P<sql>.+?)\s*\`\`\`.*?-{5}"
@dataclass
class Change:
major_version: int
minor_version: int
sql: str
def get_changes() -> list[Change]:
with open(get_changelog_path(), "r") as file:
changelog = file.read()
changes = []
for change_match in re.finditer(REGEX_CHANGE, changelog, re.MULTILINE | re.DOTALL):
changes.append(Change(
int(change_match.group("major")),
int(change_match.group("minor")),
change_match.group("sql")
))
changes.sort(key = lambda change: (change.major_version, change.minor_version), reverse = True)
return changes
def get_current_version():
changes = get_changes()
return (changes[0].major_version, changes[0].minor_version)
+22
View File
@@ -0,0 +1,22 @@
from .paths import get_config_path
from typing import Optional
def read_config() -> Optional[dict[str, str]]:
config_path = get_config_path()
if not config_path.exists():
return None
with config_path.open('r') as file:
lines = file.readlines()
entries = {}
for line in lines:
if line.startswith("#"):
continue
if " " not in line:
continue
key, value = line.split(" ", 1)
entries[key.strip()] = value.strip()
return entries
+68
View File
@@ -0,0 +1,68 @@
import atexit
import mysql.connector
import subprocess
from contextlib import closing
from .config import read_config
from .paths import get_mariadb_client_path, get_mariadb_daemon_path
def open_connection():
config = read_config()
assert config["FEEDBACK_PASSWORD"] is not None, "No password found in config file"
connection = mysql.connector.connect(
user = config["FEEDBACK_LOGIN"],
password = config["FEEDBACK_PASSWORD"],
port = int(config["PORT"]),
raise_on_warnings = True,
)
connection.autocommit = True
return closing(connection)
# We use custom things like delimiters, so we can't use the built-in cursor.execute
def execute_sql(sql: str):
config = read_config()
assert config is not None, "No config file found"
assert config["FEEDBACK_PASSWORD"] is not None, "No password found in config file"
subprocess.run(
[
str(get_mariadb_client_path()),
"-u",
"root",
"-p" + config["FEEDBACK_PASSWORD"],
"--port",
config["PORT"],
"--database",
config["FEEDBACK_DATABASE"],
],
input = sql,
encoding = "utf-8",
check = True,
stderr = subprocess.STDOUT,
)
def insert_new_schema_query(major_version: int, minor_version: int):
return f"INSERT INTO `schema_revision` (`major`, `minor`) VALUES ({major_version}, {minor_version})"
process = None
def start_daemon():
global process
if process is not None:
return
print("Starting MariaDB daemon...")
config = read_config()
assert config is not None, "No config file found"
process = subprocess.Popen(
[
str(get_mariadb_daemon_path()),
"--port",
config["PORT"],
],
stderr = subprocess.PIPE,
)
atexit.register(process.kill)
+34
View File
@@ -0,0 +1,34 @@
import pathlib
def get_root_path():
current_path = pathlib.Path(__file__)
while current_path.name != 'tools':
current_path = current_path.parent
return current_path.parent
def get_config_path():
return get_root_path() / 'config' / 'ezdb.txt'
def get_db_path():
return get_root_path() / 'db'
def get_data_path():
return get_db_path() / 'data'
def get_mariadb_bin_path():
return get_db_path() / 'bin'
def get_mariadb_client_path():
return get_mariadb_bin_path() / 'mariadb.exe'
def get_mariadb_daemon_path():
return get_mariadb_bin_path() / 'mariadbd.exe'
def get_mariadb_install_db_path():
return get_mariadb_bin_path() / 'mariadb-install-db.exe'
def get_initial_schema_path():
return get_root_path() / 'SQL' / 'tgstation_schema.sql'
def get_changelog_path():
return get_root_path() / 'SQL' / 'database_changelog.md'
+11
View File
@@ -0,0 +1,11 @@
from .download_mariadb import DownloadMariaDB
from .install_database import InstallDatabase
from .install_initial_schema import InstallInitialSchema
from .update_schema import UpdateSchema
STEPS = [
DownloadMariaDB,
InstallDatabase,
InstallInitialSchema,
UpdateSchema,
]
+50
View File
@@ -0,0 +1,50 @@
import os
import pathlib
import tempfile
import urllib.request
import zipfile
from ..ezdb.paths import get_config_path, get_data_path, get_db_path, get_mariadb_bin_path, get_mariadb_daemon_path, get_mariadb_install_db_path
from .step import Step
# Theoretically, this could use the REST API that MariaDB has to find the URL given a version:
# https://downloads.mariadb.org/rest-api/mariadb/10.11
DOWNLOAD_URL = "http://downloads.mariadb.org/rest-api/mariadb/10.11.2/mariadb-10.11.2-winx64.zip"
FOLDER_NAME = "mariadb-10.11.2-winx64"
temp_extract_path = get_db_path() / "_temp/"
class DownloadMariaDB(Step):
@staticmethod
def should_run() -> bool:
return not get_mariadb_bin_path().exists()
@staticmethod
def run(args):
if temp_extract_path.exists():
print("Deleting old temporary extract folder")
temp_extract_path.rmdir()
print("Downloading portable MariaDB...")
# delete = False so we can write to it
temporary_file = tempfile.NamedTemporaryFile(delete = False)
try:
urllib.request.urlretrieve(DOWNLOAD_URL, temporary_file.name)
print("Extracting...")
os.makedirs(temp_extract_path, exist_ok = True)
with zipfile.ZipFile(temporary_file) as zip_file:
for file in zip_file.namelist():
if file.startswith(f"{FOLDER_NAME}/bin/"):
with zip_file.open(file) as source, open(temp_extract_path / pathlib.Path(file).name, "wb") as target:
target.write(source.read())
print("Moving...")
temp_extract_path.rename(get_mariadb_bin_path())
finally:
temporary_file.close()
if temp_extract_path.exists():
temp_extract_path.rmdir()
+46
View File
@@ -0,0 +1,46 @@
import argparse
import secrets
import subprocess
from ..ezdb.paths import get_config_path, get_data_path, get_mariadb_bin_path, get_mariadb_daemon_path, get_mariadb_install_db_path
from .step import Step
def create_password() -> str:
return secrets.token_urlsafe(40)
class InstallDatabase(Step):
@staticmethod
def should_run() -> bool:
# If the db folder exists, but the config doesn't, we cancelled
# halfway through and ought to start over by deleting the data folder.
return get_mariadb_bin_path().exists() and (not get_data_path().exists() or not get_config_path().exists())
@staticmethod
def run(args: argparse.Namespace):
data_folder = get_data_path()
if data_folder.exists():
print("Deleting old data folder")
data_folder.rmdir()
password = create_password()
print("Installing database...")
subprocess.run(
[
str(get_mariadb_install_db_path()),
f"--port={args.port}",
f"--password={password}",
],
check = True,
stderr = subprocess.STDOUT,
)
print("Creating config...")
with open(get_config_path(), "w") as file:
file.write("SQL_ENABLED\n")
file.write(f"PORT {args.port}\n")
file.write(f"FEEDBACK_LOGIN root\n")
file.write(f"FEEDBACK_PASSWORD {password}\n")
file.write("FEEDBACK_DATABASE tgstation\n")
file.write("FEEDBACK_TABLEPREFIX\n")
file.write(f"DB_DAEMON {str(get_mariadb_daemon_path())}")
@@ -0,0 +1,53 @@
from contextlib import closing
from ..ezdb.changes import get_current_version
from ..ezdb.config import read_config
from ..ezdb.mysql import execute_sql, insert_new_schema_query, open_connection, start_daemon
from ..ezdb.paths import get_initial_schema_path
from .step import Step
class InstallInitialSchema(Step):
@staticmethod
def should_run() -> bool:
start_daemon()
config = read_config()
assert config is not None, "No config file found"
database = config["FEEDBACK_DATABASE"]
assert database is not None, "No database found in config file"
with open_connection() as connection:
with closing(connection.cursor()) as cursor:
cursor.execute(f"SHOW DATABASES LIKE '{database}'")
if cursor.fetchone() is None:
return True
cursor.execute(f"USE {database}")
cursor.execute("SHOW TABLES LIKE 'schema_revision'")
if cursor.fetchone() is None:
return True
cursor.execute("SELECT * FROM `schema_revision` LIMIT 1")
if cursor.fetchone() is None:
return True
return False
@staticmethod
def run(args):
print("Installing initial schema...")
config = read_config()
assert config is not None, "No config file found"
with open_connection() as connection:
with closing(connection.cursor()) as cursor:
database = config["FEEDBACK_DATABASE"]
cursor.execute(f"CREATE DATABASE {database}")
cursor.execute(f"USE {database}")
(major_version, minor_version) = get_current_version()
with open(get_initial_schema_path(), 'r') as file:
schema = file.read()
execute_sql(schema + ";" + insert_new_schema_query(major_version, minor_version))
+10
View File
@@ -0,0 +1,10 @@
import argparse
class Step:
@staticmethod
def should_run() -> bool:
raise NotImplementedError()
@staticmethod
def run(args: argparse.Namespace):
raise NotImplementedError()
+38
View File
@@ -0,0 +1,38 @@
from contextlib import closing
from ..ezdb.changes import get_changes
from ..ezdb.config import read_config
from ..ezdb.mysql import execute_sql, insert_new_schema_query, open_connection
from .step import Step
class UpdateSchema(Step):
@staticmethod
def should_run() -> bool:
# Last step is always run
return True
@staticmethod
def run(args):
config = read_config()
assert config is not None, "No config file found"
database = config["FEEDBACK_DATABASE"]
assert database is not None, "No database found in config file"
with open_connection() as connection:
with closing(connection.cursor()) as cursor:
cursor.execute(f"USE {database}")
cursor.execute("SELECT major, minor FROM `schema_revision` ORDER BY `major` DESC, `minor` DESC LIMIT 1")
(major_version, minor_version) = cursor.fetchone()
changes = get_changes()
for change in changes:
if change.major_version != major_version:
print("NOT IMPLEMENTED: Major version change, these historically require extra tooling")
continue
if change.minor_version > minor_version:
print(f"Running change {change.major_version}.{change.minor_version}")
execute_sql(change.sql + ";" + insert_new_schema_query(change.major_version, change.minor_version))
else:
print("No updates necessary")
return
+3
View File
@@ -5,3 +5,6 @@ Pillow==9.3.0
# changelogs
PyYaml==5.4
beautifulsoup4==4.9.3
# ezdb
mysql-connector-python==8.0.33