#!/usr/bin/perl # Author: Akkadius # @file: repository-generator.pl # @description: Script used to generate database repositories # @example perl ~/code/utils/scripts/generators/repository-generator.pl ~/server/ [table|all] [base|extended|all] ############################################# # modules ############################################# use warnings FATAL => 'all'; no warnings 'experimental::smartmatch'; use experimental 'smartmatch'; use File::Find; use Data::Dumper; use DBI; use DBD::mysql; use JSON; my $json = new JSON(); if (!$ARGV[0]) { print "\@example perl ~/code/utils/scripts/generators/repository-generator.pl ~/server/ [table|all] [base|extended|all]\n"; exit; } ############################################# # args ############################################# my $server_path = $ARGV[0]; my $config_path = $server_path . "/eqemu_config.json"; my $requested_table_to_generate = $ARGV[1] ? $ARGV[1] : "all"; my $repository_generation_option = $ARGV[2] ? $ARGV[2] : "all"; ############################################# # world path ############################################# my $world_binary = ($^O eq "MSWin32") ? "world.exe" : "world"; my $world_path = $server_path . "/" . $world_binary; my $world_path_bin = $server_path . "/bin/" . $world_binary; my $found_world_path = ""; if (-e $world_path) { $found_world_path = $world_path; } elsif (-e $world_path_bin) { $found_world_path = $world_path_bin; } if ($found_world_path eq "") { print "Error! Cannot find world binary!\n"; exit; } ############################################# # validate config ############################################# if (!-e $config_path) { print "Error! Config file [$config_path] not found\n"; exit; } ############################################# # fetch schema from world ############################################# my $output = `cd $server_path && $found_world_path database:schema`; my $database_schema = $json->decode($output); ############################################# # database ############################################# my $content; open(my $fh, '<', $config_path) or die "cannot open file $config_path"; { local $/; $content = <$fh>; } close($fh); ############################################# # database ############################################# my $config = $json->decode($content); my $database_name = $config->{"server"}{"database"}{"db"}; my $host = $config->{"server"}{"database"}{"host"}; my $user = $config->{"server"}{"database"}{"username"}; my $pass = $config->{"server"}{"database"}{"password"}; my $port = $config->{"server"}{"database"}{"port"}; my $dsn = "dbi:mysql:$database_name:$host:$port"; my $connect = DBI->connect($dsn, $user, $pass); my @tables = (); if ($requested_table_to_generate eq "all" || !$requested_table_to_generate) { my $table_names_exec = $connect->prepare( " SELECT TABLE_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ? GROUP BY TABLE_NAME "); $table_names_exec->execute($database_name); while (my @row = $table_names_exec->fetchrow_array()) { push(@tables, $row[0]); } } if ($requested_table_to_generate ne "all") { @tables = ($requested_table_to_generate); } my @cereal_enabled_tables = ( "data_buckets", "player_event_logs", "player_event_log_settings" ); my $generated_base_repository_files = ""; my $generated_repository_files = ""; foreach my $table_to_generate (@tables) { my $table_found_in_schema = 0; my @categories = ( "bot_tables", "content_tables", "version_tables", "state_tables", "server_tables", "player_tables", "login_tables", ); # These tables don't have a typical schema my @table_ignore_list = ( "character_enabledtasks", # "grid", # Manually created # "tradeskill_recipe", # Manually created "guild_bank", "inventory_versions", "raid_leaders", "trader_audit", "eqtime", "db_version", "keyring", "profanity_list", "zone_flags", ); foreach my $category (@categories) { if ($table_to_generate ~~ $database_schema->{$category}) { $table_found_in_schema = 1; } } if ($table_to_generate ~~ @table_ignore_list) { print "Table [$table_to_generate] is on ignore list... skipping...\n"; $table_found_in_schema = 0; } my $cereal_enabled = 0; if ($table_to_generate ~~ @cereal_enabled_tables) { $cereal_enabled = 1; } if ($table_found_in_schema == 0 && ($requested_table_to_generate eq "" || $requested_table_to_generate eq "all")) { print "Table [$table_to_generate] not found in schema, skipping\n"; next; } my $ex = $connect->prepare( " SELECT COLUMN_NAME, TABLE_NAME, DATA_TYPE, COLUMN_TYPE, ORDINAL_POSITION, COLUMN_KEY, COLUMN_DEFAULT, EXTRA FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? ORDER BY TABLE_NAME, ORDINAL_POSITION " ); $ex->execute($database_name, $table_to_generate); my $longest_column_length = 0; my $longest_data_type_length = 0; while (my @row = $ex->fetchrow_array()) { my $column_name = $row[0]; my $column_name_formatted = format_column_name_for_cpp_var($column_name); my $data_type = $row[2]; my $column_type = $row[3]; if ($longest_column_length < length($column_name_formatted)) { $longest_column_length = length($column_name_formatted); } my $struct_data_type = translate_mysql_data_type_to_c($data_type, $column_type); if ($longest_data_type_length < length($struct_data_type)) { $longest_data_type_length = length($struct_data_type); } } # 2nd pass my $default_entries = ""; my $insert_one_entries = ""; my $insert_many_entries = ""; my $find_one_entries = ""; my $column_names_quoted = ""; my $select_column_names_quoted = ""; my $table_struct_columns = ""; my $cereal_columns = ""; my $update_one_entries = ""; my $all_entries = ""; my $index = 0; my %table_data = (); my %table_primary_key = (); $ex->execute($database_name, $table_to_generate); while (my @row = $ex->fetchrow_array()) { my $column_name = $row[0]; my $column_name_formatted = format_column_name_for_cpp_var($column_name); my $table_name = $row[1]; my $data_type = $row[2]; my $column_type = $row[3]; my $ordinal_position = $row[4]; my $column_key = $row[5]; my $column_default = ($row[6] ? $row[6] : ""); my $extra = ($row[7] ? $row[7] : ""); if (!$table_primary_key{$table_name}) { if (($column_key eq "PRI" && $data_type =~ /int/) || ($ordinal_position == 0 && $column_name =~ /id/i)) { $table_primary_key{$table_name} = $column_name; } } my $default_value = 0; if ($column_default eq "current_timestamp()") { $default_value = "std::time(nullptr)" } elsif ($column_default ne "NULL" && $column_default ne "") { $column_default =~ s/'/"/g; $default_value = $column_default; } elsif ($column_default eq "''") { $default_value = '""'; } elsif ((trim($column_default) eq "" || $column_default eq "NULL") && $column_type =~ /text|varchar/i) { $default_value = '""'; } elsif ((trim($column_default) eq "" || $column_default eq "NULL") && $column_type =~ /blob/i) { $default_value = '""'; } # for datetime values that set default value all zeroed out if ($default_value =~ /0000-00-00 00:00:00/i) { $default_value = 0; } my $struct_data_type = translate_mysql_data_type_to_c($data_type, $column_type); # struct $table_struct_columns .= sprintf("\t\t\%-${longest_data_type_length}s %s;\n", $struct_data_type, $column_name_formatted); if ($cereal_enabled == 1) { $cereal_columns .= sprintf("\t\t\t\tCEREAL_NVP(%s),\n", $column_name_formatted); } # new entity $default_entries .= sprintf("\t\te.%-${longest_column_length}s = %s;\n", $column_name_formatted, $default_value); # column names (string) $column_names_quoted .= sprintf("\t\t\t\"%s\",\n", format_column_name_for_mysql($column_name)); if ($data_type =~ /datetime|timestamp/) { $select_column_names_quoted .= sprintf("\t\t\t\"UNIX_TIMESTAMP(%s)\",\n", format_column_name_for_mysql($column_name)); } else { $select_column_names_quoted .= sprintf("\t\t\t\"%s\",\n", format_column_name_for_mysql($column_name)); } # update one if ($extra ne "auto_increment") { my $query_value = sprintf('\'" + Strings::Escape(e.%s) + "\'");', $column_name_formatted); if ($data_type =~ /int|float|double|decimal/) { $query_value = sprintf('" + std::to_string(e.%s));', $column_name_formatted); } elsif ($data_type =~ /datetime|timestamp/) { $query_value = sprintf('FROM_UNIXTIME(" + (e.%s > 0 ? std::to_string(e.%s) : "null") + ")");', $column_name_formatted, $column_name_formatted); } elsif ($data_type =~ /blob/) { $query_value = sprintf('\'" + e.%s + "\'");', $column_name_formatted); } $update_one_entries .= sprintf( "\t\t" . 'v.push_back(columns[%s] + " = %s' . "\n", $index, $query_value ); } # insert my $value = sprintf("\"'\" + Strings::Escape(e.%s) + \"'\"", $column_name_formatted); if ($data_type =~ /int|float|double|decimal/) { $value = sprintf('std::to_string(e.%s)', $column_name_formatted); } elsif ($data_type =~ /datetime|timestamp/) { $value = sprintf('"FROM_UNIXTIME(" + (e.%s > 0 ? std::to_string(e.%s) : "null") + ")"', $column_name_formatted, $column_name_formatted); } elsif ($data_type =~ /blob/) { $value = sprintf("\"'\" + e.%s + \"'\"", $column_name_formatted); } $insert_one_entries .= sprintf("\t\tv.push_back(%s);\n", $value); $insert_many_entries .= sprintf("\t\t\tv.push_back(%s);\n", $value); # find one / all (select) if ($column_type =~ /unsigned/) { if ($data_type =~ /bigint/) { $all_entries .= sprintf("\t\t\te.%-${longest_column_length}s = row[%s] ? strtoull(row[%s], nullptr, 10) : %s;\n", $column_name_formatted, $index, $index, $default_value); $find_one_entries .= sprintf("\t\t\te.%-${longest_column_length}s = row[%s] ? strtoull(row[%s], nullptr, 10) : %s;\n", $column_name_formatted, $index, $index, $default_value); } elsif ($data_type =~ /int/) { $all_entries .= sprintf("\t\t\te.%-${longest_column_length}s = row[%s] ? static_cast<%s>(strtoul(row[%s], nullptr, 10)) : %s;\n", $column_name_formatted, $index, $struct_data_type, $index, $default_value); $find_one_entries .= sprintf("\t\t\te.%-${longest_column_length}s = row[%s] ? static_cast<%s>(strtoul(row[%s], nullptr, 10)) : %s;\n", $column_name_formatted, $index, $struct_data_type, $index, $default_value); } elsif ($data_type =~ /float|decimal/) { $all_entries .= sprintf("\t\t\te.%-${longest_column_length}s = row[%s] ? (strtof(row[%s], nullptr) > 0.0f ? strtof(row[%s], nullptr) : %s) : %s;\n", $column_name_formatted, $index, $index, $index, $default_value, $default_value); $find_one_entries .= sprintf("\t\t\te.%-${longest_column_length}s = row[%s] ? (strtof(row[%s], nullptr) > 0.0f ? strtof(row[%s], nullptr) : %s) : %s;\n", $column_name_formatted, $index, $index, $index, $default_value, $default_value); } } elsif ($data_type =~ /bigint/) { $all_entries .= sprintf("\t\t\te.%-${longest_column_length}s = row[%s] ? strtoll(row[%s], nullptr, 10) : %s;\n", $column_name_formatted, $index, $index, $default_value); $find_one_entries .= sprintf("\t\t\te.%-${longest_column_length}s = row[%s] ? strtoll(row[%s], nullptr, 10) : %s;\n", $column_name_formatted, $index, $index, $default_value); } elsif ($data_type =~ /datetime|timestamp/) { $all_entries .= sprintf("\t\t\te.%-${longest_column_length}s = strtoll(row[%s] ? row[%s] : \"-1\", nullptr, 10);\n", $column_name_formatted, $index, $index); $find_one_entries .= sprintf("\t\t\te.%-${longest_column_length}s = strtoll(row[%s] ? row[%s] : \"-1\", nullptr, 10);\n", $column_name_formatted, $index, $index); } elsif ($data_type =~ /int/) { $all_entries .= sprintf("\t\t\te.%-${longest_column_length}s = row[%s] ? static_cast<%s>(atoi(row[%s])) : %s;\n", $column_name_formatted, $index, $struct_data_type, $index, $default_value); $find_one_entries .= sprintf("\t\t\te.%-${longest_column_length}s = row[%s] ? static_cast<%s>(atoi(row[%s])) : %s;\n", $column_name_formatted, $index, $struct_data_type, $index, $default_value); } elsif ($data_type =~ /float|decimal/) { $all_entries .= sprintf("\t\t\te.%-${longest_column_length}s = row[%s] ? strtof(row[%s], nullptr) : %s;\n", $column_name_formatted, $index, $index, $default_value); $find_one_entries .= sprintf("\t\t\te.%-${longest_column_length}s = row[%s] ? strtof(row[%s], nullptr) : %s;\n", $column_name_formatted, $index, $index, $default_value); } elsif ($data_type =~ /double/) { $all_entries .= sprintf("\t\t\te.%-${longest_column_length}s = row[%s] ? strtod(row[%s], nullptr) : %s;\n", $column_name_formatted, $index, $index, $default_value); $find_one_entries .= sprintf("\t\t\te.%-${longest_column_length}s = row[%s] ? strtod(row[%s], nullptr) : %s;\n", $column_name_formatted, $index, $index, $default_value); } else { $all_entries .= sprintf("\t\t\te.%-${longest_column_length}s = row[%s] ? row[%s] : %s;\n", $column_name_formatted, $index, $index, $default_value); $find_one_entries .= sprintf("\t\t\te.%-${longest_column_length}s = row[%s] ? row[%s] : %s;\n", $column_name_formatted, $index, $index, $default_value); } # print $column_name . "\n"; # print "table_name [$table_name] column_name [$column_name] data_type [$data_type] column_type [$column_type]\n"; $index++; } if (!$table_primary_key{$table_to_generate}) { print "Table primary key [$table_to_generate] has no primary key! Skipping... \n"; next; } ############################################# # base repository template ############################################# my $base_repository_template_file = './common/repositories/template/base_repository.template'; my $base_repository_template = ""; if (-e $base_repository_template_file) { open(my $fh, '<:encoding(UTF-8)', $base_repository_template_file) or die "Could not open file '$base_repository_template_file' $!"; while (my $line = <$fh>) { $base_repository_template .= $line; } close $fh; } if (trim($base_repository_template) eq "") { print "Base repository template not found! [$base_repository_template_file]\n"; exit; } ############################################# # repository template ############################################# my $repository_template_file = './common/repositories/template/repository.template'; my $repository_template = ""; if (-e $repository_template_file) { open(my $fh, '<:encoding(UTF-8)', $repository_template_file) or die "Could not open file '$repository_template_file' $!"; while (my $line = <$fh>) { $repository_template .= $line; } close $fh; } if (trim($repository_template_file) eq "") { print "Repository template not found! [$repository_template_file]\n"; exit; } my $table_name_camel_case = $table_to_generate; my $table_name_upper_case = uc($table_to_generate); $table_name_camel_case =~ s#(_|^)(.)#\u$2#g; my $primary_key = ($table_primary_key{$table_to_generate} ? $table_primary_key{$table_to_generate} : ""); my $database_connection = "database"; if ($table_to_generate ~~ $database_schema->{"content_tables"}) { $database_connection = "content_db"; } my $additional_includes = ""; if ($cereal_enabled) { chomp($cereal_columns); # remove the last comma "," from string $cereal_columns = substr($cereal_columns, 0, -1); $table_struct_columns .= " // cereal template void serialize(Archive &ar) { ar(\n" . $cereal_columns . "\n\t\t\t);\n\t\t}"; $additional_includes .= "#include "; } chomp($column_names_quoted); chomp($select_column_names_quoted); chomp($table_struct_columns); chomp($default_entries); chomp($update_one_entries); chomp($insert_one_entries); chomp($insert_many_entries); chomp($all_entries); use POSIX qw(strftime); my $generated_date = strftime "%b%e, %Y", localtime; print "Table name CamelCase [$table_name_camel_case]\n"; print "Table name UPPER_CASE [$table_name_upper_case]\n"; print "Table PRIMARY KEY [$primary_key]\n"; print "Database connection [$database_connection]\n"; # Base repository my $new_base_repository = $base_repository_template; $new_base_repository =~ s/\{\{TABLE_NAME_CLASS}}/$table_name_camel_case/g; $new_base_repository =~ s/\{\{TABLE_NAME_UPPER}}/$table_name_upper_case/g; $new_base_repository =~ s/\{\{PRIMARY_KEY_STRING}}/$primary_key/g; $new_base_repository =~ s/\{\{TABLE_NAME_STRUCT}}/$table_name_camel_case/g; $new_base_repository =~ s/\{\{TABLE_NAME_VAR}}/$table_to_generate/g; $new_base_repository =~ s/\{\{DATABASE_CONNECTION}}/$database_connection/g; $new_base_repository =~ s/\{\{DEFAULT_ENTRIES}}/$default_entries/g; $new_base_repository =~ s/\{\{COLUMNS_LIST_QUOTED}}/$column_names_quoted/g; $new_base_repository =~ s/\{\{SELECT_COLUMNS_LIST_QUOTED}}/$select_column_names_quoted/g; $new_base_repository =~ s/\{\{TABLE_STRUCT_COLUMNS}}/$table_struct_columns/g; $new_base_repository =~ s/\{\{FIND_ONE_ENTRIES}}/$find_one_entries/g; $new_base_repository =~ s/\{\{UPDATE_ONE_ENTRIES}}/$update_one_entries/g; $new_base_repository =~ s/\{\{INSERT_ONE_ENTRIES}}/$insert_one_entries/g; $new_base_repository =~ s/\{\{INSERT_MANY_ENTRIES}}/$insert_many_entries/g; $new_base_repository =~ s/\{\{ALL_ENTRIES}}/$all_entries/g; $new_base_repository =~ s/\{\{GENERATED_DATE}}/$generated_date/g; $new_base_repository =~ s/\{\{ADDITIONAL_INCLUDES}}\n/$additional_includes/g; # Extended repository my $new_repository = $repository_template; $new_repository =~ s/\{\{TABLE_NAME_CLASS}}/$table_name_camel_case/g; $new_repository =~ s/\{\{TABLE_NAME_UPPER}}/$table_name_upper_case/g; $new_repository =~ s/\{\{PRIMARY_KEY_STRING}}/$primary_key/g; $new_repository =~ s/\{\{TABLE_NAME_STRUCT}}/$table_name_camel_case/g; $new_repository =~ s/\{\{TABLE_NAME_VAR}}/$table_to_generate/g; $new_repository =~ s/\{\{DATABASE_CONNECTION}}/$database_connection/g; $new_repository =~ s/\{\{DEFAULT_ENTRIES}}/$default_entries/g; $new_repository =~ s/\{\{COLUMNS_LIST_QUOTED}}/$column_names_quoted/g; $new_repository =~ s/\{\{SELECT_COLUMNS_LIST_QUOTED}}/$select_column_names_quoted/g; $new_repository =~ s/\{\{TABLE_STRUCT_COLUMNS}}/$table_struct_columns/g; $new_repository =~ s/\{\{FIND_ONE_ENTRIES}}/$find_one_entries/g; $new_repository =~ s/\{\{UPDATE_ONE_ENTRIES}}/$update_one_entries/g; $new_repository =~ s/\{\{INSERT_ONE_ENTRIES}}/$insert_one_entries/g; $new_repository =~ s/\{\{INSERT_MANY_ENTRIES}}/$insert_many_entries/g; $new_repository =~ s/\{\{ALL_ENTRIES}}/$all_entries/g; $new_repository =~ s/\{\{GENERATED_DATE}}/$generated_date/g; if ($repository_generation_option eq "all" || $repository_generation_option eq "base") { print $new_base_repository; } if ($repository_generation_option eq "all" || $repository_generation_option eq "extended") { print $new_repository; } ############################################# # write base repository ############################################# if ($repository_generation_option eq "all" || $repository_generation_option eq "base") { my $generated_base_repository = './common/repositories/base/base_' . $table_to_generate . '_repository.h'; my $cmake_generated_base_reference = $generated_base_repository; $cmake_generated_base_reference =~ s/.\/common\///g; $generated_base_repository_files .= $cmake_generated_base_reference . "\n"; open(FH, '>', $generated_base_repository) or die $!; print FH $new_base_repository; close(FH); } ############################################# # write extended repository ############################################# if ($repository_generation_option eq "all" || $repository_generation_option eq "extended") { my $generated_repository = './common/repositories/' . $table_to_generate . '_repository.h'; # check if file exists firsts if (-e $generated_repository) { print "File [$generated_repository] already exists! Can't overwrite extended once created!\n"; next; } my $cmake_generated_reference = $generated_repository; $cmake_generated_reference =~ s/.\/common\///g; $generated_repository_files .= $cmake_generated_reference . "\n"; open(FH, '>', $generated_repository) or die $!; print FH $new_repository; close(FH); } } print "\n# Make sure to add generated repositories to common/CMakeLists.txt under the repositories section\n\n"; print "\n#Base Repositories\n"; print $generated_base_repository_files . "\n"; print "\n#Extended Repositories\n"; print $generated_repository_files . "\n"; sub trim { my $string = $_[0]; $string =~ s/^\s+//; $string =~ s/\s+$//; return $string; } sub translate_mysql_data_type_to_c { my $mysql_data_type = $_[0]; my $mysql_column_type = $_[1]; my $struct_data_type = "std::string"; if ($mysql_column_type =~ /unsigned/) { if ($mysql_data_type =~ /bigint/) { $struct_data_type = 'uint64_t'; } elsif ($mysql_data_type =~ /tinyint/) { $struct_data_type = 'uint8_t'; } elsif ($mysql_data_type =~ /smallint/) { $struct_data_type = 'uint16_t'; } elsif ($mysql_data_type =~ /int/) { $struct_data_type = 'uint32_t'; } elsif ($mysql_data_type =~ /float|decimal/i) { $struct_data_type = 'float'; } elsif ($mysql_data_type =~ /double/i) { $struct_data_type = 'double'; } } elsif ($mysql_data_type =~ /bigint/) { $struct_data_type = 'int64_t'; } elsif ($mysql_data_type =~ /tinyint/) { $struct_data_type = 'int8_t'; } elsif ($mysql_data_type =~ /smallint/) { $struct_data_type = 'int16_t'; } elsif ($mysql_data_type =~ /int/) { $struct_data_type = 'int32_t'; } elsif ($mysql_data_type =~ /float|decimal/) { $struct_data_type = 'float'; } elsif ($mysql_data_type =~ /double/) { $struct_data_type = 'double'; } elsif ($mysql_data_type =~ /datetime|timestamp/) { $struct_data_type = 'time_t'; } return $struct_data_type; } # This is so we can change reserved words on the cpp side to something that will continue be functional in the compilers sub get_reserved_cpp_variable_names { return ( "class", "int", "key", "rank", "range", "interval", "group" ); } sub format_column_name_for_cpp_var { my $column_name = $_[0]; for my $word (get_reserved_cpp_variable_names()) { if ($word eq $column_name) { return $column_name . "_"; } } return $column_name; } sub format_column_name_for_mysql { my $column_name = $_[0]; for my $word (get_reserved_cpp_variable_names()) { if ($word eq $column_name) { return "`" . $column_name . "`"; } } return $column_name; }