rails-ai-bridge 3.6.1 → 3.6.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +1 -1
- data/UPGRADING.md +11 -0
- data/lib/rails_ai_bridge/doctor/checkers/schema_checker.rb +19 -6
- data/lib/rails_ai_bridge/introspectors/schema/static_structure_sql_parser.rb +293 -0
- data/lib/rails_ai_bridge/introspectors/schema_introspector.rb +24 -10
- data/lib/rails_ai_bridge/path_resolver.rb +1 -1
- data/lib/rails_ai_bridge/version.rb +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: 0ed19c9c79914f2b5ad8707d51a634c4bae469e3d7d07bd06cf23ed6d89a6027
|
|
4
|
+
data.tar.gz: b58364346943cfe18414ff86cfb08b95a5bfe5ffef5d9c9949d8eb2221af1c7d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f883afb7b2d7c806c765e54ca0169ce845fe8a087633676408f5dec3931373f0f152edd4a7a1e49f876b26f213f5e8960dc994294fd17087df5c437a1445f9b3
|
|
7
|
+
data.tar.gz: e5d84fdd428ab13733063d67ef6589421a387c4c5a844ff04950afe1e9a90a79e3ad7556336853a0087f3bdeb797f8ece670ae192f5069e2f21daa12b4438512
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.6.2] - 2026-08-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`structure.sql` support in offline/static schema introspection** (#96/#97/#116) — apps using `config.active_record.schema_format = :sql` (no `db/schema.rb`) now get table, column, index, and foreign-key context offline via `Introspectors::Schema::StaticStructureSqlParser`. The live-connection path was already format-agnostic. Output shape matches the live introspector so formatters work unchanged. Partition-child tables (`CREATE TABLE … PARTITION OF …`) are not expanded (follow-up).
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **`ai:doctor` schema check for `schema_format = :sql`** (#96/#97/#116) — Schema check passes when `db/structure.sql` is present; fix hint points at `rails db:migrate` (or `rails db:schema:dump`).
|
|
19
|
+
|
|
10
20
|
## [3.6.1] - 2026-08-07
|
|
11
21
|
|
|
12
22
|
### Security
|
data/README.md
CHANGED
|
@@ -781,7 +781,7 @@ Bug reports and pull requests: [github.com/igmarin/rails-ai-bridge/issues](https
|
|
|
781
781
|
|
|
782
782
|
## Acknowledgments & Origins
|
|
783
783
|
|
|
784
|
-
This gem ships as **rails-ai-bridge** (Ruby **`RailsAiBridge`**, version **3.6.
|
|
784
|
+
This gem ships as **rails-ai-bridge** (Ruby **`RailsAiBridge`**, version **3.6.2**). Earlier iterations of the same codebase were distributed as `rails-ai-context`.
|
|
785
785
|
|
|
786
786
|
RailsMCP evolved from
|
|
787
787
|
[crisnahine/rails-ai-context](https://github.com/crisnahine/rails-ai-context),
|
data/UPGRADING.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Upgrading rails-ai-bridge
|
|
2
2
|
|
|
3
|
+
## Upgrading from 3.6.1 to 3.6.2
|
|
4
|
+
|
|
5
|
+
**No configuration changes required.**
|
|
6
|
+
|
|
7
|
+
If your app uses `config.active_record.schema_format = :sql`, offline schema
|
|
8
|
+
introspection and `rails ai:doctor` now use `db/structure.sql` automatically
|
|
9
|
+
(no need for `db/schema.rb`). Live DB introspection was already format-agnostic.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
|
|
3
14
|
## Upgrading from 3.6.0 to 3.6.1
|
|
4
15
|
|
|
5
16
|
**One action required if you are pinned to `rubydex` 0.2.x:**
|
|
@@ -3,18 +3,31 @@
|
|
|
3
3
|
module RailsAiBridge
|
|
4
4
|
class Doctor
|
|
5
5
|
module Checkers
|
|
6
|
-
# Verifies
|
|
6
|
+
# Verifies a schema file exists for schema-driven AI context. Accepts
|
|
7
|
+
# either +db/schema.rb+ (+schema_format = :ruby+) or +db/structure.sql+
|
|
8
|
+
# (+schema_format = :sql+).
|
|
7
9
|
class SchemaChecker < BaseChecker
|
|
8
|
-
# @return [Doctor::Check] +:pass+ when
|
|
10
|
+
# @return [Doctor::Check] +:pass+ when a schema file exists; +:warn+ otherwise
|
|
9
11
|
def call
|
|
10
|
-
|
|
12
|
+
schema_file = present_schema_file
|
|
11
13
|
check(
|
|
12
14
|
'Schema',
|
|
13
|
-
|
|
14
|
-
pass: { message:
|
|
15
|
-
fail: {
|
|
15
|
+
schema_file,
|
|
16
|
+
pass: { message: "#{schema_file} found" },
|
|
17
|
+
fail: {
|
|
18
|
+
status: :warn,
|
|
19
|
+
message: 'db/schema.rb or db/structure.sql not found',
|
|
20
|
+
fix: 'Run `rails db:migrate` (or `rails db:schema:dump`) to generate one'
|
|
21
|
+
}
|
|
16
22
|
)
|
|
17
23
|
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# @return [String, nil] the schema file that exists (schema.rb preferred), or +nil+
|
|
28
|
+
def present_schema_file
|
|
29
|
+
%w[db/schema.rb db/structure.sql].find { |rel| File.exist?(File.join(app.root, rel)) }
|
|
30
|
+
end
|
|
18
31
|
end
|
|
19
32
|
end
|
|
20
33
|
end
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiBridge
|
|
4
|
+
module Introspectors
|
|
5
|
+
module Schema
|
|
6
|
+
# Parses a +db/structure.sql+ file as plain text, without a live database
|
|
7
|
+
# connection. This is the +schema_format = :sql+ counterpart to
|
|
8
|
+
# {StaticSchemaParser}: apps that keep their schema as SQL (common on
|
|
9
|
+
# Postgres, where +schema.rb+ cannot represent partitions, views,
|
|
10
|
+
# extensions, or custom SQL) have no +db/schema.rb+ to fall back to in
|
|
11
|
+
# offline environments (CI, Claude Code, agent contexts).
|
|
12
|
+
#
|
|
13
|
+
# Each instance is single-use: construct it with the file content and a
|
|
14
|
+
# configuration object, call {#call}, and discard. No mutable state
|
|
15
|
+
# escapes the instance.
|
|
16
|
+
#
|
|
17
|
+
# == Supported DDL (pg_dump / structure.sql form)
|
|
18
|
+
#
|
|
19
|
+
# * +CREATE TABLE [IF NOT EXISTS] [schema.]name (+ — opens a table context
|
|
20
|
+
# * +<name> <type> ...+ — a column line inside the table body; the leading
|
|
21
|
+
# identifier is the column and the remainder (minus +NOT NULL+/+DEFAULT+)
|
|
22
|
+
# is the SQL type. Table-level constraint lines (+CONSTRAINT+,
|
|
23
|
+
# +PRIMARY KEY+, +FOREIGN KEY+, …) are skipped.
|
|
24
|
+
# * +);+ — closes the current table context
|
|
25
|
+
# * +CREATE [UNIQUE] INDEX name ON [schema.]table USING method (cols)+ —
|
|
26
|
+
# adds an index entry (first simple column) to the named table.
|
|
27
|
+
# Functional/expression indexes (e.g. +lower(email)+) are skipped.
|
|
28
|
+
# * +ALTER TABLE [ONLY] table ADD CONSTRAINT ... FOREIGN KEY (col)
|
|
29
|
+
# REFERENCES ref_table (pk)+ — adds a foreign-key entry to +table+
|
|
30
|
+
# (pg_dump emits these in a separate constraints section).
|
|
31
|
+
#
|
|
32
|
+
# Unlike {StaticSchemaParser} (whose +schema.rb+ static form leaves foreign
|
|
33
|
+
# keys empty), +structure.sql+ spells foreign keys out as parseable DDL, so
|
|
34
|
+
# this parser populates them offline — matching what the live
|
|
35
|
+
# {SchemaIntrospector} path reports.
|
|
36
|
+
#
|
|
37
|
+
# Internal Rails tables (+ar_internal_metadata+, +schema_migrations+) and
|
|
38
|
+
# any table matching {Config::Introspection#excluded_tables} are silently
|
|
39
|
+
# skipped.
|
|
40
|
+
#
|
|
41
|
+
# @example
|
|
42
|
+
# content = File.read("db/structure.sql")
|
|
43
|
+
# result = StaticStructureSqlParser.new(content: content, config: RailsAiBridge.configuration).call
|
|
44
|
+
# # => { adapter: "static_parse", tables: { ... }, total_tables: N, note: "..." }
|
|
45
|
+
#
|
|
46
|
+
# @see RailsAiBridge::Introspectors::SchemaIntrospector
|
|
47
|
+
# @see RailsAiBridge::Introspectors::Schema::StaticSchemaParser
|
|
48
|
+
class StaticStructureSqlParser
|
|
49
|
+
# Regex matching a +CREATE TABLE+ declaration, tolerating +IF NOT EXISTS+,
|
|
50
|
+
# a schema qualifier (+public.+), and optional quoting of either part.
|
|
51
|
+
TABLE_LINE = /\ACREATE TABLE (?:IF NOT EXISTS\s+)?(?:[\w"]+\.)?"?([A-Za-z_]\w*)"?\s*\(/
|
|
52
|
+
|
|
53
|
+
# Regex matching the end of a table body (+);+ at column zero).
|
|
54
|
+
TABLE_END_LINE = /\A\)/
|
|
55
|
+
|
|
56
|
+
# Regex matching a column definition inside a table body: leading
|
|
57
|
+
# whitespace, an identifier (optionally quoted), then the type/modifiers.
|
|
58
|
+
COLUMN_LINE = /\A\s+"?([A-Za-z_]\w*)"?\s+(.+)/
|
|
59
|
+
|
|
60
|
+
# Regex matching a +CREATE INDEX+ statement. Captures the target table
|
|
61
|
+
# and the raw parenthesised column list; only the first column is kept
|
|
62
|
+
# (parity with {StaticSchemaParser}).
|
|
63
|
+
INDEX_LINE = /\ACREATE\s+(?:UNIQUE\s+)?INDEX\s+.+?\s+ON\s+(?:[\w"]+\.)?"?([A-Za-z_]\w*)"?\s+(?:USING\s+\w+\s+)?\(([^)]+)\)/
|
|
64
|
+
|
|
65
|
+
# Regex matching an +ALTER TABLE [ONLY] [schema.]table+ statement, which
|
|
66
|
+
# in pg_dump precedes an +ADD CONSTRAINT+ line. Captures the target table.
|
|
67
|
+
ALTER_TABLE_LINE = /\AALTER TABLE (?:ONLY\s+)?(?:[\w"]+\.)?"?([A-Za-z_]\w*)"?/
|
|
68
|
+
|
|
69
|
+
# Regex matching an +ADD CONSTRAINT ... FOREIGN KEY (cols) REFERENCES
|
|
70
|
+
# [schema.]ref_table (pk)+ clause. Captures local columns, referenced
|
|
71
|
+
# table, and referenced columns.
|
|
72
|
+
FOREIGN_KEY_LINE = /FOREIGN KEY\s*\(([^)]+)\)\s*REFERENCES\s+(?:[\w"]+\.)?"?([A-Za-z_]\w*)"?\s*\(([^)]+)\)/
|
|
73
|
+
|
|
74
|
+
# Regex matching an +ON DELETE <action>+ clause on a foreign-key line.
|
|
75
|
+
ON_DELETE = /ON DELETE ([A-Z ]+?)(?=\s+ON UPDATE|\s+(?:NOT\s+)?(?:DEFERRABLE|VALID)|[,;)]|\z)/i
|
|
76
|
+
|
|
77
|
+
# Regex matching an +ON UPDATE <action>+ clause on a foreign-key line.
|
|
78
|
+
ON_UPDATE = /ON UPDATE ([A-Z ]+?)(?=\s+(?:NOT\s+)?(?:DEFERRABLE|VALID)|[,;)]|\z)/i
|
|
79
|
+
|
|
80
|
+
# Rails-managed tables that must never appear in introspection output.
|
|
81
|
+
INTERNAL_TABLES = %w[ar_internal_metadata schema_migrations].freeze
|
|
82
|
+
|
|
83
|
+
# Table-level constraint keywords that share a column line's shape but
|
|
84
|
+
# are not columns.
|
|
85
|
+
CONSTRAINT_KEYWORDS = %w[CONSTRAINT PRIMARY FOREIGN UNIQUE CHECK EXCLUDE LIKE DEFERRABLE].freeze
|
|
86
|
+
|
|
87
|
+
# @param content [String] full text of +db/structure.sql+
|
|
88
|
+
# @param config [RailsAiBridge::Config::Introspection, RailsAiBridge::Configuration]
|
|
89
|
+
# any object that responds to +#excluded_table?+
|
|
90
|
+
def initialize(content:, config:)
|
|
91
|
+
@content = content
|
|
92
|
+
@config = config
|
|
93
|
+
@tables = {}
|
|
94
|
+
@current_table = nil
|
|
95
|
+
@in_table = false
|
|
96
|
+
@alter_target = nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Parse the structure.sql content and return the tables hash. Never
|
|
100
|
+
# raises — malformed or non-UTF-8 input is caught and reported as an
|
|
101
|
+
# error hash, per the introspector contract.
|
|
102
|
+
#
|
|
103
|
+
# @return [Hash{Symbol => Object}] with keys +:adapter+, +:tables+,
|
|
104
|
+
# +:total_tables+, and +:note+; or +{ error: }+ on failure
|
|
105
|
+
def call
|
|
106
|
+
@content.each_line { |line| parse_line(line) }
|
|
107
|
+
|
|
108
|
+
{
|
|
109
|
+
adapter: 'static_parse',
|
|
110
|
+
tables: @tables,
|
|
111
|
+
total_tables: @tables.size,
|
|
112
|
+
note: 'Parsed from db/structure.sql (no DB connection)'
|
|
113
|
+
}
|
|
114
|
+
rescue StandardError => error
|
|
115
|
+
{ error: "Failed to parse db/structure.sql: #{error.message}" }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
# Dispatches a single line to the table-body handler or the top-level
|
|
121
|
+
# (create/index/alter/foreign-key) handlers.
|
|
122
|
+
#
|
|
123
|
+
# @param line [String]
|
|
124
|
+
# @return [void]
|
|
125
|
+
def parse_line(line)
|
|
126
|
+
return parse_body_line(line) if @in_table
|
|
127
|
+
return if parse_table_line?(line)
|
|
128
|
+
return if parse_index_line?(line)
|
|
129
|
+
return if parse_alter_table_line?(line)
|
|
130
|
+
|
|
131
|
+
parse_foreign_key_line?(line)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Opens a table context on a +CREATE TABLE+ line. Sets +@current_table+
|
|
135
|
+
# to +nil+ for skipped tables while still tracking that we are inside a
|
|
136
|
+
# body, so the closing +);+ is honoured.
|
|
137
|
+
#
|
|
138
|
+
# @param line [String]
|
|
139
|
+
# @return [Boolean] +true+ if the line matched
|
|
140
|
+
def parse_table_line?(line)
|
|
141
|
+
match = TABLE_LINE.match(line)
|
|
142
|
+
return false unless match
|
|
143
|
+
|
|
144
|
+
name = match[1]
|
|
145
|
+
@in_table = true
|
|
146
|
+
@current_table = skip_table?(name) ? nil : name
|
|
147
|
+
@tables[@current_table] = { columns: [], indexes: [], foreign_keys: [] } if @current_table
|
|
148
|
+
true
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Handles a line while inside a table body: either the closing paren or a
|
|
152
|
+
# column definition (constraint lines are ignored).
|
|
153
|
+
#
|
|
154
|
+
# @param line [String]
|
|
155
|
+
# @return [void]
|
|
156
|
+
def parse_body_line(line)
|
|
157
|
+
if TABLE_END_LINE.match?(line)
|
|
158
|
+
@in_table = false
|
|
159
|
+
@current_table = nil
|
|
160
|
+
return
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
parse_column_line(line) if @current_table
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Appends a column to the current table unless the line is a table-level
|
|
167
|
+
# constraint.
|
|
168
|
+
#
|
|
169
|
+
# @param line [String]
|
|
170
|
+
# @return [void]
|
|
171
|
+
def parse_column_line(line)
|
|
172
|
+
match = COLUMN_LINE.match(line)
|
|
173
|
+
return unless match
|
|
174
|
+
return if constraint_keyword?(match[1])
|
|
175
|
+
|
|
176
|
+
@tables[@current_table][:columns] << { name: match[1], type: normalize_type(match[2]) }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Adds an index entry (first column only) to the matching table. No-ops
|
|
180
|
+
# when the table is not present in +@tables+.
|
|
181
|
+
#
|
|
182
|
+
# @param line [String]
|
|
183
|
+
# @return [Boolean] +true+ if the line matched
|
|
184
|
+
def parse_index_line?(line)
|
|
185
|
+
match = INDEX_LINE.match(line)
|
|
186
|
+
return false unless match
|
|
187
|
+
|
|
188
|
+
column = first_index_column(match[2])
|
|
189
|
+
@tables[match[1]]&.dig(:indexes)&.push({ columns: column }) if column
|
|
190
|
+
true
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Records the target table of an +ALTER TABLE+ statement so a following
|
|
194
|
+
# +ADD CONSTRAINT ... FOREIGN KEY+ line can attach to it. Sets
|
|
195
|
+
# +@alter_target+ to +nil+ for skipped/unknown tables.
|
|
196
|
+
#
|
|
197
|
+
# @param line [String]
|
|
198
|
+
# @return [Boolean] +true+ if the line matched
|
|
199
|
+
def parse_alter_table_line?(line)
|
|
200
|
+
match = ALTER_TABLE_LINE.match(line)
|
|
201
|
+
return false unless match
|
|
202
|
+
|
|
203
|
+
@alter_target = @tables.key?(match[1]) ? match[1] : nil
|
|
204
|
+
true
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Appends a foreign-key entry to the current +@alter_target+ table. Mirrors
|
|
208
|
+
# the live introspector's shape (+from_table+, +to_table+, +column+,
|
|
209
|
+
# +primary_key+, +on_delete+, +on_update+), keeping the first column of a
|
|
210
|
+
# composite key for parity with index handling. No-ops without a target.
|
|
211
|
+
#
|
|
212
|
+
# @param line [String]
|
|
213
|
+
# @return [Boolean] +true+ if the line matched
|
|
214
|
+
def parse_foreign_key_line?(line)
|
|
215
|
+
match = FOREIGN_KEY_LINE.match(line)
|
|
216
|
+
return false unless match
|
|
217
|
+
return true unless @alter_target
|
|
218
|
+
|
|
219
|
+
@tables[@alter_target][:foreign_keys] << {
|
|
220
|
+
from_table: @alter_target,
|
|
221
|
+
to_table: match[2],
|
|
222
|
+
column: first_identifier(match[1]),
|
|
223
|
+
primary_key: first_identifier(match[3]),
|
|
224
|
+
on_delete: fk_action(line, ON_DELETE),
|
|
225
|
+
on_update: fk_action(line, ON_UPDATE)
|
|
226
|
+
}.compact
|
|
227
|
+
true
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Strips a trailing comma and the +DEFAULT ...+ / +NOT NULL+ / +NULL+
|
|
231
|
+
# modifiers to leave the bare SQL type.
|
|
232
|
+
#
|
|
233
|
+
# @param raw [String] everything after the column name
|
|
234
|
+
# @return [String] the SQL type (e.g. +"character varying"+, +"bigint"+)
|
|
235
|
+
def normalize_type(raw)
|
|
236
|
+
raw.strip
|
|
237
|
+
.sub(/,\s*\z/, '')
|
|
238
|
+
.sub(/\s+DEFAULT\b.*\z/i, '')
|
|
239
|
+
.sub(/\s+NOT\s+NULL\s*\z/i, '')
|
|
240
|
+
.sub(/\s+NULL\s*\z/i, '')
|
|
241
|
+
.strip
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Extracts the first column identifier from an index's parenthesised
|
|
245
|
+
# column list. Keeps plain and opclass-qualified columns
|
|
246
|
+
# (+col varchar_pattern_ops+ → +col+) but returns +nil+ for functional
|
|
247
|
+
# or expression indexes (+lower(email)+) so they are skipped rather than
|
|
248
|
+
# mis-attributed to the function name.
|
|
249
|
+
#
|
|
250
|
+
# @param columns [String] raw text between the index parentheses
|
|
251
|
+
# @return [String, nil]
|
|
252
|
+
def first_index_column(columns)
|
|
253
|
+
first = columns.split(',').first&.strip
|
|
254
|
+
return nil if first.nil? || first.include?('(')
|
|
255
|
+
|
|
256
|
+
first.slice(/[A-Za-z_]\w*/)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Returns the first identifier from a (possibly composite) column list.
|
|
260
|
+
#
|
|
261
|
+
# @param columns [String] comma-separated column list
|
|
262
|
+
# @return [String, nil]
|
|
263
|
+
def first_identifier(columns)
|
|
264
|
+
columns.split(',').first&.slice(/[A-Za-z_]\w*/)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Extracts a normalized foreign-key referential action (e.g. +CASCADE+,
|
|
268
|
+
# +SET NULL+) from a line, or +nil+ when the clause is absent.
|
|
269
|
+
#
|
|
270
|
+
# @param line [String]
|
|
271
|
+
# @param pattern [Regexp] {ON_DELETE} or {ON_UPDATE}
|
|
272
|
+
# @return [String, nil]
|
|
273
|
+
def fk_action(line, pattern)
|
|
274
|
+
match = pattern.match(line)
|
|
275
|
+
match && match[1].strip.squeeze(' ').upcase
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# @param name [String]
|
|
279
|
+
# @return [Boolean] +true+ when +name+ is a table-level constraint keyword
|
|
280
|
+
def constraint_keyword?(name)
|
|
281
|
+
CONSTRAINT_KEYWORDS.include?(name.upcase)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# @param name [String]
|
|
285
|
+
# @return [Boolean] +true+ when +name+ is internal or excluded by config
|
|
286
|
+
def skip_table?(name)
|
|
287
|
+
INTERNAL_TABLES.any? { |t| name.start_with?(t) } ||
|
|
288
|
+
@config.excluded_table?(name)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
@@ -4,11 +4,13 @@ module RailsAiBridge
|
|
|
4
4
|
module Introspectors
|
|
5
5
|
# Extracts database schema information — tables, columns, indexes, and
|
|
6
6
|
# foreign keys — from a live ActiveRecord connection when available, or by
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
7
|
+
# text-parsing the schema file when no connection is present (CI, Claude
|
|
8
|
+
# Code, offline environments). The static fallback prefers +db/schema.rb+
|
|
9
|
+
# ({Schema::StaticSchemaParser}) and falls back to +db/structure.sql+
|
|
10
|
+
# ({Schema::StaticStructureSqlParser}) for +schema_format = :sql+ apps.
|
|
10
11
|
#
|
|
11
12
|
# @see Schema::StaticSchemaParser
|
|
13
|
+
# @see Schema::StaticStructureSqlParser
|
|
12
14
|
class SchemaIntrospector
|
|
13
15
|
# @return [Rails::Application]
|
|
14
16
|
attr_reader :app
|
|
@@ -127,15 +129,27 @@ module RailsAiBridge
|
|
|
127
129
|
File.join(app.root, 'db', 'schema.rb')
|
|
128
130
|
end
|
|
129
131
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
+
def structure_sql_path
|
|
133
|
+
File.join(app.root, 'db', 'structure.sql')
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Fallback used when the DB is not connected. Prefers +db/schema.rb+
|
|
137
|
+
# (Ruby DSL) and falls back to +db/structure.sql+ (raw SQL) so
|
|
138
|
+
# +schema_format = :sql+ apps still get schema context offline.
|
|
132
139
|
#
|
|
133
|
-
# @return [Hash] parsed schema result, or +{ error: }+ when
|
|
140
|
+
# @return [Hash] parsed schema result, or +{ error: }+ when neither file exists
|
|
134
141
|
def static_schema_parse
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
if File.exist?(schema_file_path)
|
|
143
|
+
Schema::StaticSchemaParser.new(content: File.read(schema_file_path), config: config).call
|
|
144
|
+
elsif File.exist?(structure_sql_path)
|
|
145
|
+
Schema::StaticStructureSqlParser.new(content: File.read(structure_sql_path), config: config).call
|
|
146
|
+
else
|
|
147
|
+
{ error: "No db/schema.rb or db/structure.sql found in #{File.join(app.root, 'db')}" }
|
|
148
|
+
end
|
|
149
|
+
rescue StandardError => error
|
|
150
|
+
# Guards the exist?/read race (file removed between check and read) and
|
|
151
|
+
# any other read failure, honouring the introspector never-raise contract.
|
|
152
|
+
{ error: "Failed to read schema file: #{error.message}" }
|
|
139
153
|
end
|
|
140
154
|
end
|
|
141
155
|
end
|
|
@@ -9,7 +9,7 @@ module RailsAiBridge
|
|
|
9
9
|
# PathResolver is deliberately used by every introspector that needs to
|
|
10
10
|
# locate files on disk (controller, model, view, stimulus, turbo, auth,
|
|
11
11
|
# api, config, action_text, activeStorage, nonArModels — 11 callers as of
|
|
12
|
-
# v3.6.
|
|
12
|
+
# v3.6.2). It is NOT a god class despite high betweenness centrality in
|
|
13
13
|
# graph analyses: a foundational path-resolution utility is expected to
|
|
14
14
|
# sit at the centre of the introspector graph. Splitting it would spread
|
|
15
15
|
# path-safety logic (traversal guards, safe joins) across multiple files
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-ai-bridge
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.6.
|
|
4
|
+
version: 3.6.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ismael Marin
|
|
@@ -218,6 +218,7 @@ files:
|
|
|
218
218
|
- lib/rails_ai_bridge/introspectors/rake_task_introspector.rb
|
|
219
219
|
- lib/rails_ai_bridge/introspectors/route_introspector.rb
|
|
220
220
|
- lib/rails_ai_bridge/introspectors/schema/static_schema_parser.rb
|
|
221
|
+
- lib/rails_ai_bridge/introspectors/schema/static_structure_sql_parser.rb
|
|
221
222
|
- lib/rails_ai_bridge/introspectors/schema_introspector.rb
|
|
222
223
|
- lib/rails_ai_bridge/introspectors/seeds_introspector.rb
|
|
223
224
|
- lib/rails_ai_bridge/introspectors/semantic_introspector.rb
|