Class: CodeToQuery::Context::Builder

Inherits:
Object
  • Object
show all
Defined in:
lib/code_to_query/context/builder.rb

Overview

rubocop:disable Metrics/ClassLength

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config = CodeToQuery.config) ⇒ Builder

Returns a new instance of Builder.



20
21
22
# File 'lib/code_to_query/context/builder.rb', line 20

def initialize(config = CodeToQuery.config)
  @config = config
end

Class Method Details

.bootstrap!Object



16
17
18
# File 'lib/code_to_query/context/builder.rb', line 16

def self.bootstrap!
  new.bootstrap!
end

Instance Method Details

#bootstrap!Object

Build a full pack and write it to disk



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/code_to_query/context/builder.rb', line 25

def bootstrap!
  # First attempt to extract schema
  schema_data = extract_schema
  initial_count = schema_data[:tables]&.length || schema_data['tables']&.length || 0
  @config.logger.info("[code_to_query] Schema data structure: #{schema_data.keys} with #{initial_count} tables")

  # If schema looks empty, try scanning the app to force-load models/connection, then retry
  models_data = scan_app
  # Optionally enrich with static scan
  if @config.prefer_static_scan
    static_data = static_scan_app
    models_data = deep_merge_models(models_data, static_data)
  end
  if initial_count.to_i.zero?
    schema_data = extract_schema
    retry_count = schema_data[:tables]&.length || schema_data['tables']&.length || 0
    @config.logger.info("[code_to_query] Retried schema extraction after app scan: #{retry_count} tables")
  end

  pack = Pack.new(
    schema: schema_data,
    models: models_data,
    glossary: enrich_glossary_with_llm(generate_glossary(schema_data), schema_data, models_data),
    policies: collect_policies,
    hints: { performance: [], joins: extract_join_hints(schema_data) }
  )
  write_pack(pack)
  pack
end

#collect_policiesObject



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/code_to_query/context/builder.rb', line 201

def collect_policies
  policies = {
    enforced_predicates: {},
    column_access: {},
    row_level_security: {},
    audit_requirements: {}
  }

  # Get policies from configuration
  if @config.policy_adapter.respond_to?(:call)
    begin
      # In a real implementation, you'd pass the actual user context
      user_policies = @config.policy_adapter.call(nil)
      policies[:enforced_predicates] = user_policies if user_policies.is_a?(Hash)
    rescue StandardError => e
      @config.logger.warn("[code_to_query] Policy collection failed: #{e.message}")
    end
  end

  # Extract policies from models (if using Pundit, CanCan, etc.)
  policies.merge!(extract_authorization_policies)

  policies
rescue StandardError => e
  @config.logger.warn("[code_to_query] Policy collection failed: #{e.message}")
  { enforced_predicates: {}, error: e.message }
end

#extract_schemaObject

— Components (stubs that won’t crash) —



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/code_to_query/context/builder.rb', line 57

def extract_schema
  unless defined?(ActiveRecord::Base)
    return { tables: [], version: 'unknown', adapter: 'none' }
  end

  # Try multiple approaches to establish connection
  connection = nil
  connection_attempts = 0
  max_attempts = 3

  while connection.nil? && connection_attempts < max_attempts
    connection_attempts += 1
    begin
      # Force-establish a connection (Rails defers until first use)
      ActiveRecord::Base.connection

      # Verify connection is actually working
      if ActiveRecord::Base.connected?
        connection = ActiveRecord::Base.connection
        @config.logger.info("[code_to_query] Connected to database with adapter: #{connection.adapter_name} (attempt #{connection_attempts})")
        break
      else
        @config.logger.info("[code_to_query] Database not connected on attempt #{connection_attempts}")
        sleep(0.1) if connection_attempts < max_attempts
      end
    rescue StandardError => e
      @config.logger.warn("[code_to_query] Connection attempt #{connection_attempts} failed: #{e.message}")
      sleep(0.1) if connection_attempts < max_attempts
    end
  end

  unless connection
    error_msg = "Failed to establish database connection after #{max_attempts} attempts"
    @config.logger.warn("[code_to_query] #{error_msg}")
    return { tables: [], version: 'unknown', adapter: @config.adapter.to_s, error: error_msg }
  end

  tables = list_tables(connection)
  @config.logger.info("[code_to_query] Found #{tables.length} tables: #{tables.join(', ')}") if tables.any?
  @config.logger.info('[code_to_query] No tables found') if tables.empty?

  result = {
    tables: tables.map do |table_name|
      {
        name: table_name,
        columns: extract_table_columns(connection, table_name),
        indexes: extract_table_indexes(connection, table_name),
        foreign_keys: extract_foreign_keys(connection, table_name),
        constraints: extract_table_constraints(connection, table_name)
      }
    end,
    version: extract_schema_version(connection),
    adapter: connection.adapter_name.downcase
  }

  @config.logger.info("[code_to_query] Schema extraction completed with #{result[:tables].length} tables")
  result
rescue StandardError => e
  @config.logger.warn("[code_to_query] Schema extraction failed: #{e.message}")
  @config.logger.warn("[code_to_query] Backtrace: #{e.backtrace.first(5).join("\n")}")
  { tables: [], version: 'unknown', adapter: @config.adapter.to_s, error: e.message }
end

#generate_glossary(existing_schema = nil) ⇒ Object



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/code_to_query/context/builder.rb', line 167

def generate_glossary(existing_schema = nil)
  glossary = {}

  # Auto-generate from schema (prefer the already extracted schema)
  schema = existing_schema || extract_schema
  tables = schema[:tables] || schema['tables'] || []
  tables.each do |table|
    table_name = table[:name] || table['name']

    # Generate table synonyms
    synonyms = generate_table_synonyms(table_name)
    glossary[table_name] = synonyms if synonyms.any?

    # Generate column synonyms
    Array(table[:columns] || table['columns']).each do |column|
      column_name = column[:name] || column['name']
      sql_type = column[:sql_type] || column['sql_type']
      column_synonyms = generate_column_synonyms(column_name, sql_type)
      if column_synonyms.any?
        key = "#{table_name}.#{column_name}"
        glossary[key] = column_synonyms
      end
    end
  end

  # Add business-specific glossary
  glossary.merge!(load_business_glossary)

  glossary
rescue StandardError => e
  @config.logger.warn("[code_to_query] Glossary generation failed: #{e.message}")
  { error: e.message }
end

#scan_appObject



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/code_to_query/context/builder.rb', line 120

def scan_app
  models = {}
  associations = {}
  validations = {}
  scopes = {}

  if defined?(ActiveRecord::Base)
    # Ensure models are loaded so descendants is populated
    if defined?(Rails) && Rails.respond_to?(:application)
      begin
        Rails.application.eager_load!
      rescue StandardError => e
        @config.logger.warn("[code_to_query] Eager load failed: #{e.message}")
      end
    end
    ActiveRecord::Base.descendants.each do |model|
      next unless model.table_exists?

      model_name = model.name
      table_name = model.table_name

      models[model_name] = {
        table_name: table_name,
        primary_key: model.primary_key,
        inheritance_column: model.inheritance_column,
        timestamps: has_timestamps?(model),
        soft_delete: has_soft_delete?(model),
        enums: extract_model_enums(model)
      }

      associations[model_name] = extract_model_associations(model)
      validations[model_name] = extract_model_validations(model)
      scopes[model_name] = extract_model_scopes(model)
    end
  end

  {
    models: models,
    associations: associations,
    validations: validations,
    scopes: scopes
  }
rescue StandardError => e
  @config.logger.warn("[code_to_query] App scanning failed: #{e.message}")
  { models: {}, associations: {}, validations: {}, scopes: {}, error: e.message }
end

#verify!Object



229
230
231
232
233
234
235
236
237
# File 'lib/code_to_query/context/builder.rb', line 229

def verify!
  path = @config.context_pack_path.to_s
  raise "Context pack not found at #{path}" unless File.exist?(path)

  json = JSON.parse(File.read(path))
  raise 'Context pack missing schema.tables' unless json.dig('schema', 'tables').is_a?(Array)

  true
end