Class: Exwiw::Adapter::MysqlAdapter
- Includes:
- IdentifierQuoting::Mysql, SqlBulkInsert
- Defined in:
- lib/exwiw/adapter/mysql_adapter.rb
Defined Under Namespace
Classes: StreamingResult
Constant Summary collapse
- SPECIAL_CHARACTER_ESCAPES =
Backslash and control-character escapes, matching mysqldump. Escaping newlines keeps every VALUES tuple on a single line, so a value that itself contains a newline cannot break consumers that split the dump into statements on a semicolon-newline boundary.
{ "\\" => "\\\\", "\n" => "\\n", "\r" => "\\r", "\u0000" => "\\0", "\u001A" => "\\Z", }.freeze
Constants included from SqlBulkInsert
SqlBulkInsert::STREAM_FLUSH_ROWS
Constants included from IdentifierQuoting
IdentifierQuoting::BARE_IDENTIFIER_PATTERN, IdentifierQuoting::MYSQL_RESERVED_WORDS, IdentifierQuoting::POSTGRESQL_RESERVED_WORDS, IdentifierQuoting::SQLITE_RESERVED_WORDS
Instance Attribute Summary
Attributes inherited from Base
Instance Method Summary collapse
- #build_query(table, dump_target, table_by_name) ⇒ Object
- #compile_ast(query_ast, count_only: false) ⇒ Object
- #dump_schema(ordered_tables, output_path) ⇒ Object
- #execute(query_ast) ⇒ Object
- #explain(query_ast, verbosity: nil) ⇒ Object
- #post_insert_sql(_table) ⇒ Object
- #pre_insert_sql(_table) ⇒ Object
- #to_bulk_delete(select_query_ast, table) ⇒ Object
Methods included from SqlBulkInsert
#to_bulk_insert, #write_inserts
Methods included from IdentifierQuoting
#force_quote_identifier, #force_quote_table_name, #qualified_name, #quote_identifier, #quote_table_name
Methods inherited from Base
#commented_sql, #default_bulk_insert_chunk_size, #describe_query, #dumpable?, #explain_scope_with_placeholders!, #initialize, #output_extension, #query_comment_text, #schema_output_extension, #sql_query_comment, #supports_bulk_delete?, table_config_class, #to_copy_from_stdin, #validate_as_dump_target!, #write_inserts
Constructor Details
This class inherits a constructor from Exwiw::Adapter::Base
Instance Method Details
#build_query(table, dump_target, table_by_name) ⇒ Object
57 58 59 |
# File 'lib/exwiw/adapter/mysql_adapter.rb', line 57 def build_query(table, dump_target, table_by_name) Exwiw::QueryAstBuilder.run(table.name, table_by_name, dump_target, @logger) end |
#compile_ast(query_ast, count_only: false) ⇒ Object
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 |
# File 'lib/exwiw/adapter/mysql_adapter.rb', line 231 def compile_ast(query_ast, count_only: false) raise NotImplementedError unless query_ast.is_a?(Exwiw::QueryAst::Select) # Lift scope id-set clauses (reverse_scope UNION / forward cascade / # single referenced_by) out of `WHERE <col> IN (subquery)` and into a # JOIN against a materialized derived table. See #compile_scope_join. scope_clauses, plain_where_clauses = partition_scope_clauses(query_ast.where_clauses) sql = "SELECT " sql += if count_only "COUNT(*)" elsif query_ast.select_all # A lifted scope JOIN brings a derived table into FROM, so a bare # `*` would also project its column. Qualify to this table's own. scope_clauses.any? ? "#{quote_table_name(query_ast.from_table_name)}.*" : "*" else query_ast.columns.map { |col| compile_column_name(query_ast, col) }.join(', ') end sql += " FROM #{quote_table_name(query_ast.from_table_name)}" query_ast.join_clauses.each do |join| sql += " JOIN #{quote_table_name(join.join_table_name)} ON #{qualified_name(join.base_table_name, join.foreign_key)} = #{qualified_name(join.join_table_name, join.primary_key)}" join.where_clauses.each do |where| compiled_where_condition = compile_where_condition(where, join.join_table_name) sql += " AND #{compiled_where_condition}" end # base_where_clauses is compiled against the joined-from table # (base_table_name), e.g. the type-column filter on a polymorphic # source table. join.base_where_clauses.each do |where| compiled_where_condition = compile_where_condition(where, join.base_table_name) sql += " AND #{compiled_where_condition}" end end scope_clauses.each_with_index do |where_clause, idx| sql += " #{compile_scope_join(query_ast.from_table_name, where_clause, idx)}" end if plain_where_clauses.any? sql += " WHERE " sql += plain_where_clauses.map { |where| compile_where_condition(where, query_ast.from_table_name) }.join(' AND ') end sql end |
#dump_schema(ordered_tables, output_path) ⇒ Object
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/exwiw/adapter/mysql_adapter.rb', line 83 def dump_schema(ordered_tables, output_path) # Full-database schema dump: emit DDL for every table in the database, # not just the config-scoped tables. A table absent from the config gets # its schema here but no data (empty INSERT), which is the intended # result — the restore target then has every relation even when exwiw was # configured to extract only a subset. `ordered_tables` no longer selects # which tables are emitted; it is kept only for the log line. # The mysqldump binary is invoked directly (not via the mysql2/trilogy # driver), so point EXWIW_MYSQLDUMP at a specific binary when the one on # PATH is incompatible with the server — e.g. a MySQL 9.x client whose # mysqldump cannot load `mysql_native_password` ("plugin ... cannot be # loaded", exit 2) against a server still using that auth plugin. mysqldump_bin = ENV['EXWIW_MYSQLDUMP'] mysqldump_bin = 'mysqldump' if mysqldump_bin.nil? || mysqldump_bin.empty? # MariaDB's mysqldump does not recognise the MySQL-specific # --set-gtid-purged flag and exits with code 7. gtid_flags = mariadb_mysqldump?(mysqldump_bin) ? [] : ['--set-gtid-purged=OFF'] cmd = [ mysqldump_bin, "--host=#{@connection_config.host}", "--port=#{@connection_config.port}", "--user=#{@connection_config.user}", '--no-data', '--skip-add-drop-table', '--skip-comments', '--skip-set-charset', '--no-tablespaces', # skip tablespace query that requires PROCESS privilege (unavailable on managed MySQL like RDS) *gtid_flags, '--compact', @connection_config.database_name, ] env = { 'MYSQL_PWD' => @connection_config.password.to_s } @logger.debug(" Running #{mysqldump_bin} for the whole database (#{@connection_config.database_name})...") stdout, stderr, status = begin Open3.capture3(env, *cmd) rescue Errno::ENOENT raise "Failed to run `#{mysqldump_bin}`. Ensure the mysql client is installed and on PATH, " \ "or set EXWIW_MYSQLDUMP to a mysqldump binary." end unless status.success? if stderr.include?('command not found') || stderr.empty? raise "Failed to run `#{mysqldump_bin}`. Ensure the mysql client is installed and on PATH, " \ "or set EXWIW_MYSQLDUMP to a mysqldump binary. stderr: #{stderr}" end raise "mysqldump failed (exit #{status.exitstatus}): #{stderr}" end idempotent = DdlPostprocessor.add_if_not_exists_to_create_table(stdout) File.open(output_path, 'w') do |file| file.puts("-- Auto-generated by exwiw via mysqldump. Idempotent CREATE TABLE statements for mysql.") file.puts("SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;") file.puts file.puts(idempotent) file.puts("SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;") end @logger.info(" Wrote full-database schema to #{output_path} (#{ordered_tables.size} table(s) in scope for data).") end |
#execute(query_ast) ⇒ Object
61 62 63 64 65 66 67 68 69 70 |
# File 'lib/exwiw/adapter/mysql_adapter.rb', line 61 def execute(query_ast) data_sql = commented_sql(query_ast) # Count via the same FROM/JOIN/WHERE (projection replaced by COUNT(*)) so # the Runner can skip empty tables and log the row count without draining # the stream. See StreamingResult for why this is not a subquery wrap. count_sql = "#{sql_query_comment(query_ast)} #{compile_ast(query_ast, count_only: true)}" @logger.debug(" Executing SQL (streaming): \n#{data_sql}") StreamingResult.new(client: connection, data_sql: data_sql, count_sql: count_sql) end |
#explain(query_ast, verbosity: nil) ⇒ Object
72 73 74 75 76 77 78 79 80 81 |
# File 'lib/exwiw/adapter/mysql_adapter.rb', line 72 def explain(query_ast, verbosity: nil) sql = commented_sql(query_ast) @logger.debug(" Executing EXPLAIN: \n#{sql}") result = connection.query("EXPLAIN #{sql}") result.rows.each_with_index.flat_map do |row, i| ["*************************** #{i + 1}. row ***************************"] + result.fields.zip(row).map { |k, v| "#{k}: #{v}" } end.join("\n") end |
#post_insert_sql(_table) ⇒ Object
151 152 153 |
# File 'lib/exwiw/adapter/mysql_adapter.rb', line 151 def post_insert_sql(_table) "SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;" end |
#pre_insert_sql(_table) ⇒ Object
147 148 149 |
# File 'lib/exwiw/adapter/mysql_adapter.rb', line 147 def pre_insert_sql(_table) "SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;" end |
#to_bulk_delete(select_query_ast, table) ⇒ Object
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 |
# File 'lib/exwiw/adapter/mysql_adapter.rb', line 169 def to_bulk_delete(select_query_ast, table) raise NotImplementedError unless select_query_ast.is_a?(Exwiw::QueryAst::Select) sql = "DELETE FROM #{quote_table_name(select_query_ast.from_table_name)}" if select_query_ast.join_clauses.empty? # Ignore filter option, because bulk delete is for cleaning before import, # so it should delete all records to avoid foreign key violation & data consistancy. compiled_where_conditions = select_query_ast. where_clauses. select { |where| where.is_a?(Exwiw::QueryAst::WhereClause) }. map do |where| compile_where_condition(where, select_query_ast.from_table_name) end if compiled_where_conditions.size > 0 sql += "\nWHERE " sql += compiled_where_conditions.join(' AND ') end sql += ";" return sql end subquery_ast = Exwiw::QueryAst::Select.new first_join = select_query_ast.join_clauses.first.clone subquery_ast.from(first_join.join_table_name) primay_key_col = table.columns.find { |col| col.name == table.primary_key } subquery_ast.select([primay_key_col]) select_query_ast.join_clauses[1..].each do |join| subquery_ast.join(join) end first_join.where_clauses.each do |where| # Ignore filter option, because bulk delete is for cleaning before import, # so it should delete all records to avoid foreign key violation & data consistancy. subquery_ast.where(where) if where.is_a?(Exwiw::QueryAst::WhereClause) end foreign_key = first_join.foreign_key subquery_sql = compile_ast(subquery_ast) sql += "\nWHERE #{qualified_name(select_query_ast.from_table_name, foreign_key)} IN (#{subquery_sql})" # first_join.base_where_clauses holds conditions on the outer # delete-target table (from_table_name), such as a polymorphic type # column. They are not part of the subquery, so add them to the outer # WHERE. This prevents deleting rows that belong to a different # polymorphic type. first_join.base_where_clauses.each do |where| next unless where.is_a?(Exwiw::QueryAst::WhereClause) sql += " AND #{compile_where_condition(where, select_query_ast.from_table_name)}" end sql += ";" sql end |