Class: RSpecTracer::Storage::SqliteBackend Private

Inherits:
Object
  • Object
show all
Defined in:
lib/rspec_tracer/storage/sqlite_backend.rb

Overview

This class is part of a private API. You should avoid using this class if possible, as it may be removed or be changed in the future.

SQLite-on-disk storage backend. Single file ‘cache_path/rspec_tracer.sqlite3` with a normalized 9-table schema so a warm run can materialize only the rows a given field needs (Filter.select hits ~50-500 example_ids out of millions, JsonBackend eagerly read the whole map). Breaks the RAM-scales-with-cache-size curve that JsonBackend cannot escape without a different on-disk shape.

MRI-only. The ‘sqlite3` gem targets MRI’s C API; JRuby’s ‘jdbc-sqlite3` has a different API that this backend does not target in 2.0. Users who select `:sqlite` on JRuby (or on MRI without the `sqlite3` gem in their Gemfile) see the construct raise `SqliteBackendError`, which the Engine’s backend dispatch converts to a warn + cold-run fallback.

No multi-run history. SqliteBackend stores only the latest run; save_graph full-replaces every table inside a single ‘BEGIN IMMEDIATE` transaction. JsonBackend’s ‘cache_retention_local_count` is a no-op here. Users who need rollback history stay on `:json`.

‘journal_mode = MEMORY` so SQLite’s WAL / SHM sidecars do not leak into the user-facing ‘rspec_tracer_cache/` directory (USER_FACING_SURFACE.md section 6 locks the layout; sidecars would surprise debug scripts that walk the dir expecting only documented files). rubocop:disable Metrics/ClassLength

Defined Under Namespace

Classes: SqliteBackendError, SqliteFieldReader

Constant Summary collapse

DB_FILENAME =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Internal constant.

'rspec_tracer.sqlite3'
JOURNAL_MODE_SQL =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Internal constant.

'PRAGMA journal_mode = MEMORY'
SYNCHRONOUS_SQL =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Internal constant.

'PRAGMA synchronous = NORMAL'
BUSY_TIMEOUT_MS =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Two concurrent save_graph calls (parallel_tests workers, fork- based test harnesses) both reach ‘BEGIN IMMEDIATE` and contend on SQLite’s RESERVED write lock. Without busy_timeout, the losing writer raises SQLite3::BusyException immediately. 5000 ms gives ~5x margin over a worst-case 1 s save on large caches (cache_load benchmark p50 ~0.6 s at 500 examples), preserving the storage layer’s “concurrent writers serialize cleanly” contract verified in spec/edge_cases/concurrent_write_spec.rb.

5_000
STATUS_FIELDS =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Internal constant.

{
  interrupted_examples: 'interrupted',
  flaky_examples: 'flaky',
  failed_examples: 'failed',
  pending_examples: 'pending',
  skipped_examples: 'skipped'
}.freeze
DIGEST_MAP_KINDS =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Internal constant.

{
  boot_set: 'boot',
  wsi_snapshot: 'wsi',
  env_snapshot: 'env'
}.freeze
READABLE_FIELDS =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Mirrors FIELD_FILENAMES from JsonBackend - the set of Snapshot fields a LazySnapshot reader may ask about. Used by SqliteFieldReader to reject unknown fields before hitting the DB. Kept in step with Snapshot.members minus the envelope.

(
  %i[all_examples duplicate_examples all_files dependency
     reverse_dependency examples_coverage env_dependency cache_hit_reason] +
    STATUS_FIELDS.keys + DIGEST_MAP_KINDS.keys
).freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(cache_path:, logger: nil) ⇒ SqliteBackend

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal method on the tracer pipeline.



136
137
138
139
140
# File 'lib/rspec_tracer/storage/sqlite_backend.rb', line 136

def initialize(cache_path:, logger: nil)
  @cache_path = File.expand_path(cache_path)
  @logger = logger
  load_sqlite_driver!
end

Instance Attribute Details

#cache_pathObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal attribute.



132
133
134
# File 'lib/rspec_tracer/storage/sqlite_backend.rb', line 132

def cache_path
  @cache_path
end

Instance Method Details

#clear!Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal method on the tracer pipeline.



204
205
206
207
208
# File 'lib/rspec_tracer/storage/sqlite_backend.rb', line 204

def clear!
  return unless File.directory?(@cache_path)

  FileUtils.rm_rf(@cache_path)
end

#last_run_idObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal method on the tracer pipeline.



144
145
146
147
148
149
150
151
152
153
154
# File 'lib/rspec_tracer/storage/sqlite_backend.rb', line 144

def last_run_id
  with_connection do |db|
    row = db.get_first_row('SELECT run_id FROM meta LIMIT 1')
    run_id = row&.first
    return nil if run_id.nil? || run_id.to_s.empty?

    run_id
  end
rescue StandardError
  nil
end

#load_graph(schema_version:) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal method on the tracer pipeline.



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/rspec_tracer/storage/sqlite_backend.rb', line 158

def load_graph(schema_version:)
  with_connection do |db|
    return nil unless meta_table_exists?(db)

    row = db.get_first_row('SELECT schema_version, run_id FROM meta LIMIT 1')
    return nil if row.nil?

    stored_sv, run_id = row
    unless Schema.supported?(stored_sv) && stored_sv == schema_version
      info("schema_version mismatch (stored=#{stored_sv.inspect}, expected=#{schema_version}); cold run")
      return nil
    end
    return nil if run_id.nil? || run_id.to_s.empty?

    LazySnapshot.new(
      schema_version: stored_sv, run_id: run_id,
      reader: SqliteFieldReader.new(backend: self)
    )
  end
rescue StandardError => e
  info("failed to load sqlite cache: #{e.class}: #{e.message}; cold run")
  nil
end

#read_field(field) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Read one field on behalf of a LazySnapshot. Missing table or empty result returns the per-field empty default (Set for id-set fields, {} for hashes) so partial caches behave identically to JsonBackend under the same conditions. ArgumentError on an unknown field is a programmer mistake and propagates; the StandardError rescue is only for wire / I/O failures on the DB itself.

Raises:

  • (ArgumentError)


231
232
233
234
235
236
237
238
239
240
# File 'lib/rspec_tracer/storage/sqlite_backend.rb', line 231

def read_field(field)
  raise ArgumentError, "unknown snapshot field: #{field.inspect}" unless READABLE_FIELDS.include?(field)

  begin
    with_connection { |db| dispatch_read(db, field) }
  rescue StandardError => e
    info("sqlite read_field(#{field.inspect}) failed: #{e.class}: #{e.message}; returning empty default")
    empty_default_for(field)
  end
end

#save_graph(snapshot, schema_version:) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal method on the tracer pipeline.

Raises:

  • (ArgumentError)


184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/rspec_tracer/storage/sqlite_backend.rb', line 184

def save_graph(snapshot, schema_version:)
  raise ArgumentError, 'snapshot must not be nil' if snapshot.nil?

  unless Schema.supported?(schema_version)
    raise ArgumentError, "unsupported schema_version: #{schema_version.inspect}"
  end

  run_id = snapshot.run_id
  raise ArgumentError, 'snapshot.run_id must be a non-empty string' if run_id.nil? || run_id.to_s.empty?

  transactional_save do
    db = @active_db
    write_all_tables(db, snapshot, schema_version: schema_version)
  end

  snapshot
end

#transactional_save(&block) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal method on the tracer pipeline.

Raises:

  • (ArgumentError)


212
213
214
215
216
217
218
219
220
221
222
# File 'lib/rspec_tracer/storage/sqlite_backend.rb', line 212

def transactional_save(&block)
  raise ArgumentError, 'block required' unless block

  FileUtils.mkdir_p(@cache_path)
  with_write_connection do |db|
    @active_db = db
    db.transaction(:immediate, &block)
  ensure
    @active_db = nil
  end
end