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,729 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
require 'time'
|
|
6
|
+
require_relative 'native'
|
|
7
|
+
require_relative 'schema_migrator'
|
|
8
|
+
|
|
9
|
+
module Mxrb
|
|
10
|
+
module Runtime
|
|
11
|
+
# SQLite-backed replacement for Runtime::Native::Store. ObjectValue stays
|
|
12
|
+
# the public value type, so the native microflow interpreter can use this
|
|
13
|
+
# store without a persistence-specific object abstraction.
|
|
14
|
+
# rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity
|
|
15
|
+
# rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
16
|
+
class SQLiteStore
|
|
17
|
+
EVENTS = %i[
|
|
18
|
+
before_create after_create before_update after_update
|
|
19
|
+
before_commit after_commit before_delete after_delete
|
|
20
|
+
].freeze
|
|
21
|
+
SYSTEM_MEMBERS = {
|
|
22
|
+
owner: %w[Owner __owner_id], created_date: %w[createdDate __created_at],
|
|
23
|
+
changed_date: %w[changedDate __changed_at], changed_by: %w[changedBy __changed_by_id]
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
attr_reader :database, :schema
|
|
27
|
+
|
|
28
|
+
def initialize(project, path: ':memory:', defaults: {}, hooks: {}, allow_destructive: false)
|
|
29
|
+
@database = SQLite3::Database.new(path.to_s)
|
|
30
|
+
@database.results_as_hash = true
|
|
31
|
+
@database.execute('PRAGMA foreign_keys = ON')
|
|
32
|
+
@database.busy_timeout = 5_000
|
|
33
|
+
@schema = SchemaMigrator.new(
|
|
34
|
+
project, database: @database, allow_destructive:
|
|
35
|
+
).tap(&:migrate!).schema
|
|
36
|
+
@defaults = defaults.transform_keys(&:to_s)
|
|
37
|
+
transient_defaults = project.modules.flat_map do |mod|
|
|
38
|
+
mod.entities.select { _1.persistable == false }.map do |entity|
|
|
39
|
+
["#{mod.name}.#{entity.name}", defaults.fetch("#{mod.name}.#{entity.name}", {})]
|
|
40
|
+
end
|
|
41
|
+
end.to_h
|
|
42
|
+
@transient_entities = transient_defaults.keys.freeze
|
|
43
|
+
@transient = Native::Store.new(defaults: transient_defaults)
|
|
44
|
+
@hooks = Hash.new { |values, event| values[event] = [] }
|
|
45
|
+
@identity = {}
|
|
46
|
+
@persisted = {}
|
|
47
|
+
@staged_new = {}
|
|
48
|
+
@sequence_values = {}
|
|
49
|
+
@manual_transaction = false
|
|
50
|
+
hooks.each { |event, callbacks| Array(callbacks).each { on(event, &_1) } }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def on(event_or_entity, positional_event = nil, entity: nil, &block)
|
|
54
|
+
entity ||= event_or_entity if positional_event
|
|
55
|
+
event = positional_event || event_or_entity
|
|
56
|
+
key = event.to_sym
|
|
57
|
+
raise ArgumentError, "unsupported lifecycle event #{event}" unless EVENTS.include?(key)
|
|
58
|
+
raise ArgumentError, 'lifecycle hook requires a block' unless block
|
|
59
|
+
|
|
60
|
+
@hooks[key] << [entity&.to_s, block]
|
|
61
|
+
@transient.on(transient_name(entity), key) { |value| block.call(value, self) } if entity && transient?(entity)
|
|
62
|
+
unless entity
|
|
63
|
+
@transient_entities.each do |name|
|
|
64
|
+
@transient.on(name, key) { |value| block.call(value, self) }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
self
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def create(entity, events: true)
|
|
71
|
+
return @transient.create(transient_name(entity), events:) if transient?(entity)
|
|
72
|
+
|
|
73
|
+
definition = schema.entity(entity)
|
|
74
|
+
value = nil
|
|
75
|
+
atomic do
|
|
76
|
+
members = defaults_for(definition)
|
|
77
|
+
value = Native::ObjectValue.new(entity: definition.name, id: SecureRandom.uuid, members:)
|
|
78
|
+
run_hooks(:before_create, value) if events
|
|
79
|
+
stage(value)
|
|
80
|
+
run_hooks(:after_create, value) if events
|
|
81
|
+
end
|
|
82
|
+
value
|
|
83
|
+
rescue StandardError
|
|
84
|
+
@identity.delete(value&.id)
|
|
85
|
+
@persisted.delete(value&.id)
|
|
86
|
+
@staged_new.delete(value&.id)
|
|
87
|
+
raise
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def retrieve(entity)
|
|
91
|
+
return @transient.retrieve(transient_name(entity)) if transient?(entity)
|
|
92
|
+
|
|
93
|
+
definition = schema.entity(entity)
|
|
94
|
+
rows = database.execute("SELECT * FROM #{quote(definition.table)} ORDER BY rowid")
|
|
95
|
+
values = rows.map { materialize(definition, _1) }
|
|
96
|
+
values.concat(@staged_new.values.select { _1.entity == definition.name })
|
|
97
|
+
load_direct_associations(values)
|
|
98
|
+
values.uniq(&:id)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def find(entity, id)
|
|
102
|
+
return @transient.find(transient_name(entity), id) if transient?(entity)
|
|
103
|
+
|
|
104
|
+
definition = schema.entity(entity)
|
|
105
|
+
staged = @staged_new[id.to_s]
|
|
106
|
+
return staged if staged&.entity == definition.name
|
|
107
|
+
|
|
108
|
+
row = database.get_first_row(
|
|
109
|
+
"SELECT * FROM #{quote(definition.table)} WHERE id = ?", id.to_s
|
|
110
|
+
)
|
|
111
|
+
return unless row
|
|
112
|
+
|
|
113
|
+
materialize(definition, row).tap { load_direct_associations([_1]) }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def release_cache!
|
|
117
|
+
@identity = @staged_new.dup
|
|
118
|
+
@persisted = {}
|
|
119
|
+
self
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def retrieve_association(association, start)
|
|
123
|
+
return [] unless start
|
|
124
|
+
|
|
125
|
+
definition = begin
|
|
126
|
+
schema.association(association)
|
|
127
|
+
rescue ArgumentError
|
|
128
|
+
return @transient.retrieve_association(association, start) if transient?(start.entity)
|
|
129
|
+
|
|
130
|
+
raise
|
|
131
|
+
end
|
|
132
|
+
return volatile_association_values(definition, association, start) if hybrid_association?(definition)
|
|
133
|
+
|
|
134
|
+
return @transient.retrieve_association(association, start) if transient?(start.entity)
|
|
135
|
+
|
|
136
|
+
if start.entity == definition.from_entity
|
|
137
|
+
ids = database.execute(
|
|
138
|
+
"SELECT target_id FROM #{quote(definition.table)} WHERE source_id = ? ORDER BY rowid", [start.id]
|
|
139
|
+
).map { _1['target_id'] }
|
|
140
|
+
materialize_ids(definition.to_entity, ids)
|
|
141
|
+
elsif start.entity == definition.to_entity
|
|
142
|
+
ids = database.execute(
|
|
143
|
+
"SELECT source_id FROM #{quote(definition.table)} WHERE target_id = ? ORDER BY rowid", [start.id]
|
|
144
|
+
).map { _1['source_id'] }
|
|
145
|
+
materialize_ids(definition.from_entity, ids)
|
|
146
|
+
else
|
|
147
|
+
[]
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def delete(value, events: true)
|
|
152
|
+
transient, persistent = Array(value).compact.partition { transient?(_1.entity) }
|
|
153
|
+
@transient.delete(transient, events:) unless transient.empty?
|
|
154
|
+
persistent.each { delete_one(_1, events:) }
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def count(entity, predicate = nil)
|
|
159
|
+
return @transient.count(transient_name(entity), predicate) if transient?(entity)
|
|
160
|
+
|
|
161
|
+
return retrieve(entity).count { predicate.call(_1) } if predicate
|
|
162
|
+
|
|
163
|
+
definition = schema.entity(entity)
|
|
164
|
+
durable = database.get_first_value("SELECT COUNT(*) FROM #{quote(definition.table)}").to_i
|
|
165
|
+
durable + @staged_new.values.count { _1.entity == definition.name }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def commit(value = nil, events: true)
|
|
169
|
+
values = value.nil? ? dirty_values : Array(value)
|
|
170
|
+
transient, persistent = values.compact.partition { transient?(_1.entity) }
|
|
171
|
+
@transient.commit(transient, events:) unless transient.empty?
|
|
172
|
+
atomic { persistent.each { persist_update(_1, events:) } }
|
|
173
|
+
finish_manual_transaction if value.nil? && @manual_transaction
|
|
174
|
+
value
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def begin_transaction
|
|
178
|
+
raise SQLite3::SQLException, 'transaction already active' if @manual_transaction
|
|
179
|
+
|
|
180
|
+
@manual_snapshot = transaction_snapshot
|
|
181
|
+
database.execute('BEGIN IMMEDIATE')
|
|
182
|
+
@manual_transaction = true
|
|
183
|
+
self
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def transaction
|
|
187
|
+
begin_transaction
|
|
188
|
+
result = yield self
|
|
189
|
+
finish_manual_transaction
|
|
190
|
+
detach_uncommitted
|
|
191
|
+
result
|
|
192
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
193
|
+
rollback if @manual_transaction
|
|
194
|
+
raise
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def rollback(value = nil)
|
|
198
|
+
return rollback_values(value) unless value.nil?
|
|
199
|
+
|
|
200
|
+
state = @manual_snapshot
|
|
201
|
+
database.execute('ROLLBACK') if database.transaction_active?
|
|
202
|
+
@manual_transaction = false
|
|
203
|
+
@manual_snapshot = nil
|
|
204
|
+
restore_transaction_snapshot(state) if state
|
|
205
|
+
self
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def snapshot
|
|
209
|
+
result = schema.entities.to_h do |entity|
|
|
210
|
+
[entity.name, retrieve(entity.name).map { duplicate_value(_1) }]
|
|
211
|
+
end
|
|
212
|
+
result['__mxrb_transient__'] = @transient.snapshot
|
|
213
|
+
result['__mxrb_uow__'] = {
|
|
214
|
+
persisted: @persisted.transform_values(&:dup),
|
|
215
|
+
staged_ids: @staged_new.keys,
|
|
216
|
+
associations: association_snapshot
|
|
217
|
+
}
|
|
218
|
+
result
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def restore(snapshot)
|
|
222
|
+
unit_of_work = snapshot['__mxrb_uow__']
|
|
223
|
+
atomic do
|
|
224
|
+
schema.associations.each { database.execute("DELETE FROM #{quote(_1.table)}") }
|
|
225
|
+
schema.entities.each { database.execute("DELETE FROM #{quote(_1.table)}") }
|
|
226
|
+
clear_cache
|
|
227
|
+
if unit_of_work
|
|
228
|
+
restore_unit_of_work(snapshot, unit_of_work)
|
|
229
|
+
else
|
|
230
|
+
restore_legacy_snapshot(snapshot)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
@transient.restore(snapshot.fetch('__mxrb_transient__', @transient.snapshot))
|
|
234
|
+
self
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def close
|
|
238
|
+
rollback if @manual_transaction
|
|
239
|
+
database.close unless database.closed?
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
private
|
|
243
|
+
|
|
244
|
+
def transaction_snapshot
|
|
245
|
+
{
|
|
246
|
+
identity: @identity.dup,
|
|
247
|
+
members: @identity.to_h do |id, value|
|
|
248
|
+
[id, value.members.transform_values { transaction_member_snapshot(_1) }]
|
|
249
|
+
end,
|
|
250
|
+
persisted: @persisted.transform_values(&:dup),
|
|
251
|
+
staged_new: @staged_new.dup,
|
|
252
|
+
sequence_values: @sequence_values.dup,
|
|
253
|
+
transient: @transient.snapshot
|
|
254
|
+
}
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def restore_transaction_snapshot(state)
|
|
258
|
+
state.fetch(:identity).each do |id, value|
|
|
259
|
+
value.members.replace(state.fetch(:members).fetch(id))
|
|
260
|
+
end
|
|
261
|
+
@identity = state.fetch(:identity)
|
|
262
|
+
@persisted = state.fetch(:persisted)
|
|
263
|
+
@staged_new = state.fetch(:staged_new)
|
|
264
|
+
@sequence_values = state.fetch(:sequence_values)
|
|
265
|
+
@transient.restore(state.fetch(:transient))
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def transaction_member_snapshot(value)
|
|
269
|
+
return value.map { transaction_member_snapshot(_1) } if value.is_a?(Array)
|
|
270
|
+
return value.transform_values { transaction_member_snapshot(_1) } if value.is_a?(Hash)
|
|
271
|
+
|
|
272
|
+
value
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def restore_legacy_snapshot(snapshot)
|
|
276
|
+
snapshot.each do |entity, values|
|
|
277
|
+
next if entity.start_with?('__mxrb_')
|
|
278
|
+
|
|
279
|
+
definition = schema.entity(entity)
|
|
280
|
+
Array(values).each do |value|
|
|
281
|
+
copy = duplicate_value(value)
|
|
282
|
+
insert_value(copy, definition, associations: false)
|
|
283
|
+
cache(copy)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
@identity.each_value { persist_associations(_1, schema.entity(_1.entity)) }
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def restore_unit_of_work(snapshot, unit_of_work)
|
|
290
|
+
working = snapshot.each_with_object({}) do |(entity, values), records|
|
|
291
|
+
next if entity.start_with?('__mxrb_')
|
|
292
|
+
|
|
293
|
+
Array(values).each { records[_1.id] = duplicate_value(_1) }
|
|
294
|
+
end
|
|
295
|
+
persisted = unit_of_work.fetch(:persisted, unit_of_work.fetch('persisted', {}))
|
|
296
|
+
persisted.each do |id, members|
|
|
297
|
+
current = working[id]
|
|
298
|
+
next unless current
|
|
299
|
+
|
|
300
|
+
definition = schema.entity(current.entity)
|
|
301
|
+
durable = Native::ObjectValue.new(entity: current.entity, id:, members: members.dup)
|
|
302
|
+
insert_value(durable, definition, associations: false)
|
|
303
|
+
end
|
|
304
|
+
restore_associations(unit_of_work.fetch(:associations, unit_of_work.fetch('associations', {})))
|
|
305
|
+
working.each_value do |value|
|
|
306
|
+
@identity[value.id] = value
|
|
307
|
+
if persisted.key?(value.id)
|
|
308
|
+
@persisted[value.id] = persisted.fetch(value.id).dup
|
|
309
|
+
else
|
|
310
|
+
@staged_new[value.id] = value
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def delete_one(value, events: true)
|
|
316
|
+
definition = schema.entity(value.entity)
|
|
317
|
+
atomic do
|
|
318
|
+
run_hooks(:before_delete, value) if events
|
|
319
|
+
schema.associations.each do |association|
|
|
320
|
+
next unless [association.from_entity, association.to_entity].include?(definition.name)
|
|
321
|
+
|
|
322
|
+
database.execute(
|
|
323
|
+
"DELETE FROM #{quote(association.table)} WHERE source_id = ? OR target_id = ?", [value.id, value.id]
|
|
324
|
+
)
|
|
325
|
+
end
|
|
326
|
+
database.execute("DELETE FROM #{quote(definition.table)} WHERE id = ?", [value.id])
|
|
327
|
+
@identity.delete(value.id)
|
|
328
|
+
@persisted.delete(value.id)
|
|
329
|
+
@staged_new.delete(value.id)
|
|
330
|
+
run_hooks(:after_delete, value) if events
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def persist_update(value, events: true)
|
|
335
|
+
definition = schema.entity(value.entity)
|
|
336
|
+
previous = @persisted[value.id]
|
|
337
|
+
return persist_new(value, definition, events:) if @staged_new.key?(value.id)
|
|
338
|
+
raise ArgumentError, "object #{value.id} is not persisted" unless previous
|
|
339
|
+
|
|
340
|
+
return value if comparable_members(value.members) == comparable_members(previous)
|
|
341
|
+
|
|
342
|
+
original = previous.dup
|
|
343
|
+
run_hooks(:before_update, value) if events
|
|
344
|
+
run_hooks(:before_commit, value) if events
|
|
345
|
+
update_system_members(value, definition)
|
|
346
|
+
fields, values = persisted_fields(value, definition)
|
|
347
|
+
assignments = fields.map { "#{quote(_1)} = ?" }.join(', ')
|
|
348
|
+
unless fields.empty?
|
|
349
|
+
database.execute(
|
|
350
|
+
"UPDATE #{quote(definition.table)} SET #{assignments} WHERE id = ?", [*values, value.id]
|
|
351
|
+
)
|
|
352
|
+
end
|
|
353
|
+
persist_associations(value, definition)
|
|
354
|
+
@persisted[value.id] = value.members.dup
|
|
355
|
+
run_hooks(:after_commit, value) if events
|
|
356
|
+
run_hooks(:after_update, value) if events
|
|
357
|
+
value
|
|
358
|
+
rescue StandardError
|
|
359
|
+
value.members.replace(original) if original
|
|
360
|
+
raise
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def persist_new(value, definition, events: true)
|
|
364
|
+
run_hooks(:before_commit, value) if events
|
|
365
|
+
insert_value(value, definition)
|
|
366
|
+
advance_sequences(value, definition)
|
|
367
|
+
@staged_new.delete(value.id)
|
|
368
|
+
cache(value)
|
|
369
|
+
run_hooks(:after_commit, value) if events
|
|
370
|
+
value
|
|
371
|
+
rescue StandardError
|
|
372
|
+
@identity[value.id] = value
|
|
373
|
+
@staged_new[value.id] = value
|
|
374
|
+
@persisted.delete(value.id)
|
|
375
|
+
raise
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def insert_value(value, definition, associations: true)
|
|
379
|
+
set_creation_system_members(value, definition)
|
|
380
|
+
fields, values = persisted_fields(value, definition)
|
|
381
|
+
fields.unshift('id')
|
|
382
|
+
values.unshift(value.id)
|
|
383
|
+
placeholders = Array.new(fields.size, '?').join(', ')
|
|
384
|
+
database.execute(
|
|
385
|
+
"INSERT INTO #{quote(definition.table)} (#{fields.map { quote(_1) }.join(', ')}) " \
|
|
386
|
+
"VALUES (#{placeholders})", values
|
|
387
|
+
)
|
|
388
|
+
persist_associations(value, definition) if associations
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def persisted_fields(value, definition)
|
|
392
|
+
fields = []
|
|
393
|
+
values = []
|
|
394
|
+
definition.columns.each do |column|
|
|
395
|
+
fields << column.sql_name
|
|
396
|
+
values << serialize(value.members[column.name], column.type)
|
|
397
|
+
end
|
|
398
|
+
definition.system_members.each do |name, (sql_name, _type)|
|
|
399
|
+
fields << sql_name
|
|
400
|
+
values << serialize_system(value.members[SYSTEM_MEMBERS.fetch(name).first])
|
|
401
|
+
end
|
|
402
|
+
[fields, values]
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def persist_associations(value, entity)
|
|
406
|
+
schema.associations.select { _1.from_entity == entity.name }.each do |association|
|
|
407
|
+
next if hybrid_association?(association)
|
|
408
|
+
next unless value.members.key?(association.name)
|
|
409
|
+
|
|
410
|
+
database.execute("DELETE FROM #{quote(association.table)} WHERE source_id = ?", [value.id])
|
|
411
|
+
targets = Array(value.members[association.name]).compact
|
|
412
|
+
targets = targets.first(1) if association.type == :Reference
|
|
413
|
+
targets.each do |target|
|
|
414
|
+
target_id = target.respond_to?(:id) ? target.id : target.to_s
|
|
415
|
+
database.execute(
|
|
416
|
+
"INSERT OR IGNORE INTO #{quote(association.table)} (source_id, target_id) VALUES (?, ?)",
|
|
417
|
+
[value.id, target_id]
|
|
418
|
+
)
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def materialize(definition, row)
|
|
424
|
+
cached = @identity[row['id']]
|
|
425
|
+
return cached if cached
|
|
426
|
+
|
|
427
|
+
members = definition.columns.to_h do |column|
|
|
428
|
+
[column.name, deserialize(row[column.sql_name], column.type)]
|
|
429
|
+
end
|
|
430
|
+
definition.system_members.each_key do |name|
|
|
431
|
+
logical, sql_name = SYSTEM_MEMBERS.fetch(name)
|
|
432
|
+
members[logical] = deserialize_system(row[sql_name], name)
|
|
433
|
+
end
|
|
434
|
+
value = Native::ObjectValue.new(entity: definition.name, id: row['id'], members:)
|
|
435
|
+
cache(value)
|
|
436
|
+
value
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def materialize_ids(entity, ids)
|
|
440
|
+
return [] if ids.empty?
|
|
441
|
+
|
|
442
|
+
definition = schema.entity(entity)
|
|
443
|
+
placeholders = Array.new(ids.size, '?').join(', ')
|
|
444
|
+
rows = database.execute(
|
|
445
|
+
"SELECT * FROM #{quote(definition.table)} WHERE id IN (#{placeholders})", ids
|
|
446
|
+
).to_h { [_1['id'], _1] }
|
|
447
|
+
ids.filter_map { |id| rows[id] && materialize(definition, rows[id]) }
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def load_direct_associations(values)
|
|
451
|
+
values.each do |value|
|
|
452
|
+
# A staged object can already carry associations that only reach the
|
|
453
|
+
# database when a later batch commit runs. Reloading those links from
|
|
454
|
+
# SQLite here would replace the in-memory values with an empty result.
|
|
455
|
+
next if @staged_new.key?(value.id)
|
|
456
|
+
|
|
457
|
+
schema.associations.select { _1.from_entity == value.entity }.each do |association|
|
|
458
|
+
next if association_dirty?(value, association.name)
|
|
459
|
+
|
|
460
|
+
related = retrieve_association(association.qualified_name, value)
|
|
461
|
+
value.members[association.name] = association.type == :Reference ? related.first : related
|
|
462
|
+
(@persisted[value.id] ||= {})[association.name] = value.members[association.name]
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def association_dirty?(value, name)
|
|
468
|
+
persisted = @persisted[value.id]
|
|
469
|
+
return false unless persisted&.key?(name)
|
|
470
|
+
|
|
471
|
+
comparable_member(value.members[name]) != comparable_member(persisted[name])
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def comparable_member(value)
|
|
475
|
+
return value.map { comparable_member(_1) } if value.is_a?(Array)
|
|
476
|
+
|
|
477
|
+
value.respond_to?(:id) ? value.id : value
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def cache(value)
|
|
481
|
+
@identity[value.id] = value
|
|
482
|
+
@persisted[value.id] = value.members.dup
|
|
483
|
+
@staged_new.delete(value.id)
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def stage(value)
|
|
487
|
+
@identity[value.id] = value
|
|
488
|
+
@staged_new[value.id] = value
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def clear_cache
|
|
492
|
+
@identity.clear
|
|
493
|
+
@persisted.clear
|
|
494
|
+
@staged_new.clear
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def dirty_values
|
|
498
|
+
@staged_new.values + @identity.values.reject do |value|
|
|
499
|
+
next true if @staged_new.key?(value.id)
|
|
500
|
+
|
|
501
|
+
comparable_members(value.members) == comparable_members(@persisted.fetch(value.id, {}))
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def comparable_members(members)
|
|
506
|
+
members.transform_values { comparable_member(_1) }
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def defaults_for(entity)
|
|
510
|
+
generated = entity.columns.to_h do |column|
|
|
511
|
+
default = column.type == :autonumber ? next_autonumber(entity, column) : column.default
|
|
512
|
+
[column.name, cast_default(default, column.type)]
|
|
513
|
+
end
|
|
514
|
+
generated.merge(@defaults.fetch(entity.name, @defaults.fetch(entity.name.split('.').last, {})).dup)
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def next_autonumber(entity, column)
|
|
518
|
+
key = [entity.storage_key, column.storage_key]
|
|
519
|
+
stored = database.get_first_value(
|
|
520
|
+
'SELECT next_value FROM mxrb_schema_sequences WHERE entity_key = ? AND attribute_key = ?', key
|
|
521
|
+
)
|
|
522
|
+
raise "missing AutoNumber sequence for #{entity.name}.#{column.name}" unless stored
|
|
523
|
+
|
|
524
|
+
value = [stored.to_i, @sequence_values.fetch(key, stored).to_i].max
|
|
525
|
+
@sequence_values[key] = value.to_i + 1
|
|
526
|
+
value.to_i
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def advance_sequences(value, entity)
|
|
530
|
+
entity.columns.select { _1.type == :autonumber }.each do |column|
|
|
531
|
+
next_value = value.members[column.name].to_i + 1
|
|
532
|
+
database.execute(
|
|
533
|
+
'UPDATE mxrb_schema_sequences SET next_value = MAX(next_value, ?) ' \
|
|
534
|
+
'WHERE entity_key = ? AND attribute_key = ?',
|
|
535
|
+
[next_value, entity.storage_key, column.storage_key]
|
|
536
|
+
)
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def cast_default(value, type)
|
|
541
|
+
return nil if value.nil? || value == ''
|
|
542
|
+
return value == true || value.to_s.casecmp?('true') if type == :boolean
|
|
543
|
+
return value.to_i if %i[integer long autonumber].include?(type)
|
|
544
|
+
return value.to_f if %i[float decimal].include?(type)
|
|
545
|
+
|
|
546
|
+
value
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def set_creation_system_members(value, entity)
|
|
550
|
+
now = Time.now.utc
|
|
551
|
+
value.members['createdDate'] ||= now if entity.system_members.key?(:created_date)
|
|
552
|
+
value.members['changedDate'] ||= now if entity.system_members.key?(:changed_date)
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
def update_system_members(value, entity)
|
|
556
|
+
value.members['changedDate'] = Time.now.utc if entity.system_members.key?(:changed_date)
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def serialize(value, type)
|
|
560
|
+
return nil if value.nil?
|
|
561
|
+
return value ? 1 : 0 if type == :boolean
|
|
562
|
+
return value.utc.iso8601(6) if type == :datetime && value.respond_to?(:utc)
|
|
563
|
+
return value.iso8601 if type == :datetime && value.respond_to?(:iso8601)
|
|
564
|
+
|
|
565
|
+
value
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def deserialize(value, type)
|
|
569
|
+
return nil if value.nil?
|
|
570
|
+
return value.to_i != 0 if type == :boolean
|
|
571
|
+
return Time.parse(value) if type == :datetime
|
|
572
|
+
|
|
573
|
+
value
|
|
574
|
+
rescue ArgumentError
|
|
575
|
+
value
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def serialize_system(value)
|
|
579
|
+
return value.id if value.respond_to?(:id)
|
|
580
|
+
return value.utc.iso8601(6) if value.respond_to?(:utc)
|
|
581
|
+
|
|
582
|
+
value
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
def deserialize_system(value, name)
|
|
586
|
+
return nil if value.nil?
|
|
587
|
+
return Time.parse(value) if %i[created_date changed_date].include?(name)
|
|
588
|
+
|
|
589
|
+
value
|
|
590
|
+
rescue ArgumentError
|
|
591
|
+
value
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
def duplicate_value(value)
|
|
595
|
+
Native::ObjectValue.new(entity: value.entity, id: value.id, members: value.members.dup)
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
def rollback_values(values)
|
|
599
|
+
transient, persistent = Array(values).compact.partition { transient?(_1.entity) }
|
|
600
|
+
@transient.rollback(transient) unless transient.empty?
|
|
601
|
+
persistent.each do |value|
|
|
602
|
+
if @staged_new.delete(value.id)
|
|
603
|
+
@identity.delete(value.id)
|
|
604
|
+
next
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
definition = schema.entity(value.entity)
|
|
608
|
+
row = database.get_first_row("SELECT * FROM #{quote(definition.table)} WHERE id = ?", value.id)
|
|
609
|
+
next unless row
|
|
610
|
+
|
|
611
|
+
@identity.delete(value.id)
|
|
612
|
+
persisted = materialize(definition, row)
|
|
613
|
+
load_direct_associations([persisted])
|
|
614
|
+
value.members.replace(persisted.members)
|
|
615
|
+
@identity[value.id] = value
|
|
616
|
+
@persisted[value.id] = value.members.dup
|
|
617
|
+
end
|
|
618
|
+
values
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
def transient?(entity)
|
|
622
|
+
!transient_name(entity).nil?
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
def transient_name(entity)
|
|
626
|
+
name = entity.to_s
|
|
627
|
+
return name if @transient_entities.include?(name)
|
|
628
|
+
|
|
629
|
+
matches = @transient_entities.select { _1.split('.').last == name }
|
|
630
|
+
matches.first if matches.one?
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def hybrid_association?(association)
|
|
634
|
+
transient?(association.from_entity) || transient?(association.to_entity)
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
def volatile_association_values(definition, requested_name, start)
|
|
638
|
+
keys = [definition.name, definition.qualified_name, requested_name.to_s].uniq
|
|
639
|
+
direct = keys.flat_map { |key| Array(start.members[key]) }
|
|
640
|
+
candidates = @identity.values + @transient_entities.flat_map { @transient.retrieve(_1) }
|
|
641
|
+
inverse = candidates.select do |object|
|
|
642
|
+
keys.any? { |key| Array(object.members[key]).include?(start) }
|
|
643
|
+
end
|
|
644
|
+
(direct + inverse).uniq
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
def association_snapshot
|
|
648
|
+
schema.associations.to_h do |association|
|
|
649
|
+
rows = database.execute(
|
|
650
|
+
"SELECT source_id, target_id FROM #{quote(association.table)} ORDER BY rowid"
|
|
651
|
+
)
|
|
652
|
+
[association.storage_key, rows.map { [_1['source_id'], _1['target_id']] }]
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
def restore_associations(snapshot)
|
|
657
|
+
schema.associations.each do |association|
|
|
658
|
+
rows = snapshot.fetch(association.storage_key, snapshot.fetch(association.storage_key.to_s, []))
|
|
659
|
+
rows.each do |source, target|
|
|
660
|
+
database.execute(
|
|
661
|
+
"INSERT OR IGNORE INTO #{quote(association.table)} (source_id, target_id) VALUES (?, ?)",
|
|
662
|
+
[source, target]
|
|
663
|
+
)
|
|
664
|
+
end
|
|
665
|
+
end
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
def detach_uncommitted
|
|
669
|
+
dirty_values.each do |value|
|
|
670
|
+
@identity.delete(value.id)
|
|
671
|
+
@persisted.delete(value.id)
|
|
672
|
+
@staged_new.delete(value.id)
|
|
673
|
+
end
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
def run_hooks(event, value)
|
|
677
|
+
@hooks[event].each do |entity, callback|
|
|
678
|
+
next if entity && entity != value.entity && entity != value.entity.split('.').last
|
|
679
|
+
|
|
680
|
+
callback.call(value, self)
|
|
681
|
+
end
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
def atomic
|
|
685
|
+
state = memory_state
|
|
686
|
+
nested = @manual_transaction
|
|
687
|
+
if nested
|
|
688
|
+
begin
|
|
689
|
+
return yield
|
|
690
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
691
|
+
rollback
|
|
692
|
+
raise
|
|
693
|
+
end
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
database.execute('BEGIN IMMEDIATE')
|
|
697
|
+
result = yield
|
|
698
|
+
database.execute('COMMIT')
|
|
699
|
+
result
|
|
700
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
701
|
+
database.execute('ROLLBACK') if database.transaction_active?
|
|
702
|
+
reset_after_atomic_failure(state) unless nested
|
|
703
|
+
raise
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
def memory_state
|
|
707
|
+
{ sequence_values: @sequence_values.dup }
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
def reset_after_atomic_failure(state)
|
|
711
|
+
@identity = @staged_new.dup
|
|
712
|
+
@persisted = {}
|
|
713
|
+
@sequence_values = state.fetch(:sequence_values)
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
def finish_manual_transaction
|
|
717
|
+
database.execute('COMMIT')
|
|
718
|
+
@manual_transaction = false
|
|
719
|
+
@manual_snapshot = nil
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
def quote(identifier)
|
|
723
|
+
%("#{identifier.to_s.gsub('"', '""')}")
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
# rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity
|
|
727
|
+
# rubocop:enable Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
728
|
+
end
|
|
729
|
+
end
|