Class: Mxrb::IO::MprFile

Inherits:
Object
  • Object
show all
Defined in:
lib/mxrb/io/mpr_file.rb

Overview

Low-level SQLite wrapper for .mpr files.

Schema reality (from reverse engineering):

Unit(UnitID BLOB, ContainerID BLOB, ContainmentName TEXT,
   TreeConflict LONG, ContentsHash TEXT, ContentsConflict TEXT, Contents BLOB)
_MetaData(_ProductVersion TEXT, _BuildVersion TEXT, _SchemaHash TEXT)
(older MPRs use MendixVersion instead of _ProductVersion)

UnitID and ContainerID are 16-byte MS-GUID blobs, not integers. Contents is a BSON blob (v1) or NULL (v2, where .mxunit files hold the data). The $Type of each unit is embedded in its BSON Contents, not in a separate table.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path, readonly: false) ⇒ MprFile

Returns a new instance of MprFile.



31
32
33
34
35
36
37
38
# File 'lib/mxrb/io/mpr_file.rb', line 31

def initialize(path, readonly: false)
  @path     = File.expand_path(path)
  @readonly = readonly
  @db       = open_db
  @write_stats = { inserted: 0, updated: 0, skipped: 0, deleted: 0 }
  validate!
  @format_version = detect_format
end

Instance Attribute Details

#format_versionObject (readonly)

Returns the value of attribute format_version.



23
24
25
# File 'lib/mxrb/io/mpr_file.rb', line 23

def format_version
  @format_version
end

#pathObject (readonly)

Returns the value of attribute path.



23
24
25
# File 'lib/mxrb/io/mpr_file.rb', line 23

def path
  @path
end

Class Method Details

.open(path, readonly: false) ⇒ Object



40
41
42
# File 'lib/mxrb/io/mpr_file.rb', line 40

def self.open(path, readonly: false)
  new(path, readonly: readonly)
end

Instance Method Details

#all_unitsObject

All units (for exploration / reverse engineering).



120
121
122
123
124
# File 'lib/mxrb/io/mpr_file.rb', line 120

def all_units
  @db.execute(
    "SELECT #{unit_select_columns} FROM Unit"
  ).map { raw_to_hash(_1) }
end

#architecture_definitionObject

mxrb-only architecture metadata for concepts without a native Mendix unit (ports/repositories) or bindings awaiting a concrete widget tree.



488
489
490
491
492
# File 'lib/mxrb/io/mpr_file.rb', line 488

def architecture_definition
  return nil unless tables.include?("_MxrbArchitecture")
  json = @db.get_first_value("SELECT Definition FROM _MxrbArchitecture WHERE ID = 1")
  json && BsonCodec.restore_extended_json(JSON.parse(json, symbolize_names: true))
end

#backup!(dest_path) ⇒ Object

Creates a consistent point-in-time backup using SQLite's VACUUM INTO. Falls back to a WAL checkpoint + file copy on older SQLite versions.



278
279
280
281
282
283
284
285
286
# File 'lib/mxrb/io/mpr_file.rb', line 278

def backup!(dest_path)
  raise ReadOnlyError, "Opened in read-only mode" if @readonly

  FileUtils.rm_f(dest_path)
  @db.execute("VACUUM INTO ?", [dest_path])
rescue SQLite3::Exception
  @db.execute("PRAGMA wal_checkpoint(FULL)") rescue nil
  FileUtils.cp(@path, dest_path)
end

#children_of(parent_uuid) ⇒ Object

Units directly contained by a given parent UUID.



100
101
102
103
104
105
106
107
# File 'lib/mxrb/io/mpr_file.rb', line 100

def children_of(parent_uuid)
  blob = BsonCodec.uuid_to_blob(parent_uuid)
  @db.execute(
    "SELECT #{unit_select_columns} FROM Unit " \
    "WHERE ContainerID = ? AND UnitID != ContainerID",
    [blob]
  ).map { raw_to_hash(_1) }
end

#clear_index_cache!Object

Clears cached semantic data while preserving the cache table.

Raises:



380
381
382
383
384
385
386
387
# File 'lib/mxrb/io/mpr_file.rb', line 380

def clear_index_cache!
  raise ReadOnlyError, "Opened in read-only mode" if @readonly
  return 0 unless tables.include?("_MxrbIndexCache")

  count = @db.get_first_value("SELECT COUNT(*) FROM _MxrbIndexCache").to_i
  @db.execute("DELETE FROM _MxrbIndexCache")
  count
end

#closeObject



627
628
629
# File 'lib/mxrb/io/mpr_file.rb', line 627

def close
  @db.close
end

#content_bytes(raw_unit) ⇒ Object



139
140
141
142
143
144
145
146
147
148
# File 'lib/mxrb/io/mpr_file.rb', line 139

def content_bytes(raw_unit)
  blob = raw_unit["Contents"]
  if (blob.nil? || blob.empty?) && @format_version == :v2
    unit_path = MxunitCodec.path_for(contents_dir, raw_unit.fetch("UnitID"))
    return nil unless File.file?(unit_path)

    return File.binread(unit_path)
  end
  blob
end

#content_filesObject



156
157
158
159
160
# File 'lib/mxrb/io/mpr_file.rb', line 156

def content_files
  return [] unless @format_version == :v2 && File.directory?(contents_dir)

  Dir.glob(File.join(contents_dir, "**", "*.mxunit")).sort
end

#content_path(raw_unit) ⇒ Object



150
151
152
153
154
# File 'lib/mxrb/io/mpr_file.rb', line 150

def content_path(raw_unit)
  return nil unless @format_version == :v2

  MxunitCodec.path_for(contents_dir, raw_unit.fetch("UnitID"))
end

#delete_unit(uuid) ⇒ Object

Raises:



251
252
253
254
255
256
257
# File 'lib/mxrb/io/mpr_file.rb', line 251

def delete_unit(uuid)
  raise ReadOnlyError, "Opened in read-only mode" if @readonly
  @db.execute("DELETE FROM Unit WHERE UnitID = ?", [BsonCodec.uuid_to_blob(uuid)])
  removed = FileUtils.rm_f(MxunitCodec.path_for(contents_dir, uuid)) if @format_version == :v2
  write_stats[:deleted] += 1
  removed
end

#domain_diagram_anchorsObject

Cross-module Mendix associations do not expose native visual connection fields. Keep their ER-editor anchors in an MXRB-only table so the diagram remains editable without inventing unsupported BSON.



552
553
554
555
556
557
558
559
560
561
# File 'lib/mxrb/io/mpr_file.rb', line 552

def domain_diagram_anchors
  return {} unless tables.include?("_MxrbDomainDiagramAssociation")

  @db.execute(<<~SQL).to_h do |row|
    SELECT AssociationID, SourceAnchor, TargetAnchor
    FROM _MxrbDomainDiagramAssociation
  SQL
    [row[0], { source_anchor: row[1], target_anchor: row[2] }]
  end
end

#ensure_vec_meta_table!(table) ⇒ Object



417
418
419
420
421
422
423
424
425
426
427
428
# File 'lib/mxrb/io/mpr_file.rb', line 417

def ensure_vec_meta_table!(table)
  ensure_vector_write!
  identifier = vector_identifier(table)
  @db.execute(<<~SQL)
    CREATE TABLE IF NOT EXISTS #{identifier} (
      ID INTEGER PRIMARY KEY CHECK (ID = 1),
      Backend TEXT NOT NULL,
      Dimension INTEGER NOT NULL,
      Fingerprint TEXT NOT NULL
    )
  SQL
end

#ensure_vec_table!(table, dimension) ⇒ Object

Raises:

  • (ArgumentError)


405
406
407
408
409
410
411
412
413
414
415
# File 'lib/mxrb/io/mpr_file.rb', line 405

def ensure_vec_table!(table, dimension)
  ensure_vector_write!
  identifier = vector_identifier(table)
  size = Integer(dimension)
  raise ArgumentError, "vector dimension must be positive" unless size.positive?

  @db.execute(<<~SQL)
    CREATE VIRTUAL TABLE IF NOT EXISTS #{identifier}
    USING vec0(artifact_id TEXT PRIMARY KEY, embedding FLOAT[#{size}])
  SQL
end

#index_cache_info(current_fingerprint: nil) ⇒ Object

Returns cache size and fingerprints without parsing the cached payload.



350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
# File 'lib/mxrb/io/mpr_file.rb', line 350

def index_cache_info(current_fingerprint: nil)
  unless tables.include?("_MxrbIndexCache")
    return {
      present: false, entries: 0, bytes: 0,
      fingerprints: [].freeze, current_fingerprint:,
      hit: false
    }.freeze
  end

  rows = @db.execute(
    "SELECT Fingerprint, LENGTH(IndexData) FROM _MxrbIndexCache ORDER BY Fingerprint"
  )
  fingerprints = rows.map { _1[0].to_s }.freeze
  {
    present: !rows.empty?,
    entries: rows.size,
    bytes: rows.sum { _1[1].to_i },
    fingerprints:,
    current_fingerprint:,
    hit: current_fingerprint && fingerprints.include?(current_fingerprint)
  }.freeze
rescue SQLite3::Exception
  {
    present: false, entries: 0, bytes: 0,
    fingerprints: [].freeze, current_fingerprint:,
    hit: false
  }.freeze
end

#insert_unit(container_uuid:, containment_name:, contents_doc:, unit_uuid: nil) ⇒ Object

Insert a new unit. Returns the assigned UUID.

Raises:



165
166
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
# File 'lib/mxrb/io/mpr_file.rb', line 165

def insert_unit(container_uuid:, containment_name:, contents_doc:, unit_uuid: nil)
  raise ReadOnlyError, "Opened in read-only mode" if @readonly

  uuid = unit_uuid || BsonCodec.extract_id(contents_doc["$ID"] || contents_doc["\$ID"]) || SecureRandom.uuid
  unless contents_doc.key?("$ID") || contents_doc.key?("\$ID")
    contents_doc = { "$ID" => uuid }.merge(contents_doc)
  end
  unit_blob    = BsonCodec.uuid_to_blob(uuid)
  parent_blob  = BsonCodec.uuid_to_blob(container_uuid)
  bson_bytes   = BsonCodec.serialize(contents_doc)
  hash         = BsonCodec.contents_hash(bson_bytes)
  stored_bytes = @format_version == :v2 ? nil : bson_bytes
  columns = %w[UnitID ContainerID ContainmentName TreeConflict ContentsHash]
  values = [unit_blob, parent_blob, containment_name, 0, hash]
  if (conflicts = conflicts_column)
    columns << conflicts
    values << ""
  end
  if contents_column?
    columns << "Contents"
    values << stored_bytes
  end
  placeholders = (["?"] * columns.length).join(", ")
  @db.execute(
    "INSERT INTO Unit (#{columns.join(', ')}) VALUES (#{placeholders})",
    values
  )
  write_v2_unit(uuid, bson_bytes) if @format_version == :v2
  write_stats[:inserted] += 1
  uuid
end

#legacy_unit_identity_mismatchesObject



593
594
595
596
597
598
599
600
601
602
603
# File 'lib/mxrb/io/mpr_file.rb', line 593

def legacy_unit_identity_mismatches
  return [] unless tables.include?("_MxrbCompatibility")

  @db.execute(<<~SQL).map do |row|
    SELECT UnitID, ContentID, UnitType
    FROM _MxrbCompatibility
    WHERE Kind = 'legacy-unit-identity'
  SQL
    { unit_id: row[0], content_id: row[1], type: row[2] }
  end
end

#load_vec_extension!Object

Loads the optional sqlite-vec extension into the active connection.



392
393
394
395
396
397
398
399
400
401
402
403
# File 'lib/mxrb/io/mpr_file.rb', line 392

def load_vec_extension!
  return true if @vec_loaded

  require "sqlite_vec"
  @db.enable_load_extension(true)
  begin
    SqliteVec.load(@db)
  ensure
    @db.enable_load_extension(false)
  end
  @vec_loaded = true
end

#mendix_versionObject

── Metadata ─────────────────────────────────────────────────────────



46
47
48
49
50
51
52
# File 'lib/mxrb/io/mpr_file.rb', line 46

def mendix_version
  @mendix_version ||= begin
    row = @db.get_first_row("SELECT _ProductVersion FROM _MetaData LIMIT 1") rescue nil
    row ||= @db.get_first_row("SELECT MendixVersion FROM _MetaData LIMIT 1") rescue nil
    row&.first
  end
end

#parse_contents(raw_unit) ⇒ Object

Parse BSON from a raw unit hash.



127
128
129
130
131
132
133
134
135
136
137
# File 'lib/mxrb/io/mpr_file.rb', line 127

def parse_contents(raw_unit)
  blob = raw_unit["Contents"]
  if (blob.nil? || blob.empty?) && @format_version == :v2
    unit_path = MxunitCodec.path_for(contents_dir, raw_unit.fetch("UnitID"))
    return {} unless File.file?(unit_path)
    return MxunitCodec.read(unit_path)
  end
  return {} if blob.nil? || blob.empty?

  BsonCodec.parse(blob)
end

#project_nameObject



66
67
68
69
70
71
72
73
74
75
# File 'lib/mxrb/io/mpr_file.rb', line 66

def project_name
  @project_name ||= begin
    # Name lives in the root Unit's BSON ($QualifiedName or Name field)
    root = root_unit
    return nil unless root

    doc = parse_contents(root)
    doc["Name"] || doc["name"] || File.basename(@path, ".mpr")
  end
end

#query(sql, *binds) ⇒ Object



310
311
312
# File 'lib/mxrb/io/mpr_file.rb', line 310

def query(sql, *binds)
  @db.execute(sql, *binds)
end

#read_index_cache(fingerprint) ⇒ Object

Returns the cached index JSON if the fingerprint matches, nil otherwise.



317
318
319
320
321
322
323
324
325
# File 'lib/mxrb/io/mpr_file.rb', line 317

def read_index_cache(fingerprint)
  return nil unless tables.include?("_MxrbIndexCache")

  @db.get_first_value(
    "SELECT IndexData FROM _MxrbIndexCache WHERE Fingerprint = ?", [fingerprint]
  )
rescue SQLite3::Exception
  nil
end

#readonly?Boolean

Returns:

  • (Boolean)


29
# File 'lib/mxrb/io/mpr_file.rb', line 29

def readonly? = @readonly

#relocate_unit(uuid, container_uuid:, containment_name:) ⇒ Object

Raises:



259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/mxrb/io/mpr_file.rb', line 259

def relocate_unit(uuid, container_uuid:, containment_name:)
  raise ReadOnlyError, "Opened in read-only mode" if @readonly

  @db.execute(
    "UPDATE Unit SET ContainerID = ?, ContainmentName = ? WHERE UnitID = ?",
    [
      BsonCodec.uuid_to_blob(container_uuid),
      containment_name.to_s,
      BsonCodec.uuid_to_blob(uuid)
    ]
  )
end

#repair_content_hashes!Object

Repairs stale Unit.ContentsHash metadata without reserializing or otherwise changing the BSON/mxunit payload. Returns an audit trail.

Raises:



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/mxrb/io/mpr_file.rb', line 228

def repair_content_hashes!
  raise ReadOnlyError, "Opened in read-only mode" if @readonly

  repairs = []
  transaction do
    all_units.each do |unit|
      bytes = content_bytes(unit)
      next if bytes.to_s.empty?

      previous = unit['ContentsHash'].to_s
      current = BsonCodec.contents_hash(bytes)
      next if previous == current

      @db.execute(
        'UPDATE Unit SET ContentsHash = ? WHERE UnitID = ?',
        [current, BsonCodec.uuid_to_blob(unit.fetch('UnitID'))]
      )
      repairs << { unit_id: unit.fetch('UnitID'), previous:, current: }
    end
  end
  repairs
end

#restore_from!(backup_path) ⇒ Object

Restores the database from a backup file, replacing the current contents. Closes and reopens the underlying SQLite connection.

Raises:



290
291
292
293
294
295
296
297
298
# File 'lib/mxrb/io/mpr_file.rb', line 290

def restore_from!(backup_path)
  raise ReadOnlyError, "Opened in read-only mode" if @readonly

  @db.close
  FileUtils.cp(backup_path, @path)
  @db             = open_db
  @mendix_version = nil
  @format_version = detect_format
end

#root_unitObject

The root Unit is the one where UnitID == ContainerID.



80
81
82
83
84
85
86
87
88
# File 'lib/mxrb/io/mpr_file.rb', line 80

def root_unit
  @root_unit ||= begin
    row = @db.get_first_row(
      "SELECT #{unit_select_columns} FROM Unit " \
      "WHERE UnitID = ContainerID LIMIT 1"
    )
    row ? raw_to_hash(row) : nil
  end
end

#ruby_app_sourcesObject

Ruby/React sources are stored outside the Mendix Unit tree. Mendix can keep editing the native model while MXRB can later restore the exact conventional application sources on a Ruby-mode export.



512
513
514
515
516
517
518
519
520
521
# File 'lib/mxrb/io/mpr_file.rb', line 512

def ruby_app_sources
  return [] unless tables.include?("_MxrbRubySource")

  has_mode = table_info("_MxrbRubySource").any? { (_1["name"] || _1[:name]) == "Mode" }
  columns = has_mode ? "Path, Contents, Sha256, Mode" : "Path, Contents, Sha256"
  @db.execute("SELECT #{columns} FROM _MxrbRubySource ORDER BY Path").map do |row|
    fallback = row[0].to_s.start_with?('bin/') ? 0o755 : 0o644
    { path: row[0], contents: row[1], sha256: row[2], mode: has_mode ? row[3] : fallback }
  end
end

#table_info(name) ⇒ Object



306
307
308
# File 'lib/mxrb/io/mpr_file.rb', line 306

def table_info(name)
  @db.table_info(name)
end

#tablesObject

── Exploration helpers ───────────────────────────────────────────────



302
303
304
# File 'lib/mxrb/io/mpr_file.rb', line 302

def tables
  @db.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").flatten
end

#transactionObject



272
273
274
# File 'lib/mxrb/io/mpr_file.rb', line 272

def transaction(&)
  @db.transaction(&)
end

#unit(uuid) ⇒ Object

Single unit by UUID string.



110
111
112
113
114
115
116
117
# File 'lib/mxrb/io/mpr_file.rb', line 110

def unit(uuid)
  blob = BsonCodec.uuid_to_blob(uuid)
  row  = @db.get_first_row(
    "SELECT #{unit_select_columns} FROM Unit WHERE UnitID = ?",
    [blob]
  )
  row ? raw_to_hash(row) : nil
end

#units_by_containment(name) ⇒ Object

All units with a given ContainmentName.



91
92
93
94
95
96
97
# File 'lib/mxrb/io/mpr_file.rb', line 91

def units_by_containment(name)
  @db.execute(
    "SELECT #{unit_select_columns} FROM Unit " \
    "WHERE ContainmentName = ?",
    [name]
  ).map { raw_to_hash(_1) }
end

#update_unit(uuid, contents_doc) ⇒ Object

Update an existing unit's contents. Recalculates ContentsHash automatically.

Raises:



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/mxrb/io/mpr_file.rb', line 198

def update_unit(uuid, contents_doc)
  raise ReadOnlyError, "Opened in read-only mode" if @readonly

  blob       = BsonCodec.uuid_to_blob(uuid)
  bson_bytes = BsonCodec.serialize(contents_doc)
  hash       = BsonCodec.contents_hash(bson_bytes)
  current = unit(uuid)
  if current && current['ContentsHash'] == hash
    write_stats[:skipped] += 1
    return false
  end

  if contents_column?
    @db.execute(
      "UPDATE Unit SET Contents = ?, ContentsHash = ? WHERE UnitID = ?",
      [@format_version == :v2 ? nil : bson_bytes, hash, blob]
    )
  else
    @db.execute(
      "UPDATE Unit SET ContentsHash = ? WHERE UnitID = ?",
      [hash, blob]
    )
  end
  write_v2_unit(uuid, bson_bytes) if @format_version == :v2
  write_stats[:updated] += 1
  true
end

#update_version!(version) ⇒ Object



54
55
56
57
58
59
60
61
62
63
64
# File 'lib/mxrb/io/mpr_file.rb', line 54

def update_version!(version)
  version_str = version.to_s
  # Try new-style column first, fall back to old-style
  begin
    @db.execute("UPDATE _MetaData SET _ProductVersion = ?, _BuildVersion = ?",
                [version_str, version_str])
  rescue SQLite3::Exception
    @db.execute("UPDATE _MetaData SET MendixVersion = ?", [version_str])
  end
  @mendix_version = version_str
end

#vec_drop_index!(vec_table, meta_table) ⇒ Object



478
479
480
481
482
# File 'lib/mxrb/io/mpr_file.rb', line 478

def vec_drop_index!(vec_table, meta_table)
  ensure_vector_write!
  @db.execute("DROP TABLE IF EXISTS #{vector_identifier(vec_table)}")
  @db.execute("DROP TABLE IF EXISTS #{vector_identifier(meta_table)}")
end

#vec_knn(table, json_vec, limit) ⇒ Object



464
465
466
467
468
469
470
471
# File 'lib/mxrb/io/mpr_file.rb', line 464

def vec_knn(table, json_vec, limit)
  identifier = vector_identifier(table)
  @db.execute(
    "SELECT artifact_id, distance FROM #{identifier} " \
    "WHERE embedding MATCH ? ORDER BY distance LIMIT ?",
    [json_vec, Integer(limit)]
  ).map { { id: _1[0], distance: _1[1] } }
end

#vec_meta(table) ⇒ Object



440
441
442
443
444
445
446
447
448
449
450
451
452
453
# File 'lib/mxrb/io/mpr_file.rb', line 440

def vec_meta(table)
  name = table.to_s
  return nil unless tables.include?(name)

  identifier = vector_identifier(name)
  row = @db.get_first_row(
    "SELECT Backend, Dimension, Fingerprint FROM #{identifier} WHERE ID = 1"
  )
  return nil unless row

  { backend: row[0], dimension: row[1], fingerprint: row[2] }
rescue SQLite3::Exception
  nil
end

#vec_transactionObject



473
474
475
476
# File 'lib/mxrb/io/mpr_file.rb', line 473

def vec_transaction(&)
  ensure_vector_write!
  @db.transaction(&)
end

#vec_upsert(table, artifact_id, json_vec) ⇒ Object



455
456
457
458
459
460
461
462
# File 'lib/mxrb/io/mpr_file.rb', line 455

def vec_upsert(table, artifact_id, json_vec)
  ensure_vector_write!
  identifier = vector_identifier(table)
  @db.execute(
    "INSERT INTO #{identifier}(artifact_id, embedding) VALUES (?, ?)",
    [artifact_id, json_vec]
  )
end

#write_architecture_definition(definition) ⇒ Object

Raises:



494
495
496
497
498
499
500
501
502
503
504
505
506
507
# File 'lib/mxrb/io/mpr_file.rb', line 494

def write_architecture_definition(definition)
  raise ReadOnlyError, "Opened in read-only mode" if @readonly
  @db.execute(<<~SQL)
    CREATE TABLE IF NOT EXISTS _MxrbArchitecture (
      ID INTEGER PRIMARY KEY CHECK (ID = 1),
      Version INTEGER NOT NULL,
      Definition TEXT NOT NULL
    )
  SQL
  @db.execute(
    "INSERT OR REPLACE INTO _MxrbArchitecture (ID, Version, Definition) VALUES (1, 1, ?)",
    [JSON.generate(definition)]
  )
end

#write_domain_diagram_anchors(layouts) ⇒ Object

Raises:



563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
# File 'lib/mxrb/io/mpr_file.rb', line 563

def write_domain_diagram_anchors(layouts)
  raise ReadOnlyError, "Opened in read-only mode" if @readonly
  items = Array(layouts)
  return 0 if items.empty?

  @db.execute(<<~SQL)
    CREATE TABLE IF NOT EXISTS _MxrbDomainDiagramAssociation (
      AssociationID TEXT PRIMARY KEY NOT NULL,
      SourceAnchor TEXT NOT NULL,
      TargetAnchor TEXT NOT NULL
    )
  SQL
  current = domain_diagram_anchors
  items.count do |layout|
    id = layout.fetch(:id).to_s
    anchors = {
      source_anchor: layout.fetch(:source_anchor).to_s,
      target_anchor: layout.fetch(:target_anchor).to_s
    }
    next false if current[id] == anchors

    @db.execute(
      "INSERT OR REPLACE INTO _MxrbDomainDiagramAssociation " \
      "(AssociationID, SourceAnchor, TargetAnchor) VALUES (?, ?, ?)",
      [id, anchors.fetch(:source_anchor), anchors.fetch(:target_anchor)]
    )
    true
  end
end

#write_index_cache(fingerprint, json) ⇒ Object

Persists the index JSON keyed by fingerprint. No-op when read-only or on error.



328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
# File 'lib/mxrb/io/mpr_file.rb', line 328

def write_index_cache(fingerprint, json)
  return if @readonly || json.nil?

  @db.execute(<<~SQL)
    CREATE TABLE IF NOT EXISTS _MxrbIndexCache (
      Fingerprint TEXT PRIMARY KEY,
      IndexData   TEXT NOT NULL
    )
  SQL
  @db.execute(
    "INSERT INTO _MxrbIndexCache (Fingerprint, IndexData) VALUES (?, ?) " \
    "ON CONFLICT(Fingerprint) DO UPDATE SET IndexData = excluded.IndexData",
    [fingerprint, json]
  )
  @db.execute(
    "DELETE FROM _MxrbIndexCache WHERE Fingerprint <> ?", [fingerprint]
  )
rescue SQLite3::Exception
  nil
end

#write_legacy_unit_identity_mismatches(mismatches) ⇒ Object

Raises:



605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
# File 'lib/mxrb/io/mpr_file.rb', line 605

def write_legacy_unit_identity_mismatches(mismatches)
  raise ReadOnlyError, "Opened in read-only mode" if @readonly

  @db.execute(<<~SQL)
    CREATE TABLE IF NOT EXISTS _MxrbCompatibility (
      Kind TEXT NOT NULL,
      UnitID TEXT NOT NULL,
      ContentID TEXT NOT NULL,
      UnitType TEXT NOT NULL,
      PRIMARY KEY (Kind, UnitID, ContentID)
    )
  SQL
  @db.execute("DELETE FROM _MxrbCompatibility WHERE Kind = 'legacy-unit-identity'")
  mismatches.each do |mismatch|
    @db.execute(
      "INSERT INTO _MxrbCompatibility (Kind, UnitID, ContentID, UnitType) VALUES (?, ?, ?, ?)",
      ["legacy-unit-identity", mismatch.fetch(:unit_id),
       mismatch.fetch(:content_id), mismatch.fetch(:type)]
    )
  end
end

#write_ruby_app_sources(files) ⇒ Object

Raises:



523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
# File 'lib/mxrb/io/mpr_file.rb', line 523

def write_ruby_app_sources(files)
  raise ReadOnlyError, "Opened in read-only mode" if @readonly

  @db.execute(<<~SQL)
    CREATE TABLE IF NOT EXISTS _MxrbRubySource (
      Path TEXT PRIMARY KEY NOT NULL,
      Contents BLOB NOT NULL,
      Sha256 TEXT NOT NULL,
      Mode INTEGER NOT NULL DEFAULT 420
    )
  SQL
  unless table_info("_MxrbRubySource").any? { (_1["name"] || _1[:name]) == "Mode" }
    @db.execute("ALTER TABLE _MxrbRubySource ADD COLUMN Mode INTEGER NOT NULL DEFAULT 420")
  end
  @db.execute("DELETE FROM _MxrbRubySource")
  files.each do |file|
    @db.execute(
      "INSERT INTO _MxrbRubySource (Path, Contents, Sha256, Mode) VALUES (?, ?, ?, ?)",
      [
        file.fetch(:path), SQLite3::Blob.new(file.fetch(:contents)),
        file.fetch(:sha256), file.fetch(:mode, 0o644)
      ]
    )
  end
end

#write_statsObject



25
26
27
# File 'lib/mxrb/io/mpr_file.rb', line 25

def write_stats
  @write_stats ||= { inserted: 0, updated: 0, skipped: 0, deleted: 0 }
end

#write_vec_meta!(table, backend, dimension, fingerprint) ⇒ Object



430
431
432
433
434
435
436
437
438
# File 'lib/mxrb/io/mpr_file.rb', line 430

def write_vec_meta!(table, backend, dimension, fingerprint)
  ensure_vector_write!
  identifier = vector_identifier(table)
  @db.execute(
    "INSERT OR REPLACE INTO #{identifier} " \
    "(ID, Backend, Dimension, Fingerprint) VALUES (1, ?, ?, ?)",
    [backend, dimension, fingerprint]
  )
end