anima-core 0.2.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.reek.yml +27 -1
- data/CHANGELOG.md +19 -0
- data/README.md +213 -43
- data/agents/codebase-analyzer.md +88 -0
- data/agents/codebase-pattern-finder.md +83 -0
- data/agents/documentation-researcher.md +59 -0
- data/agents/thoughts-analyzer.md +102 -0
- data/agents/web-search-researcher.md +71 -0
- data/anima-core.gemspec +3 -0
- data/app/channels/session_channel.rb +195 -45
- data/app/decorators/user_message_decorator.rb +16 -5
- data/app/jobs/agent_request_job.rb +55 -2
- data/app/jobs/analytical_brain_job.rb +33 -0
- data/app/jobs/count_event_tokens_job.rb +15 -4
- data/app/models/concerns/event/broadcasting.rb +81 -0
- data/app/models/event.rb +20 -1
- data/app/models/goal.rb +91 -0
- data/app/models/session.rb +366 -21
- data/config/application.rb +2 -0
- data/config/initializers/event_subscribers.rb +0 -1
- data/config/routes.rb +0 -6
- data/db/migrate/20260313010000_add_status_to_events.rb +8 -0
- data/db/migrate/20260313020000_add_processing_to_sessions.rb +7 -0
- data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
- data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
- data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
- data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
- data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
- data/db/migrate/20260315140843_create_goals.rb +16 -0
- data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
- data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
- data/lib/agent_loop.rb +65 -6
- data/lib/agents/definition.rb +116 -0
- data/lib/agents/registry.rb +106 -0
- data/lib/analytical_brain/runner.rb +276 -0
- data/lib/analytical_brain/tools/activate_skill.rb +52 -0
- data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
- data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
- data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
- data/lib/analytical_brain/tools/finish_goal.rb +62 -0
- data/lib/analytical_brain/tools/read_workflow.rb +58 -0
- data/lib/analytical_brain/tools/rename_session.rb +63 -0
- data/lib/analytical_brain/tools/set_goal.rb +60 -0
- data/lib/analytical_brain/tools/update_goal.rb +60 -0
- data/lib/analytical_brain.rb +23 -0
- data/lib/anima/cli/mcp/secrets.rb +76 -0
- data/lib/anima/cli/mcp.rb +197 -0
- data/lib/anima/cli.rb +5 -40
- data/lib/anima/installer.rb +168 -0
- data/lib/anima/settings.rb +226 -0
- data/lib/anima/version.rb +1 -1
- data/lib/anima.rb +9 -0
- data/lib/credential_store.rb +103 -0
- data/lib/environment_probe.rb +232 -0
- data/lib/events/subscribers/persister.rb +1 -0
- data/lib/events/user_message.rb +17 -0
- data/lib/llm/client.rb +29 -10
- data/lib/mcp/client_manager.rb +86 -0
- data/lib/mcp/config.rb +213 -0
- data/lib/mcp/health_check.rb +77 -0
- data/lib/mcp/secrets.rb +73 -0
- data/lib/mcp/stdio_transport.rb +206 -0
- data/lib/providers/anthropic.rb +11 -20
- data/lib/shell_session.rb +11 -10
- data/lib/skills/definition.rb +97 -0
- data/lib/skills/registry.rb +105 -0
- data/lib/tools/edit.rb +226 -0
- data/lib/tools/mcp_tool.rb +114 -0
- data/lib/tools/read.rb +151 -0
- data/lib/tools/registry.rb +14 -12
- data/lib/tools/request_feature.rb +121 -0
- data/lib/tools/return_result.rb +81 -0
- data/lib/tools/spawn_specialist.rb +109 -0
- data/lib/tools/spawn_subagent.rb +111 -0
- data/lib/tools/subagent_prompts.rb +12 -0
- data/lib/tools/web_get.rb +8 -9
- data/lib/tools/write.rb +86 -0
- data/lib/tui/app.rb +985 -26
- data/lib/tui/cable_client.rb +69 -31
- data/lib/tui/message_store.rb +103 -8
- data/lib/tui/screens/chat.rb +293 -45
- data/lib/workflows/definition.rb +97 -0
- data/lib/workflows/registry.rb +89 -0
- data/skills/activerecord/SKILL.md +255 -0
- data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
- data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
- data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
- data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
- data/skills/activerecord/examples/associations/self_referential.rb +302 -0
- data/skills/activerecord/examples/associations/through_associations.rb +203 -0
- data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
- data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
- data/skills/activerecord/examples/basics/inheritance.rb +377 -0
- data/skills/activerecord/examples/basics/type_casting.rb +317 -0
- data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
- data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
- data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
- data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
- data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
- data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
- data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
- data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
- data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
- data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
- data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
- data/skills/activerecord/examples/querying/optimization.rb +275 -0
- data/skills/activerecord/examples/querying/scopes.rb +260 -0
- data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
- data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
- data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
- data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
- data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
- data/skills/activerecord/references/associations.md +709 -0
- data/skills/activerecord/references/basics.md +622 -0
- data/skills/activerecord/references/callbacks.md +738 -0
- data/skills/activerecord/references/migrations.md +657 -0
- data/skills/activerecord/references/querying.md +655 -0
- data/skills/activerecord/references/validations.md +596 -0
- data/skills/dragonruby/SKILL.md +250 -0
- data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
- data/skills/dragonruby/examples/audio/background_music.rb +29 -0
- data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
- data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
- data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
- data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
- data/skills/dragonruby/examples/core/hello_world.rb +24 -0
- data/skills/dragonruby/examples/core/labels.rb +22 -0
- data/skills/dragonruby/examples/core/sprites.rb +35 -0
- data/skills/dragonruby/examples/core/state_management.rb +29 -0
- data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
- data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
- data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
- data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
- data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
- data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
- data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
- data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
- data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
- data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
- data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
- data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
- data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
- data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
- data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
- data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
- data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
- data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
- data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
- data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
- data/skills/dragonruby/examples/input/controller_input.rb +28 -0
- data/skills/dragonruby/examples/input/directional_input.rb +24 -0
- data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
- data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
- data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
- data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
- data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
- data/skills/dragonruby/examples/rendering/labels.rb +32 -0
- data/skills/dragonruby/examples/rendering/layering.rb +51 -0
- data/skills/dragonruby/examples/rendering/solids.rb +61 -0
- data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
- data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
- data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
- data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
- data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
- data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
- data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
- data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
- data/skills/dragonruby/references/audio.md +396 -0
- data/skills/dragonruby/references/core.md +385 -0
- data/skills/dragonruby/references/distribution.md +434 -0
- data/skills/dragonruby/references/entities.md +516 -0
- data/skills/dragonruby/references/game-logic/persistence.md +386 -0
- data/skills/dragonruby/references/game-logic/state.md +389 -0
- data/skills/dragonruby/references/input.md +414 -0
- data/skills/dragonruby/references/rendering/animation.md +467 -0
- data/skills/dragonruby/references/rendering/primitives.md +403 -0
- data/skills/dragonruby/references/scenes.md +443 -0
- data/skills/draper-decorators/SKILL.md +344 -0
- data/skills/draper-decorators/examples/application_decorator.rb +61 -0
- data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
- data/skills/draper-decorators/examples/model_decorator.rb +152 -0
- data/skills/draper-decorators/references/anti-patterns.md +640 -0
- data/skills/draper-decorators/references/patterns.md +507 -0
- data/skills/draper-decorators/references/testing.md +559 -0
- data/skills/gh-issue.md +182 -0
- data/skills/mcp-server/SKILL.md +177 -0
- data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
- data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
- data/skills/mcp-server/examples/http_client.rb +48 -0
- data/skills/mcp-server/examples/http_server.rb +97 -0
- data/skills/mcp-server/examples/rails_integration.rb +88 -0
- data/skills/mcp-server/examples/stdio_server.rb +108 -0
- data/skills/mcp-server/examples/streaming_client.rb +95 -0
- data/skills/mcp-server/references/gotchas.md +183 -0
- data/skills/mcp-server/references/prompts.md +98 -0
- data/skills/mcp-server/references/resources.md +53 -0
- data/skills/mcp-server/references/server.md +140 -0
- data/skills/mcp-server/references/tools.md +146 -0
- data/skills/mcp-server/references/transport.md +104 -0
- data/skills/ratatui-ruby/SKILL.md +315 -0
- data/skills/ratatui-ruby/references/core-concepts.md +340 -0
- data/skills/ratatui-ruby/references/events.md +387 -0
- data/skills/ratatui-ruby/references/frameworks.md +522 -0
- data/skills/ratatui-ruby/references/layout.md +423 -0
- data/skills/ratatui-ruby/references/styling.md +268 -0
- data/skills/ratatui-ruby/references/testing.md +433 -0
- data/skills/ratatui-ruby/references/widgets.md +532 -0
- data/skills/rspec/SKILL.md +340 -0
- data/skills/rspec/examples/core/basic_structure.rb +69 -0
- data/skills/rspec/examples/core/configuration.rb +126 -0
- data/skills/rspec/examples/core/hooks.rb +126 -0
- data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
- data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
- data/skills/rspec/examples/core/shared_examples.rb +145 -0
- data/skills/rspec/examples/factory_bot/associations.rb +314 -0
- data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
- data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
- data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
- data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
- data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
- data/skills/rspec/examples/factory_bot/traits.rb +293 -0
- data/skills/rspec/examples/factory_bot/transients.rb +229 -0
- data/skills/rspec/examples/matchers/change.rb +115 -0
- data/skills/rspec/examples/matchers/collections.rb +154 -0
- data/skills/rspec/examples/matchers/comparisons.rb +79 -0
- data/skills/rspec/examples/matchers/composing.rb +155 -0
- data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
- data/skills/rspec/examples/matchers/equality.rb +58 -0
- data/skills/rspec/examples/matchers/errors.rb +136 -0
- data/skills/rspec/examples/matchers/output.rb +103 -0
- data/skills/rspec/examples/matchers/predicates.rb +87 -0
- data/skills/rspec/examples/matchers/truthiness.rb +101 -0
- data/skills/rspec/examples/matchers/types.rb +82 -0
- data/skills/rspec/examples/matchers/yield.rb +147 -0
- data/skills/rspec/examples/mocks/any_instance.rb +172 -0
- data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
- data/skills/rspec/examples/mocks/constants.rb +177 -0
- data/skills/rspec/examples/mocks/doubles.rb +139 -0
- data/skills/rspec/examples/mocks/expectations.rb +137 -0
- data/skills/rspec/examples/mocks/message_chains.rb +173 -0
- data/skills/rspec/examples/mocks/ordering.rb +144 -0
- data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
- data/skills/rspec/examples/mocks/responses.rb +223 -0
- data/skills/rspec/examples/mocks/spies.rb +149 -0
- data/skills/rspec/examples/mocks/stubbing.rb +133 -0
- data/skills/rspec/examples/rails/channels.rb +250 -0
- data/skills/rspec/examples/rails/controller_specs.rb +302 -0
- data/skills/rspec/examples/rails/helper_specs.rb +245 -0
- data/skills/rspec/examples/rails/job_specs.rb +256 -0
- data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
- data/skills/rspec/examples/rails/matchers.rb +374 -0
- data/skills/rspec/examples/rails/model_specs.rb +193 -0
- data/skills/rspec/examples/rails/request_specs.rb +275 -0
- data/skills/rspec/examples/rails/routing_specs.rb +276 -0
- data/skills/rspec/examples/rails/system_specs.rb +294 -0
- data/skills/rspec/examples/rails/transactions.rb +254 -0
- data/skills/rspec/examples/rails/view_specs.rb +252 -0
- data/skills/rspec/references/core.md +816 -0
- data/skills/rspec/references/factory_bot.md +641 -0
- data/skills/rspec/references/matchers.md +516 -0
- data/skills/rspec/references/mocks.md +381 -0
- data/skills/rspec/references/rails.md +528 -0
- data/templates/soul.md +40 -0
- data/workflows/commit.md +45 -0
- data/workflows/create_handoff.md +98 -0
- data/workflows/create_note.md +82 -0
- data/workflows/create_plan.md +457 -0
- data/workflows/decompose_ticket.md +109 -0
- data/workflows/feature.md +91 -0
- data/workflows/implement_plan.md +87 -0
- data/workflows/iterate_plan.md +247 -0
- data/workflows/research_codebase.md +210 -0
- data/workflows/resume_handoff.md +217 -0
- data/workflows/review_pr.md +320 -0
- data/workflows/thoughts_init.md +71 -0
- data/workflows/validate_plan.md +166 -0
- metadata +290 -3
- data/app/controllers/api/sessions_controller.rb +0 -25
- data/lib/events/subscribers/action_cable_bridge.rb +0 -59
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# Query Optimization Examples
|
|
2
|
+
# Demonstrates pluck, calculations, select, and memory-efficient patterns
|
|
3
|
+
|
|
4
|
+
# ============================================
|
|
5
|
+
# pluck vs map - Value Extraction
|
|
6
|
+
# ============================================
|
|
7
|
+
|
|
8
|
+
# BAD - loads full User objects into memory
|
|
9
|
+
emails = User.all.map(&:email)
|
|
10
|
+
# 1. Instantiates every User object
|
|
11
|
+
# 2. Allocates memory for all attributes
|
|
12
|
+
# 3. Then extracts just email
|
|
13
|
+
|
|
14
|
+
# GOOD - only fetches email column from database
|
|
15
|
+
emails = User.pluck(:email)
|
|
16
|
+
# SELECT email FROM users
|
|
17
|
+
# Returns Array of strings directly
|
|
18
|
+
|
|
19
|
+
# Multiple columns - returns array of arrays
|
|
20
|
+
User.pluck(:id, :email)
|
|
21
|
+
# => [[1, "a@example.com"], [2, "b@example.com"]]
|
|
22
|
+
|
|
23
|
+
# With conditions
|
|
24
|
+
User.where(active: true).pluck(:email)
|
|
25
|
+
|
|
26
|
+
# ============================================
|
|
27
|
+
# pluck Caveats
|
|
28
|
+
# ============================================
|
|
29
|
+
|
|
30
|
+
# pluck returns Array, not Relation - cannot chain after
|
|
31
|
+
User.pluck(:email).where(active: true) # NoMethodError!
|
|
32
|
+
|
|
33
|
+
# pluck ignores select
|
|
34
|
+
User.select(:id).pluck(:email) # Still plucks email
|
|
35
|
+
|
|
36
|
+
# pluck with association requires join
|
|
37
|
+
Post.joins(:author).pluck("authors.name")
|
|
38
|
+
# SELECT authors.name FROM posts INNER JOIN authors...
|
|
39
|
+
|
|
40
|
+
# ============================================
|
|
41
|
+
# ids - Shortcut for Primary Keys
|
|
42
|
+
# ============================================
|
|
43
|
+
|
|
44
|
+
# These are equivalent
|
|
45
|
+
User.pluck(:id)
|
|
46
|
+
User.ids
|
|
47
|
+
|
|
48
|
+
# With conditions
|
|
49
|
+
User.active.ids
|
|
50
|
+
# SELECT id FROM users WHERE active = true
|
|
51
|
+
|
|
52
|
+
# ============================================
|
|
53
|
+
# pick - Single Value
|
|
54
|
+
# ============================================
|
|
55
|
+
|
|
56
|
+
# Get first matching value (Rails 6+)
|
|
57
|
+
User.where(admin: true).pick(:email)
|
|
58
|
+
# Equivalent to: User.where(admin: true).limit(1).pluck(:email).first
|
|
59
|
+
|
|
60
|
+
# Multiple columns
|
|
61
|
+
User.pick(:id, :email)
|
|
62
|
+
# => [1, "admin@example.com"]
|
|
63
|
+
|
|
64
|
+
# ============================================
|
|
65
|
+
# Calculations - Database-Level Math
|
|
66
|
+
# ============================================
|
|
67
|
+
|
|
68
|
+
# count
|
|
69
|
+
User.count # COUNT(*)
|
|
70
|
+
User.count(:email) # COUNT(email) - excludes NULL
|
|
71
|
+
User.distinct.count(:role) # COUNT(DISTINCT role)
|
|
72
|
+
|
|
73
|
+
# sum, average, minimum, maximum
|
|
74
|
+
Order.sum(:total)
|
|
75
|
+
Order.average(:total)
|
|
76
|
+
User.minimum(:created_at)
|
|
77
|
+
User.maximum(:login_count)
|
|
78
|
+
|
|
79
|
+
# With conditions
|
|
80
|
+
Order.where(status: "completed").sum(:total)
|
|
81
|
+
User.active.average(:age)
|
|
82
|
+
|
|
83
|
+
# ============================================
|
|
84
|
+
# Grouped Calculations
|
|
85
|
+
# ============================================
|
|
86
|
+
|
|
87
|
+
# Group by single column
|
|
88
|
+
Order.group(:status).count
|
|
89
|
+
# => {"pending" => 10, "shipped" => 25, "delivered" => 50}
|
|
90
|
+
|
|
91
|
+
Order.group(:status).sum(:total)
|
|
92
|
+
# => {"pending" => 1000, "shipped" => 5000, "delivered" => 10000}
|
|
93
|
+
|
|
94
|
+
# Group by multiple columns
|
|
95
|
+
User.group(:role, :status).count
|
|
96
|
+
# => {["admin", "active"] => 5, ["user", "active"] => 100, ...}
|
|
97
|
+
|
|
98
|
+
# Group by date
|
|
99
|
+
Order.group("DATE(created_at)").count
|
|
100
|
+
# Daily order counts
|
|
101
|
+
|
|
102
|
+
Order.group("DATE_TRUNC('month', created_at)").sum(:total)
|
|
103
|
+
# Monthly revenue (PostgreSQL)
|
|
104
|
+
|
|
105
|
+
# ============================================
|
|
106
|
+
# select - Limit Loaded Columns
|
|
107
|
+
# ============================================
|
|
108
|
+
|
|
109
|
+
# Only load needed columns - reduces memory
|
|
110
|
+
users = User.select(:id, :name, :email)
|
|
111
|
+
|
|
112
|
+
# WARNING: Accessing non-selected columns raises error
|
|
113
|
+
users.first.password_digest
|
|
114
|
+
# => ActiveModel::MissingAttributeError
|
|
115
|
+
|
|
116
|
+
# Select with alias
|
|
117
|
+
User.select("name, email, created_at AS signup_date")
|
|
118
|
+
|
|
119
|
+
# Select with calculation
|
|
120
|
+
User.select("*, (SELECT COUNT(*) FROM posts WHERE user_id = users.id) AS posts_count")
|
|
121
|
+
|
|
122
|
+
# ============================================
|
|
123
|
+
# exists? vs any? vs count
|
|
124
|
+
# ============================================
|
|
125
|
+
|
|
126
|
+
# exists? - most efficient for checking presence
|
|
127
|
+
User.where(email: "test@example.com").exists?
|
|
128
|
+
# SELECT 1 FROM users WHERE email = '...' LIMIT 1
|
|
129
|
+
|
|
130
|
+
# any? - also efficient, slightly different semantics
|
|
131
|
+
User.active.any?
|
|
132
|
+
# SELECT 1 FROM users WHERE active = true LIMIT 1
|
|
133
|
+
|
|
134
|
+
# count - returns number, less efficient for just checking
|
|
135
|
+
User.active.count > 0 # Less efficient than any?
|
|
136
|
+
|
|
137
|
+
# BAD - loads all records
|
|
138
|
+
User.active.to_a.any? # Loads everything!
|
|
139
|
+
User.active.length > 0 # Also loads everything!
|
|
140
|
+
|
|
141
|
+
# ============================================
|
|
142
|
+
# size vs count vs length
|
|
143
|
+
# ============================================
|
|
144
|
+
|
|
145
|
+
users = User.where(active: true)
|
|
146
|
+
|
|
147
|
+
# count - always hits database
|
|
148
|
+
users.count # SELECT COUNT(*) FROM users WHERE active = true
|
|
149
|
+
|
|
150
|
+
# size - smart: uses count if not loaded, length if loaded
|
|
151
|
+
users.size # COUNT(*) if not loaded
|
|
152
|
+
users.to_a
|
|
153
|
+
users.size # Uses array length (no query)
|
|
154
|
+
|
|
155
|
+
# length - always loads all records first
|
|
156
|
+
users.length # SELECT * FROM users..., then .length on array
|
|
157
|
+
|
|
158
|
+
# Rule: Use size unless you specifically need count or length behavior
|
|
159
|
+
|
|
160
|
+
# ============================================
|
|
161
|
+
# Anti-Patterns and Fixes
|
|
162
|
+
# ============================================
|
|
163
|
+
|
|
164
|
+
# Anti-pattern 1: Ruby filtering instead of SQL
|
|
165
|
+
# BAD
|
|
166
|
+
User.all.select { |u| u.active? }
|
|
167
|
+
# GOOD
|
|
168
|
+
User.where(active: true)
|
|
169
|
+
|
|
170
|
+
# Anti-pattern 2: map instead of pluck
|
|
171
|
+
# BAD
|
|
172
|
+
User.all.map(&:email)
|
|
173
|
+
# GOOD
|
|
174
|
+
User.pluck(:email)
|
|
175
|
+
|
|
176
|
+
# Anti-pattern 3: each with update
|
|
177
|
+
# BAD - N queries
|
|
178
|
+
User.where(old: true).each { |u| u.update(archived: true) }
|
|
179
|
+
# GOOD - 1 query
|
|
180
|
+
User.where(old: true).update_all(archived: true)
|
|
181
|
+
|
|
182
|
+
# Anti-pattern 4: Loading for count
|
|
183
|
+
# BAD
|
|
184
|
+
User.all.length
|
|
185
|
+
User.all.size # If records not yet loaded, ok; if force loading, bad
|
|
186
|
+
# GOOD
|
|
187
|
+
User.count
|
|
188
|
+
|
|
189
|
+
# Anti-pattern 5: exists check with count
|
|
190
|
+
# BAD
|
|
191
|
+
User.where(email: "test@example.com").count > 0
|
|
192
|
+
# GOOD
|
|
193
|
+
User.where(email: "test@example.com").exists?
|
|
194
|
+
|
|
195
|
+
# ============================================
|
|
196
|
+
# Practical Optimization Examples
|
|
197
|
+
# ============================================
|
|
198
|
+
|
|
199
|
+
# Example 1: Dashboard statistics (efficient)
|
|
200
|
+
def dashboard_stats
|
|
201
|
+
{
|
|
202
|
+
total_users: User.count,
|
|
203
|
+
active_users: User.active.count,
|
|
204
|
+
new_today: User.where("created_at >= ?", Date.current).count,
|
|
205
|
+
orders_by_status: Order.group(:status).count,
|
|
206
|
+
monthly_revenue: Order.completed.this_month.sum(:total)
|
|
207
|
+
}
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Example 2: Dropdown options (efficient)
|
|
211
|
+
def category_options
|
|
212
|
+
# Instead of Category.all.map { |c| [c.name, c.id] }
|
|
213
|
+
Category.order(:name).pluck(:name, :id)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Example 3: Bulk presence check
|
|
217
|
+
def users_exist?(emails)
|
|
218
|
+
existing = User.where(email: emails).pluck(:email)
|
|
219
|
+
emails.map { |e| existing.include?(e) }
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Example 4: Memory-efficient iteration with select
|
|
223
|
+
User.select(:id, :email).find_each do |user|
|
|
224
|
+
# Only id and email loaded, saves memory
|
|
225
|
+
SomeService.process(user.id, user.email)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Example 5: Conditional eager loading
|
|
229
|
+
def load_posts(include_comments:)
|
|
230
|
+
posts = Post.includes(:author)
|
|
231
|
+
posts = posts.includes(:comments) if include_comments
|
|
232
|
+
posts
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# ============================================
|
|
236
|
+
# EXPLAIN for Query Analysis
|
|
237
|
+
# ============================================
|
|
238
|
+
|
|
239
|
+
# See query execution plan
|
|
240
|
+
User.where(email: "test@example.com").explain
|
|
241
|
+
# EXPLAIN SELECT * FROM users WHERE email = '...'
|
|
242
|
+
|
|
243
|
+
# PostgreSQL specific
|
|
244
|
+
User.where(email: "test@example.com").explain(:analyze)
|
|
245
|
+
# EXPLAIN ANALYZE SELECT * FROM users WHERE email = '...'
|
|
246
|
+
|
|
247
|
+
# Check if index is used
|
|
248
|
+
# Look for "Index Scan" vs "Seq Scan" in output
|
|
249
|
+
|
|
250
|
+
# ============================================
|
|
251
|
+
# Raw SQL When Needed
|
|
252
|
+
# ============================================
|
|
253
|
+
|
|
254
|
+
# Complex queries with find_by_sql
|
|
255
|
+
User.find_by_sql([
|
|
256
|
+
"SELECT users.*, COUNT(posts.id) as post_count
|
|
257
|
+
FROM users
|
|
258
|
+
LEFT JOIN posts ON posts.user_id = users.id
|
|
259
|
+
WHERE users.active = ?
|
|
260
|
+
GROUP BY users.id
|
|
261
|
+
HAVING COUNT(posts.id) > ?",
|
|
262
|
+
true, 5
|
|
263
|
+
])
|
|
264
|
+
|
|
265
|
+
# Pure SQL execution
|
|
266
|
+
result = ActiveRecord::Base.connection.execute(
|
|
267
|
+
"SELECT DATE(created_at), COUNT(*) FROM users GROUP BY 1"
|
|
268
|
+
)
|
|
269
|
+
result.to_a # Array of hashes
|
|
270
|
+
|
|
271
|
+
# Using Arel for complex conditions
|
|
272
|
+
users = User.arel_table
|
|
273
|
+
User.where(
|
|
274
|
+
users[:age].gt(18).and(users[:age].lt(65))
|
|
275
|
+
)
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# Scopes Examples
|
|
2
|
+
# Demonstrates named scopes, scope vs class method, default_scope issues
|
|
3
|
+
|
|
4
|
+
# ============================================
|
|
5
|
+
# Named Scopes - Basic Usage
|
|
6
|
+
# ============================================
|
|
7
|
+
|
|
8
|
+
class Article < ApplicationRecord
|
|
9
|
+
belongs_to :author
|
|
10
|
+
has_many :comments
|
|
11
|
+
|
|
12
|
+
# Simple scope
|
|
13
|
+
scope :published, -> { where(published: true) }
|
|
14
|
+
scope :draft, -> { where(published: false) }
|
|
15
|
+
|
|
16
|
+
# Ordering scope
|
|
17
|
+
scope :recent, -> { order(created_at: :desc) }
|
|
18
|
+
scope :oldest, -> { order(created_at: :asc) }
|
|
19
|
+
|
|
20
|
+
# Scope with argument
|
|
21
|
+
scope :by_author, ->(author) { where(author:) }
|
|
22
|
+
scope :created_after, ->(date) { where("created_at > ?", date) }
|
|
23
|
+
scope :created_between, ->(start_date, end_date) { where(created_at: start_date..end_date) }
|
|
24
|
+
|
|
25
|
+
# Scope with optional argument
|
|
26
|
+
scope :tagged, ->(tag = nil) {
|
|
27
|
+
tag ? joins(:tags).where(tags: { name: tag }) : all
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Complex scope
|
|
31
|
+
scope :featured, -> {
|
|
32
|
+
published
|
|
33
|
+
.where(featured: true)
|
|
34
|
+
.where("published_at > ?", 1.week.ago)
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Usage - scopes are chainable
|
|
39
|
+
Article.published.recent
|
|
40
|
+
Article.published.by_author(current_user).recent
|
|
41
|
+
Article.draft.created_after(1.month.ago)
|
|
42
|
+
|
|
43
|
+
# ============================================
|
|
44
|
+
# Scope Chaining
|
|
45
|
+
# ============================================
|
|
46
|
+
|
|
47
|
+
# All scopes return Relations - fully chainable
|
|
48
|
+
articles = Article.published
|
|
49
|
+
.recent
|
|
50
|
+
.by_author(current_user)
|
|
51
|
+
.limit(10)
|
|
52
|
+
|
|
53
|
+
# Scopes work with associations
|
|
54
|
+
author.articles.published.recent
|
|
55
|
+
|
|
56
|
+
# Scopes work with finder methods
|
|
57
|
+
Article.published.find_by(slug: "hello-world")
|
|
58
|
+
Article.recent.first
|
|
59
|
+
Article.published.count
|
|
60
|
+
|
|
61
|
+
# ============================================
|
|
62
|
+
# Scope vs Class Method
|
|
63
|
+
# ============================================
|
|
64
|
+
|
|
65
|
+
class Post < ApplicationRecord
|
|
66
|
+
# SCOPE - always returns Relation
|
|
67
|
+
scope :active, -> {
|
|
68
|
+
if Feature.enabled?(:soft_delete)
|
|
69
|
+
where(deleted_at: nil)
|
|
70
|
+
end
|
|
71
|
+
# Returns nil if condition false, but Rails converts to .all
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# CLASS METHOD - can return nil (breaks chaining!)
|
|
75
|
+
def self.active_method
|
|
76
|
+
if Feature.enabled?(:soft_delete)
|
|
77
|
+
where(deleted_at: nil)
|
|
78
|
+
end
|
|
79
|
+
# Returns nil if condition false - breaks chaining!
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Scope handles nil gracefully
|
|
84
|
+
Post.active.recent # Works even if condition returns nil
|
|
85
|
+
|
|
86
|
+
# Class method breaks
|
|
87
|
+
Post.active_method.recent # NoMethodError if active_method returns nil!
|
|
88
|
+
|
|
89
|
+
# CLASS METHOD is better when:
|
|
90
|
+
# - Complex logic with early returns
|
|
91
|
+
# - Need to accept block
|
|
92
|
+
# - Want explicit control over return value
|
|
93
|
+
|
|
94
|
+
# Good class method example
|
|
95
|
+
class Post < ApplicationRecord
|
|
96
|
+
def self.search(query)
|
|
97
|
+
return none if query.blank? # Explicit empty result
|
|
98
|
+
|
|
99
|
+
sanitized = sanitize_sql_like(query)
|
|
100
|
+
where("title ILIKE :q OR body ILIKE :q", q: "%#{sanitized}%")
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# ============================================
|
|
105
|
+
# default_scope - USE WITH EXTREME CAUTION
|
|
106
|
+
# ============================================
|
|
107
|
+
|
|
108
|
+
# Anti-pattern example - DO NOT DO THIS
|
|
109
|
+
class Article < ApplicationRecord
|
|
110
|
+
default_scope { where(published: true) } # Dangerous!
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Problem 1: Affects NEW records
|
|
114
|
+
Article.new.published # => true (unexpected default!)
|
|
115
|
+
Article.create(title: "Draft") # published: true automatically!
|
|
116
|
+
|
|
117
|
+
# Problem 2: Hidden filtering everywhere
|
|
118
|
+
Article.all # Only published articles
|
|
119
|
+
Article.count # Only counts published
|
|
120
|
+
Article.find(id) # Fails silently if unpublished!
|
|
121
|
+
|
|
122
|
+
# Problem 3: Affects joins
|
|
123
|
+
Author.joins(:articles) # Only joins published articles
|
|
124
|
+
|
|
125
|
+
# Problem 4: Must remember to unscope
|
|
126
|
+
Article.unscoped.all # All articles
|
|
127
|
+
Article.unscoped { Article.all } # Block form
|
|
128
|
+
|
|
129
|
+
# ============================================
|
|
130
|
+
# Better Alternatives to default_scope
|
|
131
|
+
# ============================================
|
|
132
|
+
|
|
133
|
+
class Article < ApplicationRecord
|
|
134
|
+
# Alternative 1: Explicit scopes
|
|
135
|
+
scope :published, -> { where(published: true) }
|
|
136
|
+
scope :visible, -> { published } # Semantic alias
|
|
137
|
+
|
|
138
|
+
# Alternative 2: Query object
|
|
139
|
+
class PublishedArticles
|
|
140
|
+
def self.call
|
|
141
|
+
Article.where(published: true)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Alternative 3: Explicit default in controller
|
|
146
|
+
def index
|
|
147
|
+
@articles = Article.published
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# When default_scope IS acceptable (rare):
|
|
152
|
+
# - Ordering only (not filtering)
|
|
153
|
+
# - Truly universal constraint (multi-tenant tenant_id)
|
|
154
|
+
|
|
155
|
+
class Tenant < ApplicationRecord
|
|
156
|
+
# Ordering default_scope is less dangerous
|
|
157
|
+
default_scope { order(:name) }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
class Document < ApplicationRecord
|
|
161
|
+
# Multi-tenant - MUST always filter
|
|
162
|
+
default_scope { where(tenant_id: Current.tenant_id) }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# ============================================
|
|
166
|
+
# Merging Scopes
|
|
167
|
+
# ============================================
|
|
168
|
+
|
|
169
|
+
class Author < ApplicationRecord
|
|
170
|
+
has_many :articles
|
|
171
|
+
scope :active, -> { where(active: true) }
|
|
172
|
+
scope :verified, -> { where(verified: true) }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
class Article < ApplicationRecord
|
|
176
|
+
belongs_to :author
|
|
177
|
+
scope :published, -> { where(published: true) }
|
|
178
|
+
scope :recent, -> { where("created_at > ?", 1.week.ago) }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Merge scopes from different models
|
|
182
|
+
Article.published
|
|
183
|
+
.joins(:author)
|
|
184
|
+
.merge(Author.active)
|
|
185
|
+
# SELECT articles.* FROM articles
|
|
186
|
+
# INNER JOIN authors ON authors.id = articles.author_id
|
|
187
|
+
# WHERE articles.published = true
|
|
188
|
+
# AND authors.active = true
|
|
189
|
+
|
|
190
|
+
# Merge multiple scopes
|
|
191
|
+
Article.published
|
|
192
|
+
.joins(:author)
|
|
193
|
+
.merge(Author.active.verified)
|
|
194
|
+
|
|
195
|
+
# Merge with association scope
|
|
196
|
+
class Author < ApplicationRecord
|
|
197
|
+
has_many :articles
|
|
198
|
+
has_many :published_articles, -> { published }, class_name: "Article"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# ============================================
|
|
202
|
+
# unscoped - Removing Scopes
|
|
203
|
+
# ============================================
|
|
204
|
+
|
|
205
|
+
# Remove all scopes (including default_scope)
|
|
206
|
+
Article.unscoped.all
|
|
207
|
+
|
|
208
|
+
# Block form - only affects block
|
|
209
|
+
Article.unscoped do
|
|
210
|
+
Article.where(id: 1) # No default_scope
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# unscope - remove specific clauses
|
|
214
|
+
Article.published.recent.unscope(:order)
|
|
215
|
+
# Removes order, keeps where
|
|
216
|
+
|
|
217
|
+
Article.published.where(featured: true).unscope(where: :published)
|
|
218
|
+
# Removes published condition, keeps featured
|
|
219
|
+
|
|
220
|
+
# reorder - replace order
|
|
221
|
+
Article.order(:title).reorder(:created_at)
|
|
222
|
+
# Only ordered by created_at
|
|
223
|
+
|
|
224
|
+
# rewhere - replace where (Rails 7+)
|
|
225
|
+
Article.where(status: "draft").rewhere(status: "published")
|
|
226
|
+
# status = 'published' only
|
|
227
|
+
|
|
228
|
+
# ============================================
|
|
229
|
+
# Practical Scope Patterns
|
|
230
|
+
# ============================================
|
|
231
|
+
|
|
232
|
+
class Order < ApplicationRecord
|
|
233
|
+
# Status scopes
|
|
234
|
+
scope :pending, -> { where(status: "pending") }
|
|
235
|
+
scope :processing, -> { where(status: "processing") }
|
|
236
|
+
scope :completed, -> { where(status: "completed") }
|
|
237
|
+
scope :cancelled, -> { where(status: "cancelled") }
|
|
238
|
+
|
|
239
|
+
# Time-based scopes
|
|
240
|
+
scope :today, -> { where(created_at: Time.current.all_day) }
|
|
241
|
+
scope :this_week, -> { where(created_at: Time.current.all_week) }
|
|
242
|
+
scope :this_month, -> { where(created_at: Time.current.all_month) }
|
|
243
|
+
|
|
244
|
+
# Aggregate scopes
|
|
245
|
+
scope :high_value, -> { where("total > ?", 1000) }
|
|
246
|
+
scope :with_items, -> { joins(:order_items).distinct }
|
|
247
|
+
|
|
248
|
+
# Boolean shortcuts
|
|
249
|
+
scope :paid, -> { where(paid: true) }
|
|
250
|
+
scope :unpaid, -> { where(paid: false) }
|
|
251
|
+
|
|
252
|
+
# Negation pattern
|
|
253
|
+
scope :not_cancelled, -> { where.not(status: "cancelled") }
|
|
254
|
+
scope :active, -> { not_cancelled.where.not(status: "completed") }
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Combining scopes for reports
|
|
258
|
+
Order.this_month.completed.sum(:total)
|
|
259
|
+
Order.today.pending.count
|
|
260
|
+
Order.this_week.high_value.paid.includes(:customer)
|