Class: Mxrb::Runtime::SchemaMigrator

Inherits:
Object
  • Object
show all
Defined in:
lib/mxrb/runtime/schema_migrator.rb

Overview

Derives a deterministic SQLite schema from a parsed Mendix project and evolves an existing database without relying on the Java Runtime. rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity

Constant Summary collapse

SYSTEM_COLUMNS =
{
  owner: %w[__owner_id TEXT],
  created_date: %w[__created_at TEXT],
  changed_date: %w[__changed_at TEXT],
  changed_by: %w[__changed_by_id TEXT]
}.freeze
TYPE_MAP =
{
  integer: 'INTEGER', long: 'INTEGER', autonumber: 'INTEGER', boolean: 'INTEGER',
  float: 'REAL', decimal: 'REAL', binary: 'BLOB', datetime: 'TEXT', enum: 'TEXT',
  hashstring: 'TEXT', string: 'TEXT'
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(project, database:, allow_destructive: false) ⇒ SchemaMigrator

Returns a new instance of SchemaMigrator.



78
79
80
81
82
83
84
# File 'lib/mxrb/runtime/schema_migrator.rb', line 78

def initialize(project, database:, allow_destructive: false)
  @project = project
  @allow_destructive = allow_destructive == true
  @owns_database = !database.is_a?(SQLite3::Database)
  @database = @owns_database ? SQLite3::Database.new(database.to_s) : database
  @schema = self.class.derive(project)
end

Instance Attribute Details

#migration_planObject (readonly)

Returns the value of attribute migration_plan.



76
77
78
# File 'lib/mxrb/runtime/schema_migrator.rb', line 76

def migration_plan
  @migration_plan
end

#schemaObject (readonly)

Returns the value of attribute schema.



76
77
78
# File 'lib/mxrb/runtime/schema_migrator.rb', line 76

def schema
  @schema
end

Class Method Details

.derive(project) ⇒ Object



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/mxrb/runtime/schema_migrator.rb', line 86

def self.derive(project)
  entities = []
  associations = []
  project.modules.each do |mod|
    by_id = mod.entities.to_h { [_1.id.to_s, qualified_entity_name(mod, _1)] }
    by_id.merge!(mod.entities.to_h { [qualified_entity_name(mod, _1), qualified_entity_name(mod, _1)] })
    mod.entities.select { _1.persistable != false && !_1.oql_view? }.each do |entity|
      entities << entity_schema(mod, entity)
    end
    mod.associations.each do |association|
      from = by_id[association.from_entity_id.to_s] || association.from_entity_id.to_s
      to = resolve_target(project, association.to_entity_id, by_id)
      next if from.empty? || to.empty?

      qualified = "#{mod.name}.#{association.name}"
      key = association.id.to_s.empty? ? qualified : association.id.to_s
      associations << AssociationSchema.new(
        association.name.to_s, qualified, key, physical_name('association', key),
        from, to, association.association_type.to_sym
      )
    end
  end
  RuntimeSchema.new(entities.freeze, associations.freeze)
end

.entity_schema(mod, entity) ⇒ Object



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/mxrb/runtime/schema_migrator.rb', line 111

def self.entity_schema(mod, entity)
  qualified = qualified_entity_name(mod, entity)
  key = entity.data_storage_guid.to_s
  key = entity.id.to_s if key.empty?
  key = qualified if key.empty?
  columns = entity.attributes.map do |attribute|
    attribute_key = attribute.data_storage_guid.to_s
    attribute_key = attribute.id.to_s if attribute_key.empty?
    attribute_key = "#{key}:#{attribute.name}" if attribute_key.empty?
    type = attribute.type.to_sym
    SchemaColumn.new(
      attribute.name.to_s, attribute_key, physical_name('attribute', attribute_key),
      TYPE_MAP.fetch(type, 'TEXT'), type, attribute.default_value, attribute.required == true,
      attribute.unique == true || type == :autonumber
    )
  end
  flags = (entity.system_members || {}).to_h.each_with_object({}) do |(name, enabled), selected|
    selected[name.to_sym] = SYSTEM_COLUMNS.fetch(name.to_sym) if enabled && SYSTEM_COLUMNS.key?(name.to_sym)
  end
  EntitySchema.new(qualified, key, physical_name('entity', key), columns.freeze, flags.freeze)
end

.physical_name(kind, key) ⇒ Object



150
151
152
# File 'lib/mxrb/runtime/schema_migrator.rb', line 150

def self.physical_name(kind, key)
  "mxrb_#{kind}_#{Digest::SHA256.hexdigest(key.to_s)[0, 20]}"
end

.qualified_entity_name(mod, entity) ⇒ Object



133
134
135
136
# File 'lib/mxrb/runtime/schema_migrator.rb', line 133

def self.qualified_entity_name(mod, entity)
  value = entity.qualified_name.to_s
  value.empty? ? "#{mod.name}.#{entity.name}" : value
end

.resolve_target(project, pointer, local) ⇒ Object



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

def self.resolve_target(project, pointer, local)
  value = pointer.to_s
  return local[value] if local.key?(value)
  return value if value.include?('.')

  project.modules.each do |mod|
    entity = mod.entities.find { _1.id.to_s == value }
    return qualified_entity_name(mod, entity) if entity
  end
  ''
end

Instance Method Details

#migrate!Object



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/mxrb/runtime/schema_migrator.rb', line 154

def migrate!
  created = []
  added = []
  rebuilt = []
  transaction do
    
    @migration_plan = build_migration_plan
    reject_destructive_migration! if migration_plan.destructive? && !@allow_destructive
    apply_destructive_migration(rebuilt) if migration_plan.destructive?
    schema.entities.each { migrate_entity(_1, created, added, rebuilt) }
    schema.associations.each { migrate_association(_1, created, rebuilt) }
    
    
  end
  MigrationResult.new(created.freeze, added.freeze, rebuilt.freeze)
ensure
  @database.close if @owns_database
end