mxrb 0.1.0
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 +7 -0
- data/LICENSE +21 -0
- data/README.de-DE.md +108 -0
- data/README.md +172 -0
- data/README.pt-BR.md +109 -0
- data/bin/mxrb +1613 -0
- data/docs/README.md +10 -0
- data/docs/de-DE/README.md +24 -0
- data/docs/de-DE/architectural-patterns.md +26 -0
- data/docs/de-DE/architectural-standard.md +48 -0
- data/docs/de-DE/architecture.md +76 -0
- data/docs/de-DE/compiler.md +162 -0
- data/docs/de-DE/conventions.md +19 -0
- data/docs/de-DE/design-system.md +52 -0
- data/docs/de-DE/entity-dsl.md +53 -0
- data/docs/de-DE/native-runtime-quality-report.md +74 -0
- data/docs/de-DE/oql-sql.md +211 -0
- data/docs/de-DE/platform-api-integration.md +36 -0
- data/docs/de-DE/platform-operations.md +243 -0
- data/docs/de-DE/project-structure.md +32 -0
- data/docs/de-DE/ruby-first-roadmap.md +116 -0
- data/docs/de-DE/scaffolds.md +59 -0
- data/docs/de-DE/semantic-refactoring.md +56 -0
- data/docs/de-DE/team-server.md +33 -0
- data/docs/de-DE/validation-matrix.md +64 -0
- data/docs/de-DE/vetclinic-acceptance.md +24 -0
- data/docs/de-DE/writing.md +138 -0
- data/docs/en-US/README.md +24 -0
- data/docs/en-US/architectural-patterns.md +151 -0
- data/docs/en-US/architectural-standard.md +89 -0
- data/docs/en-US/architecture.md +170 -0
- data/docs/en-US/compiler.md +157 -0
- data/docs/en-US/conventions.md +82 -0
- data/docs/en-US/design-system.md +163 -0
- data/docs/en-US/entity-dsl.md +53 -0
- data/docs/en-US/native-runtime-quality-report.md +73 -0
- data/docs/en-US/oql-sql.md +201 -0
- data/docs/en-US/platform-api-integration.md +36 -0
- data/docs/en-US/platform-operations.md +244 -0
- data/docs/en-US/project-structure.md +49 -0
- data/docs/en-US/ruby-first-roadmap.md +186 -0
- data/docs/en-US/scaffolds.md +57 -0
- data/docs/en-US/semantic-refactoring.md +198 -0
- data/docs/en-US/team-server.md +32 -0
- data/docs/en-US/validation-matrix.md +257 -0
- data/docs/en-US/vetclinic-acceptance.md +23 -0
- data/docs/en-US/writing.md +655 -0
- data/docs/pt-BR/README.md +24 -0
- data/docs/pt-BR/architectural-patterns.md +25 -0
- data/docs/pt-BR/architectural-standard.md +533 -0
- data/docs/pt-BR/architecture.md +300 -0
- data/docs/pt-BR/compiler.md +162 -0
- data/docs/pt-BR/conventions.md +19 -0
- data/docs/pt-BR/design-system.md +65 -0
- data/docs/pt-BR/entity-dsl.md +116 -0
- data/docs/pt-BR/native-runtime-quality-report.md +73 -0
- data/docs/pt-BR/oql-sql.md +208 -0
- data/docs/pt-BR/platform-api-integration.md +36 -0
- data/docs/pt-BR/platform-operations.md +260 -0
- data/docs/pt-BR/project-structure.md +32 -0
- data/docs/pt-BR/ruby-first-roadmap.md +239 -0
- data/docs/pt-BR/scaffolds.md +104 -0
- data/docs/pt-BR/semantic-refactoring.md +55 -0
- data/docs/pt-BR/team-server.md +59 -0
- data/docs/pt-BR/validation-matrix.md +86 -0
- data/docs/pt-BR/vetclinic-acceptance.md +50 -0
- data/docs/pt-BR/writing.md +177 -0
- data/examples/sudoku_evaluation.rb +14 -0
- data/examples/sudoku_functional_test.rb +28 -0
- data/lib/mxrb/architecture/graph.rb +124 -0
- data/lib/mxrb/architecture/validator.rb +98 -0
- data/lib/mxrb/benchmark.rb +33 -0
- data/lib/mxrb/compare.rb +390 -0
- data/lib/mxrb/compiler/adapter.rb +96 -0
- data/lib/mxrb/compiler/artifact_document_compiler.rb +102 -0
- data/lib/mxrb/compiler/artifact_materializer.rb +53 -0
- data/lib/mxrb/compiler/client_model_materializer.rb +42 -0
- data/lib/mxrb/compiler/code_action_document_compiler.rb +64 -0
- data/lib/mxrb/compiler/code_action_materializer.rb +35 -0
- data/lib/mxrb/compiler/code_action_type_compiler.rb +32 -0
- data/lib/mxrb/compiler/combo_box_bundle_compiler.rb +314 -0
- data/lib/mxrb/compiler/compatibility_analyzer.rb +169 -0
- data/lib/mxrb/compiler/constants_materializer.rb +111 -0
- data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +290 -0
- data/lib/mxrb/compiler/database_connector_action_compiler.rb +280 -0
- data/lib/mxrb/compiler/deployment_asset_copier.rb +60 -0
- data/lib/mxrb/compiler/deployment_bootstrapper.rb +208 -0
- data/lib/mxrb/compiler/deployment_materializer.rb +43 -0
- data/lib/mxrb/compiler/deployment_metadata.rb +64 -0
- data/lib/mxrb/compiler/domain_document_compiler.rb +153 -0
- data/lib/mxrb/compiler/domain_model_materializer.rb +35 -0
- data/lib/mxrb/compiler/domain_security_compiler.rb +82 -0
- data/lib/mxrb/compiler/gallery_bundle_compiler.rb +190 -0
- data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +281 -0
- data/lib/mxrb/compiler/image_bundle_compiler.rb +216 -0
- data/lib/mxrb/compiler/java_proxy_generator.rb +509 -0
- data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +267 -0
- data/lib/mxrb/compiler/legacy_page_builder.rb +287 -0
- data/lib/mxrb/compiler/mda.rb +99 -0
- data/lib/mxrb/compiler/microflow_document_compiler.rb +85 -0
- data/lib/mxrb/compiler/microflow_materializer.rb +48 -0
- data/lib/mxrb/compiler/microflow_node_compiler.rb +250 -0
- data/lib/mxrb/compiler/model_package.rb +116 -0
- data/lib/mxrb/compiler/model_values.rb +67 -0
- data/lib/mxrb/compiler/nanoflow_program_compiler.rb +483 -0
- data/lib/mxrb/compiler/navigation_document_compiler.rb +93 -0
- data/lib/mxrb/compiler/packager.rb +100 -0
- data/lib/mxrb/compiler/page_bundle_builder.rb +56 -0
- data/lib/mxrb/compiler/page_bundle_compiler.rb +2004 -0
- data/lib/mxrb/compiler/page_document_compiler.rb +77 -0
- data/lib/mxrb/compiler/portable_packager.rb +356 -0
- data/lib/mxrb/compiler/project_jar_archive.rb +84 -0
- data/lib/mxrb/compiler/project_jar_builder.rb +121 -0
- data/lib/mxrb/compiler/project_materializer.rb +139 -0
- data/lib/mxrb/compiler/project_model_orderer.rb +52 -0
- data/lib/mxrb/compiler/runtime_data_types.rb +31 -0
- data/lib/mxrb/compiler/runtime_model_schema.rb +90 -0
- data/lib/mxrb/compiler/schemas/runtime-10.24.0.73019.json +1052 -0
- data/lib/mxrb/compiler/schemas/runtime-11.json +1215 -0
- data/lib/mxrb/compiler/schemas/runtime-6.10.8.json +1148 -0
- data/lib/mxrb/compiler/schemas/runtime-7.17.0.json +1144 -0
- data/lib/mxrb/compiler/schemas/runtime-7.5.0.json +1215 -0
- data/lib/mxrb/compiler/schemas/runtime-9.6.1.29396.json +868 -0
- data/lib/mxrb/compiler/schemas/system-model-10.24.0.73019.b64 +1135 -0
- data/lib/mxrb/compiler/schemas/system-model-11.12.1.b64 +815 -0
- data/lib/mxrb/compiler/schemas/system-model-6.10.8.b64 +497 -0
- data/lib/mxrb/compiler/schemas/system-model-7.17.0.b64 +519 -0
- data/lib/mxrb/compiler/schemas/system-model-7.5.0.b64 +504 -0
- data/lib/mxrb/compiler/schemas/system-model-9.6.1.29396.b64 +816 -0
- data/lib/mxrb/compiler/security_materializer.rb +104 -0
- data/lib/mxrb/compiler/settings_document_compiler.rb +45 -0
- data/lib/mxrb/compiler/settings_materializer.rb +25 -0
- data/lib/mxrb/compiler/source_model.rb +128 -0
- data/lib/mxrb/compiler/system_model_seed.rb +88 -0
- data/lib/mxrb/compiler/system_queue_materializer.rb +54 -0
- data/lib/mxrb/compiler/system_text_materializer.rb +49 -0
- data/lib/mxrb/compiler/translation_materializer.rb +95 -0
- data/lib/mxrb/compiler/web_bundle_builder.rb +155 -0
- data/lib/mxrb/compiler/web_list_data_source.rb +139 -0
- data/lib/mxrb/compiler/web_operation_compiler.rb +614 -0
- data/lib/mxrb/compiler/web_shell_materializer.rb +194 -0
- data/lib/mxrb/compiler/widget_package_extractor.rb +63 -0
- data/lib/mxrb/doctor.rb +116 -0
- data/lib/mxrb/dsl/builder.rb +1636 -0
- data/lib/mxrb/errors.rb +18 -0
- data/lib/mxrb/evaluation.rb +131 -0
- data/lib/mxrb/exporter.rb +1716 -0
- data/lib/mxrb/frontend/migrator.rb +775 -0
- data/lib/mxrb/functional.rb +307 -0
- data/lib/mxrb/github/annotator.rb +161 -0
- data/lib/mxrb/initializer.rb +257 -0
- data/lib/mxrb/integrity/validator.rb +136 -0
- data/lib/mxrb/io/bson_codec.rb +145 -0
- data/lib/mxrb/io/mpr_file.rb +558 -0
- data/lib/mxrb/io/mxunit_codec.rb +42 -0
- data/lib/mxrb/marketplace.rb +380 -0
- data/lib/mxrb/model/association.rb +98 -0
- data/lib/mxrb/model/attribute.rb +100 -0
- data/lib/mxrb/model/connector.rb +17 -0
- data/lib/mxrb/model/design_materializer.rb +133 -0
- data/lib/mxrb/model/design_migration.rb +126 -0
- data/lib/mxrb/model/design_system.rb +111 -0
- data/lib/mxrb/model/domain_model.rb +51 -0
- data/lib/mxrb/model/entity.rb +180 -0
- data/lib/mxrb/model/menu.rb +38 -0
- data/lib/mxrb/model/microflow.rb +100 -0
- data/lib/mxrb/model/module.rb +142 -0
- data/lib/mxrb/model/navigation.rb +127 -0
- data/lib/mxrb/model/page.rb +410 -0
- data/lib/mxrb/model/project.rb +187 -0
- data/lib/mxrb/model/unit.rb +48 -0
- data/lib/mxrb/module_initializer.rb +75 -0
- data/lib/mxrb/official_marketplace/content_api.rb +292 -0
- data/lib/mxrb/official_marketplace/dependency_resolver.rb +329 -0
- data/lib/mxrb/official_marketplace/lifecycle.rb +367 -0
- data/lib/mxrb/official_marketplace/module_package_importer.rb +318 -0
- data/lib/mxrb/official_marketplace/widget_package_installer.rb +414 -0
- data/lib/mxrb/official_marketplace.rb +720 -0
- data/lib/mxrb/oql/analyzer.rb +155 -0
- data/lib/mxrb/oql/index_advisor.rb +85 -0
- data/lib/mxrb/oql/plan_analyzer.rb +185 -0
- data/lib/mxrb/oql/server.rb +132 -0
- data/lib/mxrb/oql/sql_server_plan_analyzer.rb +190 -0
- data/lib/mxrb/oql/sql_server_workload_analyzer.rb +80 -0
- data/lib/mxrb/oql/workload_analyzer.rb +175 -0
- data/lib/mxrb/oql/workload_baseline.rb +60 -0
- data/lib/mxrb/oql.rb +424 -0
- data/lib/mxrb/project_lifecycle.rb +76 -0
- data/lib/mxrb/protocols/plan.rb +86 -0
- data/lib/mxrb/protocols.rb +123 -0
- data/lib/mxrb/runtime/database_workspace.rb +677 -0
- data/lib/mxrb/runtime/docker_executor.rb +41 -0
- data/lib/mxrb/runtime/docker_workspace.rb +29 -0
- data/lib/mxrb/runtime/executor.rb +169 -0
- data/lib/mxrb/runtime/java_locator.rb +26 -0
- data/lib/mxrb/runtime/native.rb +474 -0
- data/lib/mxrb/runtime/sql_server_database.rb +146 -0
- data/lib/mxrb/runtime/toolchain.rb +82 -0
- data/lib/mxrb/scaffold/cli.rb +113 -0
- data/lib/mxrb/scaffold/generator.rb +246 -0
- data/lib/mxrb/scaffold/help.rb +80 -0
- data/lib/mxrb/scaffold/page_templates.rb +54 -0
- data/lib/mxrb/scaffold/recipes.rb +198 -0
- data/lib/mxrb/scaffold/registry.rb +82 -0
- data/lib/mxrb/scaffold/templates.rb +723 -0
- data/lib/mxrb/scaffold/transaction.rb +84 -0
- data/lib/mxrb/schema/tables.rb +55 -0
- data/lib/mxrb/semantic/analyzer.rb +473 -0
- data/lib/mxrb/semantic/batch_plan.rb +68 -0
- data/lib/mxrb/semantic/domain_mutator.rb +419 -0
- data/lib/mxrb/semantic/embedder.rb +51 -0
- data/lib/mxrb/semantic/extractor.rb +290 -0
- data/lib/mxrb/semantic/index.rb +623 -0
- data/lib/mxrb/semantic/inliner.rb +216 -0
- data/lib/mxrb/semantic/mover.rb +153 -0
- data/lib/mxrb/semantic/onnx_embedder.rb +34 -0
- data/lib/mxrb/semantic/remover.rb +70 -0
- data/lib/mxrb/semantic/renamer.rb +211 -0
- data/lib/mxrb/semantic/tfidf_embedder.rb +46 -0
- data/lib/mxrb/semantic/vec_store.rb +75 -0
- data/lib/mxrb/team_server.rb +390 -0
- data/lib/mxrb/templates/project/10.24.0.73019.json +32 -0
- data/lib/mxrb/templates/project/11.12.1.json +32 -0
- data/lib/mxrb/templates/project/6.10.8.json +50 -0
- data/lib/mxrb/templates/project/7.17.0.json +50 -0
- data/lib/mxrb/templates/project/7.5.0.json +50 -0
- data/lib/mxrb/templates/project/9.6.1.29396.json +50 -0
- data/lib/mxrb/version.rb +5 -0
- data/lib/mxrb/widget_package.rb +338 -0
- data/lib/mxrb/widget_synchronizer.rb +31 -0
- data/lib/mxrb/writer.rb +3775 -0
- data/lib/mxrb.rb +188 -0
- data/marketplace/catalog.json +10 -0
- data/marketplace/modules/shared-kernel/domain/README.md +4 -0
- data/marketplace/modules/shared-kernel/module.rb +5 -0
- data/marketplace/modules/shared-kernel/mxrb-module.json +9 -0
- metadata +447 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sqlite3"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
require "json"
|
|
6
|
+
require_relative "bson_codec"
|
|
7
|
+
require_relative "mxunit_codec"
|
|
8
|
+
|
|
9
|
+
module Mxrb
|
|
10
|
+
module IO
|
|
11
|
+
# Low-level SQLite wrapper for .mpr files.
|
|
12
|
+
#
|
|
13
|
+
# Schema reality (from reverse engineering):
|
|
14
|
+
# Unit(UnitID BLOB, ContainerID BLOB, ContainmentName TEXT,
|
|
15
|
+
# TreeConflict LONG, ContentsHash TEXT, ContentsConflict TEXT, Contents BLOB)
|
|
16
|
+
# _MetaData(_ProductVersion TEXT, _BuildVersion TEXT, _SchemaHash TEXT)
|
|
17
|
+
# (older MPRs use MendixVersion instead of _ProductVersion)
|
|
18
|
+
#
|
|
19
|
+
# UnitID and ContainerID are 16-byte MS-GUID blobs, not integers.
|
|
20
|
+
# Contents is a BSON blob (v1) or NULL (v2, where .mxunit files hold the data).
|
|
21
|
+
# The $Type of each unit is embedded in its BSON Contents, not in a separate table.
|
|
22
|
+
class MprFile
|
|
23
|
+
attr_reader :path, :format_version
|
|
24
|
+
|
|
25
|
+
def write_stats
|
|
26
|
+
@write_stats ||= { inserted: 0, updated: 0, skipped: 0, deleted: 0 }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def readonly? = @readonly
|
|
30
|
+
|
|
31
|
+
def initialize(path, readonly: false)
|
|
32
|
+
@path = File.expand_path(path)
|
|
33
|
+
@readonly = readonly
|
|
34
|
+
@db = open_db
|
|
35
|
+
@write_stats = { inserted: 0, updated: 0, skipped: 0, deleted: 0 }
|
|
36
|
+
validate!
|
|
37
|
+
@format_version = detect_format
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.open(path, readonly: false)
|
|
41
|
+
new(path, readonly: readonly)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# ── Metadata ─────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
def mendix_version
|
|
47
|
+
@mendix_version ||= begin
|
|
48
|
+
row = @db.get_first_row("SELECT _ProductVersion FROM _MetaData LIMIT 1") rescue nil
|
|
49
|
+
row ||= @db.get_first_row("SELECT MendixVersion FROM _MetaData LIMIT 1") rescue nil
|
|
50
|
+
row&.first
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def update_version!(version)
|
|
55
|
+
version_str = version.to_s
|
|
56
|
+
# Try new-style column first, fall back to old-style
|
|
57
|
+
begin
|
|
58
|
+
@db.execute("UPDATE _MetaData SET _ProductVersion = ?, _BuildVersion = ?",
|
|
59
|
+
[version_str, version_str])
|
|
60
|
+
rescue SQLite3::Exception
|
|
61
|
+
@db.execute("UPDATE _MetaData SET MendixVersion = ?", [version_str])
|
|
62
|
+
end
|
|
63
|
+
@mendix_version = version_str
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def project_name
|
|
67
|
+
@project_name ||= begin
|
|
68
|
+
# Name lives in the root Unit's BSON ($QualifiedName or Name field)
|
|
69
|
+
root = root_unit
|
|
70
|
+
return nil unless root
|
|
71
|
+
|
|
72
|
+
doc = parse_contents(root)
|
|
73
|
+
doc["Name"] || doc["name"] || File.basename(@path, ".mpr")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# ── Unit access ───────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
# The root Unit is the one where UnitID == ContainerID.
|
|
80
|
+
def root_unit
|
|
81
|
+
@root_unit ||= begin
|
|
82
|
+
row = @db.get_first_row(
|
|
83
|
+
"SELECT #{unit_select_columns} FROM Unit " \
|
|
84
|
+
"WHERE UnitID = ContainerID LIMIT 1"
|
|
85
|
+
)
|
|
86
|
+
row ? raw_to_hash(row) : nil
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# All units with a given ContainmentName.
|
|
91
|
+
def units_by_containment(name)
|
|
92
|
+
@db.execute(
|
|
93
|
+
"SELECT #{unit_select_columns} FROM Unit " \
|
|
94
|
+
"WHERE ContainmentName = ?",
|
|
95
|
+
[name]
|
|
96
|
+
).map { raw_to_hash(_1) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Units directly contained by a given parent UUID.
|
|
100
|
+
def children_of(parent_uuid)
|
|
101
|
+
blob = BsonCodec.uuid_to_blob(parent_uuid)
|
|
102
|
+
@db.execute(
|
|
103
|
+
"SELECT #{unit_select_columns} FROM Unit " \
|
|
104
|
+
"WHERE ContainerID = ? AND UnitID != ContainerID",
|
|
105
|
+
[blob]
|
|
106
|
+
).map { raw_to_hash(_1) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Single unit by UUID string.
|
|
110
|
+
def unit(uuid)
|
|
111
|
+
blob = BsonCodec.uuid_to_blob(uuid)
|
|
112
|
+
row = @db.get_first_row(
|
|
113
|
+
"SELECT #{unit_select_columns} FROM Unit WHERE UnitID = ?",
|
|
114
|
+
[blob]
|
|
115
|
+
)
|
|
116
|
+
row ? raw_to_hash(row) : nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# All units (for exploration / reverse engineering).
|
|
120
|
+
def all_units
|
|
121
|
+
@db.execute(
|
|
122
|
+
"SELECT #{unit_select_columns} FROM Unit"
|
|
123
|
+
).map { raw_to_hash(_1) }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Parse BSON from a raw unit hash.
|
|
127
|
+
def parse_contents(raw_unit)
|
|
128
|
+
blob = raw_unit["Contents"]
|
|
129
|
+
if (blob.nil? || blob.empty?) && @format_version == :v2
|
|
130
|
+
unit_path = MxunitCodec.path_for(contents_dir, raw_unit.fetch("UnitID"))
|
|
131
|
+
return {} unless File.file?(unit_path)
|
|
132
|
+
return MxunitCodec.read(unit_path)
|
|
133
|
+
end
|
|
134
|
+
return {} if blob.nil? || blob.empty?
|
|
135
|
+
|
|
136
|
+
BsonCodec.parse(blob)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def content_bytes(raw_unit)
|
|
140
|
+
blob = raw_unit["Contents"]
|
|
141
|
+
if (blob.nil? || blob.empty?) && @format_version == :v2
|
|
142
|
+
unit_path = MxunitCodec.path_for(contents_dir, raw_unit.fetch("UnitID"))
|
|
143
|
+
return nil unless File.file?(unit_path)
|
|
144
|
+
|
|
145
|
+
return File.binread(unit_path)
|
|
146
|
+
end
|
|
147
|
+
blob
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def content_path(raw_unit)
|
|
151
|
+
return nil unless @format_version == :v2
|
|
152
|
+
|
|
153
|
+
MxunitCodec.path_for(contents_dir, raw_unit.fetch("UnitID"))
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def content_files
|
|
157
|
+
return [] unless @format_version == :v2 && File.directory?(contents_dir)
|
|
158
|
+
|
|
159
|
+
Dir.glob(File.join(contents_dir, "**", "*.mxunit")).sort
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# ── Writes ────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
# Insert a new unit. Returns the assigned UUID.
|
|
165
|
+
def insert_unit(container_uuid:, containment_name:, contents_doc:)
|
|
166
|
+
raise ReadOnlyError, "Opened in read-only mode" if @readonly
|
|
167
|
+
|
|
168
|
+
uuid = BsonCodec.extract_id(contents_doc["$ID"] || contents_doc["\$ID"]) || SecureRandom.uuid
|
|
169
|
+
unless contents_doc.key?("$ID") || contents_doc.key?("\$ID")
|
|
170
|
+
contents_doc = { "$ID" => uuid }.merge(contents_doc)
|
|
171
|
+
end
|
|
172
|
+
unit_blob = BsonCodec.uuid_to_blob(uuid)
|
|
173
|
+
parent_blob = BsonCodec.uuid_to_blob(container_uuid)
|
|
174
|
+
bson_bytes = BsonCodec.serialize(contents_doc)
|
|
175
|
+
hash = BsonCodec.contents_hash(bson_bytes)
|
|
176
|
+
stored_bytes = @format_version == :v2 ? nil : bson_bytes
|
|
177
|
+
columns = %w[UnitID ContainerID ContainmentName TreeConflict ContentsHash]
|
|
178
|
+
values = [unit_blob, parent_blob, containment_name, 0, hash]
|
|
179
|
+
if (conflicts = conflicts_column)
|
|
180
|
+
columns << conflicts
|
|
181
|
+
values << ""
|
|
182
|
+
end
|
|
183
|
+
if contents_column?
|
|
184
|
+
columns << "Contents"
|
|
185
|
+
values << stored_bytes
|
|
186
|
+
end
|
|
187
|
+
placeholders = (["?"] * columns.length).join(", ")
|
|
188
|
+
@db.execute(
|
|
189
|
+
"INSERT INTO Unit (#{columns.join(', ')}) VALUES (#{placeholders})",
|
|
190
|
+
values
|
|
191
|
+
)
|
|
192
|
+
write_v2_unit(uuid, bson_bytes) if @format_version == :v2
|
|
193
|
+
write_stats[:inserted] += 1
|
|
194
|
+
uuid
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Update an existing unit's contents. Recalculates ContentsHash automatically.
|
|
198
|
+
def update_unit(uuid, contents_doc)
|
|
199
|
+
raise ReadOnlyError, "Opened in read-only mode" if @readonly
|
|
200
|
+
|
|
201
|
+
blob = BsonCodec.uuid_to_blob(uuid)
|
|
202
|
+
bson_bytes = BsonCodec.serialize(contents_doc)
|
|
203
|
+
hash = BsonCodec.contents_hash(bson_bytes)
|
|
204
|
+
current = unit(uuid)
|
|
205
|
+
if current && current['ContentsHash'] == hash
|
|
206
|
+
write_stats[:skipped] += 1
|
|
207
|
+
return false
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
if contents_column?
|
|
211
|
+
@db.execute(
|
|
212
|
+
"UPDATE Unit SET Contents = ?, ContentsHash = ? WHERE UnitID = ?",
|
|
213
|
+
[@format_version == :v2 ? nil : bson_bytes, hash, blob]
|
|
214
|
+
)
|
|
215
|
+
else
|
|
216
|
+
@db.execute(
|
|
217
|
+
"UPDATE Unit SET ContentsHash = ? WHERE UnitID = ?",
|
|
218
|
+
[hash, blob]
|
|
219
|
+
)
|
|
220
|
+
end
|
|
221
|
+
write_v2_unit(uuid, bson_bytes) if @format_version == :v2
|
|
222
|
+
write_stats[:updated] += 1
|
|
223
|
+
true
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def delete_unit(uuid)
|
|
227
|
+
raise ReadOnlyError, "Opened in read-only mode" if @readonly
|
|
228
|
+
@db.execute("DELETE FROM Unit WHERE UnitID = ?", [BsonCodec.uuid_to_blob(uuid)])
|
|
229
|
+
removed = FileUtils.rm_f(MxunitCodec.path_for(contents_dir, uuid)) if @format_version == :v2
|
|
230
|
+
write_stats[:deleted] += 1
|
|
231
|
+
removed
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def relocate_unit(uuid, container_uuid:, containment_name:)
|
|
235
|
+
raise ReadOnlyError, "Opened in read-only mode" if @readonly
|
|
236
|
+
|
|
237
|
+
@db.execute(
|
|
238
|
+
"UPDATE Unit SET ContainerID = ?, ContainmentName = ? WHERE UnitID = ?",
|
|
239
|
+
[
|
|
240
|
+
BsonCodec.uuid_to_blob(container_uuid),
|
|
241
|
+
containment_name.to_s,
|
|
242
|
+
BsonCodec.uuid_to_blob(uuid)
|
|
243
|
+
]
|
|
244
|
+
)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def transaction(&)
|
|
248
|
+
@db.transaction(&)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Creates a consistent point-in-time backup using SQLite's VACUUM INTO.
|
|
252
|
+
# Falls back to a WAL checkpoint + file copy on older SQLite versions.
|
|
253
|
+
def backup!(dest_path)
|
|
254
|
+
raise ReadOnlyError, "Opened in read-only mode" if @readonly
|
|
255
|
+
|
|
256
|
+
FileUtils.rm_f(dest_path)
|
|
257
|
+
@db.execute("VACUUM INTO ?", [dest_path])
|
|
258
|
+
rescue SQLite3::Exception
|
|
259
|
+
@db.execute("PRAGMA wal_checkpoint(FULL)") rescue nil
|
|
260
|
+
FileUtils.cp(@path, dest_path)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Restores the database from a backup file, replacing the current contents.
|
|
264
|
+
# Closes and reopens the underlying SQLite connection.
|
|
265
|
+
def restore_from!(backup_path)
|
|
266
|
+
raise ReadOnlyError, "Opened in read-only mode" if @readonly
|
|
267
|
+
|
|
268
|
+
@db.close
|
|
269
|
+
FileUtils.cp(backup_path, @path)
|
|
270
|
+
@db = open_db
|
|
271
|
+
@mendix_version = nil
|
|
272
|
+
@format_version = detect_format
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# ── Exploration helpers ───────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
def tables
|
|
278
|
+
@db.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").flatten
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def table_info(name)
|
|
282
|
+
@db.table_info(name)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def query(sql, *binds)
|
|
286
|
+
@db.execute(sql, *binds)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# ── Semantic index cache ──────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
# Returns the cached index JSON if the fingerprint matches, nil otherwise.
|
|
292
|
+
def read_index_cache(fingerprint)
|
|
293
|
+
return nil unless tables.include?("_MxrbIndexCache")
|
|
294
|
+
|
|
295
|
+
@db.get_first_value(
|
|
296
|
+
"SELECT IndexData FROM _MxrbIndexCache WHERE Fingerprint = ?", [fingerprint]
|
|
297
|
+
)
|
|
298
|
+
rescue SQLite3::Exception
|
|
299
|
+
nil
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Persists the index JSON keyed by fingerprint. No-op when read-only or on error.
|
|
303
|
+
def write_index_cache(fingerprint, json)
|
|
304
|
+
return if @readonly || json.nil?
|
|
305
|
+
|
|
306
|
+
@db.execute(<<~SQL)
|
|
307
|
+
CREATE TABLE IF NOT EXISTS _MxrbIndexCache (
|
|
308
|
+
Fingerprint TEXT PRIMARY KEY,
|
|
309
|
+
IndexData TEXT NOT NULL
|
|
310
|
+
)
|
|
311
|
+
SQL
|
|
312
|
+
@db.execute(
|
|
313
|
+
"INSERT INTO _MxrbIndexCache (Fingerprint, IndexData) VALUES (?, ?) " \
|
|
314
|
+
"ON CONFLICT(Fingerprint) DO UPDATE SET IndexData = excluded.IndexData",
|
|
315
|
+
[fingerprint, json]
|
|
316
|
+
)
|
|
317
|
+
@db.execute(
|
|
318
|
+
"DELETE FROM _MxrbIndexCache WHERE Fingerprint <> ?", [fingerprint]
|
|
319
|
+
)
|
|
320
|
+
rescue SQLite3::Exception
|
|
321
|
+
nil
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Returns cache size and fingerprints without parsing the cached payload.
|
|
325
|
+
def index_cache_info(current_fingerprint: nil)
|
|
326
|
+
unless tables.include?("_MxrbIndexCache")
|
|
327
|
+
return {
|
|
328
|
+
present: false, entries: 0, bytes: 0,
|
|
329
|
+
fingerprints: [].freeze, current_fingerprint:,
|
|
330
|
+
hit: false
|
|
331
|
+
}.freeze
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
rows = @db.execute(
|
|
335
|
+
"SELECT Fingerprint, LENGTH(IndexData) FROM _MxrbIndexCache ORDER BY Fingerprint"
|
|
336
|
+
)
|
|
337
|
+
fingerprints = rows.map { _1[0].to_s }.freeze
|
|
338
|
+
{
|
|
339
|
+
present: !rows.empty?,
|
|
340
|
+
entries: rows.size,
|
|
341
|
+
bytes: rows.sum { _1[1].to_i },
|
|
342
|
+
fingerprints:,
|
|
343
|
+
current_fingerprint:,
|
|
344
|
+
hit: current_fingerprint && fingerprints.include?(current_fingerprint)
|
|
345
|
+
}.freeze
|
|
346
|
+
rescue SQLite3::Exception
|
|
347
|
+
{
|
|
348
|
+
present: false, entries: 0, bytes: 0,
|
|
349
|
+
fingerprints: [].freeze, current_fingerprint:,
|
|
350
|
+
hit: false
|
|
351
|
+
}.freeze
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Clears cached semantic data while preserving the cache table.
|
|
355
|
+
def clear_index_cache!
|
|
356
|
+
raise ReadOnlyError, "Opened in read-only mode" if @readonly
|
|
357
|
+
return 0 unless tables.include?("_MxrbIndexCache")
|
|
358
|
+
|
|
359
|
+
count = @db.get_first_value("SELECT COUNT(*) FROM _MxrbIndexCache").to_i
|
|
360
|
+
@db.execute("DELETE FROM _MxrbIndexCache")
|
|
361
|
+
count
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# ── Vector index (sqlite-vec) ─────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
# Loads the optional sqlite-vec extension into the active connection.
|
|
367
|
+
def load_vec_extension!
|
|
368
|
+
return true if @vec_loaded
|
|
369
|
+
|
|
370
|
+
require "sqlite_vec"
|
|
371
|
+
@db.enable_load_extension(true)
|
|
372
|
+
begin
|
|
373
|
+
SqliteVec.load(@db)
|
|
374
|
+
ensure
|
|
375
|
+
@db.enable_load_extension(false)
|
|
376
|
+
end
|
|
377
|
+
@vec_loaded = true
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def ensure_vec_table!(table, dimension)
|
|
381
|
+
ensure_vector_write!
|
|
382
|
+
identifier = vector_identifier(table)
|
|
383
|
+
size = Integer(dimension)
|
|
384
|
+
raise ArgumentError, "vector dimension must be positive" unless size.positive?
|
|
385
|
+
|
|
386
|
+
@db.execute(<<~SQL)
|
|
387
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS #{identifier}
|
|
388
|
+
USING vec0(artifact_id TEXT PRIMARY KEY, embedding FLOAT[#{size}])
|
|
389
|
+
SQL
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def ensure_vec_meta_table!(table)
|
|
393
|
+
ensure_vector_write!
|
|
394
|
+
identifier = vector_identifier(table)
|
|
395
|
+
@db.execute(<<~SQL)
|
|
396
|
+
CREATE TABLE IF NOT EXISTS #{identifier} (
|
|
397
|
+
ID INTEGER PRIMARY KEY CHECK (ID = 1),
|
|
398
|
+
Backend TEXT NOT NULL,
|
|
399
|
+
Dimension INTEGER NOT NULL,
|
|
400
|
+
Fingerprint TEXT NOT NULL
|
|
401
|
+
)
|
|
402
|
+
SQL
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def write_vec_meta!(table, backend, dimension, fingerprint)
|
|
406
|
+
ensure_vector_write!
|
|
407
|
+
identifier = vector_identifier(table)
|
|
408
|
+
@db.execute(
|
|
409
|
+
"INSERT OR REPLACE INTO #{identifier} " \
|
|
410
|
+
"(ID, Backend, Dimension, Fingerprint) VALUES (1, ?, ?, ?)",
|
|
411
|
+
[backend, dimension, fingerprint]
|
|
412
|
+
)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def vec_meta(table)
|
|
416
|
+
name = table.to_s
|
|
417
|
+
return nil unless tables.include?(name)
|
|
418
|
+
|
|
419
|
+
identifier = vector_identifier(name)
|
|
420
|
+
row = @db.get_first_row(
|
|
421
|
+
"SELECT Backend, Dimension, Fingerprint FROM #{identifier} WHERE ID = 1"
|
|
422
|
+
)
|
|
423
|
+
return nil unless row
|
|
424
|
+
|
|
425
|
+
{ backend: row[0], dimension: row[1], fingerprint: row[2] }
|
|
426
|
+
rescue SQLite3::Exception
|
|
427
|
+
nil
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def vec_upsert(table, artifact_id, json_vec)
|
|
431
|
+
ensure_vector_write!
|
|
432
|
+
identifier = vector_identifier(table)
|
|
433
|
+
@db.execute(
|
|
434
|
+
"INSERT INTO #{identifier}(artifact_id, embedding) VALUES (?, ?)",
|
|
435
|
+
[artifact_id, json_vec]
|
|
436
|
+
)
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def vec_knn(table, json_vec, limit)
|
|
440
|
+
identifier = vector_identifier(table)
|
|
441
|
+
@db.execute(
|
|
442
|
+
"SELECT artifact_id, distance FROM #{identifier} " \
|
|
443
|
+
"WHERE embedding MATCH ? ORDER BY distance LIMIT ?",
|
|
444
|
+
[json_vec, Integer(limit)]
|
|
445
|
+
).map { { id: _1[0], distance: _1[1] } }
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def vec_transaction(&)
|
|
449
|
+
ensure_vector_write!
|
|
450
|
+
@db.transaction(&)
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def vec_drop_index!(vec_table, meta_table)
|
|
454
|
+
ensure_vector_write!
|
|
455
|
+
@db.execute("DROP TABLE IF EXISTS #{vector_identifier(vec_table)}")
|
|
456
|
+
@db.execute("DROP TABLE IF EXISTS #{vector_identifier(meta_table)}")
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# ── Architecture metadata ─────────────────────────────────────────────────
|
|
460
|
+
|
|
461
|
+
# mxrb-only architecture metadata for concepts without a native Mendix
|
|
462
|
+
# unit (ports/repositories) or bindings awaiting a concrete widget tree.
|
|
463
|
+
def architecture_definition
|
|
464
|
+
return nil unless tables.include?("_MxrbArchitecture")
|
|
465
|
+
json = @db.get_first_value("SELECT Definition FROM _MxrbArchitecture WHERE ID = 1")
|
|
466
|
+
json && JSON.parse(json, symbolize_names: true)
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def write_architecture_definition(definition)
|
|
470
|
+
raise ReadOnlyError, "Opened in read-only mode" if @readonly
|
|
471
|
+
@db.execute(<<~SQL)
|
|
472
|
+
CREATE TABLE IF NOT EXISTS _MxrbArchitecture (
|
|
473
|
+
ID INTEGER PRIMARY KEY CHECK (ID = 1),
|
|
474
|
+
Version INTEGER NOT NULL,
|
|
475
|
+
Definition TEXT NOT NULL
|
|
476
|
+
)
|
|
477
|
+
SQL
|
|
478
|
+
@db.execute(
|
|
479
|
+
"INSERT OR REPLACE INTO _MxrbArchitecture (ID, Version, Definition) VALUES (1, 1, ?)",
|
|
480
|
+
[JSON.generate(definition)]
|
|
481
|
+
)
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def close
|
|
485
|
+
@db.close
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
private
|
|
489
|
+
|
|
490
|
+
def ensure_vector_write!
|
|
491
|
+
raise ReadOnlyError, "Opened in read-only mode" if @readonly
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def vector_identifier(value)
|
|
495
|
+
name = value.to_s
|
|
496
|
+
raise ArgumentError, "invalid vector table name: #{value.inspect}" \
|
|
497
|
+
unless name.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
|
|
498
|
+
|
|
499
|
+
name
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def open_db
|
|
503
|
+
mode = @readonly ? SQLite3::Constants::Open::READONLY : SQLite3::Constants::Open::READWRITE
|
|
504
|
+
SQLite3::Database.new(@path, { flags: mode })
|
|
505
|
+
rescue SQLite3::CantOpenException => e
|
|
506
|
+
raise NotMprError, "Cannot open #{@path}: #{e.message}"
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def validate!
|
|
510
|
+
magic = File.binread(@path, 16) rescue nil
|
|
511
|
+
raise NotMprError, "#{@path}: not a valid SQLite file" unless magic&.start_with?("SQLite format 3")
|
|
512
|
+
raise NotMprError, "#{@path}: Unit table missing — not a valid .mpr file" unless tables.include?("Unit")
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
# MPR v2 stores unit contents in mprcontents/ folder next to the .mpr.
|
|
516
|
+
def detect_format
|
|
517
|
+
File.directory?(contents_dir) ? :v2 : :v1
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def contents_dir
|
|
521
|
+
File.join(File.dirname(@path), "mprcontents")
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def unit_columns
|
|
525
|
+
@unit_columns ||= table_info("Unit").map { _1["name"] || _1[:name] }
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def contents_column?
|
|
529
|
+
unit_columns.include?("Contents")
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def conflicts_column
|
|
533
|
+
return "ContentsConflicts" if unit_columns.include?("ContentsConflicts")
|
|
534
|
+
return "ContentsConflict" if unit_columns.include?("ContentsConflict")
|
|
535
|
+
|
|
536
|
+
nil
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def unit_select_columns
|
|
540
|
+
contents = contents_column? ? "Contents" : "NULL AS Contents"
|
|
541
|
+
"UnitID, ContainerID, ContainmentName, ContentsHash, #{contents}"
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def write_v2_unit(uuid, bytes)
|
|
545
|
+
MxunitCodec.write_atomic(MxunitCodec.path_for(contents_dir, uuid), bytes)
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def raw_to_hash(row)
|
|
549
|
+
keys = %w[UnitID ContainerID ContainmentName ContentsHash Contents]
|
|
550
|
+
h = keys.zip(row).to_h
|
|
551
|
+
# Convert blob UUIDs to strings for ergonomics
|
|
552
|
+
h["UnitID"] = BsonCodec.blob_to_uuid(h["UnitID"]) if h["UnitID"]
|
|
553
|
+
h["ContainerID"] = BsonCodec.blob_to_uuid(h["ContainerID"]) if h["ContainerID"]
|
|
554
|
+
h
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require_relative "bson_codec"
|
|
5
|
+
|
|
6
|
+
module Mxrb
|
|
7
|
+
module IO
|
|
8
|
+
# Storage adapter for MPR v2 unit files.
|
|
9
|
+
#
|
|
10
|
+
# Mendix currently stores the serialized unit payload outside SQLite. The
|
|
11
|
+
# payload is kept opaque here: known BSON payloads can be decoded, while
|
|
12
|
+
# unknown/newer encodings fail explicitly instead of being overwritten.
|
|
13
|
+
module MxunitCodec
|
|
14
|
+
class UnsupportedFormat < SerializationError; end
|
|
15
|
+
|
|
16
|
+
def self.path_for(contents_dir, uuid)
|
|
17
|
+
hex = uuid.delete("-").downcase
|
|
18
|
+
File.join(contents_dir, hex[0, 2], hex[2, 2], "#{uuid.downcase}.mxunit")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.read(path)
|
|
22
|
+
bytes = File.binread(path)
|
|
23
|
+
BsonCodec.parse(bytes)
|
|
24
|
+
rescue SerializationError => e
|
|
25
|
+
raise UnsupportedFormat, "#{path}: unsupported .mxunit encoding (#{e.message})"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.serialize(doc)
|
|
29
|
+
BsonCodec.serialize(doc)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.write_atomic(path, bytes)
|
|
33
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
34
|
+
temporary = "#{path}.tmp-#{Process.pid}-#{Thread.current.object_id}"
|
|
35
|
+
File.binwrite(temporary, bytes)
|
|
36
|
+
File.rename(temporary, path)
|
|
37
|
+
ensure
|
|
38
|
+
FileUtils.rm_f(temporary) if temporary
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|