mxrb 0.1.2 → 0.1.4
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/bin/mxrb +458 -10
- data/docs/de-DE/README.md +1 -0
- data/docs/de-DE/oql-sql.md +42 -0
- data/docs/de-DE/project-structure.md +201 -2
- data/docs/de-DE/runtime-ruby.md +104 -0
- data/docs/de-DE/runtime-schema-migrations.md +44 -0
- data/docs/de-DE/validation-matrix.md +42 -4
- data/docs/en-US/README.md +1 -0
- data/docs/en-US/oql-sql.md +41 -0
- data/docs/en-US/project-structure.md +192 -2
- data/docs/en-US/runtime-ruby.md +100 -0
- data/docs/en-US/runtime-schema-migrations.md +41 -0
- data/docs/en-US/validation-matrix.md +39 -4
- data/docs/pt-BR/README.md +1 -0
- data/docs/pt-BR/entity-dsl.md +8 -0
- data/docs/pt-BR/oql-sql.md +41 -0
- data/docs/pt-BR/project-structure.md +197 -2
- data/docs/pt-BR/runtime-ruby.md +108 -0
- data/docs/pt-BR/runtime-schema-migrations.md +40 -0
- data/docs/pt-BR/validation-matrix.md +42 -5
- data/docs/pt-BR/writing.md +28 -0
- data/lib/mxrb/cli/help.rb +426 -0
- data/lib/mxrb/cli/release.rb +137 -0
- data/lib/mxrb/compiler/compatibility_analyzer.rb +1 -1
- data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +8 -1
- data/lib/mxrb/compiler/domain_document_compiler.rb +3 -0
- data/lib/mxrb/compiler/gallery_bundle_compiler.rb +11 -1
- data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +76 -19
- data/lib/mxrb/compiler/legacy_custom_widget_compiler.rb +159 -0
- data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +43 -7
- data/lib/mxrb/compiler/legacy_page_builder.rb +1248 -23
- data/lib/mxrb/compiler/packager.rb +27 -13
- data/lib/mxrb/compiler/page_bundle_compiler.rb +13 -4
- data/lib/mxrb/compiler/portable_packager.rb +16 -11
- data/lib/mxrb/compiler/web_bundle_builder.rb +5 -1
- data/lib/mxrb/compiler/web_shell_materializer.rb +42 -2
- data/lib/mxrb/compiler/widget_package_extractor.rb +3 -0
- data/lib/mxrb/domain_diagram/lifecycle.rb +272 -0
- data/lib/mxrb/domain_diagram.rb +409 -0
- data/lib/mxrb/dsl/builder.rb +62 -11
- data/lib/mxrb/environment.rb +226 -0
- data/lib/mxrb/exporter.rb +265 -45
- data/lib/mxrb/http/server.rb +107 -0
- data/lib/mxrb/initializer.rb +80 -9
- data/lib/mxrb/integrity/validator.rb +32 -7
- data/lib/mxrb/io/bson_codec.rb +42 -1
- data/lib/mxrb/io/mpr_file.rb +146 -3
- data/lib/mxrb/model/attribute.rb +3 -0
- data/lib/mxrb/model/entity.rb +44 -2
- data/lib/mxrb/model/microflow.rb +3 -1
- data/lib/mxrb/model/module.rb +71 -0
- data/lib/mxrb/model/page.rb +126 -15
- data/lib/mxrb/model/project.rb +3 -0
- data/lib/mxrb/modeler/catalog.rb +178 -0
- data/lib/mxrb/official_marketplace/module_package_importer.rb +23 -12
- data/lib/mxrb/official_marketplace/widget_package_installer.rb +15 -11
- data/lib/mxrb/official_marketplace.rb +9 -3
- data/lib/mxrb/oql/reverse_translator.rb +394 -0
- data/lib/mxrb/oql/server.rb +4 -8
- data/lib/mxrb/oql.rb +32 -4
- data/lib/mxrb/progress.rb +242 -0
- data/lib/mxrb/project_lifecycle.rb +5 -2
- data/lib/mxrb/ruby_app/exporter.rb +1749 -0
- data/lib/mxrb/ruby_app/preset.rb +506 -0
- data/lib/mxrb/ruby_app/session_manager.rb +122 -0
- data/lib/mxrb/ruby_app.rb +1361 -0
- data/lib/mxrb/runtime/access_control.rb +408 -0
- data/lib/mxrb/runtime/native.rb +1015 -93
- data/lib/mxrb/runtime/scheduler.rb +396 -0
- data/lib/mxrb/runtime/schema_migrator.rb +598 -0
- data/lib/mxrb/runtime/shared_store.rb +335 -0
- data/lib/mxrb/runtime/sqlite_store.rb +729 -0
- data/lib/mxrb/scaffold/generator.rb +11 -3
- data/lib/mxrb/team_server.rb +10 -7
- data/lib/mxrb/uml/activity_diagram.rb +118 -0
- data/lib/mxrb/uml/class_diagram.rb +118 -0
- data/lib/mxrb/uml/sequence_diagram.rb +112 -0
- data/lib/mxrb/uml/server.rb +146 -0
- data/lib/mxrb/uml/support.rb +35 -0
- data/lib/mxrb/version.rb +1 -1
- data/lib/mxrb/web_ui/assets/abnfDiagram-N423BO3Z-C4JD3cEC.js +1 -0
- data/lib/mxrb/web_ui/assets/arc-3Z53kgFp.js +1 -0
- data/lib/mxrb/web_ui/assets/architecture-TIHT7OUA-BStBTLea.js +1 -0
- data/lib/mxrb/web_ui/assets/architectureDiagram-T3A2C74G-DBwqnM4J.js +36 -0
- data/lib/mxrb/web_ui/assets/array-BifhSqXX.js +1 -0
- data/lib/mxrb/web_ui/assets/blockDiagram-VBNYF7ZC-DLtuHt-j.js +132 -0
- data/lib/mxrb/web_ui/assets/c4Diagram-5PPSVZJV-ConSk_pe.js +10 -0
- data/lib/mxrb/web_ui/assets/channel-BhPvD7Dt.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-2GRJ4B5K-D2HpWsf_.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-4I5QYGJK-CiIdNwaF.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-5RXB4S5H-Bu_AaEkE.js +231 -0
- data/lib/mxrb/web_ui/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- data/lib/mxrb/web_ui/assets/chunk-6Q2QTUOP-wQkNmt_X.js +88 -0
- data/lib/mxrb/web_ui/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-GF5L2VYU-G8MmiBzU.js +206 -0
- data/lib/mxrb/web_ui/assets/chunk-I66GZJ75-CulcrLPh.js +127 -0
- data/lib/mxrb/web_ui/assets/chunk-JQJVKLGR-ukjzcdlL.js +156 -0
- data/lib/mxrb/web_ui/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-KBJHAD2P-Wm35lYov.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-KEIR6QF5-BICK3FdT.js +161 -0
- data/lib/mxrb/web_ui/assets/chunk-NSK5VX7P-D2nYrCY4.js +2 -0
- data/lib/mxrb/web_ui/assets/chunk-QR6OTTB3-BrdIzg4Q.js +62 -0
- data/lib/mxrb/web_ui/assets/chunk-RYQCIY6F-CCfZMcW6.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-UBXNYLIW-BPEVYsFK.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-W5SLKNZC-2E6OPhA4.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-WRU74C26-CBIvOyWp.js +70 -0
- data/lib/mxrb/web_ui/assets/chunk-XXDRQBXY-pH58XAyl.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- data/lib/mxrb/web_ui/assets/classDiagram-JCYQIIEL-xiqo4D89.js +1 -0
- data/lib/mxrb/web_ui/assets/classDiagram-v2-OCEON4UE-xiqo4D89.js +1 -0
- data/lib/mxrb/web_ui/assets/cose-bilkent-JH36ORCC-t_flPpii.js +1 -0
- data/lib/mxrb/web_ui/assets/cynefin-VYW2F7L2-BIqktTEv.js +1 -0
- data/lib/mxrb/web_ui/assets/cynefinDiagram-MW4NZA55-C2d--lut.js +62 -0
- data/lib/mxrb/web_ui/assets/cytoscape.esm-B-NFISlW.js +321 -0
- data/lib/mxrb/web_ui/assets/dagre-Buvkdvvj.js +1 -0
- data/lib/mxrb/web_ui/assets/dagre-VZM6K2ZE-CSz2_tb5.js +4 -0
- data/lib/mxrb/web_ui/assets/defaultLocale-BFoDCU3G.js +1 -0
- data/lib/mxrb/web_ui/assets/diagram-7IWD3JNH-DCt7u7V_.js +30 -0
- data/lib/mxrb/web_ui/assets/diagram-B4RE2ZJO-DqZBqkMu.js +3 -0
- data/lib/mxrb/web_ui/assets/diagram-LBJQPF4R-ClIYbIsI.js +24 -0
- data/lib/mxrb/web_ui/assets/diagram-Q27KOJAE-DFxhvthJ.js +24 -0
- data/lib/mxrb/web_ui/assets/diagram-UB23O5K3-BSUTXptv.js +41 -0
- data/lib/mxrb/web_ui/assets/dist-D2qOEbeJ.js +1 -0
- data/lib/mxrb/web_ui/assets/domain-mGOZ1KCG.css +1 -0
- data/lib/mxrb/web_ui/assets/domain-qsjXhy4v.js +1 -0
- data/lib/mxrb/web_ui/assets/ebnfDiagram-BXEA7PRR-B5reICEi.js +1 -0
- data/lib/mxrb/web_ui/assets/erDiagram-JOGREHBK-Co5Mxn2p.js +85 -0
- data/lib/mxrb/web_ui/assets/eventmodeling-45OFAUF4-C1ylhSks.js +1 -0
- data/lib/mxrb/web_ui/assets/flowDiagram-UKHOOZJN-CHXaJIaS.js +1 -0
- data/lib/mxrb/web_ui/assets/ganttDiagram-PKOTCBZU-DPPPaWaz.js +292 -0
- data/lib/mxrb/web_ui/assets/gitGraph-TEB2WS4Q-qS77ywA9.js +1 -0
- data/lib/mxrb/web_ui/assets/gitGraphDiagram-DS77QQ5N-DpAv6-BT.js +106 -0
- data/lib/mxrb/web_ui/assets/graphlib-DS17s2tU.js +1 -0
- data/lib/mxrb/web_ui/assets/info-DKCQHKI2-C9VD1Y4e.js +1 -0
- data/lib/mxrb/web_ui/assets/infoDiagram-6WML65LV-Bk1kbcCy.js +2 -0
- data/lib/mxrb/web_ui/assets/init-C-OQMol4.js +1 -0
- data/lib/mxrb/web_ui/assets/ishikawaDiagram-WSZJBQD7-0gcISBFR.js +70 -0
- data/lib/mxrb/web_ui/assets/journeyDiagram-NVQOT4AX-Dt0RQnlh.js +139 -0
- data/lib/mxrb/web_ui/assets/jsx-runtime-DV0a5kSb.js +9 -0
- data/lib/mxrb/web_ui/assets/kanban-definition-27J2QSJJ-DIPQM7w9.js +89 -0
- data/lib/mxrb/web_ui/assets/katex-CXMH3UgJ.js +257 -0
- data/lib/mxrb/web_ui/assets/line-C0rYD-tL.js +1 -0
- data/lib/mxrb/web_ui/assets/linear-BPPxWorj.js +1 -0
- data/lib/mxrb/web_ui/assets/map-BaFkSB1l.js +1 -0
- data/lib/mxrb/web_ui/assets/mermaid-parser.core-B_UPzTxa.js +7 -0
- data/lib/mxrb/web_ui/assets/mindmap-definition-FAOFIHXS-CdJRSMuk.js +96 -0
- data/lib/mxrb/web_ui/assets/modeler-BNqHthwk.css +1 -0
- data/lib/mxrb/web_ui/assets/modeler-CQmqflbg.js +1 -0
- data/lib/mxrb/web_ui/assets/ordinal-BDEzSJ7C.js +1 -0
- data/lib/mxrb/web_ui/assets/packet-7NZHBO7P-CwQ4NSBs.js +1 -0
- data/lib/mxrb/web_ui/assets/path-fybaL0A-.js +1 -0
- data/lib/mxrb/web_ui/assets/pegDiagram-VL7TDLO6-D40S7ppx.js +1 -0
- data/lib/mxrb/web_ui/assets/pie-RZYD4A2V-CJroHMKh.js +1 -0
- data/lib/mxrb/web_ui/assets/pieDiagram-7S7Q4E2Y-QB54__BV.js +39 -0
- data/lib/mxrb/web_ui/assets/quadrantDiagram-CIZ2JOQS-DBhDS6ag.js +7 -0
- data/lib/mxrb/web_ui/assets/radar-I7S5WNFK-DdIUwd2d.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-3IZDKUUU-B1Lw6Xyq.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-abnf-AHOZXSZD-CQgQoeak.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-ebnf-EBAXGLYW-D7qEQ2x8.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-peg-LSFZ7HO6-C005Vxbb.js +1 -0
- data/lib/mxrb/web_ui/assets/railroadDiagram-AXF67PYL-BBP2qk7M.js +1 -0
- data/lib/mxrb/web_ui/assets/requirementDiagram-LRYGKXZP-D2cLsAoI.js +84 -0
- data/lib/mxrb/web_ui/assets/rough.esm-Dy-Kn_BL.js +1 -0
- data/lib/mxrb/web_ui/assets/sankeyDiagram-W5VNT64P-Bpi3FiP2.js +40 -0
- data/lib/mxrb/web_ui/assets/sequenceDiagram-SI44F4Z6-DCRcDlVe.js +162 -0
- data/lib/mxrb/web_ui/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
- data/lib/mxrb/web_ui/assets/src-CvxfUak2.js +1 -0
- data/lib/mxrb/web_ui/assets/stateDiagram-OKZ733FA-C2GCEoxl.js +1 -0
- data/lib/mxrb/web_ui/assets/stateDiagram-v2-UEYNNEHI-D2gWgedh.js +1 -0
- data/lib/mxrb/web_ui/assets/swimlanes-SLNWSIFB-CRI9irEW.js +2 -0
- data/lib/mxrb/web_ui/assets/swimlanesDiagram-ULZ7WXOC-C5Wb_-hZ.js +8 -0
- data/lib/mxrb/web_ui/assets/theme-_mxc7BdY.css +1 -0
- data/lib/mxrb/web_ui/assets/timeline-definition-Z64GVDOM-B1-mNg-h.js +120 -0
- data/lib/mxrb/web_ui/assets/treeView-QDETBFTQ-7CTvIwKq.js +1 -0
- data/lib/mxrb/web_ui/assets/treemap-6X3UGDF4-CtL22pq5.js +1 -0
- data/lib/mxrb/web_ui/assets/uml-C3KAFGTq.js +51 -0
- data/lib/mxrb/web_ui/assets/uml-Dn-qd01X.css +1 -0
- data/lib/mxrb/web_ui/assets/vennDiagram-T6HMQDX7-BzVnUEAy.js +34 -0
- data/lib/mxrb/web_ui/assets/wardley-OPB4EBWU-JNi-QNag.js +1 -0
- data/lib/mxrb/web_ui/assets/wardleyDiagram-T6FBY63Y-BUwmAQ-B.js +78 -0
- data/lib/mxrb/web_ui/assets/xychartDiagram-ELKLHX3M-DEQS8sKp.js +7 -0
- data/lib/mxrb/web_ui/domain.html +15 -0
- data/lib/mxrb/web_ui/modeler.html +16 -0
- data/lib/mxrb/web_ui/uml.html +31 -0
- data/lib/mxrb/web_ui.rb +89 -0
- data/lib/mxrb/widget_certification.rb +208 -0
- data/lib/mxrb/writer.rb +278 -41
- data/lib/mxrb.rb +38 -8
- metadata +157 -5
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'sqlite3'
|
|
5
|
+
|
|
6
|
+
module Mxrb
|
|
7
|
+
module Runtime
|
|
8
|
+
SchemaColumn = Data.define(
|
|
9
|
+
:name, :storage_key, :sql_name, :sql_type, :type, :default, :required, :unique
|
|
10
|
+
)
|
|
11
|
+
EntitySchema = Data.define(:name, :storage_key, :table, :columns, :system_members)
|
|
12
|
+
AssociationSchema = Data.define(
|
|
13
|
+
:name, :qualified_name, :storage_key, :table, :from_entity, :to_entity, :type
|
|
14
|
+
)
|
|
15
|
+
RuntimeSchema = Data.define(:entities, :associations) do
|
|
16
|
+
def entity(name)
|
|
17
|
+
qualified = name.to_s
|
|
18
|
+
exact = entities.find { _1.name == qualified }
|
|
19
|
+
return exact if exact
|
|
20
|
+
|
|
21
|
+
matches = entities.select { _1.name.split('.').last == qualified }
|
|
22
|
+
return matches.first if matches.one?
|
|
23
|
+
|
|
24
|
+
raise ArgumentError, matches.empty? ? "unknown entity #{name}" : "ambiguous entity #{name}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def association(name)
|
|
28
|
+
key = name.to_s
|
|
29
|
+
matches = associations.select do |candidate|
|
|
30
|
+
candidate.qualified_name == key || candidate.name == key ||
|
|
31
|
+
candidate.qualified_name.split('.').last == key
|
|
32
|
+
end
|
|
33
|
+
return matches.first if matches.one?
|
|
34
|
+
|
|
35
|
+
raise ArgumentError, matches.empty? ? "unknown association #{name}" : "ambiguous association #{name}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
MigrationResult = Data.define(:created_tables, :added_columns, :rebuilt_tables)
|
|
39
|
+
SchemaMigrationPlan = Data.define(
|
|
40
|
+
:removed_entities, :removed_attributes, :removed_associations
|
|
41
|
+
) do
|
|
42
|
+
def destructive?
|
|
43
|
+
removed_entities.any? || removed_attributes.any? || removed_associations.any?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def changes = [*removed_entities, *removed_attributes, *removed_associations].freeze
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Raised before DDL when an evolution cannot preserve existing data safely.
|
|
50
|
+
class UnsafeSchemaMigrationError < StandardError
|
|
51
|
+
attr_reader :changes
|
|
52
|
+
|
|
53
|
+
def initialize(message, changes: [])
|
|
54
|
+
@changes = changes.freeze
|
|
55
|
+
super(message)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Derives a deterministic SQLite schema from a parsed Mendix project and
|
|
60
|
+
# evolves an existing database without relying on the Java Runtime.
|
|
61
|
+
# rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity
|
|
62
|
+
# rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
63
|
+
class SchemaMigrator
|
|
64
|
+
SYSTEM_COLUMNS = {
|
|
65
|
+
owner: %w[__owner_id TEXT],
|
|
66
|
+
created_date: %w[__created_at TEXT],
|
|
67
|
+
changed_date: %w[__changed_at TEXT],
|
|
68
|
+
changed_by: %w[__changed_by_id TEXT]
|
|
69
|
+
}.freeze
|
|
70
|
+
TYPE_MAP = {
|
|
71
|
+
integer: 'INTEGER', long: 'INTEGER', autonumber: 'INTEGER', boolean: 'INTEGER',
|
|
72
|
+
float: 'REAL', decimal: 'REAL', binary: 'BLOB', datetime: 'TEXT', enum: 'TEXT',
|
|
73
|
+
hashstring: 'TEXT', string: 'TEXT'
|
|
74
|
+
}.freeze
|
|
75
|
+
|
|
76
|
+
attr_reader :schema, :migration_plan
|
|
77
|
+
|
|
78
|
+
def initialize(project, database:, allow_destructive: false)
|
|
79
|
+
@project = project
|
|
80
|
+
@allow_destructive = allow_destructive == true
|
|
81
|
+
@owns_database = !database.is_a?(SQLite3::Database)
|
|
82
|
+
@database = @owns_database ? SQLite3::Database.new(database.to_s) : database
|
|
83
|
+
@schema = self.class.derive(project)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.derive(project)
|
|
87
|
+
entities = []
|
|
88
|
+
associations = []
|
|
89
|
+
project.modules.each do |mod|
|
|
90
|
+
by_id = mod.entities.to_h { [_1.id.to_s, qualified_entity_name(mod, _1)] }
|
|
91
|
+
by_id.merge!(mod.entities.to_h { [qualified_entity_name(mod, _1), qualified_entity_name(mod, _1)] })
|
|
92
|
+
mod.entities.select { _1.persistable != false && !_1.oql_view? }.each do |entity|
|
|
93
|
+
entities << entity_schema(mod, entity)
|
|
94
|
+
end
|
|
95
|
+
mod.associations.each do |association|
|
|
96
|
+
from = by_id[association.from_entity_id.to_s] || association.from_entity_id.to_s
|
|
97
|
+
to = resolve_target(project, association.to_entity_id, by_id)
|
|
98
|
+
next if from.empty? || to.empty?
|
|
99
|
+
|
|
100
|
+
qualified = "#{mod.name}.#{association.name}"
|
|
101
|
+
key = association.id.to_s.empty? ? qualified : association.id.to_s
|
|
102
|
+
associations << AssociationSchema.new(
|
|
103
|
+
association.name.to_s, qualified, key, physical_name('association', key),
|
|
104
|
+
from, to, association.association_type.to_sym
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
RuntimeSchema.new(entities.freeze, associations.freeze)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.entity_schema(mod, entity)
|
|
112
|
+
qualified = qualified_entity_name(mod, entity)
|
|
113
|
+
key = entity.data_storage_guid.to_s
|
|
114
|
+
key = entity.id.to_s if key.empty?
|
|
115
|
+
key = qualified if key.empty?
|
|
116
|
+
columns = entity.attributes.map do |attribute|
|
|
117
|
+
attribute_key = attribute.data_storage_guid.to_s
|
|
118
|
+
attribute_key = attribute.id.to_s if attribute_key.empty?
|
|
119
|
+
attribute_key = "#{key}:#{attribute.name}" if attribute_key.empty?
|
|
120
|
+
type = attribute.type.to_sym
|
|
121
|
+
SchemaColumn.new(
|
|
122
|
+
attribute.name.to_s, attribute_key, physical_name('attribute', attribute_key),
|
|
123
|
+
TYPE_MAP.fetch(type, 'TEXT'), type, attribute.default_value, attribute.required == true,
|
|
124
|
+
attribute.unique == true || type == :autonumber
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
flags = (entity.system_members || {}).to_h.each_with_object({}) do |(name, enabled), selected|
|
|
128
|
+
selected[name.to_sym] = SYSTEM_COLUMNS.fetch(name.to_sym) if enabled && SYSTEM_COLUMNS.key?(name.to_sym)
|
|
129
|
+
end
|
|
130
|
+
EntitySchema.new(qualified, key, physical_name('entity', key), columns.freeze, flags.freeze)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def self.qualified_entity_name(mod, entity)
|
|
134
|
+
value = entity.qualified_name.to_s
|
|
135
|
+
value.empty? ? "#{mod.name}.#{entity.name}" : value
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def self.resolve_target(project, pointer, local)
|
|
139
|
+
value = pointer.to_s
|
|
140
|
+
return local[value] if local.key?(value)
|
|
141
|
+
return value if value.include?('.')
|
|
142
|
+
|
|
143
|
+
project.modules.each do |mod|
|
|
144
|
+
entity = mod.entities.find { _1.id.to_s == value }
|
|
145
|
+
return qualified_entity_name(mod, entity) if entity
|
|
146
|
+
end
|
|
147
|
+
''
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def self.physical_name(kind, key)
|
|
151
|
+
"mxrb_#{kind}_#{Digest::SHA256.hexdigest(key.to_s)[0, 20]}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def migrate!
|
|
155
|
+
created = []
|
|
156
|
+
added = []
|
|
157
|
+
rebuilt = []
|
|
158
|
+
transaction do
|
|
159
|
+
create_metadata_tables
|
|
160
|
+
@migration_plan = build_migration_plan
|
|
161
|
+
reject_destructive_migration! if migration_plan.destructive? && !@allow_destructive
|
|
162
|
+
apply_destructive_migration(rebuilt) if migration_plan.destructive?
|
|
163
|
+
schema.entities.each { migrate_entity(_1, created, added, rebuilt) }
|
|
164
|
+
schema.associations.each { migrate_association(_1, created, rebuilt) }
|
|
165
|
+
synchronize_metadata
|
|
166
|
+
prune_obsolete_metadata
|
|
167
|
+
end
|
|
168
|
+
MigrationResult.new(created.freeze, added.freeze, rebuilt.freeze)
|
|
169
|
+
ensure
|
|
170
|
+
@database.close if @owns_database
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
def transaction
|
|
176
|
+
@database.execute('BEGIN IMMEDIATE')
|
|
177
|
+
yield
|
|
178
|
+
@database.execute('COMMIT')
|
|
179
|
+
rescue StandardError
|
|
180
|
+
@database.execute('ROLLBACK') if @database.transaction_active?
|
|
181
|
+
raise
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def create_metadata_tables
|
|
185
|
+
@database.execute(<<~SQL)
|
|
186
|
+
CREATE TABLE IF NOT EXISTS mxrb_schema_entities (
|
|
187
|
+
storage_key TEXT PRIMARY KEY, logical_name TEXT NOT NULL, table_name TEXT NOT NULL
|
|
188
|
+
)
|
|
189
|
+
SQL
|
|
190
|
+
@database.execute(<<~SQL)
|
|
191
|
+
CREATE TABLE IF NOT EXISTS mxrb_schema_attributes (
|
|
192
|
+
entity_key TEXT NOT NULL, storage_key TEXT NOT NULL, logical_name TEXT NOT NULL,
|
|
193
|
+
column_name TEXT NOT NULL, logical_type TEXT NOT NULL,
|
|
194
|
+
required INTEGER NOT NULL DEFAULT 0, unique_value INTEGER NOT NULL DEFAULT 0,
|
|
195
|
+
PRIMARY KEY (entity_key, storage_key)
|
|
196
|
+
)
|
|
197
|
+
SQL
|
|
198
|
+
@database.execute(<<~SQL)
|
|
199
|
+
CREATE TABLE IF NOT EXISTS mxrb_schema_associations (
|
|
200
|
+
storage_key TEXT PRIMARY KEY, logical_name TEXT NOT NULL, table_name TEXT NOT NULL,
|
|
201
|
+
from_entity TEXT NOT NULL, to_entity TEXT NOT NULL, association_type TEXT NOT NULL
|
|
202
|
+
)
|
|
203
|
+
SQL
|
|
204
|
+
@database.execute(<<~SQL)
|
|
205
|
+
CREATE TABLE IF NOT EXISTS mxrb_schema_sequences (
|
|
206
|
+
entity_key TEXT NOT NULL, attribute_key TEXT NOT NULL, next_value INTEGER NOT NULL,
|
|
207
|
+
PRIMARY KEY (entity_key, attribute_key)
|
|
208
|
+
)
|
|
209
|
+
SQL
|
|
210
|
+
ensure_metadata_column('mxrb_schema_attributes', 'required', 'INTEGER NOT NULL DEFAULT 0')
|
|
211
|
+
ensure_metadata_column('mxrb_schema_attributes', 'unique_value', 'INTEGER NOT NULL DEFAULT 0')
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def ensure_metadata_column(table, name, definition)
|
|
215
|
+
return if table_columns(table).key?(name)
|
|
216
|
+
|
|
217
|
+
@database.execute("ALTER TABLE #{quote(table)} ADD COLUMN #{quote(name)} #{definition}")
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def build_migration_plan
|
|
221
|
+
current_entities = schema.entities.to_h { [_1.storage_key, _1] }
|
|
222
|
+
current_attributes = schema.entities.flat_map do |entity|
|
|
223
|
+
entity.columns.map { [[entity.storage_key, _1.storage_key], [entity, _1]] }
|
|
224
|
+
end.to_h
|
|
225
|
+
current_associations = schema.associations.to_h { [_1.storage_key, _1] }
|
|
226
|
+
|
|
227
|
+
removed_entities = metadata_rows(
|
|
228
|
+
'SELECT storage_key, logical_name, table_name FROM mxrb_schema_entities',
|
|
229
|
+
%w[storage_key logical_name table_name]
|
|
230
|
+
).filter_map do |key, name, table|
|
|
231
|
+
{ kind: :entity, storage_key: key, name:, table: } unless current_entities.key?(key)
|
|
232
|
+
end
|
|
233
|
+
removed_entity_keys = removed_entities.to_h { [_1.fetch(:storage_key), true] }
|
|
234
|
+
removed_attributes = metadata_rows(
|
|
235
|
+
'SELECT entity_key, storage_key, logical_name, column_name FROM mxrb_schema_attributes',
|
|
236
|
+
%w[entity_key storage_key logical_name column_name]
|
|
237
|
+
).filter_map do |entity_key, key, name, column|
|
|
238
|
+
next if removed_entity_keys.key?(entity_key) || current_attributes.key?([entity_key, key])
|
|
239
|
+
|
|
240
|
+
entity = current_entities[entity_key]
|
|
241
|
+
next unless entity
|
|
242
|
+
|
|
243
|
+
{ kind: :attribute, storage_key: key, entity_key:, name:, table: entity.table, column: }
|
|
244
|
+
end
|
|
245
|
+
removed_associations = metadata_rows(
|
|
246
|
+
'SELECT storage_key, logical_name, table_name FROM mxrb_schema_associations',
|
|
247
|
+
%w[storage_key logical_name table_name]
|
|
248
|
+
).filter_map do |key, name, table|
|
|
249
|
+
{ kind: :association, storage_key: key, name:, table: } unless current_associations.key?(key)
|
|
250
|
+
end
|
|
251
|
+
SchemaMigrationPlan.new(
|
|
252
|
+
removed_entities.freeze, removed_attributes.freeze, removed_associations.freeze
|
|
253
|
+
)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def metadata_rows(sql, columns)
|
|
257
|
+
@database.execute(sql).map do |row|
|
|
258
|
+
row.is_a?(Hash) ? columns.map { row[_1] } : row
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def reject_destructive_migration!
|
|
263
|
+
descriptions = migration_plan.changes.map do |change|
|
|
264
|
+
"#{change.fetch(:kind)} #{change.fetch(:name)}"
|
|
265
|
+
end
|
|
266
|
+
raise UnsafeSchemaMigrationError.new(
|
|
267
|
+
"schema migration would remove #{descriptions.join(', ')}; " \
|
|
268
|
+
'rerun with allow_destructive: true after backing up the database',
|
|
269
|
+
changes: migration_plan.changes
|
|
270
|
+
)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def apply_destructive_migration(rebuilt)
|
|
274
|
+
migration_plan.removed_associations.each { drop_managed_table(_1.fetch(:table)) }
|
|
275
|
+
migration_plan.removed_entities.each { drop_managed_table(_1.fetch(:table)) }
|
|
276
|
+
migration_plan.removed_attributes.group_by { _1.fetch(:entity_key) }.each_key do |entity_key|
|
|
277
|
+
entity = schema.entities.find { _1.storage_key == entity_key }
|
|
278
|
+
next unless entity && table?(entity.table)
|
|
279
|
+
|
|
280
|
+
rebuild_entity(entity, table_columns(entity.table))
|
|
281
|
+
rebuilt << entity.table unless rebuilt.include?(entity.table)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def drop_managed_table(table)
|
|
286
|
+
@database.execute("DROP TABLE #{quote(table)}") if table?(table)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def migrate_entity(entity, created, added, rebuilt)
|
|
290
|
+
unless table?(entity.table)
|
|
291
|
+
@database.execute(create_entity_sql(entity))
|
|
292
|
+
create_entity_indexes(entity)
|
|
293
|
+
created << entity.table
|
|
294
|
+
return
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
actual = table_columns(entity.table)
|
|
298
|
+
expected = entity_columns(entity)
|
|
299
|
+
missing = expected.keys - actual.keys
|
|
300
|
+
incompatible = (expected.keys & actual.keys).any? do |name|
|
|
301
|
+
normalize_type(actual.fetch(name).fetch(:type)) != normalize_type(expected.fetch(name)) ||
|
|
302
|
+
required_changed?(name, actual.fetch(name), expected.fetch(name))
|
|
303
|
+
end
|
|
304
|
+
required_missing = missing.any? { required_definition?(expected.fetch(_1)) }
|
|
305
|
+
validate_required_values!(entity, actual, missing) if incompatible || required_missing
|
|
306
|
+
if incompatible || required_missing
|
|
307
|
+
rebuild_entity(entity, actual)
|
|
308
|
+
rebuilt << entity.table
|
|
309
|
+
else
|
|
310
|
+
missing.each do |name|
|
|
311
|
+
@database.execute(
|
|
312
|
+
"ALTER TABLE #{quote(entity.table)} ADD COLUMN #{quote(name)} #{expected.fetch(name)}"
|
|
313
|
+
)
|
|
314
|
+
added << "#{entity.table}.#{name}"
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
create_entity_indexes(entity)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def migrate_association(association, created, rebuilt)
|
|
321
|
+
unless table?(association.table)
|
|
322
|
+
@database.execute(create_association_sql(association))
|
|
323
|
+
created << association.table
|
|
324
|
+
return
|
|
325
|
+
end
|
|
326
|
+
actual = table_columns(association.table)
|
|
327
|
+
required = %w[source_id target_id]
|
|
328
|
+
return if (required - actual.keys).empty?
|
|
329
|
+
|
|
330
|
+
temporary = "#{association.table}_new"
|
|
331
|
+
@database.execute("DROP TABLE IF EXISTS #{quote(temporary)}")
|
|
332
|
+
@database.execute(create_association_sql(association, table: temporary))
|
|
333
|
+
source = %w[source_id source owner_id].find { actual.key?(_1) }
|
|
334
|
+
target = %w[target_id target child_id].find { actual.key?(_1) }
|
|
335
|
+
if source && target
|
|
336
|
+
@database.execute(
|
|
337
|
+
"INSERT OR IGNORE INTO #{quote(temporary)} (source_id, target_id) " \
|
|
338
|
+
"SELECT #{quote(source)}, #{quote(target)} FROM #{quote(association.table)} " \
|
|
339
|
+
"WHERE #{quote(source)} IS NOT NULL AND #{quote(target)} IS NOT NULL"
|
|
340
|
+
)
|
|
341
|
+
end
|
|
342
|
+
@database.execute("DROP TABLE #{quote(association.table)}")
|
|
343
|
+
@database.execute("ALTER TABLE #{quote(temporary)} RENAME TO #{quote(association.table)}")
|
|
344
|
+
rebuilt << association.table
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def rebuild_entity(entity, actual)
|
|
348
|
+
temporary = "#{entity.table}_new"
|
|
349
|
+
@database.execute("DROP TABLE IF EXISTS #{quote(temporary)}")
|
|
350
|
+
@database.execute(create_entity_sql(entity, table: temporary))
|
|
351
|
+
copies = copy_expressions(entity, actual)
|
|
352
|
+
unless copies.empty?
|
|
353
|
+
fields = copies.map { quote(_1.first) }.join(', ')
|
|
354
|
+
expressions = copies.map(&:last).join(', ')
|
|
355
|
+
@database.execute(
|
|
356
|
+
"INSERT INTO #{quote(temporary)} (#{fields}) " \
|
|
357
|
+
"SELECT #{expressions} FROM #{quote(entity.table)}"
|
|
358
|
+
)
|
|
359
|
+
end
|
|
360
|
+
@database.execute("DROP TABLE #{quote(entity.table)}")
|
|
361
|
+
@database.execute("ALTER TABLE #{quote(temporary)} RENAME TO #{quote(entity.table)}")
|
|
362
|
+
create_entity_indexes(entity)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def create_entity_sql(entity, table: entity.table)
|
|
366
|
+
definitions = entity_columns(entity).map do |name, definition|
|
|
367
|
+
suffix = name == 'id' ? ' PRIMARY KEY' : ''
|
|
368
|
+
"#{quote(name)} #{definition}#{suffix}"
|
|
369
|
+
end
|
|
370
|
+
"CREATE TABLE #{quote(table)} (#{definitions.join(', ')})"
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def create_entity_indexes(entity)
|
|
374
|
+
entity.columns.select(&:unique).each do |column|
|
|
375
|
+
index = self.class.physical_name('unique', "#{entity.storage_key}:#{column.storage_key}")
|
|
376
|
+
@database.execute(
|
|
377
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS #{quote(index)} " \
|
|
378
|
+
"ON #{quote(entity.table)} (#{quote(column.sql_name)})"
|
|
379
|
+
)
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def create_association_sql(association, table: association.table)
|
|
384
|
+
uniqueness = association.type == :Reference ? ', UNIQUE (source_id)' : ''
|
|
385
|
+
<<~SQL.strip
|
|
386
|
+
CREATE TABLE #{quote(table)} (
|
|
387
|
+
source_id TEXT NOT NULL, target_id TEXT NOT NULL,
|
|
388
|
+
PRIMARY KEY (source_id, target_id)#{uniqueness}
|
|
389
|
+
)
|
|
390
|
+
SQL
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def entity_columns(entity)
|
|
394
|
+
result = { 'id' => 'TEXT' }
|
|
395
|
+
entity.columns.each { result[_1.sql_name] = column_definition(_1) }
|
|
396
|
+
entity.system_members.each_value { |(name, type)| result[name] = type }
|
|
397
|
+
result
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def column_definition(column)
|
|
401
|
+
definition = column.sql_type.dup
|
|
402
|
+
definition << ' NOT NULL' if column.required
|
|
403
|
+
definition << " DEFAULT #{sql_literal(column.default, column.type)}" unless column.default.nil?
|
|
404
|
+
definition
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def required_definition?(definition) = definition.match?(/\bNOT NULL\b/i)
|
|
408
|
+
|
|
409
|
+
def required_changed?(name, actual, expected)
|
|
410
|
+
return false if name == 'id'
|
|
411
|
+
|
|
412
|
+
actual.fetch(:not_null) != required_definition?(expected)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def validate_required_values!(entity, actual, missing)
|
|
416
|
+
return unless table_row_count(entity.table).positive?
|
|
417
|
+
|
|
418
|
+
entity.columns.select(&:required).each do |column|
|
|
419
|
+
if missing.include?(column.sql_name)
|
|
420
|
+
reject_missing_required!(entity, column) if column.default.nil?
|
|
421
|
+
elsif actual.key?(column.sql_name) && column.default.nil? && null_values?(entity.table, column.sql_name)
|
|
422
|
+
raise UnsafeSchemaMigrationError.new(
|
|
423
|
+
"cannot make #{entity.name}.#{column.name} required while NULL values exist",
|
|
424
|
+
changes: [{ kind: :required, entity: entity.name, attribute: column.name }]
|
|
425
|
+
)
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def reject_missing_required!(entity, column)
|
|
431
|
+
raise UnsafeSchemaMigrationError.new(
|
|
432
|
+
"cannot add required attribute #{entity.name}.#{column.name} to a populated table without a default",
|
|
433
|
+
changes: [{ kind: :required, entity: entity.name, attribute: column.name }]
|
|
434
|
+
)
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def null_values?(table, column)
|
|
438
|
+
@database.get_first_value(
|
|
439
|
+
"SELECT 1 FROM #{quote(table)} WHERE #{quote(column)} IS NULL LIMIT 1"
|
|
440
|
+
) == 1
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def table_row_count(table)
|
|
444
|
+
@database.get_first_value("SELECT COUNT(*) FROM #{quote(table)}").to_i
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def copy_expressions(entity, actual)
|
|
448
|
+
columns = entity.columns.to_h { [_1.sql_name, _1] }
|
|
449
|
+
entity_columns(entity).filter_map do |name, _definition|
|
|
450
|
+
if actual.key?(name)
|
|
451
|
+
column = columns[name]
|
|
452
|
+
expression = if column&.required && !column.default.nil?
|
|
453
|
+
"COALESCE(#{quote(name)}, #{sql_literal(column.default, column.type)})"
|
|
454
|
+
else
|
|
455
|
+
quote(name)
|
|
456
|
+
end
|
|
457
|
+
[name, expression]
|
|
458
|
+
elsif columns[name]&.required && !columns[name].default.nil?
|
|
459
|
+
[name, sql_literal(columns[name].default, columns[name].type)]
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def sql_literal(value, type)
|
|
465
|
+
return value ? '1' : '0' if type == :boolean
|
|
466
|
+
return value.to_i.to_s if %i[integer long autonumber].include?(type)
|
|
467
|
+
return value.to_f.to_s if %i[float decimal].include?(type)
|
|
468
|
+
|
|
469
|
+
"'#{value.to_s.gsub("'", "''")}'"
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def table?(name)
|
|
473
|
+
!@database.get_first_value(
|
|
474
|
+
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?", name
|
|
475
|
+
).nil?
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def table_columns(name)
|
|
479
|
+
@database.execute("PRAGMA table_info(#{quote(name)})").to_h do |row|
|
|
480
|
+
if row.is_a?(Hash)
|
|
481
|
+
[row['name'], { type: row['type'], not_null: row['notnull'].to_i == 1 }]
|
|
482
|
+
else
|
|
483
|
+
[row[1], { type: row[2], not_null: row[3].to_i == 1 }]
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def normalize_type(type)
|
|
489
|
+
type.to_s.upcase.split.first
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def synchronize_metadata
|
|
493
|
+
schema.entities.each do |entity|
|
|
494
|
+
@database.execute(
|
|
495
|
+
'INSERT INTO mxrb_schema_entities VALUES (?, ?, ?) ' \
|
|
496
|
+
'ON CONFLICT(storage_key) DO UPDATE SET logical_name=excluded.logical_name, table_name=excluded.table_name',
|
|
497
|
+
[entity.storage_key, entity.name, entity.table]
|
|
498
|
+
)
|
|
499
|
+
entity.columns.each do |column|
|
|
500
|
+
@database.execute(
|
|
501
|
+
'INSERT INTO mxrb_schema_attributes ' \
|
|
502
|
+
'(entity_key, storage_key, logical_name, column_name, logical_type, required, unique_value) ' \
|
|
503
|
+
'VALUES (?, ?, ?, ?, ?, ?, ?) ' \
|
|
504
|
+
'ON CONFLICT(entity_key, storage_key) DO UPDATE SET ' \
|
|
505
|
+
'logical_name=excluded.logical_name, column_name=excluded.column_name, ' \
|
|
506
|
+
'logical_type=excluded.logical_type, required=excluded.required, ' \
|
|
507
|
+
'unique_value=excluded.unique_value',
|
|
508
|
+
[entity.storage_key, column.storage_key, column.name, column.sql_name,
|
|
509
|
+
column.type.to_s, column.required ? 1 : 0, column.unique ? 1 : 0]
|
|
510
|
+
)
|
|
511
|
+
synchronize_sequence(entity, column) if column.type == :autonumber
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
schema.associations.each do |association|
|
|
515
|
+
@database.execute(
|
|
516
|
+
'INSERT INTO mxrb_schema_associations VALUES (?, ?, ?, ?, ?, ?) ' \
|
|
517
|
+
'ON CONFLICT(storage_key) DO UPDATE SET logical_name=excluded.logical_name, ' \
|
|
518
|
+
'table_name=excluded.table_name, from_entity=excluded.from_entity, ' \
|
|
519
|
+
'to_entity=excluded.to_entity, association_type=excluded.association_type',
|
|
520
|
+
[association.storage_key, association.qualified_name, association.table,
|
|
521
|
+
association.from_entity, association.to_entity, association.type.to_s]
|
|
522
|
+
)
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def prune_obsolete_metadata
|
|
527
|
+
entity_keys = schema.entities.map(&:storage_key)
|
|
528
|
+
association_keys = schema.associations.map(&:storage_key)
|
|
529
|
+
attribute_keys = schema.entities.flat_map do |entity|
|
|
530
|
+
entity.columns.map { [entity.storage_key, _1.storage_key] }
|
|
531
|
+
end
|
|
532
|
+
delete_missing_keys('mxrb_schema_associations', 'storage_key', association_keys)
|
|
533
|
+
delete_missing_attribute_keys(attribute_keys)
|
|
534
|
+
delete_missing_keys('mxrb_schema_entities', 'storage_key', entity_keys)
|
|
535
|
+
sequence_keys = schema.entities.flat_map do |entity|
|
|
536
|
+
entity.columns.select { _1.type == :autonumber }.map { [entity.storage_key, _1.storage_key] }
|
|
537
|
+
end
|
|
538
|
+
delete_missing_sequence_keys(sequence_keys)
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def delete_missing_keys(table, column, keys)
|
|
542
|
+
if keys.empty?
|
|
543
|
+
@database.execute("DELETE FROM #{quote(table)}")
|
|
544
|
+
return
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
placeholders = Array.new(keys.size, '?').join(', ')
|
|
548
|
+
@database.execute(
|
|
549
|
+
"DELETE FROM #{quote(table)} WHERE #{quote(column)} NOT IN (#{placeholders})", keys
|
|
550
|
+
)
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def delete_missing_attribute_keys(keys)
|
|
554
|
+
current = keys.to_h { [_1, true] }
|
|
555
|
+
metadata_rows(
|
|
556
|
+
'SELECT entity_key, storage_key FROM mxrb_schema_attributes', %w[entity_key storage_key]
|
|
557
|
+
).each do |entity_key, storage_key|
|
|
558
|
+
next if current.key?([entity_key, storage_key])
|
|
559
|
+
|
|
560
|
+
@database.execute(
|
|
561
|
+
'DELETE FROM mxrb_schema_attributes WHERE entity_key = ? AND storage_key = ?',
|
|
562
|
+
[entity_key, storage_key]
|
|
563
|
+
)
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def delete_missing_sequence_keys(attribute_keys)
|
|
568
|
+
current = attribute_keys.to_h { [_1, true] }
|
|
569
|
+
metadata_rows(
|
|
570
|
+
'SELECT entity_key, attribute_key FROM mxrb_schema_sequences', %w[entity_key attribute_key]
|
|
571
|
+
).each do |entity_key, attribute_key|
|
|
572
|
+
next if current.key?([entity_key, attribute_key])
|
|
573
|
+
|
|
574
|
+
@database.execute(
|
|
575
|
+
'DELETE FROM mxrb_schema_sequences WHERE entity_key = ? AND attribute_key = ?',
|
|
576
|
+
[entity_key, attribute_key]
|
|
577
|
+
)
|
|
578
|
+
end
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
def synchronize_sequence(entity, column)
|
|
582
|
+
maximum = @database.get_first_value(
|
|
583
|
+
"SELECT COALESCE(MAX(#{quote(column.sql_name)}), 0) FROM #{quote(entity.table)}"
|
|
584
|
+
).to_i
|
|
585
|
+
@database.execute(
|
|
586
|
+
'INSERT OR IGNORE INTO mxrb_schema_sequences VALUES (?, ?, ?)',
|
|
587
|
+
[entity.storage_key, column.storage_key, maximum + 1]
|
|
588
|
+
)
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def quote(identifier)
|
|
592
|
+
%("#{identifier.to_s.gsub('"', '""')}")
|
|
593
|
+
end
|
|
594
|
+
end
|
|
595
|
+
# rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity
|
|
596
|
+
# rubocop:enable Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
597
|
+
end
|
|
598
|
+
end
|