rails-ai-context 5.32.0 → 5.32.1
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/CHANGELOG.md +64 -0
- data/docs/COMPATIBILITY.md +21 -0
- data/docs/INTROSPECTORS.md +1 -1
- data/lib/rails_ai_context/database_yml.rb +52 -2
- data/lib/rails_ai_context/introspectors/listeners/schema_dsl_listener.rb +30 -9
- data/lib/rails_ai_context/introspectors/model_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/pg_naming.rb +133 -0
- data/lib/rails_ai_context/introspectors/schema_conventions.rb +0 -6
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +188 -51
- data/lib/rails_ai_context/introspectors/schema_reader.rb +98 -12
- data/lib/rails_ai_context/introspectors/structure_sql_reader.rb +56 -33
- data/lib/rails_ai_context/payload.rb +25 -2
- data/lib/rails_ai_context/tools/get_model_details.rb +2 -0
- data/lib/rails_ai_context/tools/get_schema.rb +17 -14
- data/lib/rails_ai_context/version.rb +1 -1
- data/server.json +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: da0f12372e58a903300979aa8be03f53f53e9d5de073709e549a47b46cb8a5eb
|
|
4
|
+
data.tar.gz: cc0b3c5e77840cef702e9e163bd3e2d73f24a56ec324fbdfe3ec5b8c6e58eaf7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5cc39f9e5208f88eea4ac18c3dac8a9abbe5da282c8a9b8f62594e5f9069483db9ced2f537768a97b864d17921b622ce7c640590e47546292fe3dafb5c7cd65e
|
|
7
|
+
data.tar.gz: 56d23208cbd739927ac9d85af26a39bfab5da0cebd75731791717bd67b83752bc28a096d1307e956ee9de593287b17efc70d91b81fa47c6deec2678beb064fd4
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,70 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [5.32.1] - 2026-10-06
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **A structure.sql app names its PostgreSQL extensions the way the booted app
|
|
13
|
+
does.** The static tier named an extension bare only when it lived in
|
|
14
|
+
`public`. Rails names it bare when it lives in the connection's current
|
|
15
|
+
schema, the first entry of `schema_search_path`, so an app with
|
|
16
|
+
`schema_search_path: "app,public"` saw `app.pg_trgm` without a boot and
|
|
17
|
+
`pg_trgm` with one. The static tier now reads the search path from
|
|
18
|
+
config/database.yml, and on Rails before 8.0 names every extension bare, as
|
|
19
|
+
that connection does.
|
|
20
|
+
- **Tables in every schema on the search path show without a boot.** The
|
|
21
|
+
static tier listed only the tables in `public`, while the booted app lists
|
|
22
|
+
every table in a schema on `schema_search_path` by its bare name. Both dump
|
|
23
|
+
formats now read the search path from config/database.yml, or its older
|
|
24
|
+
name `schema_order`, take `"$user"` as the configured username when the dump
|
|
25
|
+
creates that schema, and when two schemas hold the same table name, show the
|
|
26
|
+
one in the earlier schema, as PostgreSQL resolves it. When the search path
|
|
27
|
+
holds more than `public`, the schema answer names it on a `**Search path:**`
|
|
28
|
+
line, and the JSON answer under `search_path`.
|
|
29
|
+
- **A table Rails 8.0 dumps twice lists its columns, foreign keys and check
|
|
30
|
+
constraints once.** Before Rails 8.1, schema.rb writes a table and its
|
|
31
|
+
foreign keys once per search path schema holding its name, and the static
|
|
32
|
+
tier listed each of them twice.
|
|
33
|
+
- **A structure.sql app lists plpgsql, as the booted app does.** Every
|
|
34
|
+
PostgreSQL database starts with plpgsql, so the booted app lists it, but
|
|
35
|
+
pg_dump leaves it out of the dump. The static tier now adds it, named
|
|
36
|
+
`pg_catalog.plpgsql` on Rails 8 and `plpgsql` before, unless the dump drops it.
|
|
37
|
+
- **Extensions are listed in name order, with or without a boot.** The booted
|
|
38
|
+
app listed them in the order the database created them and the static tier
|
|
39
|
+
in the order the dump wrote them, so the same app gave two orders.
|
|
40
|
+
- **Enum types are named the way the booted app names them.** The enum list
|
|
41
|
+
holds the types in a schema on the search path, bare in the current schema and
|
|
42
|
+
schema-qualified otherwise, or every type by its bare name before Rails 7.1. A
|
|
43
|
+
column's enum type is bare when the search path finds that type first, and
|
|
44
|
+
qualified otherwise. A schema.rb from before Rails 8.1 already holds these
|
|
45
|
+
names and is read as written; a Rails 8.1 schema.rb qualifies every name, and
|
|
46
|
+
the static tier renames them. How the dump names things follows its
|
|
47
|
+
`ActiveRecord::Schema[8.1]` stamp, the Rails that wrote it, or none for one
|
|
48
|
+
Rails 6.1 or earlier wrote, so a schema.rb not dumped again since a Rails
|
|
49
|
+
upgrade still reads right; how the connection names them follows Gemfile.lock,
|
|
50
|
+
or the stamp when the lock names none. Both tiers list enum types in name
|
|
51
|
+
order, and a table's Enum types section pairs each column with the type the
|
|
52
|
+
search path finds for it, so a column `mood` whose type is `public.mood` shows
|
|
53
|
+
that entry. The booted app reads its search path with SQL on every Rails
|
|
54
|
+
version.
|
|
55
|
+
- **A table outside the search path stays out of the table list but can still
|
|
56
|
+
be looked up.** Without a boot, a schema.rb table in another schema was
|
|
57
|
+
listed and counted, while the booted app leaves it out. Both tiers now leave
|
|
58
|
+
it out of the list in both dump formats, and both find it by its
|
|
59
|
+
schema-qualified name: `schema --table audit.events`, and the columns of a
|
|
60
|
+
model whose `table_name` is `audit.events`. A listed table is found by its
|
|
61
|
+
qualified name too, so `app.users` answers as `users` does. The booted app
|
|
62
|
+
reads such a table only when asked for it, so an app with a schema per
|
|
63
|
+
tenant pays nothing for it at boot. On Rails 8.1, whose schema.rb holds only
|
|
64
|
+
the search path schemas by default, the not-found answer for another
|
|
65
|
+
schema's table adds that `config.active_record.dump_schemas = :all`
|
|
66
|
+
includes it.
|
|
67
|
+
- **`"$user"` on the search path reads the user from a database URL too.** A
|
|
68
|
+
`DATABASE_URL`, `<NAME>_DATABASE_URL` or the entry's `url:` now supplies the
|
|
69
|
+
username, and a `schema_search_path` in its query, over the entry's own
|
|
70
|
+
keys, as Rails merges a URL over the rest of the entry.
|
|
71
|
+
|
|
8
72
|
## [5.32.0] - 2026-10-06
|
|
9
73
|
|
|
10
74
|
### Added
|
data/docs/COMPATIBILITY.md
CHANGED
|
@@ -234,6 +234,11 @@ Proof sources:
|
|
|
234
234
|
Sinatra and a plain gem); the lab regressions on SQLite in all three tiers,
|
|
235
235
|
PostgreSQL and MySQL; and the unit suite on Ruby 3.1 with Rails 7.0 through
|
|
236
236
|
Ruby 4.0 with Rails 8.1.
|
|
237
|
+
In the v5.32.1 release QA: a PostgreSQL 17 lab on Rails 8.1 and 8.0 with a
|
|
238
|
+
two-schema search path, answered booted and static from schema.rb and
|
|
239
|
+
structure.sql side by side; the small apps and the same private app
|
|
240
|
+
answering as v5.32.0 did; and the unit suite on Ruby 3.1 with Rails 7.0
|
|
241
|
+
through Ruby 4.0 with Rails 8.1.
|
|
237
242
|
2. Non-crash coverage for every built-in tool including `get_view` in
|
|
238
243
|
`spec/e2e/in_gemfile_install_spec.rb`'s full-tool sweep; output correctness
|
|
239
244
|
(ivar cross-check, render-form detection, partial interfaces) verified
|
|
@@ -324,6 +329,22 @@ Postgres instance in `spec/e2e/postgres_install_spec.rb`, opt-in via
|
|
|
324
329
|
in the static tier, so databases, replicas, sharding and `connects_to`
|
|
325
330
|
declarations come from the files rather than from the live connection
|
|
326
331
|
handlers. A database only a runtime `connects_to` names is missed.
|
|
332
|
+
- **Rails 7.0 merges same-name enum types.** On Rails 7.0, two PostgreSQL enum
|
|
333
|
+
types with one name in different schemas come back from the connection as
|
|
334
|
+
one type holding both label lists; the static tier keeps the first type's
|
|
335
|
+
labels.
|
|
336
|
+
- **A schema.rb from before Rails 8.1 does not say which schema holds a
|
|
337
|
+
table.** It names each table bare, whichever search path schema holds it,
|
|
338
|
+
so without a boot a schema-qualified name such as `app.users` finds a listed
|
|
339
|
+
table only when the search path has one schema. A Rails 7.0 schema.rb names
|
|
340
|
+
no schema at all, so the static tier takes every schema the search path
|
|
341
|
+
names as existing, except the one `"$user"` stands for.
|
|
342
|
+
- **A dump made with a `schema_search_path` holds only those schemas.** By
|
|
343
|
+
default Rails passes each search path schema to pg_dump, so structure.sql
|
|
344
|
+
holds no other schema's tables and no extension, and a Rails 8.1 schema.rb
|
|
345
|
+
holds no other schema's tables. Without a boot those tables are not found and
|
|
346
|
+
the extension list is plpgsql alone. `config.active_record.dump_schemas =
|
|
347
|
+
:all` dumps everything.
|
|
327
348
|
- **Inherited controller actions and filters are resolved by parent name, so
|
|
328
349
|
some walks end early.** A controller that defines no action of its own takes
|
|
329
350
|
the actions of the nearest app ancestor the listing holds, walked through the
|
data/docs/INTROSPECTORS.md
CHANGED
|
@@ -214,7 +214,7 @@ Passed to `SourceIntrospector.walk(path, key => Listener)` when a specific file
|
|
|
214
214
|
| DefinitionFilePathsListener | Where a test helper points factory_bot: `FactoryBot.definition_file_paths =` (replaces the defaults) and `<<`/`+=`/`push`/`concat` (adds), each write as `{ replace:, paths: }` in the same forms as FixturePathsListener (`unread: true` and no paths when it cannot read them), and each `FactoryBot.find_definitions`/`FactoryBot.reload` as `{ load: }`, in source order; `::FactoryBot` counts |
|
|
215
215
|
| ViewPathsListener | View roots `config/application.rb` adds to `config.paths["app/views"]`: `unshift` puts one before app/views, `<<`/`push`/`concat` after it, in the same literal forms as AutoloadPathsListener; a write whose path it cannot read is left out |
|
|
216
216
|
| NamespacedRootsListener | Roots `config/application.rb` or an initializer hands Zeitwerk under a namespace: `push_dir(path, namespace: Const)`, as phlex:install writes, in the same literal forms as AutoloadPathsListener; a write whose path it cannot read is left out |
|
|
217
|
-
| SchemaDslListener | `schema.rb`: `create_table`, `t.string`, `t.column`, `t.index`, `add_foreign_key`, `create_enum`, `create_view`, `create_virtual_table`, and the comment the dumper writes for a table it could not describe |
|
|
217
|
+
| SchemaDslListener | `schema.rb`: `create_table`, `t.string`, `t.column`, `t.index`, `add_foreign_key`, `create_enum`, `create_schema`, `create_view`, `create_virtual_table`, the `ActiveRecord::Schema[x.y]` version stamp (a nil one for the unstamped `Schema.define` of Rails before 7.0), and the comment the dumper writes for a table it could not describe. Names drop `public.` unless `raw_names:` is set, which SchemaReader uses to name them against the search path |
|
|
218
218
|
| MigrationDslListener | Migration DSL: `create_table`, `add_column`, `add_index`, `add_reference`, and friends |
|
|
219
219
|
| MigrationReplayListener | What a replay needs beside the DSL: `def down`, `down`/`revert` blocks, and `t.timestamps` |
|
|
220
220
|
| RoutesDslListener | `config/routes.rb`, resolving namespace/scope/resources nesting into flat routes (a `controller:` with a leading slash is absolute, as in Rails); routing concerns (`concern` definitions replayed at each `concerns:` site), `with_options` defaults merged under each inner call, `match ... via:` (or the `via:` of an enclosing `scope`) as one route answering each verb it names (`GET|POST`, and `ANY` for `via: :all`, as the booted table has it), the controller a `scope(controller:)` or a route's own `controller:` names (with Rails' `a/b` shorthand when no action is given), and the `as:`, `param:`, `module:`, `path:` and `only:`/`except:` options. A block drawn through an app class (`ApiRouteSet::V1.draw(self) do`) takes the path and `as:` prefixes that class's `self.prefix` and `mapper_prefix` return as literals, or a literal prefix argument; a class whose prefix is not a literal, and that class's own `resources`, are counted as unexpanded. Routes drawn into an engine's table (`Spree::Core::Engine.routes.draw`) sit under the engine's namespace and carry `engine:`, which the route introspector files under that engine's mount, apart from the app's count. A file pulled in by `draw` is walked inside the scope its `draw` sits in, once per scope that draws it, so `draw :api` under `namespace :api` and `scope module: :v1` routes to `api/v1/...` at `/api/...` |
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "uri"
|
|
3
4
|
require "yaml"
|
|
4
5
|
|
|
5
6
|
module RailsAiContext
|
|
@@ -69,6 +70,41 @@ module RailsAiContext
|
|
|
69
70
|
databases(root)[primary_name(root)]
|
|
70
71
|
end
|
|
71
72
|
|
|
73
|
+
# A database's schema_search_path as PostgreSQL reads it: an unquoted name folds to
|
|
74
|
+
# lowercase and "$user" is the configured username. Unset, PostgreSQL's "$user", public.
|
|
75
|
+
# namespace.c recomputeNamespacePath keeps a repeated schema's first place only.
|
|
76
|
+
def schema_search_path(root, name = "primary")
|
|
77
|
+
search_path_entries(root, name).map(&:first).uniq
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The schema only the search path's "$user" names, or nil: one the path also names outright is configured.
|
|
81
|
+
def user_schema(root, name = "primary")
|
|
82
|
+
entries = search_path_entries(root, name)
|
|
83
|
+
user = entries.find(&:last)&.first
|
|
84
|
+
user unless entries.any? { |schema, from_user| !from_user && schema == user }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# [schema, from_user] per search path entry.
|
|
88
|
+
def search_path_entries(root, name)
|
|
89
|
+
settings = entry(root, name)
|
|
90
|
+
settings = {} unless settings.is_a?(Hash)
|
|
91
|
+
known = adapter(name, settings).first
|
|
92
|
+
return [ [ "public", false ] ] if known && !known.start_with?("postg")
|
|
93
|
+
|
|
94
|
+
settings = settings.merge(url_settings(name, settings["url"]))
|
|
95
|
+
# postgresql_adapter.rb:984 (8.1), :865 (7.0): schema_order is the older name.
|
|
96
|
+
text = (settings["schema_search_path"] || settings["schema_order"]).to_s
|
|
97
|
+
text = '"$user", public' if text.strip.empty?
|
|
98
|
+
user = settings["username"].to_s
|
|
99
|
+
text.split(",").filter_map do |part|
|
|
100
|
+
part = part.strip
|
|
101
|
+
next if part.empty? || computed?(part)
|
|
102
|
+
next ([ user, true ] unless user.empty? || computed?(user)) if part.delete('"') == "$user"
|
|
103
|
+
|
|
104
|
+
[ part.start_with?('"') ? part.delete('"') : part.downcase, false ]
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
72
108
|
# The named database's settings in the running environment, or nil.
|
|
73
109
|
def entry(root, name)
|
|
74
110
|
entry_in(databases(root), name)
|
|
@@ -99,13 +135,27 @@ module RailsAiContext
|
|
|
99
135
|
# Rails' DatabaseConfigurations: an entry's own url wins over its keys, and an entry
|
|
100
136
|
# without one takes <NAME>_DATABASE_URL, or DATABASE_URL for the primary.
|
|
101
137
|
def url_adapter(name, own_url)
|
|
102
|
-
url =
|
|
103
|
-
return nil if url.to_s.empty? || computed?(url)
|
|
138
|
+
url = url_for(name, own_url) or return nil
|
|
104
139
|
|
|
105
140
|
scheme = url[/\A([a-z][a-z0-9+.-]*):/i, 1]&.tr("-", "_")
|
|
106
141
|
scheme && URL_SCHEME_ADAPTERS.fetch(scheme, scheme)
|
|
107
142
|
end
|
|
108
143
|
|
|
144
|
+
def url_for(name, own_url)
|
|
145
|
+
url = own_url.nil? ? ENV["#{name.upcase}_DATABASE_URL"] || (ENV["DATABASE_URL"] if name == "primary") : own_url.to_s
|
|
146
|
+
url unless url.to_s.empty? || computed?(url)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The username and schema_search_path a database URL carries, which Rails merges over the entry's keys.
|
|
150
|
+
def url_settings(name, own_url)
|
|
151
|
+
uri = URI.parse(url_for(name, own_url).to_s)
|
|
152
|
+
found = uri.query ? URI.decode_www_form(uri.query).to_h.slice("schema_search_path") : {}
|
|
153
|
+
found["username"] = URI.decode_www_form_component(uri.user) if uri.user
|
|
154
|
+
found
|
|
155
|
+
rescue URI::Error, ArgumentError
|
|
156
|
+
{}
|
|
157
|
+
end
|
|
158
|
+
|
|
109
159
|
# [adapter, from_default]: the URL's scheme wins, then the entry's adapter. An ERB-computed
|
|
110
160
|
# value is unknown unless it is one tag carrying its own literal default.
|
|
111
161
|
def adapter(name, entry)
|
|
@@ -52,12 +52,18 @@ module RailsAiContext
|
|
|
52
52
|
# The dumper writes a comment, not a create_table, for a table it could not describe.
|
|
53
53
|
NOT_DUMPED = /\A#\s*Could not dump table "([^"]+)" because of following (\S+)/
|
|
54
54
|
|
|
55
|
+
# raw_names: each name as the dump writes it, for a reader that names them after the walk.
|
|
56
|
+
def initialize(raw_names: false)
|
|
57
|
+
super()
|
|
58
|
+
@raw_names = raw_names
|
|
59
|
+
end
|
|
60
|
+
|
|
55
61
|
def on_program_node_enter(_node)
|
|
56
62
|
comments = Array(@comments)
|
|
57
63
|
comments.each_with_index do |comment, i|
|
|
58
64
|
match = NOT_DUMPED.match(comment.location.slice) or next
|
|
59
65
|
message = comments[i + 1]&.location&.slice.to_s[/\A#\s+(\S.*)/, 1]
|
|
60
|
-
@results << { type: :not_dumped, table:
|
|
66
|
+
@results << { type: :not_dumped, table: local_name(match[1]),
|
|
61
67
|
reason: [ match[2], message ].compact.join(": "), location: comment.location.start_line }
|
|
62
68
|
end
|
|
63
69
|
end
|
|
@@ -66,6 +72,13 @@ module RailsAiContext
|
|
|
66
72
|
note_block(node)
|
|
67
73
|
if node.receiver.nil?
|
|
68
74
|
extract_top_level_call(node)
|
|
75
|
+
elsif node.name == :[] && node.receiver.is_a?(Prism::ConstantPathNode) && node.receiver.slice == "ActiveRecord::Schema"
|
|
76
|
+
# ActiveRecord::Schema[8.1].define: the Rails version that wrote the dump.
|
|
77
|
+
stamp = node.arguments&.arguments&.first
|
|
78
|
+
@results << { type: :stamp, version: stamp.slice, location: node.location.start_line } if stamp.is_a?(Prism::FloatNode)
|
|
79
|
+
elsif node.name == :define && node.receiver.is_a?(Prism::ConstantPathNode) && node.receiver.slice == "ActiveRecord::Schema"
|
|
80
|
+
# Rails before 7.0 writes no stamp.
|
|
81
|
+
@results << { type: :stamp, version: nil, location: node.location.start_line }
|
|
69
82
|
elsif column_call?(node)
|
|
70
83
|
extract_column(node)
|
|
71
84
|
elsif index_call?(node)
|
|
@@ -87,6 +100,11 @@ module RailsAiContext
|
|
|
87
100
|
|
|
88
101
|
private
|
|
89
102
|
|
|
103
|
+
# Without a search path to read, public's are the bare names.
|
|
104
|
+
def local_name(name)
|
|
105
|
+
@raw_names ? name : name.delete_prefix("public.")
|
|
106
|
+
end
|
|
107
|
+
|
|
90
108
|
def read_elsewhere?(name)
|
|
91
109
|
OTHER_TABLE_METHODS.include?(name) || MigrationReplayListener::TABLE_OPS.key?(name)
|
|
92
110
|
end
|
|
@@ -104,6 +122,9 @@ module RailsAiContext
|
|
|
104
122
|
extract_foreign_key(node)
|
|
105
123
|
when :create_enum
|
|
106
124
|
extract_enum(node)
|
|
125
|
+
when :create_schema
|
|
126
|
+
name = literal_string(node.arguments&.arguments&.first)
|
|
127
|
+
@results << { type: :create_schema, name: name, location: node.location.start_line } if name
|
|
107
128
|
when :add_index
|
|
108
129
|
extract_top_level_add_index(node)
|
|
109
130
|
when :add_check_constraint
|
|
@@ -123,7 +144,7 @@ module RailsAiContext
|
|
|
123
144
|
name = literal_string(node.arguments&.arguments&.first) or return
|
|
124
145
|
options = extract_keyword_nodes(node)
|
|
125
146
|
@results << {
|
|
126
|
-
type: :view, name:
|
|
147
|
+
type: :view, name: local_name(name), materialized: options[:materialized].is_a?(Prism::TrueNode),
|
|
127
148
|
sql: literal_string(options[:sql_definition])&.strip, location: node.location.start_line
|
|
128
149
|
}.compact
|
|
129
150
|
end
|
|
@@ -143,7 +164,7 @@ module RailsAiContext
|
|
|
143
164
|
|
|
144
165
|
@results << {
|
|
145
166
|
type: :create_table,
|
|
146
|
-
table:
|
|
167
|
+
table: local_name(table_arg.unescaped),
|
|
147
168
|
# id: false / id: :uuid / primary_key: ... decide whether the
|
|
148
169
|
# implicit primary-key column exists and what to call it.
|
|
149
170
|
options: extract_keyword_options(node),
|
|
@@ -161,8 +182,8 @@ module RailsAiContext
|
|
|
161
182
|
|
|
162
183
|
@results << {
|
|
163
184
|
type: :foreign_key,
|
|
164
|
-
from:
|
|
165
|
-
to:
|
|
185
|
+
from: local_name(from_arg.unescaped),
|
|
186
|
+
to: local_name(to_arg.unescaped),
|
|
166
187
|
# Absent means the Rails convention holds; naming it here would
|
|
167
188
|
# make a declared column indistinguishable from a guessed one.
|
|
168
189
|
column: SchemaConventions.primary_key_value(options[:column]),
|
|
@@ -191,7 +212,7 @@ module RailsAiContext
|
|
|
191
212
|
|
|
192
213
|
@results << {
|
|
193
214
|
type: :enum,
|
|
194
|
-
name:
|
|
215
|
+
name: local_name(name_arg.unescaped),
|
|
195
216
|
values: values,
|
|
196
217
|
location: node.location.start_line
|
|
197
218
|
}
|
|
@@ -207,7 +228,7 @@ module RailsAiContext
|
|
|
207
228
|
|
|
208
229
|
@results << {
|
|
209
230
|
type: :add_index,
|
|
210
|
-
table:
|
|
231
|
+
table: local_name(table_arg.unescaped),
|
|
211
232
|
columns: columns,
|
|
212
233
|
options: options,
|
|
213
234
|
location: node.location.start_line
|
|
@@ -222,7 +243,7 @@ module RailsAiContext
|
|
|
222
243
|
|
|
223
244
|
@results << {
|
|
224
245
|
type: :add_check_constraint,
|
|
225
|
-
table:
|
|
246
|
+
table: local_name(table_arg.unescaped),
|
|
226
247
|
expression: expr_arg.unescaped,
|
|
227
248
|
name: literal_string(extract_keyword_nodes(node)[:name]),
|
|
228
249
|
location: node.location.start_line
|
|
@@ -288,7 +309,7 @@ module RailsAiContext
|
|
|
288
309
|
|
|
289
310
|
options = braced.map { |hash| hash_node_to_hash(hash) }.reduce(extract_keyword_options(node), :merge)
|
|
290
311
|
virtual = node.name == :virtual
|
|
291
|
-
options[:enum_type] =
|
|
312
|
+
options[:enum_type] = local_name(options[:enum_type]) if options[:enum_type].is_a?(String)
|
|
292
313
|
# A generated column takes its own type from type: (each adapter's virtual, 7.0 to 8.1).
|
|
293
314
|
column_type = options[:type].to_s if virtual && (options[:type].is_a?(Symbol) || options[:type].is_a?(String))
|
|
294
315
|
names.each do |col_name|
|
|
@@ -172,7 +172,7 @@ module RailsAiContext
|
|
|
172
172
|
# Rails runs no STI on a table without the type column; a table the dump does not hold is not known to lack it.
|
|
173
173
|
def lacks_column?(table, column)
|
|
174
174
|
reader = SchemaReader.for(app.root)
|
|
175
|
-
reader.
|
|
175
|
+
reader.table?(table) && column != RailsAiContext::Confidence::INFERRED && !reader.column?(table, column)
|
|
176
176
|
end
|
|
177
177
|
|
|
178
178
|
# Rails' default column, the literal a class sets, or nil where `inheritance_column = nil` turns STI off.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiContext
|
|
4
|
+
module Introspectors
|
|
5
|
+
# How Rails' PostgreSQL connection and schema.rb dumper name extensions, enum types and
|
|
6
|
+
# relations, by Rails version, so the static tier names them as the booted app does.
|
|
7
|
+
# Each rule cites the activerecord source it mirrors.
|
|
8
|
+
module PgNaming
|
|
9
|
+
DEFAULT_SEARCH_PATH = %w[public].freeze
|
|
10
|
+
# 6.1 schema_dumper.rb:89 writes Schema.define, 7.0 :90 Schema[x.y]; every rule here starts at 7.0 or later.
|
|
11
|
+
UNSTAMPED_DUMP = "6.1"
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# The running Rails when booted, else the lockfile's; nil when neither says.
|
|
16
|
+
def rails_version(root)
|
|
17
|
+
return Rails.version if defined?(Rails) && Rails.respond_to?(:version) && !RailsAiContext.static_tier?
|
|
18
|
+
|
|
19
|
+
lock = GemLock.for(root)
|
|
20
|
+
lock.version("rails") || lock.version("railties")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# An unknown version takes the newest rules.
|
|
24
|
+
def at_least?(version, release)
|
|
25
|
+
version.nil? || Gem::Version.new(version) >= Gem::Version.new(release)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# 8.0 postgresql_adapter.rb:506-520 drops the schema only when it is current_schema;
|
|
29
|
+
# 7.0 :453, 7.1 :497 and 7.2 :496 select extname alone.
|
|
30
|
+
def extension_name(name, schema, path, version)
|
|
31
|
+
return name if schema.nil? || !at_least?(version, "8.0") || schema == path.first
|
|
32
|
+
|
|
33
|
+
"#{schema}.#{name}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# 7.1 postgresql_adapter.rb:502-522 (to 8.1 :533-553): the types in current_schemas(false),
|
|
37
|
+
# bare in current_schema. 7.0 :458-469 merges same-name types' labels; the static tier keeps the first set.
|
|
38
|
+
def enum_list(enums, path, version)
|
|
39
|
+
legacy = !at_least?(version, "7.1")
|
|
40
|
+
listed = enums.filter_map do |name, values|
|
|
41
|
+
schema, dot, bare = name.rpartition(".")
|
|
42
|
+
shown = if dot.empty? || legacy then bare
|
|
43
|
+
elsif path.include?(schema) then schema == path.first ? bare : name
|
|
44
|
+
end
|
|
45
|
+
{ name: shown, values: values } if shown
|
|
46
|
+
end
|
|
47
|
+
listed.uniq { |enum| enum[:name] }.sort_by { |enum| enum[:name] }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# 8.1 schema_dumper.rb:154-162 (relation_name) qualifies every name in a dump of more than one
|
|
51
|
+
# schema. Before, the dumper writes connection.tables, enum_types and column.sql_type
|
|
52
|
+
# (7.2 postgresql/schema_dumper.rb:19-29, :94), the names the booted app reads.
|
|
53
|
+
def dump_qualifies_names?(version)
|
|
54
|
+
at_least?(version, "8.1")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Whether a bare name in the dump says its schema. Before 8.1 the dumper names each of
|
|
58
|
+
# connection.tables bare, whichever search path schema holds it, so only a one-schema path places it.
|
|
59
|
+
def dump_places_names?(version, path)
|
|
60
|
+
dump_qualifies_names?(version) || path.size < 2
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# 7.1 postgresql/schema_dumper.rb:31-40 writes create_schema for every schema; 7.0 has no schemas().
|
|
64
|
+
def dump_lists_schemas?(version)
|
|
65
|
+
at_least?(version, "7.1")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# current_schemas(false) skips a schema that does not exist; pg_dump never creates public.
|
|
69
|
+
def existing_path(path, created)
|
|
70
|
+
path.select { |schema| schema == "public" || created.include?(schema) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The names a dump's relations and enum types go by, given its qualified names.
|
|
74
|
+
def names(path, relations: [], types: [])
|
|
75
|
+
Names.new(path, shadowed(relations, path), shadowed(types, path))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The qualified names an earlier search path schema hides by holding the same bare name.
|
|
79
|
+
def shadowed(names, path)
|
|
80
|
+
return Set.new if path.size < 2
|
|
81
|
+
|
|
82
|
+
ranked = names.uniq.filter_map do |name|
|
|
83
|
+
schema, dot, bare = name.rpartition(".")
|
|
84
|
+
rank = path.index(schema) unless dot.empty?
|
|
85
|
+
[ rank, bare, name ] if rank
|
|
86
|
+
end
|
|
87
|
+
first = ranked.sort_by(&:first).reverse.to_h { |_, bare, name| [ bare, name ] }
|
|
88
|
+
ranked.filter_map { |_, bare, name| name unless first[bare] == name }.to_set
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# PostgreSQL resolves a bare name through the search path (data_sources and format_type alike).
|
|
92
|
+
Names = Struct.new(:path, :shadowed_relations, :shadowed_types) do
|
|
93
|
+
def relation(name)
|
|
94
|
+
visible(name, shadowed_relations)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def type(name)
|
|
98
|
+
visible(name, shadowed_types)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def visible(name, shadowed)
|
|
104
|
+
schema, dot, bare = name.rpartition(".")
|
|
105
|
+
dot.empty? || !path.include?(schema) || shadowed.include?(name) ? name : bare
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The enum types a table's columns use: a bare column type is the first search path schema's
|
|
110
|
+
# holding it, and a bare list name is the current schema's.
|
|
111
|
+
def enums_used(enum_types, column_types, path)
|
|
112
|
+
path = Array(path).presence || DEFAULT_SEARCH_PATH
|
|
113
|
+
listed = Array(enum_types).to_h { |enum| [ enum[:name].to_s.include?(".") ? enum[:name].to_s : "#{path.first}.#{enum[:name]}", enum ] }
|
|
114
|
+
used = column_types.map do |type|
|
|
115
|
+
type.include?(".") ? type : path.map { |schema| "#{schema}.#{type}" }.find { |key| listed.key?(key) }
|
|
116
|
+
end
|
|
117
|
+
listed.select { |key, _| used.include?(key) }.values
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# 8.1 postgresql/schema_dumper.rb:13-23, :50-68 dumps only ActiveRecord.dump_schemas, the search
|
|
121
|
+
# path by default (active_record.rb:436); 7.1 to 8.0 dump every schema's create_schema and ignore it.
|
|
122
|
+
# A dump naming no schema off the path is the default; one that does has dump_schemas set.
|
|
123
|
+
def missing_from_schema_rb(name, path, created, version, dump)
|
|
124
|
+
schema, dot, = name.to_s.rpartition(".")
|
|
125
|
+
return if dot.empty? || !at_least?(version, "8.1") || path.include?(schema) || created.include?(schema)
|
|
126
|
+
return unless (created - path).empty?
|
|
127
|
+
|
|
128
|
+
"If schema '#{schema}' exists, #{dump} leaves it out: Rails 8.1 dumps only the search path schemas there by default. " \
|
|
129
|
+
"`config.active_record.dump_schemas = :all` includes it."
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -189,12 +189,6 @@ module RailsAiContext
|
|
|
189
189
|
yml.adapter(db_name, entry).first || (yml.adapter("primary", yml.primary(root)).first unless entry)
|
|
190
190
|
end
|
|
191
191
|
|
|
192
|
-
# A dump of more than one schema qualifies every name (relation_name, 8.1);
|
|
193
|
-
# the app sees a public table by its bare name under the default search_path.
|
|
194
|
-
def local_name(name)
|
|
195
|
-
name.delete_prefix("public.")
|
|
196
|
-
end
|
|
197
|
-
|
|
198
192
|
# A primary key as connection.primary_key gives it: the column's name, or
|
|
199
193
|
# the names in order for a composite key; nil for none.
|
|
200
194
|
def primary_key_value(key)
|