rails-ai-context 5.35.0 → 5.35.2

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.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1024 -0
  3. data/CONTEXT.md +37 -7
  4. data/CONTRIBUTING.md +30 -1
  5. data/README.md +9 -9
  6. data/SECURITY.md +2 -2
  7. data/docs/ARCHITECTURE.md +12 -4
  8. data/docs/CLI.md +44 -15
  9. data/docs/COMPATIBILITY.md +34 -8
  10. data/docs/CONFIGURATION.md +28 -5
  11. data/docs/CUSTOM_TOOLS.md +8 -2
  12. data/docs/FAQ.md +5 -5
  13. data/docs/GUIDE.md +33 -65
  14. data/docs/INTROSPECTORS.md +4 -4
  15. data/docs/QUICKSTART.md +1 -1
  16. data/docs/RECIPES.md +5 -5
  17. data/docs/RELEASING.md +3 -1
  18. data/docs/SECURITY.md +255 -74
  19. data/docs/SETUP.md +1 -1
  20. data/docs/STANDALONE.md +2 -2
  21. data/docs/TOOLS.md +151 -90
  22. data/docs/TROUBLESHOOTING.md +123 -8
  23. data/docs/_config.yml +1 -1
  24. data/docs/adr/0008-query-runs-only-where-it-can-guard.md +38 -0
  25. data/docs/adr/0009-dbhub-shapes-in-ruby.md +26 -0
  26. data/docs/adr/0010-one-config-tool-with-sections.md +32 -0
  27. data/docs/index.md +2 -2
  28. data/exe/rails-ai-context +24 -9
  29. data/lib/generators/rails_ai_context/install/install_generator.rb +2 -0
  30. data/lib/rails_ai_context/app_databases.rb +377 -0
  31. data/lib/rails_ai_context/app_kind.rb +2 -0
  32. data/lib/rails_ai_context/cli/app_root.rb +1 -0
  33. data/lib/rails_ai_context/cli/tool_runner.rb +110 -99
  34. data/lib/rails_ai_context/configuration.rb +15 -8
  35. data/lib/rails_ai_context/database_family/mysql_runtime.rb +170 -0
  36. data/lib/rails_ai_context/database_family/postgresql_plan.rb +124 -0
  37. data/lib/rails_ai_context/database_family/postgresql_runtime.rb +211 -0
  38. data/lib/rails_ai_context/database_family/sqlite_runtime.rb +391 -0
  39. data/lib/rails_ai_context/database_family.rb +282 -0
  40. data/lib/rails_ai_context/database_file.rb +61 -7
  41. data/lib/rails_ai_context/database_yml.rb +7 -29
  42. data/lib/rails_ai_context/doctor.rb +15 -16
  43. data/lib/rails_ai_context/instrumentation.rb +1 -1
  44. data/lib/rails_ai_context/introspectors/connection_pool_introspector.rb +8 -23
  45. data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +6 -70
  46. data/lib/rails_ai_context/introspectors/declared_constant.rb +19 -7
  47. data/lib/rails_ai_context/introspectors/env_introspector.rb +1 -1
  48. data/lib/rails_ai_context/introspectors/env_references.rb +1 -1
  49. data/lib/rails_ai_context/introspectors/job_introspector.rb +7 -7
  50. data/lib/rails_ai_context/introspectors/migration_introspector.rb +8 -11
  51. data/lib/rails_ai_context/introspectors/migration_replay/statements.rb +9 -9
  52. data/lib/rails_ai_context/introspectors/model_introspector.rb +33 -39
  53. data/lib/rails_ai_context/introspectors/multi_database_introspector.rb +33 -30
  54. data/lib/rails_ai_context/introspectors/node_source.rb +2 -0
  55. data/lib/rails_ai_context/introspectors/performance_introspector.rb +80 -24
  56. data/lib/rails_ai_context/introspectors/pg_partitions.rb +1 -4
  57. data/lib/rails_ai_context/introspectors/schema_conventions.rb +6 -8
  58. data/lib/rails_ai_context/introspectors/schema_dump_path.rb +12 -14
  59. data/lib/rails_ai_context/introspectors/schema_introspector.rb +47 -27
  60. data/lib/rails_ai_context/introspectors/schema_reader.rb +1 -1
  61. data/lib/rails_ai_context/introspectors/source_scan.rb +39 -4
  62. data/lib/rails_ai_context/migration_status.rb +18 -46
  63. data/lib/rails_ai_context/payload.rb +48 -1
  64. data/lib/rails_ai_context/pending_migrations.rb +3 -4
  65. data/lib/rails_ai_context/run_cache.rb +5 -0
  66. data/lib/rails_ai_context/schema_adapter.rb +13 -18
  67. data/lib/rails_ai_context/secret_columns.rb +506 -0
  68. data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +2 -2
  69. data/lib/rails_ai_context/serializers/markdown_serializer.rb +14 -10
  70. data/lib/rails_ai_context/serializers/section_facts.rb +12 -2
  71. data/lib/rails_ai_context/serializers/stack_overview_helper.rb +5 -2
  72. data/lib/rails_ai_context/serializers/tool_guide_helper.rb +35 -7
  73. data/lib/rails_ai_context/server.rb +143 -29
  74. data/lib/rails_ai_context/sql_gate.rb +1007 -0
  75. data/lib/rails_ai_context/tasks/rails_ai_context.rake +1 -2
  76. data/lib/rails_ai_context/test_helper.rb +3 -1
  77. data/lib/rails_ai_context/tools/analyze_feature.rb +11 -7
  78. data/lib/rails_ai_context/tools/base_tool.rb +71 -48
  79. data/lib/rails_ai_context/tools/dependency_graph.rb +1 -1
  80. data/lib/rails_ai_context/tools/diagnose.rb +105 -42
  81. data/lib/rails_ai_context/tools/generate_test.rb +4 -6
  82. data/lib/rails_ai_context/tools/get_active_support.rb +1 -6
  83. data/lib/rails_ai_context/tools/get_api.rb +1 -6
  84. data/lib/rails_ai_context/tools/get_autoload.rb +1 -6
  85. data/lib/rails_ai_context/tools/get_callbacks.rb +5 -5
  86. data/lib/rails_ai_context/tools/get_component_catalog.rb +6 -7
  87. data/lib/rails_ai_context/tools/get_concern.rb +6 -6
  88. data/lib/rails_ai_context/tools/get_config.rb +189 -12
  89. data/lib/rails_ai_context/tools/get_context.rb +111 -105
  90. data/lib/rails_ai_context/tools/get_controllers.rb +8 -5
  91. data/lib/rails_ai_context/tools/get_conventions.rb +1 -6
  92. data/lib/rails_ai_context/tools/get_engines.rb +1 -6
  93. data/lib/rails_ai_context/tools/get_env.rb +1 -6
  94. data/lib/rails_ai_context/tools/get_env_config.rb +2 -8
  95. data/lib/rails_ai_context/tools/get_frontend_stack.rb +1 -6
  96. data/lib/rails_ai_context/tools/get_gems.rb +1 -6
  97. data/lib/rails_ai_context/tools/get_helper_methods.rb +4 -5
  98. data/lib/rails_ai_context/tools/get_i18n.rb +1 -7
  99. data/lib/rails_ai_context/tools/get_job_pattern.rb +1 -1
  100. data/lib/rails_ai_context/tools/get_mailers.rb +1 -1
  101. data/lib/rails_ai_context/tools/get_model_details.rb +5 -8
  102. data/lib/rails_ai_context/tools/get_partial_interface.rb +3 -4
  103. data/lib/rails_ai_context/tools/get_schema.rb +10 -8
  104. data/lib/rails_ai_context/tools/get_service_pattern.rb +60 -24
  105. data/lib/rails_ai_context/tools/get_turbo_map.rb +1 -1
  106. data/lib/rails_ai_context/tools/migration_advisor.rb +47 -32
  107. data/lib/rails_ai_context/tools/onboard.rb +24 -24
  108. data/lib/rails_ai_context/tools/performance_check.rb +15 -2
  109. data/lib/rails_ai_context/tools/query.rb +142 -1356
  110. data/lib/rails_ai_context/tools/read_logs.rb +1 -1
  111. data/lib/rails_ai_context/tools/review_changes.rb +6 -7
  112. data/lib/rails_ai_context/tools/runtime_info.rb +125 -88
  113. data/lib/rails_ai_context/tools/safe_call.rb +70 -28
  114. data/lib/rails_ai_context/tools/search_code.rb +0 -4
  115. data/lib/rails_ai_context/tools/search_docs.rb +3 -1
  116. data/lib/rails_ai_context/tools/security_scan.rb +9 -5
  117. data/lib/rails_ai_context/tools/session_context.rb +18 -12
  118. data/lib/rails_ai_context/tools/validate.rb +1 -0
  119. data/lib/rails_ai_context/tools/validate_semantics.rb +5 -5
  120. data/lib/rails_ai_context/version.rb +1 -1
  121. data/lib/rails_ai_context.rb +4 -7
  122. data/server.json +2 -2
  123. metadata +14 -3
@@ -54,7 +54,7 @@ module RailsAiContext
54
54
  if MigrationReplay.migration_files(migrate_dirs, root: root).any?
55
55
  pk_type = SchemaConventions.implicit_pk_type(root)
56
56
  return from_tables(MigrationReplay.tables(migrate_dirs, pk_type: pk_type, root: root,
57
- adapter: SchemaConventions.database_adapter_for(root, "primary")),
57
+ adapter: SchemaConventions.database_adapter_for(root, nil)),
58
58
  source: :migrations, path: migrate_dirs.first)
59
59
  end
60
60
 
@@ -168,12 +168,14 @@ module RailsAiContext
168
168
  declared = Set.new
169
169
  ignored = PathResolver.ignored_dirs(root).map { |dir| PathResolver.root_key(dir) }
170
170
  found = PathResolver.extra_model_roots(root).each_with_object([]) do |dir, records|
171
- scan_dir(dir, root, real_root, true) do |record|
171
+ scan_dir(dir, root, real_root, false) do |record|
172
172
  next unless seen.add?(record.path)
173
173
  next if ignored.any? { |ignored_dir| SafePath.contained?(record.path, ignored_dir) }
174
174
 
175
+ source = SafeFile.read(record.path).to_s
176
+ record = record.with(path_name: concerns_name(record.path_name, source))
175
177
  declared << record.path_name
176
- pairs = class_declarations(SafeFile.read(record.path).to_s)
178
+ pairs = class_declarations(source)
177
179
  next if pairs.empty?
178
180
 
179
181
  records << [ record, pairs ]
@@ -183,6 +185,29 @@ module RailsAiContext
183
185
  [ found, declared ]
184
186
  end
185
187
 
188
+ # A concerns/ under an app/ root is an autoload root of its own, so its files carry no
189
+ # `Concerns::`; under a root the app adds it is a namespace, which the file then declares.
190
+ def concerns_name(path_name, source)
191
+ return path_name unless path_name.start_with?("Concerns::")
192
+
193
+ declared = DeclaredConstant.declared_names(source) + DeclaredConstant.declared_module_names(source)
194
+ declared.any? { |name| name.split("::").include?("Concerns") } ? path_name : path_name.delete_prefix("Concerns::")
195
+ end
196
+
197
+ # The class a file under a concerns/ is named for; an inner class of a mixin is not it.
198
+ def concern_class(source, path_name)
199
+ DeclaredConstant.declaration_named(DeclaredConstant.declarations(source, path_name: path_name), path_name)
200
+ end
201
+
202
+ # Whether a concerns file's own class names a model file's class or a record base as its superclass.
203
+ def concern_model?(source, path_name, model_names, base_model)
204
+ declaration = concern_class(source, path_name)
205
+ return false unless declaration&.superclass
206
+
207
+ candidates = declaration.nesting.map { |scope| "#{scope}::#{declaration.superclass}" } << declaration.superclass
208
+ candidates.any? { |candidate| model_names.include?(candidate.downcase) } || (base_model ? base_model.call(candidates) : false)
209
+ end
210
+
186
211
  # [name, superclass] of each `class X < Y` that starts its line. A `^` anchor
187
212
  # makes Onigmo try every offset, ten times slower over a service tree.
188
213
  def class_declarations(source)
@@ -195,7 +220,7 @@ module RailsAiContext
195
220
  pairs
196
221
  end
197
222
 
198
- private_class_method :scan, :scan_dir, :ruby_files, :walk_dir, :within?, :extra_model_candidates, :extra_model_declarations, :class_declarations, :lookup, :qualified
223
+ private_class_method :scan, :scan_dir, :ruby_files, :walk_dir, :within?, :extra_model_candidates, :extra_model_declarations, :class_declarations, :lookup, :qualified, :concern_model?
199
224
 
200
225
  # `kind: :models` reads model_paths: what model_details lists, not only app/models.
201
226
  def each(root, kind:, skip_concerns: true, base_model: nil, &block)
@@ -207,7 +232,17 @@ module RailsAiContext
207
232
  end
208
233
  return paths(root, kind: kind, skip_concerns: skip_concerns, &read) unless kind == :models
209
234
 
210
- model_paths(root, base_model: base_model) { |record| read.call(record) unless skip_concerns && record.path_name.start_with?("Concerns::") }
235
+ model_names = nil
236
+ model_paths(root, base_model: base_model) do |record|
237
+ next read.call(record) unless skip_concerns && record.path_name.start_with?("Concerns::")
238
+
239
+ # The concerns root does not namespace its files, and one named for a class with a model superclass is a model.
240
+ source = SafeFile.read(record.path) or next
241
+ name = concerns_name(record.path_name, source)
242
+ # An inflection changes only the case of a path's name, so the names compare without it.
243
+ model_names ||= model_paths(root, base_model: base_model).to_set { |known| known.path_name.delete_prefix("Concerns::").downcase }.merge(MODEL_BASES.map(&:downcase))
244
+ block.call(record.with(source: source, path_name: name)) if concern_model?(source, name, model_names, base_model)
245
+ end
211
246
  end
212
247
 
213
248
  # The eager form: reads and parses every file for its declared name.
@@ -33,28 +33,26 @@ module RailsAiContext
33
33
  end
34
34
 
35
35
  # One database's pending migrations, as `db:migrate:status:<name>` reads
36
- # them: from that database's own schema_migrations, through a pool of its
37
- # own that is removed after, so the app's connections are left as they
38
- # are. The primary is asked through the app's own pool. The connection is
39
- # made first, so a database that does not exist, or a server that does
40
- # not answer, says so rather than reading as nothing pending; a SQLite
41
- # file that is not there is said to be missing without connecting, which
42
- # would create it.
43
- #
44
- # @param db_config [ActiveRecord::DatabaseConfigurations::DatabaseConfig]
45
- # @return [Hash] { pending: [{ version:, name: }, ...] }, or { error: exception }
46
- def self.of_database(db_config, migrate_dir, primary: false)
47
- missing = DatabaseFile.missing(db_config)
48
- return { error: missing } if missing
49
-
36
+ # them: from that database's own schema_migrations, through its pool.
37
+ def self.pending_on(pool, migrate_dir)
50
38
  dirs = Array(migrate_dir).select { |dir| Dir.exist?(dir) }
51
- with_pool(db_config, primary: primary) do |pool|
52
- pool.with_connection(&:verify!)
53
- { pending: dirs.empty? ? [] : pending_in(migration_context(dirs, pool), dirs) }
54
- end
55
- rescue StandardError => e
56
- { error: e }
39
+ return [] if dirs.empty?
40
+ # Rails 7.0's schema_migration is a model that finds its pool through the app's handler, where a leased pool is not.
41
+ return PendingMigrations.for(migrate_dir: dirs, applied: applied_versions(pool.connection)) if schema_migration_model?(pool)
42
+
43
+ pending_in(migration_context(dirs, pool), dirs)
44
+ end
45
+
46
+ def self.schema_migration_model?(pool)
47
+ !pool.respond_to?(:schema_migration) && !pool.connection.respond_to?(:internal_metadata)
48
+ end
49
+ private_class_method :schema_migration_model?
50
+
51
+ def self.applied_versions(connection)
52
+ table = ActiveRecord::Base.schema_migrations_table_name
53
+ connection.table_exists?(table) ? connection.select_values("SELECT version FROM #{connection.quote_table_name(table)}") : []
57
54
  end
55
+ private_class_method :applied_versions
58
56
 
59
57
  def self.pending_in(context, dirs)
60
58
  applied = context.get_all_versions
@@ -65,32 +63,6 @@ module RailsAiContext
65
63
  PendingMigrations.for(migrate_dir: dirs, applied: context.get_all_versions)
66
64
  end
67
65
 
68
- # What a pool established for another database belongs to: Rails names a
69
- # pool by its owner, and an owner that is no primary class leaves the
70
- # app's own pools alone (ActiveRecord::PendingMigrationConnection, which
71
- # Rails 7.0 lacks and 7.1 shapes differently).
72
- class TemporaryOwner
73
- def self.primary_class?
74
- false
75
- end
76
-
77
- def self.current_preventing_writes
78
- false
79
- end
80
- end
81
-
82
- def self.with_pool(db_config, primary:)
83
- return yield(ActiveRecord::Base.connection_pool) if primary
84
-
85
- handler = ActiveRecord::Base.connection_handler
86
- begin
87
- yield handler.establish_connection(db_config, owner_name: TemporaryOwner)
88
- ensure
89
- handler.remove_connection_pool(TemporaryOwner.name)
90
- end
91
- end
92
- private_class_method :with_pool
93
-
94
66
  # @return [ActiveRecord::MigrationContext]
95
67
  def self.migration_context(migrate_dir, pool = ActiveRecord::Base.connection_pool)
96
68
  # Rails 7.2+: MigrationContext takes a schema_migration + internal_metadata
@@ -118,6 +118,53 @@ module RailsAiContext
118
118
  end
119
119
  end
120
120
 
121
+ # Where a multi-database entry is configured when that is not the running environment, in words, or nil.
122
+ def scope_words(database)
123
+ return nil unless database.is_a?(Hash)
124
+
125
+ environments = Array(database[:environments])
126
+ return "#{environments.first} only" if environments.one?
127
+ return AppDatabases.environment_phrase(environments) if environments.any?
128
+
129
+ "committed dump only" if database[:dump_only]
130
+ end
131
+
132
+ # The same in parentheses, to follow the database's name.
133
+ def database_mark(database)
134
+ words = scope_words(database)
135
+ words ? " (#{words})" : ""
136
+ end
137
+
138
+ # The multi_database section. A payload that holds none (a preset without it) gets the same list
139
+ # from the app databases, so no mark or name depends on the preset.
140
+ def multi_database(context)
141
+ section(context, :multi_database) || Introspectors::MultiDatabaseIntrospector.new(RailsAiContext.default_app).listing
142
+ end
143
+
144
+ def databases(context)
145
+ Array(multi_database(context)[:databases]).grep(Hash)
146
+ end
147
+
148
+ def named_scope_words(context, name)
149
+ scope_words(databases(context).find { |database| database[:name].to_s == name.to_s })
150
+ end
151
+
152
+ # What a generated file calls the primary: its own name where another database is the one named primary.
153
+ def primary_label(context)
154
+ name = multi_database(context)[:primary_name]
155
+ name && database_named?(context, "primary") ? name.to_s : "primary"
156
+ end
157
+
158
+ def database_named?(context, name)
159
+ databases(context).any? { |database| database[:name].to_s == name }
160
+ end
161
+
162
+ # The mark for a database known by its database.yml name.
163
+ def named_database_mark(context, name)
164
+ words = named_scope_words(context, name)
165
+ words ? " (#{words})" : ""
166
+ end
167
+
121
168
  # Each database past the primary that the schema section read tables
122
169
  # for, by its database.yml name.
123
170
  def secondary_databases(schema)
@@ -193,7 +240,7 @@ module RailsAiContext
193
240
 
194
241
  secondary = schema[:secondary_databases].is_a?(Hash) ? schema[:secondary_databases].keys : []
195
242
  [ nil, *secondary ].filter_map do |db|
196
- live = db.nil? && !parsed_schema?(schema) && schema[:adapter].to_s.match?(/postg/i)
243
+ live = db.nil? && !parsed_schema?(schema) && DatabaseFamily.of(schema[:adapter].to_s).postgresql?
197
244
  shown, data = Introspectors::SchemaIntrospector.qualified_table(name.to_s, database: db&.to_s, live: live)
198
245
  # The fourth entry is the name the listing shows the table under, which its models read.
199
246
  [ db&.to_s, name.to_s, data, shown ] if data
@@ -37,11 +37,10 @@ module RailsAiContext
37
37
  migrate_dirs_of(root, Introspectors::SchemaDumpPath.database_name(root, dump_path))
38
38
  end
39
39
 
40
- # The same, for the database database.yml names `name`.
40
+ # The same, for the app database named `name`.
41
41
  def migrate_dirs_of(root, name)
42
- entry = RailsAiContext::DatabaseYml.entry(root, name)
43
- Introspectors::MigrationReplay.configured_dirs(root, entry) ||
44
- [ File.join(root.to_s, "db", name == "primary" || entry ? "migrate" : "#{name}_migrate") ]
42
+ found = AppDatabases.for(root).of_dump_name(name)
43
+ found ? found.migrate_dirs : [ File.join(root.to_s, "db", name == "primary" ? "migrate" : "#{name}_migrate") ]
45
44
  end
46
45
 
47
46
  # Every versioned migration file under the directory or directories. One file scan behind
@@ -21,6 +21,11 @@ module RailsAiContext
21
21
  end
22
22
  end
23
23
 
24
+ # What an open run holds was read before this moment; the run goes on empty.
25
+ def clear
26
+ Thread.current[KEY]&.clear
27
+ end
28
+
24
29
  def active?
25
30
  !Thread.current[KEY].nil?
26
31
  end
@@ -30,15 +30,6 @@ module RailsAiContext
30
30
  POSTGRES_ONLY_TYPES = %w[jsonb hstore inet cidr citext tsvector ltree macaddr interval].freeze
31
31
  MYSQL_ONLY_TYPES = %w[tinytext mediumtext longtext tinyblob mediumblob longblob].freeze
32
32
 
33
- # structure.sql dialects as SchemaIntrospector records them, plus adapter names that
34
- # mean one of them (activerecord-postgis-adapter registers `postgis`).
35
- DIALECTS = {
36
- "postgresql" => "PostgreSQL",
37
- "postgis" => "PostgreSQL",
38
- "mysql" => "MySQL",
39
- "sqlite" => "SQLite"
40
- }.freeze
41
-
42
33
  DISPLAY_ORDER = %w[PostgreSQL MySQL SQLite].freeze
43
34
 
44
35
  module_function
@@ -58,9 +49,13 @@ module RailsAiContext
58
49
  from_examples(context) || from_gems(context) || "unknown"
59
50
  end
60
51
 
61
- # A connection's adapter_name as the other surfaces name it (PostGIS is PostgreSQL).
52
+ # A connection's adapter_name as the other surfaces name it (PostGIS is PostgreSQL, Trilogy is MySQL).
62
53
  def display(adapter_name)
63
- DIALECTS.fetch(adapter_name.to_s.downcase, adapter_name)
54
+ family = DatabaseFamily.of(adapter_name)
55
+ return adapter_name unless family.known?
56
+
57
+ # A label read back from the payload carries the mark, and the family reads past it.
58
+ adapter_name.to_s.end_with?(DatabaseFamily::DEFAULT_SUFFIX) ? family.name + DatabaseFamily::DEFAULT_SUFFIX : family.name
64
59
  end
65
60
 
66
61
  def label_with_reason(context)
@@ -91,9 +86,8 @@ module RailsAiContext
91
86
  multi = context[:multi_database]
92
87
  return nil unless Tools::SectionFetch.usable?(multi)
93
88
 
94
- primary = Array(multi[:databases]).find { |db| db[:name].to_s == "primary" } ||
95
- Array(multi[:databases]).first
96
- database_label(primary)
89
+ # The app databases list puts the primary first.
90
+ database_label(Array(multi[:databases]).first)
97
91
  end
98
92
 
99
93
  # A secondary database's adapter: its database.yml entry, else the dialect
@@ -114,15 +108,15 @@ module RailsAiContext
114
108
  adapter = database[:adapter]
115
109
  return nil if placeholder?(adapter)
116
110
 
117
- name = DIALECTS[adapter.to_s] || GEM_ADAPTERS[adapter.to_s] || adapter
118
- database[:adapter_default] ? "#{name} by database.yml default" : name
111
+ name = display(adapter.to_s)
112
+ database[:adapter_default] ? name + DatabaseFamily::DEFAULT_SUFFIX : name
119
113
  end
120
114
 
121
115
  def from_examples(context)
122
116
  multi = context[:multi_database]
123
117
  return nil unless Tools::SectionFetch.usable?(multi)
124
118
 
125
- candidates(Array(multi[:example_adapters]).map { |name| DIALECTS[name.to_s] || GEM_ADAPTERS[name.to_s] || name.to_s })
119
+ candidates(Array(multi[:example_adapters]).map { |name| display(name.to_s) })
126
120
  end
127
121
 
128
122
  def from_column_types(schema)
@@ -144,7 +138,8 @@ module RailsAiContext
144
138
  def from_dialect(schema)
145
139
  return nil unless schema.is_a?(Hash)
146
140
 
147
- DIALECTS[schema[:dialect].to_s]
141
+ family = DatabaseFamily.of(schema[:dialect].to_s)
142
+ family.name if family.known?
148
143
  end
149
144
 
150
145
  # Never a guess: naming one of `pg` and `mysql2` can put a database the app does not