Skip to content

Phase 1b: Extract analyses, QueryRunner, QueryExplainer + 0.4.0 paired release#7

Merged
antarr merged 16 commits into
mainfrom
feature/mysql-genius-core-phase-1b
Apr 11, 2026
Merged

Phase 1b: Extract analyses, QueryRunner, QueryExplainer + 0.4.0 paired release#7
antarr merged 16 commits into
mainfrom
feature/mysql-genius-core-phase-1b

Conversation

@antarr

@antarr antarr commented Apr 11, 2026

Copy link
Copy Markdown
Owner

Summary

Completes Phase 1 of the desktop-app extraction by moving the 5 database analyses plus the query runner and query explainer out of the Rails controller concerns into mysql_genius-core, then does the paired release of mysql_genius-core 0.4.0 + mysql_genius 0.4.0.

Host apps see no public API change — this is entirely an internal refactor. After bundle update, the mysql_genius gem will pull in mysql_genius-core as a transitive dependency; no Gemfile changes are required.

Versioning — lockstep 0.4.0

Per session decision, mysql_genius-core jumps straight from 0.1.0.pre to 0.4.0 so that both gems release in lockstep under matching version numbers going forward. The gemspec pins mysql_genius-core at "~> 0.4.0" — pessimistic on the minor, allowing 0.4.x patches but blocking 0.5.0 — enforcing exact-minor pairing. One git tag (v0.4.0) covers both gems.

What's included

Extractions (Stage A + B):

  • 5 new analysis classes under MysqlGenius::Core::Analysis::* (TableSizes, DuplicateIndexes, QueryStats, UnusedIndexes, ServerOverview)
  • Core::QueryRunner + Core::QueryRunner::Config + Core::ExecutionResult
  • Core::QueryExplainer
  • DatabaseAnalysis concern shrunk from ~295 lines to 47 lines of thin delegation
  • QueryExecution concern's execute / explain delegated to core; private helpers removed

Release prep (Stage C):

  • .github/workflows/publish.yml rewritten to build and push both gems in order (core first so the dep resolves at install time), with working-directory isolating the core build to avoid glob collisions
  • Version bumps: mysql_genius-core0.4.0, mysql_genius0.4.0
  • CHANGELOG.md flipped from ## Unreleased## 0.4.0
  • New gems/mysql_genius-core/CHANGELOG.md documenting the first published core release
  • Gemspec dep updated from "~> 0.1.0.pre""~> 0.4.0"
  • Core gemspec changelog_uri fixed to point at the nested gems/mysql_genius-core/CHANGELOG.md (was pointing at the adapter's CHANGELOG)
  • mysql_genius.gemspec spec.files reject regex now excludes gems/ so the adapter gem no longer bundles a dormant copy of the core gem subtree in its package (~150KB / 47 files removed per release)

Test plan

  • bundle exec rspec at repo root — 36 examples, 0 failures
  • (cd gems/mysql_genius-core && bundle exec rspec) — 166 examples, 0 failures
  • bundle exec rubocop at repo root — 68 files, clean
  • (cd gems/mysql_genius-core && bundle exec rubocop) — 45 files, clean
  • gem build mysql_genius.gemspec succeeds; package no longer contains files under gems/
  • Independent code review passed (no ship-blockers)
  • Integration smoke-test against a real Rails host app (recommended before merge if available)
  • After merge: tag v0.4.0, verify publish workflow publishes both gems to RubyGems
  • After release: bundle install against gem "mysql_genius", "0.4.0" resolves mysql_genius-core 0.4.0 transitively

What's next

Phase 2 opens a fresh branch to build mysql_genius-desktop — a standalone Sinatra-hosted app using the same mysql_genius-core library with a Trilogy adapter.

See the design spec §9 for the full Phase 2+ roadmap.

antarr added 15 commits April 10, 2026 23:02
Thirteen-task plan extracting the 5 database analyses, Core::QueryRunner,
and Core::QueryExplainer from the Rails controller concerns into
mysql_genius-core, then doing the paired release of mysql_genius-core
0.1.0 + mysql_genius 0.4.0 to rubygems.

Stage A extracts the 5 analyses one per task (TableSizes,
DuplicateIndexes, QueryStats, UnusedIndexes, ServerOverview) using
the same TDD pattern: spec with FakeAdapter, implementation, require
wiring, concern delegation, suite verification, commit.

Stage B adds Core::ExecutionResult (value object), Core::QueryRunner::
Config (keyword-init Struct), Core::QueryRunner (owns SQL validation,
row-limit/timeout-hint application, execution, column masking, timeout
detection), and Core::QueryExplainer. Audit logging stays in the Rails
concern.

Stage C updates .github/workflows/publish.yml to build and push both
gems in the correct order (core first so the dependency resolves at
install time), bumps versions, updates the gemspec pin from
"~> 0.1.0.pre" to "~> 0.1", flips the CHANGELOG Unreleased section to
0.4.0, merges Phase 1b via PR, tags v0.4.0, verifies the release.

Stage D is final verification: both suites green, both gems on
rubygems, fresh install resolves transitively.

Two intentional scope exclusions vs spec: the 7 inline AI features in
AiFeatures concern stay in the adapter (they already delegate through
Core::Ai::Client via the Phase 1a helper and aren't analyses-on-a-
connection), and ERB template extraction stays deferred to Phase 2
alongside a concrete non-Rails consumer.
The superpowers design specs and implementation plans live in
docs/superpowers/ as local planning artifacts and aren't meant to be
shipped as part of the gem or referenced from the user-facing
codebase. Add the directory to .gitignore and remove the existing
tracked files from the index. The files remain on disk for local
reference; they just won't be committed going forward.

Historical commits on main and earlier on this branch still contain
these files — this is a going-forward cleanup, not a history rewrite.
Move the information_schema.tables query and result transformation
out of the DatabaseAnalysis concern into a new
MysqlGenius::Core::Analysis::TableSizes class that takes a
Core::Connection. The concern's table_sizes action now builds an
ActiveRecordAdapter and delegates.

Behavior is byte-identical — same SQL, same result shape, same
per-table COUNT(*) fallback on error, same needs_optimize threshold.
The 5 new core specs use FakeAdapter to stub information_schema
results and verify row handling (hash access, MariaDB uppercase
column names, COUNT(*) error fallback, needs_optimize computation).
Code-review feedback on Task 1 of Phase 1b (commit 673c144):

1. RuboCop was regressing against the Phase 1a baseline. Fixed via
   `rubocop -A`:
   - RSpec/LeadingSubject: subject declared before let
   - RSpec/EmptyLineAfterFinalLet: blank line before subject
   - Style/WordArray: replaced %w[table_name ...] with string array
     literals (rubocop-shopify rejects %w containing underscores)

2. RSpec/ExampleLength: split the 31-line "hash per table" example
   into two smaller examples under the project's 25-line cap:
   - "returns one hash per BASE TABLE row" (top-level shape)
   - "includes full size metadata for each row" (numeric keys)

3. Added two coverage gap tests flagged by the reviewer:
   - nil size columns (e.g., MEMORY engine tables) coerce to 0.0
   - exact 10% boundary does not trigger needs_optimize
Move the left-prefix duplicate-index detection out of the
DatabaseAnalysis concern into a new
MysqlGenius::Core::Analysis::DuplicateIndexes class. The class takes
a Core::Connection plus a blocked_tables list and returns an array
of duplicate pairs, with the symmetrical-coverage dedup preserved.

The concern now builds an ActiveRecordAdapter and delegates,
passing blocked_tables from the configuration.
Move the performance_schema.events_statements_summary_by_digest
query out of the DatabaseAnalysis concern into a new
MysqlGenius::Core::Analysis::QueryStats class. Sort whitelist and
limit clamping now live in the class as VALID_SORTS and MAX_LIMIT
constants; the concern action parses params and hands them to
.call(sort:, limit:).

performance_schema availability errors continue to be caught by
the concern (Rails-specific ActiveRecord::StatementInvalid) and
rendered with the same user-facing message as before.
Move the performance_schema.table_io_waits_summary_by_index_usage
JOIN query out of the DatabaseAnalysis concern into a new
MysqlGenius::Core::Analysis::UnusedIndexes class. The class takes
a Core::Connection and returns an array of hashes with a
ready-to-run ALTER TABLE DROP INDEX statement per entry.

performance_schema availability errors stay in the concern's
ActiveRecord::StatementInvalid rescue.
Move SHOW GLOBAL STATUS / SHOW GLOBAL VARIABLES / SELECT VERSION()
collection and derived-metric computation out of the DatabaseAnalysis
concern into a new MysqlGenius::Core::Analysis::ServerOverview class.
Structure the output into server/connections/innodb/queries blocks
with the same keys and types as before.

This completes the 5-analysis extraction. DatabaseAnalysis concern
is now fully delegated to core; the concern's 5 actions have
shrunk from 295 lines of inline SQL and transformations to
47 lines of thin delegating wrappers.
Immutable frozen value object returned from Core::QueryRunner#run
(to be added in the next task). Contains columns, rows (post-masking),
row_count, execution_time_ms, and a truncated flag. Distinct from
Core::Result because it carries runtime metadata that plain results
don't need.
Holds the runner-specific subset of MysqlGenius::Configuration:
blocked_tables, masked_column_patterns, query_timeout_ms.
Frozen on initialize for defensive immutability.

Forward-declares the QueryRunner class so Config can be namespaced
under it. The full QueryRunner implementation lands in the next
task.
Core::QueryRunner owns SQL validation, row-limit application,
timeout-hint wrapping (MySQL vs MariaDB flavors), execution, column
masking, and timeout detection. Returns a Core::ExecutionResult
or raises Rejected / Timeout. Does NOT handle audit logging —
the concern still owns that.

The execute action in the QueryExecution concern now parses params,
builds an ActiveRecordAdapter and a QueryRunner::Config, calls
runner.run, and catches the specific error classes to produce the
same JSON response shapes as before. Rails-specific ActiveRecord
error translation stays in the concern.

Helpers validate_sql / apply_timeout_hint / mariadb? / apply_row_limit /
timeout_error? / masked_column? stay in the concern temporarily
because explain still uses validate_sql — they're removed in Task 9
when QueryExplainer replaces that last consumer.

13 new core specs cover happy-path execution, each rejection reason,
row limit application, column masking (single and multiple
patterns), truncation detection, MySQL vs MariaDB timeout hints,
timeout detection via error message, and non-timeout error
propagation.
Core::QueryExplainer wraps EXPLAIN around validated SELECT queries,
with an optional skip_validation flag for explaining captured slow
queries from mysql's own logs. Reuses Core::QueryRunner::Rejected
for validation failures and adds a new Core::QueryExplainer::Truncated
class for SQL that appears cut mid-statement.

The explain action in the QueryExecution concern now builds an
ActiveRecordAdapter + Config and delegates. The concern's private
helpers validate_sql, apply_timeout_hint, mariadb?, apply_row_limit,
timeout_error?, and masked_column? are deleted — they were the
internal plumbing that is now owned by Core::QueryRunner and
Core::QueryExplainer. sanitize_ai_sql and audit remain because
they're used elsewhere in the controller.

This completes Stage B and all code extraction for Phase 1b. The
next stage is the paired release to rubygems.
The workflow now builds and publishes mysql_genius-core first,
then mysql_genius, so the runtime dependency on mysql_genius-core
resolves at gem install time. The test job also runs the core
gem's spec suite to ensure it passes before publishing anything.

working-directory is used to keep the core gem's build artifacts
in gems/mysql_genius-core/ so the root-level "gem push
mysql_genius-*.gem" glob matches only the Rails adapter gem and
not the core gem.
- mysql_genius-core: 0.1.0.pre -> 0.4.0
- mysql_genius: 0.3.2 -> 0.4.0
- mysql_genius.gemspec dep: "~> 0.1.0.pre" -> "~> 0.4.0"
- CHANGELOG.md: ## Unreleased -> ## 0.4.0 with Phase 1b additions
- Added gems/mysql_genius-core/CHANGELOG.md documenting the first
  published release of the core gem

mysql_genius-core jumps straight from 0.1.0.pre to 0.4.0 so that
both gems release in lockstep under matching version numbers going
forward. The gemspec dep uses "~> 0.4.0" (allowing 0.4.x patches
but not 0.5) to enforce exact-minor pairing.

Also fixed the core gemspec:
- changelog_uri now points at gems/mysql_genius-core/CHANGELOG.md
  (was pointing at the adapter's CHANGELOG at the repo root)
- spec.files now includes CHANGELOG.md in the packaged gem

The next commit tags v0.4.0 and kicks off the publish workflow,
which will push mysql_genius-core 0.4.0 first and then
mysql_genius 0.4.0 to rubygems.
spec.files used git ls-files filtered only on ^(test|spec|features)/,
which meant every release of the mysql_genius gem was bundling a
dormant copy of the entire gems/mysql_genius-core/ subtree (47 files,
~150KB) into its package. Those files aren't on the gem's
require_paths (["lib"] only), so nothing broke at load time — it was
just dead weight shipped in every release and a drift-reading hazard
for downstream users who might think the bundled copy is live.

mysql_genius-core is published as its own gem now; the adapter picks
it up as a normal runtime dependency.

Verified: tar -tzf mysql_genius-0.4.0.gem shows 0 files under gems/
after the fix, down from 47.
@antarr antarr self-assigned this Apr 11, 2026
Core::Connection::ActiveRecordAdapter was added in Phase 1a but
never wired into any production require chain. On main it was
latent because no production code referenced it. Phase 1b's
extracted delegators instantiate it in every DatabaseAnalysis /
QueryExecution / AiFeatures concern action, so the missing require
surfaces as:

    uninitialized constant MysqlGenius::Core::Connection::ActiveRecordAdapter

on the first request to a real host app — breaking tables, query
stats, unused indexes, duplicate indexes, server overview, execute,
explain, AI suggest, and AI optimize. Every tab except slow_queries.

Root cause: lib/mysql_genius.rb required mysql_genius/core (which
loads FakeAdapter from the core gem) but not the adapter-side file
at lib/mysql_genius/core/connection/active_record_adapter.rb. The
adapter spec file did its own explicit require, so the unit suite
was green and CI never saw the bug. Only a real Rails host-app boot
exercises the missing require.

Fix:

- lib/mysql_genius.rb now requires mysql_genius/core/connection/
  active_record_adapter right after mysql_genius/core, so any
  consumer doing `require "mysql_genius"` gets the full constant
  namespace the concerns reference.

- spec/spec_helper.rb has a new boot-order regression guard that
  raises at spec_helper load time if Core::Connection::
  ActiveRecordAdapter is not reachable after `require "mysql_genius"`.
  Placed in spec_helper rather than an RSpec example because by the
  time individual spec files run, other spec files may have done
  their own explicit requires and masked the bug. Verified the guard
  catches the regression by temporarily removing the require and
  watching `bundle exec rspec` abort with the error message.

- spec/mysql_genius/core/connection/active_record_adapter_spec.rb no
  longer needs its explicit `require "mysql_genius/core/connection/
  active_record_adapter"` — spec_helper now guarantees the constant
  is loaded. Removing it keeps the invariant tight: any spec file
  that still needs an explicit require is a signal the production
  require chain is incomplete.

- CHANGELOG.md 0.4.0 section gets a new ### Fixed entry documenting
  the incident so anyone on an affected pre-release install knows
  what happened.

Repro (before fix):

    $ ruby -Ilib -Igems/mysql_genius-core/lib -r mysql_genius \
        -e 'p defined?(MysqlGenius::Core::Connection::ActiveRecordAdapter)'
    nil

After fix:

    $ ruby -Ilib -Igems/mysql_genius-core/lib -r mysql_genius \
        -e 'p defined?(MysqlGenius::Core::Connection::ActiveRecordAdapter)'
    "constant"
@antarr antarr merged commit 3272a80 into main Apr 11, 2026
32 checks passed
@antarr antarr deleted the feature/mysql-genius-core-phase-1b branch April 11, 2026 05:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant