Class: Exwiw::Adapter::PostgresqlAdapter
- Includes:
- SqlBulkInsert
- Defined in:
- lib/exwiw/adapter/postgresql_adapter.rb
Defined Under Namespace
Classes: StreamingResult
Constant Summary
Constants included from SqlBulkInsert
SqlBulkInsert::STREAM_FLUSH_ROWS
Instance Attribute Summary
Attributes inherited from Base
Instance Method Summary collapse
- #build_query(table, dump_target, table_by_name) ⇒ Object
- #compile_ast(query_ast, select_cast_to: nil) ⇒ Object
- #dump_schema(ordered_tables, output_path) ⇒ Object
- #execute(query_ast) ⇒ Object
- #explain(query_ast, verbosity: nil) ⇒ Object
-
#post_insert_sql(table) ⇒ Object
Transcribe the FROM-side sequence cursor backing
table.primary_keyonto the import target. - #to_bulk_delete(select_query_ast, table) ⇒ Object
- #to_copy_from_stdin(results, table) ⇒ Object
Methods included from SqlBulkInsert
#to_bulk_insert, #write_inserts
Methods inherited from Base
#commented_sql, #default_bulk_insert_chunk_size, #describe_query, #dumpable?, #explain_scope_with_placeholders!, #initialize, #output_extension, #pre_insert_sql, #query_comment_text, #schema_output_extension, #sql_query_comment, #supports_bulk_delete?, table_config_class, #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
79 80 81 |
# File 'lib/exwiw/adapter/postgresql_adapter.rb', line 79 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, select_cast_to: nil) ⇒ Object
282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 |
# File 'lib/exwiw/adapter/postgresql_adapter.rb', line 282 def compile_ast(query_ast, select_cast_to: nil) 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 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? ? "#{query_ast.from_table_name}.*" : "*" else cols = query_ast.columns.map { |col| compile_column_name(query_ast, col) } cols = cols.map { |c| "#{c}::#{select_cast_to}" } if select_cast_to cols.join(', ') end sql += " FROM #{query_ast.from_table_name}" query_ast.join_clauses.each do |join| fk_expr = "#{join.base_table_name}.#{join.foreign_key}" pk_expr = "#{join.join_table_name}.#{join.primary_key}" if types_need_cast?( column_pg_type(join.base_table_name, join.foreign_key), column_pg_type(join.join_table_name, join.primary_key) ) fk_expr = "#{fk_expr}::text" pk_expr = "#{pk_expr}::text" end sql += " JOIN #{join.join_table_name} ON #{fk_expr} = #{pk_expr}" 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
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 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/exwiw/adapter/postgresql_adapter.rb', line 100 def dump_schema(ordered_tables, output_path) require 'open3' # 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, so a later reference to an # unscoped table does not fail with "relation does not exist". # `ordered_tables` no longer selects which tables are emitted; it is kept # only for the log line (how many tables are in scope for data). cmd = [ 'pg_dump', "--host=#{@connection_config.host}", "--port=#{@connection_config.port}", "--username=#{@connection_config.user}", '--schema-only', '--no-owner', '--no-acl', @connection_config.database_name, ] env = { 'PGPASSWORD' => @connection_config.password.to_s } @logger.debug(" Running pg_dump for the whole database (#{@connection_config.database_name})...") stdout, stderr, status = Open3.capture3(env, *cmd) unless status.success? if stderr.include?('command not found') || stderr.empty? raise "Failed to run `pg_dump`. Ensure the postgresql client is installed and on PATH. stderr: #{stderr}" end raise "pg_dump failed (exit #{status.exitstatus}): #{stderr}" end # A full-database dump emits CREATE EXTENSION and CREATE TYPE ... AS ENUM # itself (a `--table` dump omitted both, which is why they used to be # prepended by hand), but pg_dump's bare forms are neither idempotent nor # restore-tolerant. Wrap them in place to restore the previous # robustness: enums swallow duplicate_object on re-restore; extensions # warn-and-skip feature_not_supported / invalid_schema_name so a target # that cannot provide the extension (e.g. pglogical's schema absent, or a # managed-platform extension) does not abort the restore. idempotent = stdout idempotent = DdlPostprocessor.wrap_create_type_enum_in_do_block(idempotent) idempotent = DdlPostprocessor.wrap_create_extension_in_do_block(idempotent) idempotent = DdlPostprocessor.add_if_not_exists_to_create_schema(idempotent) idempotent = DdlPostprocessor.add_if_not_exists_to_create_sequence(idempotent) idempotent = DdlPostprocessor.add_if_not_exists_to_create_table(idempotent) idempotent = DdlPostprocessor.add_if_not_exists_to_create_index(idempotent) idempotent = DdlPostprocessor.wrap_add_constraint_in_do_block(idempotent) idempotent = DdlPostprocessor.strip_triggers(idempotent) File.open(output_path, 'w') do |file| file.puts("-- Auto-generated by exwiw via pg_dump. Idempotent DDL for postgresql.") file.write(idempotent) end @logger.info(" Wrote full-database schema to #{output_path} (#{ordered_tables.size} table(s) in scope for data).") end |
#execute(query_ast) ⇒ Object
83 84 85 86 87 88 89 90 91 |
# File 'lib/exwiw/adapter/postgresql_adapter.rb', line 83 def execute(query_ast) data_sql = commented_sql(query_ast) # Count via the same query (wrapped as a subquery) so the Runner can # skip empty tables and log the row count without draining the stream. count_sql = "#{sql_query_comment(query_ast)} SELECT COUNT(*) FROM (#{compile_ast(query_ast)}) AS exwiw_count_src" @logger.debug(" Executing SQL (single-row stream): \n#{data_sql}") StreamingResult.new(connection: connection, data_sql: data_sql, count_sql: count_sql) end |
#explain(query_ast, verbosity: nil) ⇒ Object
93 94 95 96 97 98 |
# File 'lib/exwiw/adapter/postgresql_adapter.rb', line 93 def explain(query_ast, verbosity: nil) sql = commented_sql(query_ast) @logger.debug(" Executing EXPLAIN: \n#{sql}") connection.exec("EXPLAIN #{sql}").values.map(&:first).join("\n") end |
#post_insert_sql(table) ⇒ Object
Transcribe the FROM-side sequence cursor backing table.primary_key
onto the import target. Without this, importing into a clean DB leaves
the sequence at 1 while the inserted rows occupy higher IDs, so the
next default-PK INSERT collides. We query FROM's last_value /
is_called directly (matching what pg_dump emits) rather than using
MAX(pk), so a subsetted dump still preserves the source's "next id".
Returns nil for non-auto-increment PKs (pg_get_serial_sequence -> NULL).
Scope: ONLY the sequence attached to the primary key is synced. If a
table has additional auto-increment columns (e.g. a non-PK SERIAL),
those sequences are NOT transcribed and a subsequent default-value
INSERT on them can collide. Rails-managed schemas don't hit this
because only id is auto-increment, but bare PostgreSQL schemas may.
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 |
# File 'lib/exwiw/adapter/postgresql_adapter.rb', line 198 def post_insert_sql(table) pk = table.primary_key return nil if pk.nil? || pk.empty? seq_name = connection .exec_params("SELECT pg_get_serial_sequence($1, $2)", [table.name, pk]) .values.dig(0, 0) return nil if seq_name.nil? last_value, is_called = connection .exec("SELECT last_value, is_called FROM #{seq_name}") .values.first is_called_sql = (is_called == 't' || is_called == true) ? 'true' : 'false' "SELECT pg_catalog.setval('#{escape_single_quote(seq_name)}', #{last_value}, #{is_called_sql});" end |
#to_bulk_delete(select_query_ast, table) ⇒ Object
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 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 279 280 |
# File 'lib/exwiw/adapter/postgresql_adapter.rb', line 215 def to_bulk_delete(select_query_ast, table) raise NotImplementedError unless select_query_ast.is_a?(Exwiw::QueryAst::Select) sql = "DELETE FROM #{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 outer_table = select_query_ast.from_table_name inner_table = first_join.join_table_name inner_column = first_join.primary_key cast_to = types_need_cast?( column_pg_type(outer_table, foreign_key), column_pg_type(inner_table, inner_column) ) ? 'text' : nil subquery_sql = compile_ast(subquery_ast, select_cast_to: cast_to) outer_expr = "#{outer_table}.#{foreign_key}" outer_expr = "#{outer_expr}::text" if cast_to sql += "\nWHERE #{outer_expr} 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 |
#to_copy_from_stdin(results, table) ⇒ Object
170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
# File 'lib/exwiw/adapter/postgresql_adapter.rb', line 170 def to_copy_from_stdin(results, table) header = if table.rails_managed? "COPY #{table.name} FROM stdin;" else column_names = table.columns.map(&:name).join(', ') "COPY #{table.name} (#{column_names}) FROM stdin;" end lines = [header] results.each do |row| lines << row.map { |v| escape_copy_value(v) }.join("\t") end lines << '\\.' lines.join("\n") end |