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
- #admin_credentials ⇒ Object
- #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
32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 32 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
#admin_credentials ⇒ Object
152 153 154 155 156 157 158 159 160 161 162 163 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 152 def admin_credentials unless File.file?(credentials_path) raise ToolchainError, "Runtime credentials not found; run `mxrb db up #{@project_path}` first" end values = JSON.parse(File.read(credentials_path)) password = values.fetch('admin_password') AdminCredentials.new(admin_user_name, password, @state_dir, credentials_path) rescue JSON::ParserError, KeyError => e raise ToolchainError, "invalid Runtime credentials file #{credentials_path}: #{e.}" end |
#connection_url ⇒ Object
147 148 149 150 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 147 def connection_url secret = credentials.fetch('reader_password') "postgresql://#{READER}:#{secret}@127.0.0.1:#{@port}/#{DATABASE}" end |
#destroy(confirm: false) ⇒ Object
67 68 69 70 71 72 73 74 75 76 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 67 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
61 62 63 64 65 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 61 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.
115 116 117 118 119 120 121 122 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 115 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
135 136 137 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 135 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
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 85 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
103 104 105 106 107 108 109 110 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 103 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
139 140 141 142 143 144 145 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 139 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
78 79 80 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 78 def status info(running: container_running?(database_container) && container_running?(runtime_container)) end |
#sync ⇒ Object
59 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 59 def sync = up(force_build: true) |
#up(force_build: false) ⇒ Object
47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 47 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
124 125 126 127 128 129 130 131 132 133 |
# File 'lib/mxrb/runtime/database_workspace.rb', line 124 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 |