Class: Rails::Command::QueryCommand
| Relationships & Source Files | |
| Super Chains via Extension / Inclusion / Inheritance | |
|
Class Chain:
self,
Base,
Thor
|
|
|
Instance Chain:
|
|
| Inherits: |
Rails::Command::Base
|
| Defined in: | railties/lib/rails/commands/query/query_command.rb |
Class Attribute Summary
Base - Inherited
| .bin, .bin?, | |
| .engine? | Returns true when the app is a Rails engine. |
| .exit_on_failure? | |
Class Method Summary
Base - Inherited
| .banner, | |
| .base_name | Sets the base_name taking into account the current class namespace. |
| .command_name | Return command name without namespaces. |
| .default_command_root | Default file root to place extra files a command might need, placed one folder above the command file. |
| .desc | Tries to get the description from a USAGE file one folder above the command root. |
| .executable, | |
| .hide_command! | Convenience method to hide this command from the available ones when running rails command. |
| .namespace | Convenience method to get the namespace from the class name. |
| .printing_commands, | |
| .usage_path | Path to lookup a USAGE description in a file. |
| .create_command | Allow the command method to be called perform. |
| .namespaced_name, .resolve_path, .class_usage, | |
| .help | Override Thor's class-level help to also show the USAGE. |
| .inherited, .perform | |
Instance Attribute Summary
Instance Method Summary
- #perform(expression = nil, *args)
- #execute_ar(expression:, page:, per:) private
- #execute_sql(connection:, sql:, page:, per:) private
- #format_associations(model) private
- #format_result(columns:, rows:, sql:, elapsed_ms: 0, page: 1, per: rows.length, truncated: false) private
- #format_table_detail(connection, table) private
- #format_table_list(connection) private
- #model_for_table(table) private
- #output_error(message) private
- #reading_role_available?(connection_class) ⇒ Boolean private
- #resolve_expression(expression) private
- #run_explain(expression = nil) private
- #run_models private
- #run_query(expression) private
- #run_schema(table = nil) private
- #with_explicit_database(database) private
- #with_readonly_connection(&block) private
- #with_readonly_connection_for(connection_class, &block) private
EnvironmentArgument - Included
Actions - Included
| #boot_application!, #load_environment_config!, | |
| #load_generators | See additional method definition at line 36. |
| #load_tasks | See additional method definition at line 31. |
| #require_application!, | |
| #set_application_directory! | Change to the application's path if there is no config.ru file in current directory. |
Instance Method Details
#execute_ar(expression:, page:, per:) (private)
[ GitHub ]# File 'railties/lib/rails/commands/query/query_command.rb', line 150
def execute_ar(expression:, page:, per:) result = eval(expression, TOPLEVEL_BINDING, "(query)", 1) case result when ActiveRecord::Relation relation = result.offset((page - 1) * per).limit(per + 1) sql = relation.to_sql with_readonly_connection_for(relation.model.connection_class_for_self) do |connection| ar_result = connection.select_all(sql) truncated = ar_result.rows.length > per { columns: ar_result.columns, rows: ar_result.rows.first(per), sql: sql, truncated: truncated } end when ActiveRecord::Result { columns: result.columns, rows: result.rows, sql: expression, truncated: false } when Hash rows = result.map { |key, val| [ key, val ] } { columns: [ "key", "value" ], rows: rows, sql: expression, truncated: false } when Array rows = result.map { |val| Array(val) } cols = Array.new(rows.first&.length.to_i) { |i| "column_#{i}" } { columns: cols, rows: rows, sql: expression, truncated: false } else { columns: [ "result" ], rows: [ [ result ] ], sql: expression, truncated: false } end end
#execute_sql(connection:, sql:, page:, per:) (private)
[ GitHub ]# File 'railties/lib/rails/commands/query/query_command.rb', line 176
def execute_sql(connection:, sql:, page:, per:) unless sql.gsub(/--.*$|\/\*.*?\*\//m, "").match?(/\bLIMIT\b/i) offset = (page - 1) * per sql = "#{sql.rstrip.chomp(';')} LIMIT #{per + 1}" sql += " OFFSET #{offset}" if offset > 0 end ar_result = connection.select_all(sql) truncated = ar_result.rows.length > per { columns: ar_result.columns, rows: ar_result.rows.first(per), sql: sql, truncated: truncated } end
#format_associations(model) (private)
[ GitHub ]# File 'railties/lib/rails/commands/query/query_command.rb', line 238
def format_associations(model) return unless model model.reflect_on_all_associations.map do |assoc| hash = { type: assoc.macro, name: assoc.name, class_name: assoc.class_name } hash[:foreign_key] = assoc.foreign_key if assoc.respond_to?(:foreign_key) hash[:through] = assoc.through_reflection.name if assoc.through_reflection? hash end end
#format_result(columns:, rows:, sql:, elapsed_ms: 0, page: 1, per: rows.length, truncated: false) (private)
[ GitHub ]# File 'railties/lib/rails/commands/query/query_command.rb', line 188
def format_result(columns:, rows:, sql:, elapsed_ms: 0, page: 1, per: rows.length, truncated: false) JSON.generate({ columns: columns, rows: rows, meta: { row_count: rows.length, query_time_ms: elapsed_ms, page: page, per_page: per, has_more: truncated, sql: sql } }) end
#format_table_detail(connection, table) (private)
# File 'railties/lib/rails/commands/query/query_command.rb', line 210
def format_table_detail(connection, table) raise ArgumentError, "Table '#{table}' does not exist" unless connection.table_exists?(table) columns = connection.columns(table) indexes = connection.indexes(table) model = model_for_table(table) JSON.generate({ table: table, columns: columns.map do |col| { name: col.name, type: col.sql_type, null: col.null, default: col.default } end, indexes: indexes.map do |idx| { name: idx.name, columns: idx.columns, unique: idx.unique } end, enums: model&.defined_enums.presence, associations: format_associations(model) }.compact) end
#format_table_list(connection) (private)
[ GitHub ]# File 'railties/lib/rails/commands/query/query_command.rb', line 203
def format_table_list(connection) tables = connection.tables.sort rows = tables.map { |table| [ table ] } format_result(columns: [ "table_name" ], rows: rows, sql: "") end
#model_for_table(table) (private)
[ GitHub ]# File 'railties/lib/rails/commands/query/query_command.rb', line 230
def model_for_table(table) Rails.application.eager_load! ActiveRecord::Base.descendants.find do |klass| !klass.abstract_class? && klass.table_name == table end end
#output_error(message) (private)
[ GitHub ]# File 'railties/lib/rails/commands/query/query_command.rb', line 263
def output_error() error JSON.generate({ error: , meta: { query_time_ms: 0 } }) end
#perform(expression = nil, *args)
[ GitHub ]# File 'railties/lib/rails/commands/query/query_command.rb', line 24
def perform(expression = nil, *args) boot_application! Rails.application.load_runner ActiveSupport::Notifications.instrument("query.rails", expression: expression) do case expression when "schema" run_schema(args.first) when "models" run_models when "explain" run_explain(args.first) else run_query(expression) end end rescue StandardError, SyntaxError, NotImplementedError => e output_error(e.) exit 1 end
#reading_role_available?(connection_class) ⇒ Boolean (private)
# File 'railties/lib/rails/commands/query/query_command.rb', line 141
def reading_role_available?(connection_class) connection_class.connected_to(role: :reading) do connection_class.lease_connection end true rescue ActiveRecord::ConnectionNotEstablished false end
#resolve_expression(expression) (private)
[ GitHub ]# File 'railties/lib/rails/commands/query/query_command.rb', line 253
def resolve_expression(expression) if expression == "-" || (expression.nil? && !$stdin.tty?) $stdin.read.strip elsif expression expression else raise ArgumentError, "No query expression provided. Run '#{self.class.executable} -h' for help." end end
#run_explain(expression = nil) (private)
[ GitHub ]# File 'railties/lib/rails/commands/query/query_command.rb', line 93
def run_explain(expression = nil) expression = resolve_expression(expression) if [:sql] with_readonly_connection do |connection| result = connection.select_all("EXPLAIN #{expression}") say format_result(columns: result.columns, rows: result.rows, sql: "EXPLAIN #{expression}") end else relation = eval(expression, TOPLEVEL_BINDING, "(query)", 1) sql = relation.to_sql with_readonly_connection_for(relation.model.connection_class_for_self) do |connection| result = connection.select_all("EXPLAIN #{sql}") say format_result(columns: result.columns, rows: result.rows, sql: "EXPLAIN #{sql}") end end end
#run_models (private)
[ GitHub ]# File 'railties/lib/rails/commands/query/query_command.rb', line 74
def run_models Rails.application.eager_load! models = ActiveRecord::Base.descendants .reject(&:abstract_class?) .select { |model| model.table_name.present? } .sort_by(&:name) data = models.map do |model| { model: model.name, table_name: model.table_name, associations: format_associations(model) } end say JSON.generate(data) end
#run_query(expression) (private)
[ GitHub ]# File 'railties/lib/rails/commands/query/query_command.rb', line 46
def run_query(expression) expression = resolve_expression(expression) page = [ [:page], 1 ].max per = [ [ [:per], 1 ].max, 10_000 ].min start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) result = if [:sql] with_readonly_connection do |connection| execute_sql(connection: connection, sql: expression, page: page, per: per) end else execute_ar(expression: expression, page: page, per: per) end elapsed_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(1) say format_result(**result, elapsed_ms: elapsed_ms, page: page, per: per) end
#run_schema(table = nil) (private)
[ GitHub ]# File 'railties/lib/rails/commands/query/query_command.rb', line 64
def run_schema(table = nil) with_readonly_connection do |connection| if table say format_table_detail(connection, table) else say format_table_list(connection) end end end
#with_explicit_database(database) (private)
[ GitHub ]# File 'railties/lib/rails/commands/query/query_command.rb', line 129
def with_explicit_database(database) original = ActiveRecord::Base.connection_db_config begin ActiveRecord::Base.establish_connection(database.to_sym) ActiveRecord::Base.while_preventing_writes do yield ActiveRecord::Base.lease_connection end ensure ActiveRecord::Base.establish_connection(original) end end
#with_readonly_connection(&block) (private)
[ GitHub ]# File 'railties/lib/rails/commands/query/query_command.rb', line 111
def with_readonly_connection(&block) with_readonly_connection_for(ActiveRecord::Base, &block) end
#with_readonly_connection_for(connection_class, &block) (private)
[ GitHub ]# File 'railties/lib/rails/commands/query/query_command.rb', line 115
def with_readonly_connection_for(connection_class, &block) if [:database] with_explicit_database([:database], &block) elsif reading_role_available?(connection_class) connection_class.connected_to(role: :reading) do connection_class.with_connection(&block) end else connection_class.while_preventing_writes do connection_class.with_connection(&block) end end end