Class: Mxrb::Runtime::DatabaseWorkspace
- Inherits:
-
Object
- Object
- Mxrb::Runtime::DatabaseWorkspace
- Defined in:
- lib/mxrb/runtime/database_workspace.rb
Overview
Materializes a private PostgreSQL database for one MPR. The Mendix Runtime owns schema synchronization; analyst access uses a separate read-only role. rubocop:disable Metrics/ClassLength
Constant Summary collapse
- POSTGRES_IMAGE =
'postgres:13-alpine'- DATABASE =
'mxrb'- OWNER =
'mxrb_runtime'- READER =
'mxrb_reader'- NATIVE_INPUT_DIRECTORIES =
%w[ mprcontents deployment javasource javascriptsource userlib vendorlib resources widgets theme theme-cache themesource ].freeze
Instance Method Summary collapse
- #connection_url ⇒ Object
- #destroy(confirm: false) ⇒ Object
- #down ⇒ Object
-
#explain(sql, analyze: false) ⇒ Object
Uses PostgreSQL's machine-readable planner output.
- #index_advice(limit: 50) ⇒ Object
-
#initialize(project_path, state_dir: nil, port: 55_432, runtime_port: 18_080, runner: nil, sleeper: nil) ⇒ DatabaseWorkspace
constructor
rubocop:disable Metrics/AbcSize, Metrics/ParameterLists.
-
#query(sql, write: false) ⇒ Object
Keeping the transaction envelope next to role selection makes the read-only default auditable.
-
#query_rows(sql, params: {}) ⇒ Object
rubocop:enable Metrics/MethodLength.
- #shell_command(write: false) ⇒ Object
- #status ⇒ Object
- #sync ⇒ Object
- #up(force_build: false) ⇒ Object
- #workload(limit: 20) ⇒ Object
Constructor Details
#initialize(project_path, state_dir: nil, port: 55_432, runtime_port: 18_080, runner: nil, sleeper: nil) ⇒ DatabaseWorkspace
rubocop:disable Metrics/AbcSize, Metrics/ParameterLists
31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 31 def initialize(project_path, state_dir: nil, port: 55_432, runtime_port: 18_080, # rubocop:disable Metrics/AbcSize, Metrics/ParameterLists runner: nil, sleeper: nil) @project_path = File.(project_path) @project_id = Digest::SHA256.hexdigest(@project_path)[0, 12] @state_dir = File.(state_dir || default_state_dir) @port = Integer(port) @runtime_port = Integer(runtime_port) raise ArgumentError, 'database port must be between 1 and 65535' unless (1..65_535).cover?(@port) raise ArgumentError, 'runtime port must be between 1 and 65535' unless (1..65_535).cover?(@runtime_port) @runner = runner || method(:capture) @sleeper = sleeper || Kernel.method(:sleep) @plan = Toolchain.new(@project_path).plan end |
Instance Method Details
#connection_url ⇒ Object
146 147 148 149 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 146 def connection_url secret = credentials.fetch('reader_password') "postgresql://#{READER}:#{secret}@127.0.0.1:#{@port}/#{DATABASE}" end |
#destroy(confirm: false) ⇒ Object
66 67 68 69 70 71 72 73 74 75 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 66 def destroy(confirm: false) raise ArgumentError, 'destroy requires explicit confirmation' unless confirm remove_container(runtime_container) remove_container(database_container) remove_owned_resource('volume', database_volume) remove_owned_resource('network', network_name) FileUtils.rm_rf(@state_dir) info(running: false) end |
#down ⇒ Object
60 61 62 63 64 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 60 def down stop_container(runtime_container) stop_container(database_container) info(running: false) end |
#explain(sql, analyze: false) ⇒ Object
Uses PostgreSQL's machine-readable planner output. ANALYZE is opt-in because it executes the SELECT, although the analyst role remains in a read-only transaction and cannot mutate application data.
114 115 116 117 118 119 120 121 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 114 def explain(sql, analyze: false) statement = read_only_statement(sql) configure_reader! payload = JSON.parse(run_explain(statement, analyze).strip) Oql::PlanAnalyzer.new(indexes: index_catalog).analyze(payload, analyzed: analyze) rescue JSON::ParserError => e raise ToolchainError, "PostgreSQL returned invalid EXPLAIN JSON: #{e.}" end |
#index_advice(limit: 50) ⇒ Object
134 135 136 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 134 def index_advice(limit: 50) Oql::IndexAdvisor.new.analyze(workload(limit:)) end |
#query(sql, write: false) ⇒ Object
Keeping the transaction envelope next to role selection makes the read-only default auditable. rubocop:disable Metrics/MethodLength
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 84 def query(sql, write: false) raise ArgumentError, 'SQL must not be empty' if sql.to_s.strip.empty? raise ArgumentError, 'SQL contains a NUL byte' if sql.include?("\0") configure_reader! unless write role = write ? OWNER : READER command = [ 'docker', 'exec', database_container, 'psql', '--no-psqlrc', '--set', 'ON_ERROR_STOP=1', '--username', role, '--dbname', DATABASE ] command.concat(['--command', 'BEGIN READ ONLY']) unless write command.concat(['--command', sql]) command.concat(['--command', 'COMMIT']) unless write run!(*command) end |
#query_rows(sql, params: {}) ⇒ Object
rubocop:enable Metrics/MethodLength
102 103 104 105 106 107 108 109 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 102 def query_rows(sql, params: {}) statement = read_only_statement(sql) statement, variables = bind_parameters(statement, params) configure_reader! output = run!(*query_rows_command(statement, variables)) CSV.parse(output, headers: true).map(&:to_h).freeze end |
#shell_command(write: false) ⇒ Object
138 139 140 141 142 143 144 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 138 def shell_command(write: false) role = write ? OWNER : READER [ 'docker', 'exec', '-it', database_container, 'psql', '--no-psqlrc', '--username', role, '--dbname', DATABASE ].freeze end |
#status ⇒ Object
77 78 79 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 77 def status info(running: container_running?(database_container) && container_running?(runtime_container)) end |
#sync ⇒ Object
58 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 58 def sync = up(force_build: true) |
#up(force_build: false) ⇒ Object
46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 46 def up(force_build: false) validate! prepare_state refresh_runtime_package(force_build) prepare_database! configure_reader! restart_runtime! wait_for_runtime! configure_reader! info(running: true) end |
#workload(limit: 20) ⇒ Object
123 124 125 126 127 128 129 130 131 132 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 123 def workload(limit: 20) maximum = Integer(limit) raise ArgumentError, 'workload limit must be between 1 and 1000' unless (1..1000).cover?(maximum) Oql::WorkloadAnalyzer.new.analyze( query_rows: workload_queries(maximum), table_rows: workload_tables, index_rows: workload_indexes ) end |