diff --git a/.claude/commands/sync-schema-guides.md b/.claude/commands/sync-schema-guides.md new file mode 100644 index 000000000..e96c7b8e2 --- /dev/null +++ b/.claude/commands/sync-schema-guides.md @@ -0,0 +1,86 @@ +--- +description: Evaluate whether a change touches the SQL-schema documentation set (SQLSCHEMA.md + both SQLSCHEMA-GUIDE docs + glossaries) and bring every affected document back in sync. Invoke after ANY change to the SQL schema artifacts, the SQL code generator, the PIM DTOs/serializers, or the Extend derived-property layer — or run it standalone as a consistency audit. +argument-hint: [optional: short description of what changed, or "audit" for a full consistency check] +--- + +# /sync-schema-guides + +Keep the SQL-schema documentation set consistent with the code and with itself. The set: + +| Document | Role | +|---|---| +| `SysML2.NET.CodeGenerator/SQLSCHEMA.md` | Compact reference: artifact table, audit findings, measured numbers, verification recipe, guide section map | +| `SysML2.NET.CodeGenerator/SQLSCHEMA-GUIDE.md` | Long-form architectural guide (English) — 19 sections + glossary appendix (§19) | +| `SysML2.NET.CodeGenerator/SQLSCHEMA-GUIDE.nl.md` | Dutch translation of the guide — MUST stay structurally identical (same sections, same anchors pattern, same tables) | +| `SysML2.NET.CodeGenerator/IMPACT-RADIUS.md` | Design sketch for the impact-radius engine (guide obligation §15.1). English-only. Update when propagation kinds, the `derived_dependency` catalog concept, closure indexes, or the §15/§18 obligations it cites change. | + +**Language policy for the Dutch guide:** Dutch prose, English conceptual terms (derived +properties, stored state, fold, checkpoint, overlay, impact radius, census, …). Never +translate the conceptual vocabulary — it must match the English guide, the schema comments, +and the code. + +## 1. Determine what changed (skip if invoked with an explicit change description) + +``` +git status --porcelain +git diff --stat HEAD +``` + +The documentation set is AFFECTED when a change touches any of: + +- `SysML2.NET.CodeGenerator/Sql/**` (golden schema, generated schema, smoke test) +- `SysML2.NET.CodeGenerator/Templates/Uml/core-sql-schema-2.hbs` +- `SysML2.NET.CodeGenerator/HandleBarHelpers/SqlSchemaHelpers.cs` +- `SysML2.NET.CodeGenerator/Extensions/SqlSchemaExtensions.cs` +- `SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/SQLSchemaGenerator.cs` +- `SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/ClassKindRegistry.cs` (and the + `ClassKindRegistration` / `ModelVersionRegistration` records) — the append-only registry + freezing class_kind ids and model_version ordinals; any append/close here changes the + multi-version narrative (guide §6.4/§12.1) and the seed counts +- `SysML2.NET/PIM/**` and `SysML2.NET.Serializer.Json/PIM/**` (the PIM the schema §3 models) +- `SysML2.NET/Extend/**` (only when it changes WHICH derived properties exist or their + semantics — the guides' census numbers and impact-radius narrative depend on them) +- `Resources/KerML_only_xmi.uml` / `Resources/SysML_only_xmi.uml` (metamodel change ⇒ census + numbers, table counts, catalog counts all suspect) + +If nothing relevant changed and the argument is not "audit": report "no sync needed" and stop. + +## 2. Sync procedure (English guide is the master) + +1. **Update the English guide first**, then mirror into the Dutch guide, then update + SQLSCHEMA.md's compact statements and its guide section map. Never let the three diverge. +2. **Numbers are load-bearing — verify, don't trust.** These recur across all three documents + and MUST match reality after any schema/generator change: + - smoke-test assertion count (`grep -c "RAISE NOTICE 'PASS" SysML2.NET.CodeGenerator/Sql/schema.smoke.sql`) + - subtype-table count (currently 47), link-table count (7), enum count (7), view count (167), + class_kind rows (175, ids frozen by ClassKindRegistry), model_version rows (currently 1), + reference-validation sources in the two-tier validate functions (currently 42), + partitioned-table count × modulus + - measured performance numbers: only replace with NEW measurements, never extrapolate + silently — label extrapolations as such +3. **Section integrity:** the guides' section numbers are referenced from SQLSCHEMA.md's + section map and from within the guides themselves. When adding a section, prefer appending + subsections (x.y) over renumbering; if renumbering is unavoidable, grep all three documents + for stale `section N` / `§N` / anchor references and fix the TOCs. +4. **Glossary appendix (§19 in both guides):** for every new term of art introduced by the + change, add a row — shortest clear definition + guide-section reference — to BOTH + glossaries, alphabetically placed. When a section is renumbered, re-verify every reference + in the *See*/*Zie* columns. +5. **Schema-file § banners vs guide sections:** the schema files' `§N` banners are a separate + numbering space. If a banner is added/renumbered in `schema.golden.sql`, update the + template in lockstep (hand-written sections must stay byte-identical) and fix the `(§N)` + suffixes in the guides' section headings. + +## 3. Verification before reporting done + +- The three documents agree on every shared number and section reference. +- Both guides have identical section structure (`grep -c "^## " both files` — counts match). +- If the schema itself changed: the generator fixture passes and the smoke test passes + against both golden and generated schema (see SQLSCHEMA.md → Verification). +- Report a short diff summary per document: what was updated and why. + +## Standing rule (also enforced via CLAUDE.md) + +Any task that edits the affected paths of step 1 is NOT complete until this evaluation has +run. A change that alters behavior, counts, measured numbers, invariants, or terminology and +leaves the documentation set untouched is a defect, not a shortcut. diff --git a/CLAUDE.md b/CLAUDE.md index eb38ce834..9a0ddb7f4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -79,6 +79,32 @@ Generator classes in `SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerator - Extension methods (Extend) → `SysML2.NET/Extend/` - DAL factories → `SysML2.NET.Dal/Core/` +### SQL-schema documentation sync is MANDATORY + +The SQL persistence schema carries a documentation set that MUST stay consistent with the +code and with itself: `SysML2.NET.CodeGenerator/SQLSCHEMA.md` (compact reference), +`SysML2.NET.CodeGenerator/SQLSCHEMA-GUIDE.md` (long-form guide, English master), +`SysML2.NET.CodeGenerator/SQLSCHEMA-GUIDE.nl.md` (Dutch mirror — Dutch prose, English +conceptual terms, identical section structure) and +`SysML2.NET.CodeGenerator/IMPACT-RADIUS.md` (design sketch for the impact-radius engine, +English-only). Both guides end in a glossary appendix (§19) that must list every term of art +with a short definition and a guide-section reference. + +**At the end of EVERY task, evaluate whether the change touched any of:** +`SysML2.NET.CodeGenerator/Sql/**`, `Templates/Uml/core-sql-schema-2.hbs`, +`HandleBarHelpers/SqlSchemaHelpers.cs`, `Extensions/SqlSchemaExtensions.cs`, +`Generators/UmlHandleBarsGenerators/SQLSchemaGenerator.cs`, +`Generators/UmlHandleBarsGenerators/ClassKindRegistry.cs` (the append-only class-kind / +model-version registry), `SysML2.NET/PIM/**`, +`SysML2.NET.Serializer.Json/PIM/**`, `SysML2.NET/Extend/**` (when it changes which derived +properties exist or their semantics), or the XMI metamodel files. **If yes, run the +`/sync-schema-guides` skill before reporting the task complete** — it carries the full +procedure (English-first ordering, load-bearing numbers to re-verify, section/anchor +integrity, glossary maintenance in BOTH languages). A change that alters behavior, counts, +measured numbers, invariants, or terminology while leaving the documentation set untouched is +a defect, not a shortcut. New terms of art introduced anywhere in these documents get a +glossary row in both guides in the same change. + ### Grounding SysML v2 / KerML work with the Hypha plugin If the **Hypha** plugin is installed, it is the **preferred grounding source for every SysML v2 / KerML semantic question** — metamodel structure (`hypha:metamodel-lookup`, or the `hypha:metamodel-navigator` agent for cross-cutting fan-out), normative specification intent (`hypha:spec-citation`), and textual-notation validity (`hypha:sysml-validation`). Use it **before** implementing or reviewing anything that depends on the SysML v2 / KerML metamodel — do not rely on a sibling analogue, the doc-comment OCL, or prior knowledge as the source of truth. The metamodel is large and precise; a plausible prior is exactly what produces confident-but-wrong derivations. diff --git a/SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/PostgreSqlSchemaTestHost.cs b/SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/PostgreSqlSchemaTestHost.cs new file mode 100644 index 000000000..482a0cd91 --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/PostgreSqlSchemaTestHost.cs @@ -0,0 +1,171 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.CodeGenerator.Tests.Generators.UmlHandleBarsGenerators +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Threading.Tasks; + + using Npgsql; + + using NUnit.Framework; + + using Testcontainers.PostgreSql; + + /// + /// Hosts a disposable PostgreSQL 18 Testcontainer for the SQL-schema integration fixtures, + /// configured per the deployment requirement of SysML2.NET.CodeGenerator/SQLSCHEMA.md + /// (max_locks_per_transaction=4096 — whole-schema DDL fails on the default of 64). Skips the + /// owning fixture with when Docker is not available. + /// + public sealed class PostgreSqlSchemaTestHost : IAsyncDisposable + { + /// + /// The PostgreSQL Testcontainer, null until succeeds + /// + private PostgreSqlContainer container; + + /// + /// Gets the connection string of the started container + /// + public string ConnectionString { get; private set; } + + /// + /// Starts the container, ignoring the owning fixture when Docker is not available + /// + /// + /// an awaitable + /// + public async Task StartAsync() + { + try + { + // the postgres image's entrypoint prepends "postgres" when the first + // command argument starts with '-', so this is `docker run postgres:18 -c ...` + var builder = new PostgreSqlBuilder("postgres:18") + .WithCommand("-c", "max_locks_per_transaction=4096"); + + var dockerEndpoint = ResolveDockerEndpoint(); + + if (dockerEndpoint != null) + { + builder = builder.WithDockerEndpoint(dockerEndpoint); + } + + this.container = builder.Build(); + + await this.container.StartAsync(); + } + catch (Exception exception) + { + Assert.Ignore($"Docker is not available for the SQL-schema integration tests: {exception.Message}"); + } + + this.ConnectionString = this.container.GetConnectionString(); + } + + /// + /// Resolves the Docker endpoint when Testcontainers' own probe would miss it: an explicit + /// DOCKER_HOST always wins; on Windows, Docker Desktop 4.x exposes the + /// dockerDesktopLinuxEngine named pipe instead of the legacy docker_engine one the default + /// probe targets. Returns null to let Testcontainers use its own resolution strategies. + /// + /// + /// The endpoint to pass to WithDockerEndpoint, or null + /// + private static string ResolveDockerEndpoint() + { + var configuredHost = Environment.GetEnvironmentVariable("DOCKER_HOST"); + + if (!string.IsNullOrWhiteSpace(configuredHost)) + { + return configuredHost; + } + + if (OperatingSystem.IsWindows() && File.Exists(@"\\.\pipe\dockerDesktopLinuxEngine")) + { + return "npipe://./pipe/dockerDesktopLinuxEngine"; + } + + return null; + } + + /// + /// Executes a multi-statement SQL script (dollar-quoted DO/CREATE FUNCTION bodies included) + /// as a single command + /// + /// + /// The script text + /// + /// + /// an awaitable + /// + public async Task ExecuteScriptAsync(string sql) + { + await using var connection = new NpgsqlConnection(this.ConnectionString); + await connection.OpenAsync(); + + await using var command = new NpgsqlCommand(sql, connection); + command.CommandTimeout = 600; + await command.ExecuteNonQueryAsync(); + } + + /// + /// Executes a multi-statement SQL script and collects the RAISE NOTICE messages it emits — + /// the PASS/FAIL channel of schema.smoke.sql and schema.concurrency.verify.sql + /// + /// + /// The script text + /// + /// + /// an awaitable carrying the notice messages in emission order + /// + public async Task> ExecuteScriptCollectingNoticesAsync(string sql) + { + var notices = new List(); + + await using var connection = new NpgsqlConnection(this.ConnectionString); + connection.Notice += (_, noticeArguments) => notices.Add(noticeArguments.Notice.MessageText); + await connection.OpenAsync(); + + await using var command = new NpgsqlCommand(sql, connection); + command.CommandTimeout = 600; + await command.ExecuteNonQueryAsync(); + + return notices; + } + + /// + /// Disposes the container + /// + /// + /// an awaitable + /// + public async ValueTask DisposeAsync() + { + if (this.container != null) + { + await this.container.DisposeAsync(); + } + } + } +} diff --git a/SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/SQLSchemaGeneratorTestFixture.cs b/SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/SQLSchemaGeneratorTestFixture.cs new file mode 100644 index 000000000..3f12ba943 --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/SQLSchemaGeneratorTestFixture.cs @@ -0,0 +1,77 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.CodeGenerator.Tests.Generators.UmlHandleBarsGenerators +{ + using System.IO; + using System.Threading.Tasks; + + using NUnit.Framework; + + using SysML2.NET.CodeGenerator.Generators.UmlHandleBarsGenerators; + + [TestFixture] + public class SQLSchemaGeneratorTestFixture + { + private DirectoryInfo sqlSchemaDirectoryInfo; + private SQLSchemaGenerator sqlSchemaGenerator; + + [OneTimeSetUp] + public void OneTimeSetUp() + { + var directoryInfo = new DirectoryInfo(TestContext.CurrentContext.TestDirectory); + + var path = Path.Combine("UML", "_SysML2.NET.Sql"); + + this.sqlSchemaDirectoryInfo = directoryInfo.CreateSubdirectory(path); + + this.sqlSchemaGenerator = new SQLSchemaGenerator(); + } + + [Test] + public async Task Verify_that_sql_schema_is_generated() + { + var generatedSchema = await this.sqlSchemaGenerator.GenerateSqlSchemaAsync(GeneratorSetupFixture.XmiReaderResult, this.sqlSchemaDirectoryInfo); + + Assert.That(generatedSchema, Is.Not.Null.And.Not.Empty); + + Assert.That(generatedSchema, Does.Contain("CREATE TABLE sysml2.element_version")); + Assert.That(generatedSchema, Does.Contain("CREATE TABLE sysml2.derived_version")); + Assert.That(generatedSchema, Does.Contain("CREATE TYPE sysml2.visibility_kind AS ENUM ('private', 'protected', 'public');")); + Assert.That(generatedSchema, Does.Contain("CREATE TABLE sysml2.feature_v")); + Assert.That(generatedSchema, Does.Contain("CREATE TABLE sysml2.element_owned_relationship")); + Assert.That(generatedSchema, Does.Contain("CREATE VIEW sysml2.vw_part_usage")); + Assert.That(generatedSchema, Does.Contain("CREATE TABLE sysml2.model_version")); + Assert.That(generatedSchema, Does.Contain("INSERT INTO sysml2.model_version (id, name, source_fingerprint) VALUES")); + Assert.That(generatedSchema, Does.Contain("INSERT INTO sysml2.class_kind (id, name, is_abstract, introduced_in, removed_in) VALUES")); + Assert.That(generatedSchema, Does.Contain("ON CONFLICT (id) DO NOTHING;"), "the registry seeds must be idempotent"); + Assert.That(generatedSchema, Does.Contain("(120, 'PartUsage', false, 1, NULL)"), "class_kind ids must come from the frozen registry"); + Assert.That(generatedSchema, Does.Contain("model_version_id smallint NOT NULL REFERENCES sysml2.model_version (id)")); + Assert.That(generatedSchema, Does.Contain("FOREIGN KEY (identity_id, class_kind) REFERENCES sysml2.data_identity (id, class_kind)"), "the typed-identity composite FK must anchor every version"); + Assert.That(generatedSchema, Does.Contain("CREATE OR REPLACE FUNCTION sysml2.validate_references_at_commit(")); + Assert.That(generatedSchema, Does.Contain("CREATE OR REPLACE FUNCTION sysml2.validate_references_in_commit(")); + Assert.That(generatedSchema, Does.Contain("ANALYZE validation_snapshot;"), "the full pass must feed the planner true snapshot cardinality"); + Assert.That(generatedSchema, Does.Contain("'wrong-type'"), "the reference validation must type-check via the typed identity"); + Assert.That(generatedSchema, Does.Not.Contain("CREATE TABLE sysml2.property_catalog"), "the property->storage routing lives in generated C#, not in the database"); + Assert.That(generatedSchema, Does.Not.Contain("CREATE TABLE sysml2.class_kind_table"), "subtype-table participation lives in generated C#, not in the database"); + Assert.That(generatedSchema, Does.Not.Contain("{{"), "no unresolved handlebars expressions may survive generation"); + } + } +} diff --git a/SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/SqlSchemaConcurrencyTestFixture.cs b/SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/SqlSchemaConcurrencyTestFixture.cs new file mode 100644 index 000000000..20c4bcfc9 --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/SqlSchemaConcurrencyTestFixture.cs @@ -0,0 +1,254 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.CodeGenerator.Tests.Generators.UmlHandleBarsGenerators +{ + using System; + using System.Diagnostics; + using System.IO; + using System.Linq; + using System.Text.RegularExpressions; + using System.Threading.Tasks; + + using Npgsql; + + using NUnit.Framework; + + using SysML2.NET.CodeGenerator.Generators.UmlHandleBarsGenerators; + + /// + /// The .NET form of the multi-user suite (SysML2.NET.CodeGenerator/Sql/schema.concurrency.*): + /// parallel writers race the normative §18.2 compare-and-swap commit protocol through + /// sysml2.bench_try_commit against the LIVE generated schema on a PostgreSQL 18 Testcontainer, + /// and every scenario ends by running the C1–C5 invariant verifier. The pgbench scripts' + /// \set loops are replaced by Tasks; the SQL under test is identical. + /// + [TestFixture] + [Category("Integration")] + public class SqlSchemaConcurrencyTestFixture + { + private const int WriterCount = 16; + private const int AttemptsPerWriter = 250; + private const int ReaderCount = 8; + private const int ReadsPerReader = 200; + + private PostgreSqlSchemaTestHost host; + private string verifyScript; + + [OneTimeSetUp] + public async Task OneTimeSetUp() + { + var scriptDirectory = Path.Combine(TestContext.CurrentContext.TestDirectory, "Sql"); + var setupScript = await File.ReadAllTextAsync(Path.Combine(scriptDirectory, "schema.concurrency.setup.sql")); + this.verifyScript = await File.ReadAllTextAsync(Path.Combine(scriptDirectory, "schema.concurrency.verify.sql")); + + this.host = new PostgreSqlSchemaTestHost(); + await this.host.StartAsync(); + + var outputDirectory = new DirectoryInfo(TestContext.CurrentContext.TestDirectory) + .CreateSubdirectory(Path.Combine("UML", "_SysML2.NET.SqlConcurrency")); + + var generatedSchema = await new SQLSchemaGenerator() + .GenerateSqlSchemaAsync(GeneratorSetupFixture.XmiReaderResult, outputDirectory); + + await this.host.ExecuteScriptAsync(generatedSchema); + await this.host.ExecuteScriptAsync(setupScript); + } + + [OneTimeTearDown] + public async Task OneTimeTearDown() + { + await this.host.DisposeAsync(); + } + + [Test] + public async Task HotBranchRace_SixteenWriters_ExactlyOneWinnerPerHead() + { + var results = await Task.WhenAll(Enumerable.Range(0, WriterCount) + .Select(_ => Task.Run(() => this.RunWriterAsync(1, AttemptsPerWriter)))); + + var wins = results.Sum(result => result.Wins); + var losses = results.Sum(result => result.Losses); + + TestContext.Out.WriteLine($"hot branch: {wins} wins, {losses} CAS losses out of {WriterCount * AttemptsPerWriter} attempts"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(wins + losses, Is.EqualTo(WriterCount * AttemptsPerWriter)); + Assert.That(wins, Is.GreaterThan(0)); + Assert.That(losses, Is.GreaterThan(0), "a 16-writer race on one branch without a single CAS conflict means the CAS was not contended at all"); + } + + await this.AssertInvariantsHoldAsync(); + } + + [Test] + public async Task SpreadBranches_OneWriterPerBranch_NoConflicts() + { + var results = await Task.WhenAll(Enumerable.Range(0, WriterCount) + .Select(writerIndex => Task.Run(() => this.RunWriterAsync(writerIndex + 1, AttemptsPerWriter)))); + + var wins = results.Sum(result => result.Wins); + var losses = results.Sum(result => result.Losses); + + TestContext.Out.WriteLine($"spread: {wins} wins, {losses} CAS losses out of {WriterCount * AttemptsPerWriter} attempts"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(wins, Is.EqualTo(WriterCount * AttemptsPerWriter), "an exclusive writer per branch must win every attempt — contention is branch-local"); + Assert.That(losses, Is.Zero); + } + + await this.AssertInvariantsHoldAsync(); + } + + [Test] + public async Task ReadsDuringWriteStorm_ReadersNeverBlocked() + { + var writerTasks = Enumerable.Range(0, WriterCount) + .Select(_ => Task.Run(() => this.RunWriterAsync(1, AttemptsPerWriter))) + .ToList(); + + var readerResults = await Task.WhenAll(Enumerable.Range(0, ReaderCount) + .Select(_ => Task.Run(() => this.RunReaderAsync(ReadsPerReader)))); + + await Task.WhenAll(writerTasks); + + var reads = readerResults.Sum(result => result.Reads); + var nonNullReads = readerResults.Sum(result => result.NonNullReads); + var averageLatency = readerResults.Average(result => result.AverageLatencyMilliseconds); + + // reported, deliberately not asserted: latency thresholds are flaky across machines + TestContext.Out.WriteLine($"reads under write storm: {reads} reads, average latency {averageLatency:F2} ms"); + + Assert.That(nonNullReads, Is.EqualTo(reads), "every seeded element must resolve to a payload while the write storm runs"); + + await this.AssertInvariantsHoldAsync(); + } + + /// + /// Runs one writer: a private connection issuing the given number of commit-protocol + /// attempts against the branch, tallying CAS wins and losses from the function's result + /// + /// + /// The 1-based bench branch index + /// + /// + /// The number of attempts to issue + /// + /// + /// an awaitable carrying the win/loss tally + /// + private async Task<(int Wins, int Losses)> RunWriterAsync(int branchIndex, int attempts) + { + var wins = 0; + var losses = 0; + + await using var connection = new NpgsqlConnection(this.host.ConnectionString); + await connection.OpenAsync(); + + await using var command = new NpgsqlCommand("SELECT sysml2.bench_try_commit($1, $2)", connection); + var branchParameter = new NpgsqlParameter { TypedValue = branchIndex }; + var seedParameter = new NpgsqlParameter { TypedValue = 0 }; + command.Parameters.Add(branchParameter); + command.Parameters.Add(seedParameter); + await command.PrepareAsync(); + + for (var attemptIndex = 0; attemptIndex < attempts; attemptIndex++) + { + seedParameter.TypedValue = Random.Shared.NextInt64(1, 1_000_000_000); + + if ((bool)await command.ExecuteScalarAsync()) + { + wins++; + } + else + { + losses++; + } + } + + return (wins, losses); + } + + /// + /// Runs one reader: a private connection issuing branch-head element reads on the hot + /// branch, tallying successful non-null payloads and the average latency + /// + /// + /// The number of reads to issue + /// + /// + /// an awaitable carrying the read tally and average latency + /// + private async Task<(int Reads, int NonNullReads, double AverageLatencyMilliseconds)> RunReaderAsync(int reads) + { + var nonNullReads = 0; + + await using var connection = new NpgsqlConnection(this.host.ConnectionString); + await connection.OpenAsync(); + + await using var command = new NpgsqlCommand("SELECT sysml2.bench_read($1)", connection); + var indexParameter = new NpgsqlParameter { TypedValue = 0 }; + command.Parameters.Add(indexParameter); + await command.PrepareAsync(); + + var stopwatch = Stopwatch.StartNew(); + + for (var readIndex = 0; readIndex < reads; readIndex++) + { + indexParameter.TypedValue = Random.Shared.Next(0, 1000); + + if (await command.ExecuteScalarAsync() is string) + { + nonNullReads++; + } + } + + stopwatch.Stop(); + + return (reads, nonNullReads, stopwatch.Elapsed.TotalMilliseconds / reads); + } + + /// + /// Runs schema.concurrency.verify.sql (invariants C1–C5) and asserts every declared + /// invariant PASSes — the count is read from the script itself, so extending the verifier + /// never requires touching this fixture + /// + /// + /// an awaitable + /// + private async Task AssertInvariantsHoldAsync() + { + var expectedPassCount = Regex.Matches(this.verifyScript, "RAISE NOTICE 'PASS").Count; + + var notices = await this.host.ExecuteScriptCollectingNoticesAsync(this.verifyScript); + var passNotices = notices.Where(notice => notice.StartsWith("PASS", StringComparison.Ordinal)).ToList(); + + TestContext.Out.WriteLine(string.Join(Environment.NewLine, passNotices)); + + using (Assert.EnterMultipleScope()) + { + Assert.That(notices.Where(notice => notice.Contains("FAIL")), Is.Empty); + Assert.That(passNotices, Has.Count.EqualTo(expectedPassCount)); + } + } + } +} diff --git a/SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/SqlSchemaSmokeTestFixture.cs b/SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/SqlSchemaSmokeTestFixture.cs new file mode 100644 index 000000000..d4d37b3b4 --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/SqlSchemaSmokeTestFixture.cs @@ -0,0 +1,92 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.CodeGenerator.Tests.Generators.UmlHandleBarsGenerators +{ + using System; + using System.IO; + using System.Linq; + using System.Text.RegularExpressions; + using System.Threading.Tasks; + + using NUnit.Framework; + + using SysML2.NET.CodeGenerator.Generators.UmlHandleBarsGenerators; + + /// + /// Runs SysML2.NET.CodeGenerator/Sql/schema.smoke.sql against the LIVE generator output — the + /// schema is generated from the UML model in-process and installed on a PostgreSQL 18 + /// Testcontainer, so this fixture also catches registry/template drift the string assertions + /// of cannot. + /// + [TestFixture] + [Category("Integration")] + public class SqlSchemaSmokeTestFixture + { + private PostgreSqlSchemaTestHost host; + private string smokeScriptPath; + + [OneTimeSetUp] + public async Task OneTimeSetUp() + { + this.smokeScriptPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "Sql", "schema.smoke.sql"); + + this.host = new PostgreSqlSchemaTestHost(); + await this.host.StartAsync(); + + var outputDirectory = new DirectoryInfo(TestContext.CurrentContext.TestDirectory) + .CreateSubdirectory(Path.Combine("UML", "_SysML2.NET.SqlSmoke")); + + var generatedSchema = await new SQLSchemaGenerator() + .GenerateSqlSchemaAsync(GeneratorSetupFixture.XmiReaderResult, outputDirectory); + + await this.host.ExecuteScriptAsync(generatedSchema); + } + + [OneTimeTearDown] + public async Task OneTimeTearDown() + { + await this.host.DisposeAsync(); + } + + [Test] + public async Task Verify_that_smoke_test_passes_against_generated_schema() + { + var smokeScript = await File.ReadAllTextAsync(this.smokeScriptPath); + + // self-calibrating: the script itself declares how many PASS notices it must emit, + // so extending the smoke test never requires touching this fixture + var expectedPassCount = Regex.Matches(smokeScript, "RAISE NOTICE 'PASS").Count; + + var notices = await this.host.ExecuteScriptCollectingNoticesAsync(smokeScript); + var passNotices = notices.Where(notice => notice.StartsWith("PASS", StringComparison.Ordinal)).ToList(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(expectedPassCount, Is.GreaterThan(0), "the smoke script must declare PASS notices"); + Assert.That(notices.Where(notice => notice.Contains("FAIL")), Is.Empty); + Assert.That(passNotices, Has.Count.EqualTo(expectedPassCount)); + Assert.That(passNotices, Has.Some.StartWith("PASS 2a"), "the derived-state axiom assertion must be present"); + Assert.That(passNotices, Has.Some.StartWith("PASS 11a"), "the multi-version registry assertion must be present"); + Assert.That(passNotices, Has.Some.StartWith("PASS 13c"), "the incremental reference-validation assertion must be present"); + } + } + } +} diff --git a/SysML2.NET.CodeGenerator.Tests/SysML2.NET.CodeGenerator.Tests.csproj b/SysML2.NET.CodeGenerator.Tests/SysML2.NET.CodeGenerator.Tests.csproj index a4f371c30..369e14e30 100644 --- a/SysML2.NET.CodeGenerator.Tests/SysML2.NET.CodeGenerator.Tests.csproj +++ b/SysML2.NET.CodeGenerator.Tests/SysML2.NET.CodeGenerator.Tests.csproj @@ -1146,6 +1146,7 @@ + @@ -1160,6 +1161,7 @@ + @@ -1173,4 +1175,15 @@ + + + Always + + + Always + + + Always + + \ No newline at end of file diff --git a/SysML2.NET.CodeGenerator/Extensions/SqlSchemaExtensions.cs b/SysML2.NET.CodeGenerator/Extensions/SqlSchemaExtensions.cs new file mode 100644 index 000000000..d8fb84bbd --- /dev/null +++ b/SysML2.NET.CodeGenerator/Extensions/SqlSchemaExtensions.cs @@ -0,0 +1,348 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.CodeGenerator.Extensions +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + + using uml4net.Classification; + using uml4net.Extensions; + using uml4net.StructuredClassifiers; + + /// + /// Extension methods used by the SQL schema generator (SysML2.NET.CodeGenerator/Templates/Uml/core-sql-schema-2.hbs) + /// to derive the PostgreSQL persistence schema from the UML metamodel. The schema stores only + /// non-derived, non-redefining properties; everything derived is materialized separately at + /// commit time (see SysML2.NET.CodeGenerator/Sql/schema.golden.sql for the reference design). + /// + public static class SqlSchemaExtensions + { + /// + /// The derived properties that are promoted to real columns on the derived_version table + /// (key: UML property name, value: SQL column name). All other derived properties live in + /// the derived_json document. + /// + private static readonly Dictionary PromotedDerivedColumns = new() + { + ["owner"] = "owner", + ["owningNamespace"] = "owning_namespace", + ["qualifiedName"] = "qualified_name", + ["name"] = "name", + ["shortName"] = "short_name", + ["isLibraryElement"] = "is_library_element" + }; + + /// + /// Converts a camelCase or PascalCase UML name to the snake_case form used for SQL identifiers + /// + /// + /// The UML name to convert, e.g. "OccurrenceUsage" or "declaredShortName" + /// + /// + /// The snake_case form, e.g. "occurrence_usage" or "declared_short_name" + /// + public static string QuerySqlSnakeCaseName(this string name) + { + ArgumentException.ThrowIfNullOrEmpty(name); + + var stringBuilder = new StringBuilder(); + + foreach (var (character, characterIndex) in name.Select((character, characterIndex) => (character, characterIndex))) + { + if (char.IsUpper(character) && characterIndex > 0) + { + stringBuilder.Append('_'); + } + + stringBuilder.Append(char.ToLowerInvariant(character)); + } + + return stringBuilder.ToString(); + } + + /// + /// Queries the non-derived properties DECLARED by the class itself — the flattened property set + /// minus everything the direct generalizations already carry. This deliberately does NOT use + /// OwnedAttribute: reference properties that are association ends (e.g. Membership::memberElement) + /// are owned by the association, not the class, and would be silently dropped. + /// + /// Same-name redefinitions (e.g. CollectExpression::operator redefining OperatorExpression::operator) + /// are excluded — they resolve to the storage of the property they redefine. A redefinition under a + /// NEW name (e.g. Membership::memberElement redefining Relationship::target) is a distinct API + /// property and introduces storage of its own, exactly as it does in the generated DTOs. + /// + /// + /// The subject + /// + /// + /// The stored properties declared by the class, ordered by name + /// + public static IReadOnlyList QueryStoredOwnProperties(this IClass @class) + { + ArgumentNullException.ThrowIfNull(@class); + + var inheritedPropertyIds = @class.Generalization + .Select(generalization => generalization.General) + .OfType() + .SelectMany(general => general.QueryAllProperties()) + .Select(property => property.XmiId) + .ToHashSet(); + + return @class.QueryAllProperties() + .Where(property => !inheritedPropertyIds.Contains(property.XmiId)) + .Where(property => !property.IsDerived && !property.IsDerivedUnion && !property.QueryIsSameNameRedefinition()) + .OrderBy(property => property.Name, StringComparer.Ordinal) + .ToList(); + } + + /// + /// Asserts whether the property is a same-name redefinition — a redeclaration of an inherited + /// property under the same name, carrying no storage of its own + /// + /// + /// The subject + /// + /// + /// True when the property redefines a property of the same name + /// + public static bool QueryIsSameNameRedefinition(this IProperty property) + { + ArgumentNullException.ThrowIfNull(property); + + return property.RedefinedProperty.Any(redefined => redefined.Name == property.Name); + } + + /// + /// Queries the single-valued stored properties declared by the class itself — the properties that + /// become columns on the class's subtype table + /// + /// + /// The subject + /// + /// + /// The single-valued stored properties declared by the class, ordered by name + /// + public static IReadOnlyList QueryStoredScalarOwnProperties(this IClass @class) + { + return @class.QueryStoredOwnProperties() + .Where(property => !property.QueryIsEnumerable()) + .ToList(); + } + + /// + /// Queries the multi-valued stored properties declared by the class itself — the properties that + /// become ordered link tables + /// + /// + /// The subject + /// + /// + /// The multi-valued stored properties declared by the class, ordered by name + /// + public static IReadOnlyList QueryStoredMultiOwnProperties(this IClass @class) + { + return @class.QueryStoredOwnProperties() + .Where(property => property.QueryIsEnumerable()) + .ToList(); + } + + /// + /// Asserts whether the class gets a subtype table of its own. Element is excluded because its + /// stored scalars are folded into the element_version core table (every element has them, so a + /// join would be pure overhead). + /// + /// + /// The subject + /// + /// + /// True when the class introduces at least one single-valued stored property and is not Element + /// + public static bool QueryIsStorageIntroducing(this IClass @class) + { + ArgumentNullException.ThrowIfNull(@class); + + return @class.Name != "Element" && @class.QueryStoredScalarOwnProperties().Count != 0; + } + + /// + /// Queries the storage-introducing classes in the class's generalization closure (the class itself + /// included), ordered shallowest supertype first. The generalization graph is a DAG (multiple + /// inheritance), so this is a closure over , + /// not a walk up a chain. + /// + /// + /// The subject + /// + /// + /// The storage-introducing classes whose subtype tables an instance of this class participates in + /// + public static IReadOnlyList QueryStorageAncestors(this IClass @class) + { + ArgumentNullException.ThrowIfNull(@class); + + var closure = @class.QueryAllGeneralClassifiers() + .OfType() + .Union([@class]); + + return closure + .Where(ancestor => ancestor.QueryIsStorageIntroducing()) + .OrderBy(ancestor => ancestor.QueryAllGeneralClassifiers().Count) + .ThenBy(ancestor => ancestor.Name, StringComparer.Ordinal) + .ToList(); + } + + /// + /// Queries the SQL name of the class's subtype table, e.g. "occurrence_usage_version" for OccurrenceUsage + /// + /// + /// The subject + /// + /// + /// The subtype table name + /// + public static string QuerySqlSubtypeTableName(this IClass @class) + { + ArgumentNullException.ThrowIfNull(@class); + + return $"{@class.Name.QuerySqlSnakeCaseName()}_version"; + } + + /// + /// Queries the SQL name of the link table for a multi-valued stored property, e.g. + /// "element_owned_relationship" for Element::ownedRelationship + /// + /// + /// The declaring the property + /// + /// + /// The multi-valued stored + /// + /// + /// The link table name + /// + public static string QuerySqlLinkTableName(this IClass @class, IProperty property) + { + ArgumentNullException.ThrowIfNull(@class); + ArgumentNullException.ThrowIfNull(property); + + return $"{@class.Name.QuerySqlSnakeCaseName()}_{property.Name.QuerySqlSnakeCaseName()}"; + } + + /// + /// Queries the SQL column name for a property, e.g. "is_implied_included" for isImpliedIncluded + /// + /// + /// The subject + /// + /// + /// The snake_case column name + /// + public static string QuerySqlColumnName(this IProperty property) + { + ArgumentNullException.ThrowIfNull(property); + + return property.Name.QuerySqlSnakeCaseName(); + } + + /// + /// Queries the PostgreSQL type of a single-valued stored property. References map to uuid + /// (they target data_identity), enums to the generated enum type, primitives to their SQL + /// counterpart. + /// + /// + /// The subject + /// + /// + /// The PostgreSQL type name + /// + public static string QuerySqlTypeName(this IProperty property) + { + ArgumentNullException.ThrowIfNull(property); + + if (property.QueryIsEnum()) + { + return $"sysml2.{property.QueryTypeName().QuerySqlSnakeCaseName()}"; + } + + if (property.QueryIsReferenceType()) + { + return "uuid"; + } + + return property.QueryTypeName() switch + { + "Boolean" => "boolean", + "Integer" => "integer", + "Real" => "double precision", + "String" => "text", + _ => throw new NotSupportedException($"No SQL type mapping for UML type {property.QueryTypeName()} of property {property.Name}") + }; + } + + /// + /// Resolves a property to the property whose storage it occupies: a same-name redefinition (e.g. + /// CollectExpression::operator) resolves transitively to the root property it redefines + /// (OperatorExpression::operator); any other property — including redefinitions under a new name — + /// resolves to itself. + /// + /// + /// The subject + /// + /// + /// The property that owns the storage + /// + public static IProperty QueryStorageRootProperty(this IProperty property) + { + ArgumentNullException.ThrowIfNull(property); + + var current = property; + + while (current.QueryIsSameNameRedefinition()) + { + current = current.RedefinedProperty.First(redefined => redefined.Name == current.Name); + } + + return current; + } + + /// + /// Tries to query the derived_version column that a derived property is promoted to. Only the + /// six hot Element-level derived properties are promoted; the rest live in derived_json. + /// + /// + /// The subject derived + /// + /// + /// The derived_version column name when promoted + /// + /// + /// True when the property is promoted to a real column + /// + public static bool TryQueryPromotedDerivedColumn(this IProperty property, out string columnName) + { + ArgumentNullException.ThrowIfNull(property); + + return PromotedDerivedColumns.TryGetValue(property.Name, out columnName); + } + } +} diff --git a/SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/ClassKindRegistration.cs b/SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/ClassKindRegistration.cs new file mode 100644 index 000000000..ca2689517 --- /dev/null +++ b/SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/ClassKindRegistration.cs @@ -0,0 +1,47 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.CodeGenerator.Generators.UmlHandleBarsGenerators +{ + /// + /// One frozen row of the append-only class-kind registry (). + /// The id is assigned once — when the metaclass first appears in a registered metamodel + /// release — and never renumbered: sysml2.element_version.class_kind values and the generated + /// ClassKind C# enum persist it forever. A metaclass dropped by a later release keeps its + /// registration, closed with . + /// + /// + /// The frozen interned id (sysml2.class_kind.id) + /// + /// + /// The API @type value, e.g. "PartUsage" + /// + /// + /// Whether the metaclass is abstract in the metamodel + /// + /// + /// The of the release that introduced the metaclass + /// + /// + /// The of the first release WITHOUT the metaclass, + /// or null while it is still part of the newest registered release + /// + public sealed record ClassKindRegistration(int Id, string Name, bool IsAbstract, int IntroducedIn, int? RemovedIn = null); +} \ No newline at end of file diff --git a/SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/ClassKindRegistry.cs b/SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/ClassKindRegistry.cs new file mode 100644 index 000000000..7ac7de13f --- /dev/null +++ b/SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/ClassKindRegistry.cs @@ -0,0 +1,235 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.CodeGenerator.Generators.UmlHandleBarsGenerators +{ + using System.Collections.Generic; + + /// + /// The APPEND-ONLY registry that freezes the sysml2.class_kind ids and the sysml2.model_version + /// ordinals across metamodel releases. This file is the source of truth the SQL schema seeds + /// are emitted from — NOT the UML model: the model only VALIDATES against it. + /// + /// Maintenance contract (multi-version support, see SysML2.NET.CodeGenerator/SQLSCHEMA-GUIDE.md): + /// * NEVER renumber or delete an entry — persisted element_version.class_kind values and the + /// generated ClassKind enum depend on the ids being frozen forever. + /// * A new metamodel release appends one ModelVersions entry (next id, new fingerprint) and + /// appends its NEW metaclasses after the highest existing class-kind id, alphabetically + /// among themselves. + /// * A metaclass dropped by a release keeps its entry, closed with RemovedIn = the new + /// release's id. + /// * The generator fails fast on any drift between the newest registered release and the UML + /// model on disk (unregistered class, stale registration, fingerprint mismatch), printing + /// the exact entries to append. + /// + public static class ClassKindRegistry + { + /// + /// Gets the registered metamodel releases, ordered by ordinal. The last entry is the release + /// the UML model on disk must match. + /// + public static IReadOnlyList ModelVersions { get; } = + [ + new(1, "sysml-2.0-beta-4", "SysML:_mczcUFn3EfG_XZTXp4TXuA"), + ]; + + /// + /// Gets the frozen class-kind registrations, ordered by id. + /// + public static IReadOnlyList ClassKinds { get; } = + [ + new(1, "AcceptActionUsage", false, 1), + new(2, "ActionDefinition", false, 1), + new(3, "ActionUsage", false, 1), + new(4, "ActorMembership", false, 1), + new(5, "AllocationDefinition", false, 1), + new(6, "AllocationUsage", false, 1), + new(7, "AnalysisCaseDefinition", false, 1), + new(8, "AnalysisCaseUsage", false, 1), + new(9, "AnnotatingElement", false, 1), + new(10, "Annotation", false, 1), + new(11, "AssertConstraintUsage", false, 1), + new(12, "AssignmentActionUsage", false, 1), + new(13, "Association", false, 1), + new(14, "AssociationStructure", false, 1), + new(15, "AttributeDefinition", false, 1), + new(16, "AttributeUsage", false, 1), + new(17, "Behavior", false, 1), + new(18, "BindingConnector", false, 1), + new(19, "BindingConnectorAsUsage", false, 1), + new(20, "BooleanExpression", false, 1), + new(21, "CalculationDefinition", false, 1), + new(22, "CalculationUsage", false, 1), + new(23, "CaseDefinition", false, 1), + new(24, "CaseUsage", false, 1), + new(25, "Class", false, 1), + new(26, "Classifier", false, 1), + new(27, "CollectExpression", false, 1), + new(28, "Comment", false, 1), + new(29, "ConcernDefinition", false, 1), + new(30, "ConcernUsage", false, 1), + new(31, "ConjugatedPortDefinition", false, 1), + new(32, "ConjugatedPortTyping", false, 1), + new(33, "Conjugation", false, 1), + new(34, "ConnectionDefinition", false, 1), + new(35, "ConnectionUsage", false, 1), + new(36, "Connector", false, 1), + new(37, "ConnectorAsUsage", true, 1), + new(38, "ConstraintDefinition", false, 1), + new(39, "ConstraintUsage", false, 1), + new(40, "ConstructorExpression", false, 1), + new(41, "ControlNode", true, 1), + new(42, "CrossSubsetting", false, 1), + new(43, "DataType", false, 1), + new(44, "DecisionNode", false, 1), + new(45, "Definition", false, 1), + new(46, "Dependency", false, 1), + new(47, "Differencing", false, 1), + new(48, "Disjoining", false, 1), + new(49, "Documentation", false, 1), + new(50, "Element", true, 1), + new(51, "ElementFilterMembership", false, 1), + new(52, "EndFeatureMembership", false, 1), + new(53, "EnumerationDefinition", false, 1), + new(54, "EnumerationUsage", false, 1), + new(55, "EventOccurrenceUsage", false, 1), + new(56, "ExhibitStateUsage", false, 1), + new(57, "Expose", true, 1), + new(58, "Expression", false, 1), + new(59, "Feature", false, 1), + new(60, "FeatureChainExpression", false, 1), + new(61, "FeatureChaining", false, 1), + new(62, "FeatureInverting", false, 1), + new(63, "FeatureMembership", false, 1), + new(64, "FeatureReferenceExpression", false, 1), + new(65, "FeatureTyping", false, 1), + new(66, "FeatureValue", false, 1), + new(67, "Flow", false, 1), + new(68, "FlowDefinition", false, 1), + new(69, "FlowEnd", false, 1), + new(70, "FlowUsage", false, 1), + new(71, "ForLoopActionUsage", false, 1), + new(72, "ForkNode", false, 1), + new(73, "FramedConcernMembership", false, 1), + new(74, "Function", false, 1), + new(75, "IfActionUsage", false, 1), + new(76, "Import", true, 1), + new(77, "IncludeUseCaseUsage", false, 1), + new(78, "IndexExpression", false, 1), + new(79, "InstantiationExpression", true, 1), + new(80, "Interaction", false, 1), + new(81, "InterfaceDefinition", false, 1), + new(82, "InterfaceUsage", false, 1), + new(83, "Intersecting", false, 1), + new(84, "Invariant", false, 1), + new(85, "InvocationExpression", false, 1), + new(86, "ItemDefinition", false, 1), + new(87, "ItemUsage", false, 1), + new(88, "JoinNode", false, 1), + new(89, "LibraryPackage", false, 1), + new(90, "LiteralBoolean", false, 1), + new(91, "LiteralExpression", false, 1), + new(92, "LiteralInfinity", false, 1), + new(93, "LiteralInteger", false, 1), + new(94, "LiteralRational", false, 1), + new(95, "LiteralString", false, 1), + new(96, "LoopActionUsage", true, 1), + new(97, "Membership", false, 1), + new(98, "MembershipExpose", false, 1), + new(99, "MembershipImport", false, 1), + new(100, "MergeNode", false, 1), + new(101, "Metaclass", false, 1), + new(102, "MetadataAccessExpression", false, 1), + new(103, "MetadataDefinition", false, 1), + new(104, "MetadataFeature", false, 1), + new(105, "MetadataUsage", false, 1), + new(106, "Multiplicity", false, 1), + new(107, "MultiplicityRange", false, 1), + new(108, "Namespace", false, 1), + new(109, "NamespaceExpose", false, 1), + new(110, "NamespaceImport", false, 1), + new(111, "NullExpression", false, 1), + new(112, "ObjectiveMembership", false, 1), + new(113, "OccurrenceDefinition", false, 1), + new(114, "OccurrenceUsage", false, 1), + new(115, "OperatorExpression", false, 1), + new(116, "OwningMembership", false, 1), + new(117, "Package", false, 1), + new(118, "ParameterMembership", false, 1), + new(119, "PartDefinition", false, 1), + new(120, "PartUsage", false, 1), + new(121, "PayloadFeature", false, 1), + new(122, "PerformActionUsage", false, 1), + new(123, "PortConjugation", false, 1), + new(124, "PortDefinition", false, 1), + new(125, "PortUsage", false, 1), + new(126, "Predicate", false, 1), + new(127, "Redefinition", false, 1), + new(128, "ReferenceSubsetting", false, 1), + new(129, "ReferenceUsage", false, 1), + new(130, "Relationship", true, 1), + new(131, "RenderingDefinition", false, 1), + new(132, "RenderingUsage", false, 1), + new(133, "RequirementConstraintMembership", false, 1), + new(134, "RequirementDefinition", false, 1), + new(135, "RequirementUsage", false, 1), + new(136, "RequirementVerificationMembership", false, 1), + new(137, "ResultExpressionMembership", false, 1), + new(138, "ReturnParameterMembership", false, 1), + new(139, "SatisfyRequirementUsage", false, 1), + new(140, "SelectExpression", false, 1), + new(141, "SendActionUsage", false, 1), + new(142, "Specialization", false, 1), + new(143, "StakeholderMembership", false, 1), + new(144, "StateDefinition", false, 1), + new(145, "StateSubactionMembership", false, 1), + new(146, "StateUsage", false, 1), + new(147, "Step", false, 1), + new(148, "Structure", false, 1), + new(149, "Subclassification", false, 1), + new(150, "SubjectMembership", false, 1), + new(151, "Subsetting", false, 1), + new(152, "Succession", false, 1), + new(153, "SuccessionAsUsage", false, 1), + new(154, "SuccessionFlow", false, 1), + new(155, "SuccessionFlowUsage", false, 1), + new(156, "TerminateActionUsage", false, 1), + new(157, "TextualRepresentation", false, 1), + new(158, "TransitionFeatureMembership", false, 1), + new(159, "TransitionUsage", false, 1), + new(160, "TriggerInvocationExpression", false, 1), + new(161, "Type", false, 1), + new(162, "TypeFeaturing", false, 1), + new(163, "Unioning", false, 1), + new(164, "Usage", false, 1), + new(165, "UseCaseDefinition", false, 1), + new(166, "UseCaseUsage", false, 1), + new(167, "VariantMembership", false, 1), + new(168, "VerificationCaseDefinition", false, 1), + new(169, "VerificationCaseUsage", false, 1), + new(170, "ViewDefinition", false, 1), + new(171, "ViewRenderingMembership", false, 1), + new(172, "ViewUsage", false, 1), + new(173, "ViewpointDefinition", false, 1), + new(174, "ViewpointUsage", false, 1), + new(175, "WhileLoopActionUsage", false, 1), + ]; + } +} diff --git a/SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/ModelVersionRegistration.cs b/SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/ModelVersionRegistration.cs new file mode 100644 index 000000000..884d4a7c5 --- /dev/null +++ b/SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/ModelVersionRegistration.cs @@ -0,0 +1,41 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.CodeGenerator.Generators.UmlHandleBarsGenerators +{ + /// + /// One registered metamodel release in the append-only model-version registry + /// (). The id is an ordinal: a higher id is a later release, + /// and ids are never renumbered once assigned — the SQL schema's model_version table and + /// commit.model_version_id stamps persist them. + /// + /// + /// The frozen ordinal of the release (sysml2.model_version.id) + /// + /// + /// The human-readable release label, e.g. "sysml-2.0-beta-4" + /// + /// + /// The root-package fingerprint ("Name:XmiId") of the UML model the release was generated + /// from — the generator refuses to run when the model on disk no longer matches the newest + /// registered fingerprint, so a metamodel change forces a conscious registry update + /// + public sealed record ModelVersionRegistration(int Id, string Name, string SourceFingerprint); +} \ No newline at end of file diff --git a/SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/SQLSchemaGenerator.cs b/SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/SQLSchemaGenerator.cs new file mode 100644 index 000000000..1264359e5 --- /dev/null +++ b/SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/SQLSchemaGenerator.cs @@ -0,0 +1,126 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.CodeGenerator.Generators.UmlHandleBarsGenerators +{ + using System; + using System.IO; + using System.Threading.Tasks; + + using SysML2.NET.CodeGenerator.HandleBarHelpers; + + using uml4net.HandleBars; + using uml4net.xmi.Readers; + + /// + /// A Handlebars based generator that produces the PostgreSQL persistence schema for SysML v2 + /// models from the UML metamodel. Only non-derived, non-redefining properties are stored; + /// derived state is materialized separately at commit time. The reference design is + /// SysML2.NET.CodeGenerator/Sql/schema.golden.sql. + /// + public class SQLSchemaGenerator : UmlHandleBarsGenerator + { + /// + /// The name of the generated schema file + /// + public const string SchemaFileName = "schema2.sql"; + + /// + /// Generates the PostgreSQL schema + /// + /// + /// the that contains the UML model to generate from + /// + /// + /// The target + /// + /// + /// an awaitable + /// + public override async Task GenerateAsync(XmiReaderResult xmiReaderResult, DirectoryInfo outputDirectory) + { + await this.GenerateSqlSchemaAsync(xmiReaderResult, outputDirectory); + } + + /// + /// Generates the PostgreSQL schema and returns the generated DDL + /// + /// + /// the that contains the UML model to generate from + /// + /// + /// The target + /// + /// + /// an awaitable carrying the generated DDL + /// + public Task GenerateSqlSchemaAsync(XmiReaderResult xmiReaderResult, DirectoryInfo outputDirectory) + { + ArgumentNullException.ThrowIfNull(xmiReaderResult); + ArgumentNullException.ThrowIfNull(outputDirectory); + + return this.GenerateSqlSchemaInternalAsync(xmiReaderResult, outputDirectory); + } + + /// + /// Register the custom helpers + /// + protected override void RegisterHelpers() + { + this.Handlebars.RegisterStringHelper(); + this.Handlebars.RegisterEnumerableHelper(); + + this.Handlebars.RegisterUmlTemplateSqlSchemaHelpers(); + } + + /// + /// Register the code templates + /// + protected override void RegisterTemplates() + { + this.RegisterTemplate("core-sql-schema-2"); + } + + /// + /// Generates the PostgreSQL schema and returns the generated DDL + /// + /// + /// the that contains the UML model to generate from + /// + /// + /// The target + /// + /// + /// an awaitable carrying the generated DDL + /// + private async Task GenerateSqlSchemaInternalAsync(XmiReaderResult xmiReaderResult, DirectoryInfo outputDirectory) + { + var template = this.Templates["core-sql-schema-2"]; + + var payload = CreateHandlebarsPayload(xmiReaderResult); + + var generatedSchema = template(payload); + + await WriteAsync(generatedSchema, outputDirectory, SchemaFileName); + + return generatedSchema; + } + } +} diff --git a/SysML2.NET.CodeGenerator/HandleBarHelpers/SqlSchemaHelpers.cs b/SysML2.NET.CodeGenerator/HandleBarHelpers/SqlSchemaHelpers.cs new file mode 100644 index 000000000..0e8917092 --- /dev/null +++ b/SysML2.NET.CodeGenerator/HandleBarHelpers/SqlSchemaHelpers.cs @@ -0,0 +1,779 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.CodeGenerator.HandleBarHelpers +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + + using HandlebarsDotNet; + + using SysML2.NET.CodeGenerator.Extensions; + using SysML2.NET.CodeGenerator.Generators.UmlHandleBarsGenerators; + + using uml4net.Classification; + using uml4net.Extensions; + using uml4net.SimpleClassifiers; + using uml4net.StructuredClassifiers; + + /// + /// Handlebars block helpers for the PostgreSQL schema generator. Each helper emits one + /// [GENERATED] section of SysML2.NET.CodeGenerator/Templates/Uml/core-sql-schema-2.hbs; + /// the hand-written sections (PIM, element_version, derived_version, snapshot resolution) + /// live verbatim in the template. The reference design is + /// SysML2.NET.CodeGenerator/Sql/schema.golden.sql. + /// + public static class SqlSchemaHelpers + { + /// + /// Registers the SQL schema helpers + /// + /// + /// The context with which the helpers need to be registered + /// + public static void RegisterUmlTemplateSqlSchemaHelpers(this IHandlebars handlebars) + { + handlebars.RegisterHelper("uml_template.SQL2.WriteEnumTypes", (writer, _, parameters) => + { + var payload = ResolvePayload(parameters[0]); + + var stringBuilder = new StringBuilder(); + + foreach (var enumeration in payload.Enumerations.OrderBy(enumeration => enumeration.Name, StringComparer.Ordinal)) + { + var literals = string.Join(", ", enumeration.OwnedLiteral.Select(literal => $"'{literal.Name.ToLowerInvariant()}'")); + + stringBuilder.AppendLine($"CREATE TYPE sysml2.{enumeration.Name.QuerySqlSnakeCaseName()} AS ENUM ({literals});"); + } + + writer.WriteSafeString(stringBuilder.ToString()); + }); + + handlebars.RegisterHelper("uml_template.SQL2.WriteMetamodelCatalogRows", (writer, _, parameters) => + { + var payload = ResolvePayload(parameters[0]); + + AssertRegistryInSyncWithModel(payload); + + var stringBuilder = new StringBuilder(); + + stringBuilder.AppendLine("INSERT INTO sysml2.model_version (id, name, source_fingerprint) VALUES"); + + var modelVersionRows = ClassKindRegistry.ModelVersions + .Select(modelVersion => $" ({modelVersion.Id}, '{modelVersion.Name}', '{modelVersion.SourceFingerprint}')"); + + stringBuilder.AppendLine(string.Join(",\n", modelVersionRows)); + stringBuilder.AppendLine("ON CONFLICT (id) DO NOTHING;"); + stringBuilder.AppendLine(""); + stringBuilder.AppendLine("INSERT INTO sysml2.class_kind (id, name, is_abstract, introduced_in, removed_in) VALUES"); + + var classKindRows = ClassKindRegistry.ClassKinds + .Select(registration => $" ({registration.Id}, '{registration.Name}', {FormatSqlBoolean(registration.IsAbstract)}, {registration.IntroducedIn}, {(registration.RemovedIn.HasValue ? registration.RemovedIn.Value.ToString() : "NULL")})"); + + stringBuilder.AppendLine(string.Join(",\n", classKindRows)); + stringBuilder.AppendLine("ON CONFLICT (id) DO NOTHING;"); + + writer.WriteSafeString(stringBuilder.ToString()); + }); + + handlebars.RegisterHelper("uml_template.SQL2.WriteLinkTables", (writer, _, parameters) => + { + var payload = ResolvePayload(parameters[0]); + + var stringBuilder = new StringBuilder(); + + foreach (var @class in QueryOrderedClasses(payload).Where(@class => @class.QueryStoredMultiOwnProperties().Count != 0)) + { + foreach (var property in @class.QueryStoredMultiOwnProperties()) + { + var tableName = @class.QuerySqlLinkTableName(property); + var isReference = property.QueryIsReferenceType(); + var valueColumn = isReference ? "target_identity" : "value"; + var valueType = isReference ? "uuid" : property.QuerySqlTypeName(); + // deliberately NO cascade: identity deletion is an explicit, ordered, per-table + // procedure (see the data_identity section of the template) — an ON DELETE CASCADE + // here would execute per-row deletes filtered on target_identity alone, which no + // index leads with + var valueConstraint = isReference ? " REFERENCES sysml2.data_identity (id)" : string.Empty; + + stringBuilder.AppendLine($"CREATE TABLE sysml2.{tableName} ("); + stringBuilder.AppendLine(" project_id uuid NOT NULL,"); + stringBuilder.AppendLine(" version_id uuid NOT NULL,"); + stringBuilder.AppendLine(" ordinal int NOT NULL,"); + stringBuilder.AppendLine($" {valueColumn} {valueType} NOT NULL{valueConstraint},"); + stringBuilder.AppendLine(" PRIMARY KEY (project_id, version_id, ordinal),"); + stringBuilder.AppendLine(" FOREIGN KEY (project_id, version_id)"); + stringBuilder.AppendLine(" REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE"); + stringBuilder.AppendLine(") PARTITION BY HASH (project_id);"); + stringBuilder.AppendLine(""); + + if (isReference) + { + stringBuilder.AppendLine($"CREATE INDEX ix_{tableName}_target"); + stringBuilder.AppendLine($" ON sysml2.{tableName} (project_id, target_identity);"); + stringBuilder.AppendLine(""); + } + } + } + + writer.WriteSafeString(stringBuilder.ToString()); + }); + + handlebars.RegisterHelper("uml_template.SQL2.WriteSubtypeTables", (writer, _, parameters) => + { + var payload = ResolvePayload(parameters[0]); + + var stringBuilder = new StringBuilder(); + + foreach (var @class in QueryStorageIntroducingClasses(payload)) + { + var tableName = @class.QuerySqlSubtypeTableName(); + + stringBuilder.AppendLine($"CREATE TABLE sysml2.{tableName} ("); + stringBuilder.AppendLine(" project_id uuid NOT NULL,"); + stringBuilder.AppendLine(" version_id uuid NOT NULL,"); + + foreach (var property in @class.QueryStoredScalarOwnProperties()) + { + stringBuilder.AppendLine($" {FormatSubtypeColumn(property)},"); + } + + stringBuilder.AppendLine(" PRIMARY KEY (project_id, version_id),"); + stringBuilder.AppendLine(" FOREIGN KEY (project_id, version_id)"); + stringBuilder.AppendLine(" REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE"); + stringBuilder.AppendLine(") PARTITION BY HASH (project_id);"); + stringBuilder.AppendLine(""); + + foreach (var property in @class.QueryStoredScalarOwnProperties().Where(property => property.QueryIsReferenceType())) + { + stringBuilder.AppendLine($"CREATE INDEX ix_{tableName}_{property.QuerySqlColumnName()}"); + stringBuilder.AppendLine($" ON sysml2.{tableName} (project_id, {property.QuerySqlColumnName()});"); + stringBuilder.AppendLine(""); + } + } + + writer.WriteSafeString(stringBuilder.ToString()); + }); + + handlebars.RegisterHelper("uml_template.SQL2.WriteFlatteningViews", (writer, _, parameters) => + { + var payload = ResolvePayload(parameters[0]); + var classKinds = QueryClassKinds(payload); + + var stringBuilder = new StringBuilder(); + + foreach (var classKind in classKinds.Where(classKind => !classKind.Key.IsAbstract)) + { + var ancestors = classKind.Key.QueryStorageAncestors(); + + stringBuilder.AppendLine($"CREATE VIEW sysml2.vw_{classKind.Key.Name.QuerySqlSnakeCaseName()} AS"); + stringBuilder.AppendLine(" SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id,"); + stringBuilder.Append(" ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship"); + + foreach (var ancestor in ancestors) + { + var columns = ancestor.QueryStoredScalarOwnProperties() + .Select(property => $"{ancestor.QuerySqlSubtypeTableName()}.{property.QuerySqlColumnName()}"); + + stringBuilder.AppendLine(","); + stringBuilder.Append($" {string.Join(", ", columns)}"); + } + + stringBuilder.AppendLine(""); + stringBuilder.AppendLine(" FROM sysml2.element_version ev"); + + foreach (var ancestor in ancestors) + { + stringBuilder.AppendLine($" JOIN sysml2.{ancestor.QuerySqlSubtypeTableName()} USING (project_id, version_id)"); + } + + stringBuilder.AppendLine($" WHERE ev.class_kind = {classKind.Value} AND NOT ev.tombstone;"); + stringBuilder.AppendLine(""); + } + + writer.WriteSafeString(stringBuilder.ToString()); + }); + + handlebars.RegisterHelper("uml_template.SQL2.WriteReferenceValidation", (writer, _, parameters) => + { + var payload = ResolvePayload(parameters[0]); + var registrationsByName = ClassKindRegistry.ClassKinds.ToDictionary(registration => registration.Name); + + var concreteClassCount = QueryOrderedClasses(payload).Count(@class => !@class.IsAbstract); + var referenceSources = new List<(string TableName, string ColumnName, string TypeName)>(); + + foreach (var @class in QueryOrderedClasses(payload)) + { + var scalarTable = @class.Name == "Element" ? "element_version" : @class.QuerySqlSubtypeTableName(); + + referenceSources.AddRange(@class.QueryStoredScalarOwnProperties() + .Where(property => property.QueryIsReferenceType()) + .Select(property => (scalarTable, property.QuerySqlColumnName(), property.QueryTypeName()))); + + referenceSources.AddRange(@class.QueryStoredMultiOwnProperties() + .Where(property => property.QueryIsReferenceType()) + .Select(property => (@class.QuerySqlLinkTableName(property), "target_identity", property.QueryTypeName()))); + } + + var boundedSources = referenceSources + .Select(source => + { + var allowedTargetKinds = QueryAllowedTargetKinds(payload, registrationsByName, source.TypeName); + + return (source.TableName, source.ColumnName, + AllowedTargetKinds: allowedTargetKinds.Count == concreteClassCount ? null : allowedTargetKinds); + }) + .ToList(); + + var fullPassBlocks = boundedSources + .Select(source => FormatReferenceValidationBlock(source.TableName, source.ColumnName, source.AllowedTargetKinds)); + + var outgoingBlocks = boundedSources + .Select(source => FormatOutgoingValidationBlock(source.TableName, source.ColumnName, source.AllowedTargetKinds)); + + var incomingBlocks = boundedSources + .Select(source => FormatIncomingValidationBlock(source.TableName, source.ColumnName)); + + var stringBuilder = new StringBuilder(); + + stringBuilder.AppendLine("CREATE OR REPLACE FUNCTION sysml2.validate_references_at_commit("); + stringBuilder.AppendLine(" p_project_id uuid,"); + stringBuilder.AppendLine(" p_commit_id uuid"); + stringBuilder.AppendLine(")"); + stringBuilder.AppendLine("RETURNS TABLE ("); + stringBuilder.AppendLine(" source_table text,"); + stringBuilder.AppendLine(" source_column text,"); + stringBuilder.AppendLine(" source_identity uuid,"); + stringBuilder.AppendLine(" target_identity uuid,"); + stringBuilder.AppendLine(" problem text"); + stringBuilder.AppendLine(")"); + stringBuilder.AppendLine("LANGUAGE plpgsql"); + stringBuilder.AppendLine("AS $$"); + stringBuilder.AppendLine("BEGIN"); + stringBuilder.AppendLine(" -- Materialize + ANALYZE the snapshot so the planner knows its TRUE cardinality and"); + stringBuilder.AppendLine(" -- can choose per arm between hashing the source (young history) and snapshot-driven"); + stringBuilder.AppendLine(" -- PK probes (deep history) — bounding the pass at O(snapshot x log history) instead"); + stringBuilder.AppendLine(" -- of O(history). A bare function CTE would be estimated at ~1000 rows."); + stringBuilder.AppendLine(" CREATE TEMP TABLE IF NOT EXISTS validation_snapshot ("); + stringBuilder.AppendLine(" identity_id uuid NOT NULL,"); + stringBuilder.AppendLine(" version_id uuid NOT NULL"); + stringBuilder.AppendLine(" ) ON COMMIT DROP;"); + stringBuilder.AppendLine(""); + stringBuilder.AppendLine(" TRUNCATE validation_snapshot;"); + stringBuilder.AppendLine(""); + stringBuilder.AppendLine(" INSERT INTO validation_snapshot (identity_id, version_id)"); + stringBuilder.AppendLine(" SELECT r.identity_id, r.version_id"); + stringBuilder.AppendLine(" FROM sysml2.resolve_commit_state(p_project_id, p_commit_id) r;"); + stringBuilder.AppendLine(""); + stringBuilder.AppendLine(" CREATE INDEX IF NOT EXISTS ix_validation_snapshot_version ON validation_snapshot (version_id);"); + stringBuilder.AppendLine(" CREATE INDEX IF NOT EXISTS ix_validation_snapshot_identity ON validation_snapshot (identity_id);"); + stringBuilder.AppendLine(""); + stringBuilder.AppendLine(" ANALYZE validation_snapshot;"); + stringBuilder.AppendLine(""); + stringBuilder.AppendLine(" RETURN QUERY"); + stringBuilder.AppendLine(string.Join("\n UNION ALL\n", fullPassBlocks) + ";"); + stringBuilder.AppendLine("END;"); + stringBuilder.AppendLine("$$;"); + stringBuilder.AppendLine(""); + stringBuilder.AppendLine("-- The INCREMENTAL tier: validates only commit p_commit_id's change set — outgoing"); + stringBuilder.AppendLine("-- references of its new versions, plus the reverse direction its tombstones break"); + stringBuilder.AppendLine("-- (a live, UNCHANGED element left referencing a deleted identity). O(change set),"); + stringBuilder.AppendLine("-- independent of history and snapshot size; the full pass above remains the"); + stringBuilder.AppendLine("-- periodic audit that backstops it."); + stringBuilder.AppendLine("CREATE OR REPLACE FUNCTION sysml2.validate_references_in_commit("); + stringBuilder.AppendLine(" p_project_id uuid,"); + stringBuilder.AppendLine(" p_commit_id uuid"); + stringBuilder.AppendLine(")"); + stringBuilder.AppendLine("RETURNS TABLE ("); + stringBuilder.AppendLine(" source_table text,"); + stringBuilder.AppendLine(" source_column text,"); + stringBuilder.AppendLine(" source_identity uuid,"); + stringBuilder.AppendLine(" target_identity uuid,"); + stringBuilder.AppendLine(" problem text"); + stringBuilder.AppendLine(")"); + stringBuilder.AppendLine("LANGUAGE sql"); + stringBuilder.AppendLine("STABLE"); + stringBuilder.AppendLine("AS $$"); + stringBuilder.AppendLine(" SELECT DISTINCT findings.source_table, findings.source_column,"); + stringBuilder.AppendLine(" findings.source_identity, findings.target_identity, findings.problem"); + stringBuilder.AppendLine(" FROM ("); + stringBuilder.AppendLine(string.Join("\n UNION ALL\n", outgoingBlocks.Concat(incomingBlocks))); + stringBuilder.AppendLine(" ) AS findings (source_table, source_column, source_identity, target_identity, problem);"); + stringBuilder.AppendLine("$$;"); + + writer.WriteSafeString(stringBuilder.ToString()); + }); + + handlebars.RegisterHelper("uml_template.SQL2.WritePartitionedTableArray", (writer, _, parameters) => + { + var payload = ResolvePayload(parameters[0]); + + var linkTables = QueryOrderedClasses(payload) + .SelectMany(@class => @class.QueryStoredMultiOwnProperties().Select(property => @class.QuerySqlLinkTableName(property))); + + var subtypeTables = QueryStorageIntroducingClasses(payload) + .Select(@class => @class.QuerySqlSubtypeTableName()); + + var tableNames = linkTables + .Concat(subtypeTables) + .Select(tableName => $" '{tableName}'"); + + writer.WriteSafeString(",\n" + string.Join(",\n", tableNames)); + }); + + handlebars.RegisterHelper("uml_template.SQL2.WriteModelVersion", (writer, _, parameters) => + { + var payload = ResolvePayload(parameters[0]); + + writer.WriteSafeString($"{payload.RootPackage.Name}:{payload.RootPackage.XmiId}"); + }); + } + + /// + /// Resolves the from the helper's first argument + /// + /// + /// The first helper argument + /// + /// + /// The + /// + private static HandlebarsPayload ResolvePayload(object candidate) + { + if (candidate is not HandlebarsPayload payload) + { + throw new ArgumentException("The SQL schema helpers must be invoked with a HandlebarsPayload argument"); + } + + return payload; + } + + /// + /// Queries the classes of the payload in the deterministic order that assigns class_kind ids + /// + /// + /// The subject + /// + /// + /// The classes ordered by name + /// + private static IReadOnlyList QueryOrderedClasses(HandlebarsPayload payload) + { + return payload.Classes + .OrderBy(@class => @class.Name, StringComparer.Ordinal) + .ToList(); + } + + /// + /// Queries the interned class_kind id of every class in the payload. The ids come from the + /// append-only — frozen once assigned, never positional — + /// so generated artifacts (seeds, view predicates) stay stable across metamodel releases. + /// + /// + /// The subject + /// + /// + /// The class → id pairs, ordered by id + /// + private static IReadOnlyList> QueryClassKinds(HandlebarsPayload payload) + { + var registrationsByName = ClassKindRegistry.ClassKinds.ToDictionary(registration => registration.Name); + + return QueryOrderedClasses(payload) + .Select(@class => new KeyValuePair(@class, registrationsByName[@class.Name].Id)) + .OrderBy(classKind => classKind.Value) + .ToList(); + } + + /// + /// Asserts that the UML model on disk matches the newest release registered in the + /// append-only . Any drift fails generation LOUDLY — + /// silently renumbering class_kind ids would corrupt every populated database and every + /// consumer of the generated ClassKind enum. + /// + /// + /// The subject + /// + private static void AssertRegistryInSyncWithModel(HandlebarsPayload payload) + { + var newestVersion = ClassKindRegistry.ModelVersions[^1]; + var fingerprint = $"{payload.RootPackage.Name}:{payload.RootPackage.XmiId}"; + + if (fingerprint != newestVersion.SourceFingerprint) + { + throw new InvalidOperationException( + $"The UML model fingerprint '{fingerprint}' does not match the newest registered model version '{newestVersion.Name}' " + + $"('{newestVersion.SourceFingerprint}') in ClassKindRegistry. Append a new ModelVersionRegistration for a new metamodel " + + "release, or update the fingerprint in place for an editorial change that adds or removes no metaclasses."); + } + + var registrationsByName = ClassKindRegistry.ClassKinds.ToDictionary(registration => registration.Name); + var orderedClasses = QueryOrderedClasses(payload); + + var unregisteredClasses = orderedClasses + .Where(@class => !registrationsByName.ContainsKey(@class.Name)) + .ToList(); + + if (unregisteredClasses.Count != 0) + { + var maxId = ClassKindRegistry.ClassKinds.Max(registration => registration.Id); + + var suggestedRegistrations = string.Join("\n", unregisteredClasses + .Select((@class, classIndex) => $" new({maxId + classIndex + 1}, \"{@class.Name}\", {(@class.IsAbstract ? "true" : "false")}, {newestVersion.Id}),")); + + throw new InvalidOperationException( + "The UML model contains metaclasses that are not registered in ClassKindRegistry. APPEND them after the highest " + + $"existing id — never renumber existing entries:\n{suggestedRegistrations}"); + } + + var modelClassNames = orderedClasses + .Select(@class => @class.Name) + .ToHashSet(); + + var staleRegistrations = ClassKindRegistry.ClassKinds + .Where(registration => registration.RemovedIn == null && !modelClassNames.Contains(registration.Name)) + .ToList(); + + if (staleRegistrations.Count != 0) + { + var staleNames = string.Join(", ", staleRegistrations.Select(registration => registration.Name)); + + throw new InvalidOperationException( + $"ClassKindRegistry registers metaclasses the UML model no longer contains: {staleNames}. Keep their entries and " + + "close them with RemovedIn = the id of the release that dropped them — never delete a registration."); + } + + var driftedRegistrations = orderedClasses + .Where(@class => registrationsByName[@class.Name].IsAbstract != @class.IsAbstract) + .ToList(); + + if (driftedRegistrations.Count != 0) + { + var driftedNames = string.Join(", ", driftedRegistrations.Select(@class => @class.Name)); + + throw new InvalidOperationException( + $"The abstractness of {driftedNames} differs between the UML model and ClassKindRegistry — update the registry entries to match."); + } + } + + /// + /// Queries the storage-introducing classes of the payload, ordered shallowest first so that + /// supertype tables are created before the deeper ones that conceptually extend them + /// + /// + /// The subject + /// + /// + /// The storage-introducing classes + /// + private static IReadOnlyList QueryStorageIntroducingClasses(HandlebarsPayload payload) + { + return QueryOrderedClasses(payload) + .Where(SqlSchemaExtensions.QueryIsStorageIntroducing) + .OrderBy(@class => @class.QueryAllGeneralClassifiers().Count) + .ThenBy(@class => @class.Name, StringComparer.Ordinal) + .ToList(); + } + + /// + /// Formats a single column definition of a subtype table, including nullability from the + /// property's lower bound, the FK to data_identity for references, and the UML-declared + /// default value when one exists + /// + /// + /// The subject scalar stored + /// + /// + /// The column definition, without the trailing comma + /// + private static string FormatSubtypeColumn(IProperty property) + { + var stringBuilder = new StringBuilder(); + + stringBuilder.Append(property.QuerySqlColumnName()); + stringBuilder.Append(' '); + stringBuilder.Append(property.QuerySqlTypeName()); + stringBuilder.Append(property.QueryIsNullable() ? " NULL" : " NOT NULL"); + + var defaultValue = FormatSqlDefaultValue(property); + + if (defaultValue != null) + { + stringBuilder.Append($" DEFAULT {defaultValue}"); + } + + if (property.QueryIsReferenceType()) + { + stringBuilder.Append(" REFERENCES sysml2.data_identity (id)"); + } + + return stringBuilder.ToString(); + } + + /// + /// Formats the SQL DEFAULT expression for a property's UML-declared default value, or null + /// when the property declares none + /// + /// + /// The subject + /// + /// + /// The SQL literal, or null + /// + private static string FormatSqlDefaultValue(IProperty property) + { + if (!property.QueryHasDefaultValue()) + { + return null; + } + + var defaultValue = property.QueryDefaultValueAsString(); + + if (string.IsNullOrWhiteSpace(defaultValue) || defaultValue == "null") + { + return null; + } + + if (property.QueryIsEnum() || property.QueryIsString()) + { + return $"'{defaultValue.ToLowerInvariant()}'"; + } + + return defaultValue.ToLowerInvariant(); + } + + /// + /// Queries the interned class_kind ids a reference property of the given declared type may + /// legally target: the concrete descendants of the declared type, the type itself included + /// when concrete. Resolved against the append-only . + /// + /// + /// The subject + /// + /// + /// The registry lookup by metaclass name + /// + /// + /// The name of the property's declared type, e.g. "Relationship" + /// + /// + /// The allowed class_kind ids, ordered ascending + /// + private static IReadOnlyList QueryAllowedTargetKinds(HandlebarsPayload payload, Dictionary registrationsByName, string typeName) + { + return QueryOrderedClasses(payload) + .Where(@class => !@class.IsAbstract) + .Where(@class => @class.Name == typeName + || @class.QueryAllGeneralClassifiers().OfType().Any(general => general.Name == typeName)) + .Select(@class => registrationsByName[@class.Name].Id) + .OrderBy(classKindId => classKindId) + .ToList(); + } + + /// + /// Formats one UNION ALL arm of validate_references_at_commit for a single stored reference + /// column. Reports 'wrong-type' via the typed identity (checked for cross-project targets + /// too) and 'dangling' for same-project targets absent from the snapshot; liveness of + /// cross-project targets is deliberately out of scope (it depends on the used-project + /// commit, which is service-layer resolution). + /// + /// + /// The source table carrying the reference column + /// + /// + /// The reference column + /// + /// + /// The legal target class_kind ids, or null when every concrete metaclass is legal (an + /// Element-typed reference) and the type check is omitted + /// + /// + /// The formatted SELECT arm, without a trailing separator + /// + private static string FormatReferenceValidationBlock(string tableName, string columnName, IReadOnlyList allowedTargetKinds) + { + var stringBuilder = new StringBuilder(); + + if (allowedTargetKinds == null) + { + stringBuilder.AppendLine($" SELECT '{tableName}'::text, '{columnName}'::text,"); + stringBuilder.AppendLine($" snap.identity_id, src.{columnName}, 'dangling'::text"); + stringBuilder.AppendLine($" FROM sysml2.{tableName} src"); + stringBuilder.AppendLine(" JOIN validation_snapshot snap ON snap.version_id = src.version_id"); + stringBuilder.AppendLine($" JOIN sysml2.data_identity ti ON ti.id = src.{columnName}"); + stringBuilder.AppendLine($" LEFT JOIN validation_snapshot live ON live.identity_id = src.{columnName}"); + stringBuilder.AppendLine(" WHERE src.project_id = p_project_id"); + stringBuilder.AppendLine($" AND src.{columnName} IS NOT NULL"); + stringBuilder.AppendLine(" AND ti.project_id = p_project_id"); + stringBuilder.Append(" AND live.identity_id IS NULL"); + + return stringBuilder.ToString(); + } + + var allowedIds = string.Join(", ", allowedTargetKinds); + + stringBuilder.AppendLine($" SELECT '{tableName}'::text, '{columnName}'::text,"); + stringBuilder.AppendLine($" snap.identity_id, src.{columnName},"); + stringBuilder.AppendLine($" CASE WHEN ti.class_kind NOT IN ({allowedIds}) THEN 'wrong-type' ELSE 'dangling' END"); + stringBuilder.AppendLine($" FROM sysml2.{tableName} src"); + stringBuilder.AppendLine(" JOIN validation_snapshot snap ON snap.version_id = src.version_id"); + stringBuilder.AppendLine($" JOIN sysml2.data_identity ti ON ti.id = src.{columnName}"); + stringBuilder.AppendLine($" LEFT JOIN validation_snapshot live ON live.identity_id = src.{columnName}"); + stringBuilder.AppendLine(" WHERE src.project_id = p_project_id"); + stringBuilder.AppendLine($" AND src.{columnName} IS NOT NULL"); + stringBuilder.AppendLine($" AND (ti.class_kind NOT IN ({allowedIds})"); + stringBuilder.Append(" OR (ti.project_id = p_project_id AND live.identity_id IS NULL))"); + + return stringBuilder.ToString(); + } + + /// + /// Formats one UNION ALL arm of the OUTGOING half of validate_references_in_commit: the + /// references carried by the versions the commit itself wrote. Liveness is probed per + /// target through resolve_element_at_commit (zero rows == not alive), so the arm is + /// O(change set) — no snapshot materialization. + /// + /// + /// The source table carrying the reference column + /// + /// + /// The reference column + /// + /// + /// The legal target class_kind ids, or null when every concrete metaclass is legal and the + /// type check is omitted + /// + /// + /// The formatted SELECT arm, without a trailing separator + /// + private static string FormatOutgoingValidationBlock(string tableName, string columnName, IReadOnlyList allowedTargetKinds) + { + var stringBuilder = new StringBuilder(); + var isCoreTable = tableName == "element_version"; + var sourceAlias = isCoreTable ? "changed" : "src"; + + stringBuilder.AppendLine($" SELECT '{tableName}'::text, '{columnName}'::text,"); + stringBuilder.AppendLine($" changed.identity_id, {sourceAlias}.{columnName},"); + + if (allowedTargetKinds == null) + { + stringBuilder.AppendLine(" 'dangling'::text"); + } + else + { + stringBuilder.AppendLine($" CASE WHEN ti.class_kind NOT IN ({string.Join(", ", allowedTargetKinds)}) THEN 'wrong-type' ELSE 'dangling' END"); + } + + stringBuilder.AppendLine(" FROM sysml2.element_version changed"); + + if (!isCoreTable) + { + stringBuilder.AppendLine($" JOIN sysml2.{tableName} src"); + stringBuilder.AppendLine(" ON src.project_id = changed.project_id AND src.version_id = changed.version_id"); + } + + stringBuilder.AppendLine($" JOIN sysml2.data_identity ti ON ti.id = {sourceAlias}.{columnName}"); + stringBuilder.AppendLine(" WHERE changed.project_id = p_project_id"); + stringBuilder.AppendLine(" AND changed.commit_id = p_commit_id"); + stringBuilder.AppendLine(" AND NOT changed.tombstone"); + stringBuilder.AppendLine($" AND {sourceAlias}.{columnName} IS NOT NULL"); + + var livenessCheck = + $"(ti.project_id = p_project_id\n" + + $" AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, {sourceAlias}.{columnName})))"; + + if (allowedTargetKinds == null) + { + stringBuilder.Append($" AND {livenessCheck}"); + } + else + { + stringBuilder.AppendLine($" AND (ti.class_kind NOT IN ({string.Join(", ", allowedTargetKinds)})"); + stringBuilder.Append($" OR {livenessCheck})"); + } + + return stringBuilder.ToString(); + } + + /// + /// Formats one UNION ALL arm of the INCOMING half of validate_references_in_commit: for + /// every identity the commit tombstones, the live-but-UNCHANGED holders whose stored + /// reference now dangles. Driven by the reverse-lookup index on the reference column; + /// a candidate holder row only counts when it IS its identity's live version at the + /// commit (probed through resolve_element_at_commit). + /// + /// + /// The source table carrying the reference column + /// + /// + /// The reference column + /// + /// + /// The formatted SELECT arm, without a trailing separator + /// + private static string FormatIncomingValidationBlock(string tableName, string columnName) + { + var stringBuilder = new StringBuilder(); + var isCoreTable = tableName == "element_version"; + + stringBuilder.AppendLine($" SELECT '{tableName}'::text, '{columnName}'::text,"); + stringBuilder.AppendLine(" holder.identity_id, dead.identity_id, 'dangling'::text"); + stringBuilder.AppendLine(" FROM sysml2.element_version dead"); + + if (isCoreTable) + { + stringBuilder.AppendLine(" JOIN sysml2.element_version holder"); + stringBuilder.AppendLine($" ON holder.project_id = dead.project_id AND holder.{columnName} = dead.identity_id"); + } + else + { + stringBuilder.AppendLine($" JOIN sysml2.{tableName} src"); + stringBuilder.AppendLine($" ON src.project_id = dead.project_id AND src.{columnName} = dead.identity_id"); + stringBuilder.AppendLine(" JOIN sysml2.element_version holder"); + stringBuilder.AppendLine(" ON holder.project_id = src.project_id AND holder.version_id = src.version_id"); + } + + stringBuilder.AppendLine(" WHERE dead.project_id = p_project_id"); + stringBuilder.AppendLine(" AND dead.commit_id = p_commit_id"); + stringBuilder.AppendLine(" AND dead.tombstone"); + stringBuilder.AppendLine(" AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive"); + stringBuilder.Append(" WHERE alive.version_id = holder.version_id)"); + + return stringBuilder.ToString(); + } + + /// + /// Formats a boolean as a SQL literal + /// + /// + /// The value to format + /// + /// + /// "true" or "false" + /// + private static string FormatSqlBoolean(bool value) + { + return value ? "true" : "false"; + } + + } +} diff --git a/SysML2.NET.CodeGenerator/IMPACT-RADIUS.md b/SysML2.NET.CodeGenerator/IMPACT-RADIUS.md new file mode 100644 index 000000000..0588cfa14 --- /dev/null +++ b/SysML2.NET.CodeGenerator/IMPACT-RADIUS.md @@ -0,0 +1,243 @@ +# IMPACT-RADIUS.md — Design sketch for the derived-property impact-radius engine + +> **Status: design sketch, not built.** This document works out service-layer obligation +> §15.1 of `SysML2.NET.CodeGenerator/SQLSCHEMA-GUIDE.md` — the component the guide calls +> "the hard one" and "where the correctness bugs of the whole system will live." It is the +> reference for whoever implements the engine; decisions recorded here are proposals, not +> commitments, except where marked **contract**. +> +> Companion documents: `SysML2.NET.CodeGenerator/SQLSCHEMA.md` (compact schema reference), +> `SysML2.NET.CodeGenerator/SQLSCHEMA-GUIDE.md` / `SQLSCHEMA-GUIDE.nl.md` (architecture +> guide — read §4 Axiom 2, §9, §15 and §18 first). + +--- + +## 1. Problem statement and contract + +**Input:** a change set Δ at a prospective commit C on branch B — the set of element +versions being written (creations, modifications, tombstones), plus the branch-head state +the commit builds on. + +**Output (contract):** exactly the set of `derived_version` rows to write at C — one row +per element whose derived values *actually differ* at C from their previously resolved +values, containing the complete recomputed derived state for that element (all of its +derived properties, since `derived_json` is written whole-row). + +Two failure directions, deliberately asymmetric in severity: + +- **A missed invalidation** (row not written although values changed) is **silent + corruption**: reads and queries at C serve stale derived values with full-conformance + confidence. This is the failure class the design must make structurally unlikely. +- **A superfluous recomputation** (candidate considered although nothing changed) costs + only CPU — and is then eliminated by diff pruning before it costs storage. + +Therefore the guiding rule everywhere below: **over-approximate candidates, never +under-approximate; prune by value equality, never by guesswork.** + +## 2. The core observation: ~325 derived properties, ~5 propagation kinds + +Writing bespoke invalidation logic for 325 derived properties is neither feasible nor +necessary. Classified by *how a change reaches them*, they collapse into a handful of +propagation kinds — and the schema already carries a reverse-lookup index for each +(SQLSCHEMA-GUIDE §8.3 noted these indexes exist precisely for this analysis): + +| Kind | Example properties | Propagation | Carrying index | +|---|---|---|---| +| **K1 self / one-hop** | most of the 325: `memberName` fallbacks, `constraintDefinition`, typing shortcuts | the element itself, plus elements it directly references | (PKs) | +| **K2 ownership-down** | `qualifiedName`, `isLibraryElement`, `path` | a change at N invalidates N's entire owned subtree | `element_owned_relationship` (+ `owning_relationship` back-pointers) | +| **K3 specialization-down** | `feature`, `inheritedMembership`, `membership`, `input`/`output` | a change to a Type invalidates all its transitive SPECIFIC types | `ix_specialization_version_general` (+ `subclassification`, `feature_typing` variants) | +| **K4 import-closure** | `importedMembership`, `member` | a change to namespace N invalidates every (transitively, when `isRecursive`) importing namespace | `ix_namespace_import_version_imported_namespace`, `ix_membership_import_version_imported_membership` | +| **K5 reverse-reference** | `documentation`, `ownedAnnotation`, `textualRepresentation` | a change to an annotating element invalidates the annotated element | `ix_annotation_version_annotated_element`, the `ix_*_target` family | + +Kinds compose: a rename at N triggers K2 from N; adding a Specialization triggers K3 from +its `general`; the closures may cascade into each other (a K3-invalidated namespace-typed +member can seed K4). The engine must run expansion to a fixed point across kinds — in +practice shallow, because the kinds' outputs mostly re-trigger only K1. + +## 3. The `derived_dependency` catalog + +**Proposal:** a generated-plus-curated catalog — same generated-from-the-metamodel nature as +the per-release model-version descriptors (guide §12.2) — that makes the engine data-driven +instead of 325 hand-coded branches: + +```sql +CREATE TABLE sysml2.derived_dependency ( + trigger_class_kind smallint NOT NULL, -- metaclass whose STORED property changed + trigger_property text NOT NULL, -- the stored property (or '*' for create/delete) + kind text NOT NULL, -- 'K1'..'K5' + affected_property text NOT NULL, -- the derived property to recompute + affected_class_kind smallint NULL, -- narrows the candidate type when applicable + PRIMARY KEY (trigger_class_kind, trigger_property, kind, affected_property) +); +``` + +How to populate it: + +1. **Machine-assisted first pass:** the OCL derivation bodies live in the XMI (the same + source the `Compute*` methods were implemented from). Each derivation body reads a set + of navigation paths; *inverting* those paths yields the triggers. E.g. `qualifiedName` + reads `owningNamespace.qualifiedName` and the owner's member names → inverted: a change + to `declaredName`/`declaredShortName` or to ownership at N invalidates `qualifiedName` + K2-down from N. +2. **Curated second pass:** OCL parsing will not be airtight; every row is reviewed by a + human, **grounded via the Hypha plugin** (per this repository's CLAUDE.md mandate for + all derived-property semantics: `hypha:metamodel-lookup` for structure, + `hypha:spec-citation` where the OCL needs interpretation). +3. **The safety valve:** any derived property that cannot be confidently classified gets a + catch-all row that maps its triggers to **full recompute** (section 7). Unclassified + must never mean unhandled. + +The catalog ships like the model-version descriptors: emitted by the generator where +derivable, with the curated layer version-controlled beside it. Whether it lands as a +database table or as generated C# is open decision 6 — note that its former sibling +`property_catalog` was ultimately dropped from the database in favor of generated C#. + +## 4. The pipeline + +``` +Δ (change set at head H) + │ 1. SEED look up (trigger → kind, affected) pairs in derived_dependency + ▼ +seeds {(kind, root, affected-properties)} + │ 2. EXPAND recursive CTEs over the reverse indexes, evaluated against the + ▼ overlay-resolved head state; iterate kinds to fixed point; dedupe +candidate set C ──────────────► |C| is a §15.15 monitoring signal (alert on spikes) + │ 3. EVALUATE lazy DAL session over get_element_at_branch_head; stratified order; + ▼ run only the Compute* methods for the affected properties' element +recomputed derived state per candidate + │ 4. DIFF compare against each candidate's currently resolved derived row; + ▼ drop candidates whose values did not change (prune, and CUT OFF — §5) +rows that actually changed + │ 5. WRITE bulk insert derived_version (COPY / multi-row; R5 GIN pending-list + ▼ session bump) + branch_head overlay updates, inside the commit txn +commit C complete +``` + +Notes per stage: + +- **Expansion is SQL work, not .NET work.** A K2 expansion is one recursive CTE over + `element_owned_relationship` from the seed roots, resolved through the branch overlay + (overlay row wins over base checkpoint — same COALESCE pattern as the read path §11). + The database is strictly better at this than object traversal. +- **Stratified evaluation** exploits the closures' structure: process K2 candidates in + ownership order (parents before children) so `qualifiedName(child) = + qualifiedName(parent) + '::' + escapedName` is O(1) per node instead of O(depth); + process K3 candidates in topological order over the specialization DAG (supertypes + first) so `feature` folds reuse the parents' results. +- **The lazy DAL session** is what keeps memory bounded by radius-plus-fringe instead of + model size: candidates plus whatever the `Compute*` methods navigate ("support") are + materialized on demand from the head state and memoized for the duration of the commit. + The existing `SysML2.NET.Dal` assembler/factory infrastructure is the natural host. +- **Evaluate only what was triggered.** A K2 hit on an element requires recomputing its + K2-affected properties — not all 325. But since `derived_json` is written whole-row, a + *written* row carries the full derived state: recompute the untriggered properties from + the previous row's values (they are by construction unchanged) rather than re-deriving + them. + +## 5. Early cutoff — the worst-case killer + +The single most valuable optimization, borrowed from incremental-computation engines +(Adapton; Salsa, which rust-analyzer is built on): **when a candidate's recomputed values +equal its previous values, do not propagate through it.** + +Concretely for K2: rename element N, recompute N's subtree top-down; if a child's +`qualifiedName` comes out unchanged (rename to an effectively identical name, or a child +whose qualified name was overridden by a different mechanism), the entire subtree below +that child is pruned from the radius. The theoretical worst case (root rename ≈ whole +model) remains possible, but the *actual* cost becomes proportional to the *actual* +change — which is what users experience. + +This requires interleaving stages 3 and 4 per stratum (evaluate a level, diff it, expand +only through the changed nodes) rather than running them as strict phases. The pipeline +diagram above should be read with that refinement for K2/K3/K4. + +## 6. Concurrency and merge integration + +- **Commits:** the engine slots into the optimistic pattern of SQLSCHEMA-GUIDE §18.3(1): + compute the radius *before* taking the branch CAS (against the expected parent head); + on CAS success write; on CAS failure recompute only the delta against the new head and + retry. Never hold the branch lock during a large evaluation. +- **Pathological radii** (a true root rename at 1M elements): measured COPY throughput + puts the write at roughly 35–60 s — an intrinsic bulk operation (R5 budget). Consider + surfacing it in the API/UX as a confirmable operation ("this affects ~940,000 elements + — proceed?") rather than hiding a minute-long commit. +- **Merges (contract, from §15.13):** the radius of a merge commit is computed against + the **merged snapshot**, never as the union of the two branches' derived results — + cross-branch interactions (branch 1 adds a Specialization, branch 2 adds a feature to + its target) produce derived changes neither branch ever saw. +- **Cycle guards (contract, from §15.12):** a merge can create ownership or + specialization cycles that no single branch contained. Every closure walk in the engine + carries a visited-set; on cycle detection the commit is rejected with a model-validation + error — the walk must never hang, and `qualifiedName` of a cyclic containment is + undefined anyway. +- **Conversion commits (§6.4, obligation §15.16):** the release upgrade is a second client + of this engine. Its seed is not a user change set but the *version-diff* between two + metamodel releases (every element whose metaclass or properties changed shape); expansion, + evaluation, and bulk write then proceed identically, followed by a forced + `commit_checkpoint` on the conversion commit. + +## 7. The oracle — build the safety net first + +The correctness strategy is **differential testing against a trivially correct oracle**, +and it should be built *before* the engine: + +- **The oracle** is the full recompute: evaluate every derived property of every element + of the model at the new state, diff against the previous state, emit changed rows. + Slow, simple, obviously correct. +- **The property test:** for arbitrary change sets Δ on test models, + `engine(Δ) ≡ oracle(before, after)`. Run in CI on small models with randomized + ("fuzzed") mutations — creates, renames, moves, retypes, deletes, specialization edits, + import edits, and adversarial mixes. Any `derived_dependency` row that is too narrow + (the dangerous, silent direction of section 1) fails this test immediately. +- **The production fallback:** keep the oracle callable at runtime. Commits whose + triggers hit a catch-all row (section 3) or exceed a sanity bound run the oracle + path — slow-but-correct over fast-but-silently-wrong, consistent with the schema's + design philosophy throughout. +- **Golden scenarios** per propagation kind as regression anchors, mirroring the style of + `schema.smoke.sql`: the rename (K2 — already PASS 2a/2b at the SQL level), a supertype + feature addition (K3), a recursive import (K4), an annotation edit (K5), and the merge + cross-interaction of §15.13. + +## 8. Performance budget and monitoring + +- Radius **evaluation** dominates for large K3 folds; radius **write** dominates for + large K2 subtrees (measured: derived bulk insert ≈ 30k rows/s through the live GIN — + re-measure with production-size `derived_json`, per SQLSCHEMA.md). +- `|C|` per commit is a first-class monitoring signal (SQLSCHEMA-GUIDE §15.15): alert + when it exceeds a few percent of model size; spikes identify hot-rename usage patterns + and candidate-rule bugs alike. +- Track *prune ratio* (candidates dropped by diff) — a persistently low ratio means the + dependency catalog is too coarse; a ratio near 1.0 with large `|C|` means early cutoff + (section 5) is not engaging. + +## 9. What already exists to build on + +| Asset | Role in the engine | +|---|---| +| `SysML2.NET/Extend/*.Compute*` (366 implemented) | the evaluation functions | +| Reverse-lookup indexes (§8.3) | the closure expansions of section 2 | +| Model-version descriptors (guide §12.2) | template and sibling for `derived_dependency`; they enumerate every derived property per metaclass, per release | +| The generator pipeline (§17) | emits the machine-derivable part of the catalog | +| `SysML2.NET.Dal` assembler/factory | host for the lazy DAL session | +| Hypha plugin (CLAUDE.md mandate) | grounding for the curated classification pass | +| §15.15 monitoring hooks | `|C|`, prune ratio, GIN pending-list health | + +## 10. Open decisions for the implementer + +1. **Catalog derivation depth** — how much OCL inversion to automate versus classify by + hand (325 one-time rows is tractable by hand; automation pays off at metamodel + upgrades). +2. **Fixed-point iteration bound** — prove or bound the kind-cascade depth (expected ≤ 3); + define the escape to the oracle path if exceeded. +3. **Whole-model in memory for small projects** — below some size (say 50k elements) the + lazy session is overhead; a threshold switch to full in-memory evaluation may win. +4. **Confirmable-bulk UX** — where the "this affects N elements" confirmation lives + (API 202 + dry-run endpoint?) and its threshold. +5. **Derived recompute for passthrough-level providers** — none needed (SQLSCHEMA-GUIDE + §9.4); the engine is a full-conformance component and should be cleanly absent at the + other conformance levels. +6. **Catalog carrier: database table or generated C#** — the schema's own catalogs went the + generated-C# route (the model-version descriptors replaced `property_catalog`, guide + §12.2), and with multi-version support the dependency rules are per-release too; a + database table would need release columns to say the same thing. Lean generated-C# unless + an in-database consumer emerges. diff --git a/SysML2.NET.CodeGenerator/SQLSCHEMA-GUIDE.md b/SysML2.NET.CodeGenerator/SQLSCHEMA-GUIDE.md new file mode 100644 index 000000000..8e6875d0f --- /dev/null +++ b/SysML2.NET.CodeGenerator/SQLSCHEMA-GUIDE.md @@ -0,0 +1,2013 @@ +# The SysML2.NET PostgreSQL Schema — An Architectural Guide + +> **Who this is for.** You know SQL. You want to understand *why* this schema looks the way it +> does — every table, every index, every function, and the reasoning chain that led there. +> This document is the long-form companion to `SysML2.NET.CodeGenerator/SQLSCHEMA.md` (the +> compact reference). Where SQLSCHEMA.md states decisions, this guide *derives* them. +> +> **The artifacts it explains:** +> +> | File | Role | +> |---|---| +> | `SysML2.NET.CodeGenerator/Sql/schema.golden.sql` | Hand-written, annotated reference design | +> | `SysML2.NET.CodeGenerator/Sql/schema2.generated.sql` | Actual generator output (checked in for review) | +> | `SysML2.NET.CodeGenerator/Sql/schema.smoke.sql` | 30-assertion functional test | +> | `SysML2.NET.CodeGenerator/Templates/Uml/core-sql-schema-2.hbs` | The Handlebars template that emits the schema | +> +> Section numbers like **§5** refer to the numbered banners inside the schema files themselves. +> +> Een Nederlandse vertaling van deze gids: `SysML2.NET.CodeGenerator/SQLSCHEMA-GUIDE.nl.md`. + +--- + +## Table of contents + +1. [The problem being solved](#1-the-problem-being-solved) +2. [The two worlds: element data and PIM data](#2-the-two-worlds-element-data-and-pim-data) +3. [The census: why 77% of the metamodel is not stored](#3-the-census-why-77-of-the-metamodel-is-not-stored) +4. [The two axioms everything follows from](#4-the-two-axioms-everything-follows-from) +5. [Rejected alternatives, and why](#5-rejected-alternatives-and-why) +6. [Layer A — the PIM: projects, commits, branches, tags (§3)](#6-layer-a--the-pim-projects-commits-branches-tags-3) +7. [Identity: `data_identity` and the referential-integrity philosophy (§4)](#7-identity-data_identity-and-the-referential-integrity-philosophy-4) +8. [Layer B — stored element state (§5, §6, §7)](#8-layer-b--stored-element-state-5-6-7) +9. [Layer C — derived element state (§8)](#9-layer-c--derived-element-state-8) +10. [Layer D — snapshot resolution (§9)](#10-layer-d--snapshot-resolution-9) +11. [The read path (§10)](#11-the-read-path-10) +12. [The metamodel catalogs and the Query service (§2, §11)](#12-the-metamodel-catalogs-and-the-query-service-2-11) +13. [Partitioning and physical tuning (§12)](#13-partitioning-and-physical-tuning-12) +14. [The performance audit: war stories with numbers](#14-the-performance-audit-war-stories-with-numbers) +15. [What the service layer still owes the schema](#15-what-the-service-layer-still-owes-the-schema) +16. [Worked examples — following data through the schema](#16-worked-examples--following-data-through-the-schema) +17. [Code generation: what is emitted from the UML model and how](#17-code-generation-what-is-emitted-from-the-uml-model-and-how) +18. [Multi-user and concurrency](#18-multi-user-and-concurrency) +19. [Glossary](#19-glossary) + +--- + +## 1. The problem being solved + +This schema is the persistence layer for a **SysML v2 model repository** that implements the +OMG *Systems Modeling API and Services* specification, version 1.0. That one sentence carries +three hard requirements, and each one shapes the schema more than any ordinary CRUD concern: + +**Requirement 1 — it stores *models*, not records.** A SysML v2 model is a graph of typed +elements (`PartUsage`, `Membership`, `Specialization`, …) drawn from a metamodel of 175 +metaclasses. Elements reference each other densely — ownership trees, type hierarchies, +namespace imports. A "row" here is one element of a systems-engineering model that may contain +a million of them. + +**Requirement 2 — it is a *version control system*.** The OMG API is deliberately Git-shaped: +projects contain commits, commits form a directed acyclic graph (merges have multiple parents), +branches are movable pointers into that DAG, tags are frozen ones. Every API read happens *at* +a commit: `GET /projects/{p}/commits/{c}/elements/{e}`. Commits are immutable and indestructible +by specification. This rules out the classic "current-state tables + audit log" shape — history +is not an audit concern here, it *is* the data model. + +**Requirement 3 — it must answer with *derived* properties.** This is the requirement that most +people underestimate, and it is the single biggest driver of this design. The SysML v2 metamodel +defines most of its properties as **derived**: computed from other elements by traversal rules +written in OCL. An element's `qualifiedName` is computed by walking its ownership chain to the +root namespace. A type's `feature` set is computed by folding memberships across its whole +specialization hierarchy. The OMG API (Clause 2, "Derived Property Conformance") lets a server +claim one of three levels: + +- *no conformance* — never return derived properties; +- *passthrough* — store whatever derived values clients send and echo them back, never compute; +- **full conformance** — every response contains correctly computed, up-to-date derived values, + and derived properties are usable in query filters. + +This schema targets **full conformance with commit-time precomputation**: derived values are +computed once, when a commit is written, and reads just return bytes. Section 9 explains why +that choice (rather than compute-on-read) and what it costs. + +Finally, the scale profile the schema is engineered for (confirmed with the project owner): + +- **~1 million elements** per project, +- **100–500 concurrently live branches** per project, created and deleted routinely, +- **tens of thousands of commits** per project (years of daily editing), +- **tens to hundreds of projects** sharing one PostgreSQL instance, +- read traffic dominated by *branch-head* element reads and query filters; occasional + historical reads. + +Keep those numbers in mind throughout. Several designs that are perfectly fine at 100k elements +with 5 branches die at this profile, and section 14 shows the measurements. + +--- + +## 2. The two worlds: element data and PIM data + +The OMG specification splits its data model into two levels, and the schema mirrors the split. + +**The PIM (Platform-Independent Model)** is the *repository machinery*: `Project`, `Commit`, +`Branch`, `Tag`, `DataVersion`, `DataIdentity`, `Query`. These types are defined in Clause 7 of +the API spec, not in the SysML metamodel. There are 16 of them, they are stable (they change +when OMG revises the API, roughly never), and their semantics are subtle (commit DAGs, merge +invariants). They are **hand-written** in the schema (§3) — code-generating 16 stable tables +would add machinery without adding value, and the subtle parts (the monotonicity trigger, the +deletion procedure) need human-written comments anyway. + +**Element data** is the actual model content: the 175 metaclasses of KerML + SysML v2. This part +is **generated** from the same UML XMI files (`Resources/KerML_only_xmi.uml`, +`Resources/SysML_only_xmi.uml`) that generate the rest of SysML2.NET — the DTOs, POCOs, JSON +serializers, and so on. When OMG revises the language (they do, regularly), you re-run the +generator and get a schema that matches the new metamodel exactly, with no hand-maintenance of +167 table definitions. Section 17 covers the generation pipeline. + +The boundary between the worlds is a single concept: the **DataVersion**. In the spec, a +`DataVersion` wraps an element payload in the context of a commit — "element X had these +contents at commit C". In the schema, that concept is the `element_version` row. The PIM tables +organize *which* versions exist; the element tables record *what* each version contained. + +```mermaid +flowchart TB + subgraph PIM["PIM — repository machinery (hand-written, §3)"] + project --> commit + commit --> commit_parent + project --> branch + project --> tag + end + subgraph ID["Identity (§4)"] + data_identity + end + subgraph STORED["Stored element state (§5–§7, append-only)"] + element_version --> subtype["47 subtype tables"] + element_version --> link["7 link tables"] + end + subgraph DERIVED["Derived element state (§8, append-only)"] + derived_version + end + subgraph SNAP["Snapshot resolution (§9)"] + branch_head["branch_head (overlay)"] + commit_checkpoint + registry["commit_checkpoint_registry"] + end + commit -.->|"one version row per\nchanged element"| element_version + commit -.->|"one derived row per\nimpacted element"| derived_version + element_version -->|identity_id| data_identity + derived_version -->|identity_id| data_identity + branch --> branch_head + branch -->|base_commit_id| commit_checkpoint +``` + +--- + +## 3. The census: why 77% of the metamodel is not stored + +Before a single table was designed, the metamodel was counted. This step mattered more than any +other, because the numbers destroy the intuition you would otherwise design from. + +The metamodel, as realized in this repository's generated code, contains: + +| Measure | Count | +|---|---| +| Metaclasses | 175 (167 concrete, 8 abstract) | +| Flattened properties across all concrete classes (own + inherited) | 12,963 | +| …of which **stored** (`{ get; set; }` in the DTOs) | 2,698 | +| …of which **derived** (`{ get; internal set; }`) | 9,582 | +| …explicit-interface redefinition aliases (no storage) | 683 | +| Distinct *declarations* behind the 2,698 stored properties | **97, across 49 metaclasses** | +| Distinct stored property *names* | ~80 | +| Widest stored footprint of any single metaclass | **24 columns** (`FlowUsage` and kin) | +| Multi-valued stored reference properties, distinct | **6** (`ownedRelationship`, `ownedRelatedElement`, `source`, `target`, `client`, `supplier`) plus 1 multi-valued string (`aliasIds`) | +| Enumerations | 7, with 19 literals total | + +Read those numbers again, because they are the whole game: + +**First: the stored surface is tiny.** Twelve thousand flattened properties sounds enormous — +until you notice that only ~2,700 are stored, and those collapse to 97 declarations because +inheritance does the multiplying. `Element` declares 7 stored properties and every one of the +167 concrete classes inherits them; that is 1,169 of the 2,698 right there. The metamodel's +stored core is genuinely small: a handful of booleans, names, one enum here and there, and a +modest set of single-valued references on the relationship metaclasses. + +**Second: the derived surface is enormous, and it is not decorative.** 9,582 flattened derived +properties, ~325 distinct names. These are not conveniences — they are the API's primary +vocabulary. `owner`, `qualifiedName`, `ownedElement`, `feature`, `membership`, `documentation` — +every one of them derived, every one of them expected in every API payload under full +conformance. And crucially, the important ones are **recursive**: + +- `qualifiedName` walks the ownership chain to the root, consulting sibling names along the way; +- `Type::feature` and `inheritedMembership` fold across the *entire specialization closure* of + a type (a breadth-first search over `Specialization` edges); +- `Namespace::importedMembership` is a recursive walk over imports, where `Import::isRecursive` + makes it unbounded; +- `isLibraryElement` walks ownership to check for a library root. + +None of these are computable in a single SQL `SELECT`. They need recursive CTEs or materialized +closures — or precomputation, which is the road taken. + +**Third: the storage type conflicts are real and force structure.** The metamodel reuses +property names with *different types*: `LiteralBoolean::value` is a Boolean, +`LiteralInteger::value` an Integer, `LiteralRational::value` a Real, `LiteralString::value` a +String — four incompatible SQL types under one name. Likewise `kind` is a *different enum* on +each of `RequirementConstraintMembership`, `StateSubactionMembership`, +`TransitionFeatureMembership`, and `TriggerInvocationExpression`. Any design with one shared +`value` column is dead on arrival. This single fact eliminates "one wide table" (section 5). + +**Fourth: inheritance is a DAG, not a tree.** 34 metaclasses have multiple direct supertypes +(up to 3: `FlowUsage` is simultaneously a `ConnectorAsUsage`, a `Flow`, and an `ActionUsage`, +which makes it both a *Feature* and a *Relationship*). Any design that assumes a linear +"join up the parent chain" is also dead on arrival. Deepest chain: 11 levels. + +Everything in sections 5–9 is a consequence of these four facts. + +### Two traps discovered while counting + +The census also surfaced two facts about the UML source that a naive generator gets wrong, and +they are worth recording because they will bite anyone who touches the generator later: + +**Trap 1 — association-owned ends.** In UML, a reference property that participates in an +association can be owned by the *association*, not by the class. `Membership::memberElement`, +`Specialization::general`, `FeatureTyping::type` — the load-bearing reference properties of the +entire metamodel — do **not** appear in `IClass.OwnedAttribute`. A generator that reads +`OwnedAttribute` silently produces a `membership_version` table *without the member element column* +(this actually happened during development; 22 of the 47 subtype tables came out wrong). The +correct definition of "declared by class C" is: *flattened properties of C, minus the union of +flattened properties of C's direct generalizations*. See +`SysML2.NET.CodeGenerator/Extensions/SqlSchemaExtensions.cs`, `QueryStoredOwnProperties`. + +**Trap 2 — two kinds of redefinition.** UML property redefinition covers two very different +situations. *Same-name redefinition* (`CollectExpression::operator` redefines +`OperatorExpression::operator`) is a constraint restatement — the redefining property is the +same storage slot, and the DTOs give it no independent field. There are exactly 9 of these. +*New-name redefinition* (`Membership::memberElement` redefines `Relationship::target`) is a +**new API property with its own storage** — the DTOs store both `memberElement` *and* the +inherited `target` list, and API payloads carry both. The storage rule that matches the rest of +SysML2.NET is: only same-name redefinitions are storage-free; they resolve transitively to the +root property's column. (This is the same discriminator that +`SysML2.NET.CodeGenerator/HandleBarHelpers/PropertyHelper.cs` uses for the DTO generator.) + +--- + +## 4. The two axioms everything follows from + +If you remember nothing else from this document, remember these two statements. Every +structural decision in the schema is a corollary of one of them. + +### Axiom 1 — References name identities, never versions + +A SysML element's `@id` is stable across its entire life. When a `FeatureTyping` says "this +feature is typed by element `4ace3d89-…`", it means *whatever that element is at whatever commit +you are looking from* — not "version 17 of that element". Version-independence is the point: +you can retype, rename, and edit the target element for years, and the reference stays valid. + +The schema consequence: **every element-to-element reference column is a foreign key to +`data_identity(id)` — never to `element_version`.** There is no FK anywhere in the schema from +one element version to another element version. + +This also defines what referential integrity can and cannot mean here. The FK guarantees the +target *identity exists in the database*. It cannot guarantee the target *exists at the commit +you are reading* — an element can legitimately reference something that was deleted on this +branch (that's a dangling reference *in the model*, which is a model-validation concern the +service reports, not a database-integrity violation). Trying to make the database enforce +per-commit reference validity would require FKs into a virtual, computed set — impossible, and +also wrong, because the spec explicitly permits models to be in intermediate states across +commits. + +### Axiom 2 — A derived value is a function of (identity, commit), not of (version) + +This is subtler and more consequential. Consider: + +``` +Package "Old" <- element P, version p1 + └── PartUsage "wheel" <- element W, version w1, qualifiedName = "Old::wheel" +``` + +Now commit a rename of the package to `"New"`. The commit's change set contains **one** element: +P (new version p2). W is untouched — no new version, `w1` remains its current stored state on +every branch. And yet W's `qualifiedName` is now `"New::wheel"`. + +So: W's derived state changed *without W changing*. A derived value is not a property of a +version — the same version `w1` has `qualifiedName = "Old::wheel"` at commit 1 and +`"New::wheel"` at commit 2. It is a property of the **(identity, snapshot)** pair. The OMG spec +says this in as many words (Clause 2): *"the values of derived properties of a given Element +may be affected by commits that do not directly change that Element."* + +The schema consequence: derived state **cannot live on `element_version`**. If it did, the +rename would force writing a new `element_version` row for W (and every other descendant) whose +*stored* half is byte-identical to the old one — you would be versioning elements that did not +change, corrupting the very meaning of "change set", and multiplying stored-state storage by +the impact radius of every rename. + +Instead the schema has **two parallel append-only streams**: + +- `element_version` — keyed by version; a row exists per *(element, commit-that-changed-it)*; + immutable; the system of record for stored state. +- `derived_version` — keyed by *(identity, commit)*; a row exists per *(element, + commit-that-changed-its-derived-state)*; immutable; the read model for derived state. + +At the rename commit, the write is: **one** new `element_version` row (for P) and **N + 1** +new `derived_version` rows (for P and every element whose derived values the rename affected — +its "impact radius"). W's stored state is untouched; W's derived state has a new row. + +The 30-assertion smoke test (`SysML2.NET.CodeGenerator/Sql/schema.smoke.sql`) makes this exact +scenario its first and central assertion pair (PASS 2a/2b): after the rename, W's +`qualifiedName` resolves to `"New::wheel"` *while W still resolves to its original version row*. +If you ever refactor this schema, keep that test passing — it is the design's load-bearing wall. + +--- + +## 5. Rejected alternatives, and why + +Four plausible architectures were considered and rejected. Understanding why they fail +clarifies why the chosen one looks the way it does. + +### 5.1 One wide table ("God table") + +*One `element` table with a column for every stored property across all metaclasses.* + +With ~80 distinct stored names this is superficially tempting — 80 columns is not insane. It +dies on the type-collision fact from the census: `value` must simultaneously be `boolean`, +`integer`, `double precision`, and `text`; `kind` must be four different enum types. You end up +either with typed column families (`value_bool`, `value_int`, …, `kind_req`, `kind_state`, …) +— at which point you have reinvented subtype tables inside one table, badly, with every row +mostly NULL and every CHECK constraint conditional on `class_kind` — or with everything as +`text` and casts everywhere, surrendering type safety in the layer whose entire purpose is to +provide it. + +### 5.2 Table-per-metaclass (full TPT, the "COMET shape") + +*One table per concrete metaclass (167), plus one link table per multi-valued property +(~230), joined up the inheritance chain for reads.* + +This is the shape of the earlier `core-sql-schema.hbs` skeleton this project inherited (a port +from the CDP4-COMET server, which uses it successfully for a different metamodel). It fails +here for three reasons: + +1. **The inheritance DAG breaks the join chain.** TPT reads reconstruct an instance by joining + parent tables up the chain. With 34 multiply-inheriting metaclasses there is no chain — a + `FlowUsage` read would join up *two* branches of an inheritance diamond. Doable, but every + query generator now has to understand the DAG. +2. **Scale of machinery vs. scale of content.** 167 + ~230 tables to hold 97 property + declarations. The overwhelming majority of those tables would contain *only* the `iid` + column (most metaclasses declare no stored properties of their own — they exist for their + derived semantics). Deepest reads become 11-way joins. +3. **The COMET shape assumes single-version storage.** Its FKs point at element rows and its + `revisionNumber` is a monotonic integer — both incompatible with a commit DAG and + version-independent references (Axioms 1 and 2). This is not a criticism of COMET; its + problem domain has linear revisions and reference-to-current semantics. This one does not. + +### 5.3 Pure generic EAV + +*Two tables: `element_version(…, value_data jsonb)` and +`element_reference(version_id, property_id, ordinal, target_identity)`.* + +Fastest to generate, and the reference table is genuinely attractive for graph traversal (one +index serves every "who references X?" question). Rejected because it flattens the type system +into data: no per-property FK semantics, no per-property NOT NULL/enum enforcement, no +column statistics for the planner (every property lookup has the same generic selectivity), +and CHECK-level guarantees ("`isParallel` is a boolean") become application discipline. The +chosen design keeps a *narrow* EAV-ish surface where it is justified (the 7 link tables, the +property catalog) without giving up typed columns for the scalar core. + +### 5.4 Document store (jsonb-only) + +*Store each element version as one jsonb document; index with GIN.* + +This handles reads beautifully — and in fact the chosen design *contains* this design as its +read path (`stored_json`/`derived_json`). Rejected as the *system of record* because +referential integrity, typed constraints, reverse-reference indexes, and per-column statistics +all vanish; every integrity property of the model would live in application code. The lesson +taken instead: **normalize for writing and integrity, denormalize for reading** — keep both, +in the same rows, written in the same transaction. + +### 5.5 What was chosen + +**Element core + sparse subtype tables + typed link tables + a second derived stream:** + +- one `element_version` core table carrying identity/commit bookkeeping plus `Element`'s own 7 + stored properties (every element has them; splitting them out would be a join for nothing); +- **47 subtype tables**, one per metaclass that *declares* stored scalar properties, keyed by + `(project_id, version_id)` — an instance of a metaclass has rows in exactly the subtype + tables of its storage-declaring ancestors (a set, not a chain — the DAG is handled by + membership, not by joins); +- **7 link tables** for the 6 multi-valued reference properties + `aliasIds`, all ordered + (`ordinal` in the PK — every one of these is `isOrdered` in the metamodel); +- `derived_version` as the second stream (Axiom 2); +- `stored_json` on the version row and `derived_json` on the derived row as the deliberate, + transactionally-consistent read denormalization. + +The count of 47 is not a design parameter — it falls out of the census (49 storage-declaring +metaclasses, minus `Element` which is folded into the core table, minus `Dependency` whose only +stored properties are the two multi-valued ones that become link tables). + +--- + +## 6. Layer A — the PIM: projects, commits, branches, tags (§3) + +### 6.1 The commit DAG + +First, the term itself. **DAG** stands for *Directed Acyclic Graph* — a directed graph without +cycles — and it is the shape a commit history takes on as soon as branches and merges are +allowed. Without branches, history would be a simple **chain**, every commit having exactly +one parent: + +``` +c1 ← c2 ← c3 ← c4 (chain: linear history) +``` + +Branches let history *split* (two commits sharing a parent), and merges let it *converge* +again (one commit with **two or more parents**): + +``` + c2 ← c3 (branch "main") + ↙ ↖ +c1 c5 (merge: c5 has TWO parents, c3 and c4) + ↖ ↙ + c4 (feature branch) +``` + +*Directed*: every arrow points from child to parent ("c5 grew out of c3 and c4"). *Acyclic*: +following parent arrows can never lead back to where you started — a commit cannot be its own +ancestor, since a parent already existed when its child was created. Resolving "what did the +model look like at c5?" means walking this graph *backwards* from c5 (the recursive +`ancestry` CTE of §9) and picking, per element, the newest version found along the way — +exactly Git's model, which the OMG spec adopted deliberately. + +```sql +CREATE TABLE sysml2.commit ( + id uuid NOT NULL, + project_id uuid NOT NULL REFERENCES sysml2.project (id) ON DELETE CASCADE, + created timestamptz NOT NULL DEFAULT now(), + description text NULL, + model_version_id smallint NOT NULL REFERENCES sysml2.model_version (id), -- see 6.4 + PRIMARY KEY (id) +); + +CREATE TABLE sysml2.commit_parent ( + commit_id uuid NOT NULL REFERENCES sysml2.commit (id) ON DELETE CASCADE, + parent_commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + ordinal smallint NOT NULL, + PRIMARY KEY (commit_id, parent_commit_id) +); +``` + +The spec is explicit that `Commit.previousCommit` is a **set** — a merge commit has two or more +parents. Hence the separate `commit_parent` edge table rather than a `previous_commit_id` +column. `ordinal` preserves parent order ("first parent" matters for merge semantics, exactly +as in Git). The commit's PK is the bare uuid because commits are referenced from everywhere +(branches, versions, checkpoints) and are project-scoped via their own `project_id`. + +Two spec invariants are worth internalizing because the *resolvers depend on them*: + +**Immutability.** *"Commits are immutable… Commits are not destructible"* (Clause 7.1.2). The +schema takes this at face value: nothing ever UPDATEs a commit or an `element_version` row. +Append-only is not an optimization here; it is the spec's own semantics. + +**Monotonicity.** *"Version histories must monotonically increase in time: for Commit C, the +value of C.created must be strictly newer than the value of D.created for any commit D in +C.previousCommit."* The schema *enforces* this with a trigger: + +```sql +CREATE TRIGGER trg_commit_parent_monotonic + AFTER INSERT ON sysml2.commit_parent + FOR EACH ROW + EXECUTE FUNCTION sysml2.assert_commit_monotonic(); +``` + +Why enforce rather than trust? Because the snapshot resolver (section 10) selects, for each +element, the version from the **newest ancestor commit** — "newest by `created`". If a commit +were ever inserted with a timestamp older than its parent, the resolver would silently return +the *wrong snapshot* — no error, just wrong data. Silent-wrong-answer classes of bug get +triggers; noisy ones can be left to the service layer. (Smoke assertion PASS 6 proves the +trigger fires.) + +Note what monotonicity does *not* give you: an ordering between **siblings**. Two commits on +parallel branches may legally share a timestamp. Section 10.4 explains the tiebreaker that +handles this. + +**The four merge invariants at a glance.** An *invariant* is a rule that must hold at all +times, no matter what — and these four (Clause 7.1.2) are not formalities: the snapshot +resolver's correctness depends on them directly. Consolidated, with where each one lives: + +1. **Monotonicity** — a commit is strictly newer than every parent, along every path. This is + what makes "newest ancestor wins" a sound resolution rule. Enforced in the schema by + `trg_commit_parent_monotonic` (smoke PASS 6), because a violation would produce silently + wrong snapshots rather than errors. +2. **Conflict restatement** — a merge must restate the resolution of every conflict in its + OWN change set. Combined with invariant 1, the merge is the newest commit in its ancestry, + so its restatement automatically wins over both parents (smoke PASS 8a). Monotonicity does + not order the *siblings* themselves — which is why a merge that illegally skips + restatement falls to the deterministic `id DESC` tiebreaker (§10.4, audit R13, smoke + PASS 10a/10b). Restating is a service-layer obligation (§15, item 7). +3. **Deletions must delete something** — a tombstone (`DataVersion` with null payload) is + only valid if at least one parent had that element alive in its snapshot. Service-layer + validation; the schema stores the tombstone either way. +4. **One version per element per commit** — `DataVersion.identity` is unique within + `Commit.change`. Enforced by `ux_element_version_identity_commit` (§8.1). + +To these four spec invariants this schema adds a fifth of its own, for multi-version support +(section 6.4): **release compatibility** — a commit is never in an older metamodel release +than a parent, and a merge requires all parents in the merge's own release. Enforced by +`trg_commit_parent_version` (smoke PASS 11c–11e). + +In one sentence: the commit DAG is the *shape* of history (splitting and converging), and the +merge invariants are the *rules of the game* that guarantee reading that history back — the +fold of §9 — has exactly one well-defined, deterministic answer. + +### 6.2 How deltas become snapshots — the spec's own algorithm + +The spec defines `Commit.change` (the delta: the DataVersions written by this commit) as stored +and `Commit.versionedData` (the full model snapshot at this commit) as **derived**, with an OCL +algorithm that is worth reading because the schema's resolver is its direct translation: + +``` +let updatedNotDeleted = change->select(payload <> null) in +let updatedIdentities = change.identity in +let retainedWithDuplicates = + previousCommits.versionedData->select(oldData | + updatedIdentities->excludes(oldData.identity)) in +let retained = in +versionedData = updatedNotDeleted->union(retained) +``` + +In words: a commit's snapshot is *its own changes, plus everything from its parents' snapshots +that it did not override*. Recursion over `previousCommit` bottoms out at the root. Deletions +are DataVersions whose `payload` is null — which the schema stores as `tombstone = true` on the +version row. + +This algorithm is correct and hopeless to run per read at scale — it is a fold over the entire +commit history. The whole of §9 (section 10 of this guide) exists to make it cheap. + +### 6.3 Branches and tags + +```sql +CREATE TABLE sysml2.branch ( + id uuid NOT NULL, + project_id uuid NOT NULL REFERENCES sysml2.project (id) ON DELETE CASCADE, + name text NULL, + description text NULL, + head_commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + base_commit_id uuid NULL REFERENCES sysml2.commit (id), -- see section 10.2 + created timestamptz NOT NULL DEFAULT now(), + deleted timestamptz NULL, + PRIMARY KEY (id), + UNIQUE (project_id, name) +); +``` + +Per the spec's mutability table: branches are mutable and destructible (the *only* mutable +thing in the versioning core — `head_commit_id` moves on every commit); tags are immutable but +destructible; commits are neither. `deleted` is a nullable timestamp rather than a hard delete +because the spec models CommitReference deletion as a recorded event. + +`base_commit_id` is a performance structure, not a spec concept — it anchors the branch-head +*overlay* and is explained fully in section 10.2. + +Also in this layer: `tag` (same shape as branch, frozen), `project_usage` (cross-project +imports: "project A uses project B at commit C", with the spec constraint +`usedProject = usedProjectCommit.owningProject` left to the service), and `project` itself, +whose `default_branch_id` FK is added *after* `branch` exists and made +`DEFERRABLE INITIALLY DEFERRED` — project and its default branch are created in one +transaction, and the circular FK (project → branch → project) can only be satisfied at commit +time. + +### 6.4 Model-version stamping — multiple metamodel releases in one database + +The OMG metamodel itself has releases (Beta 4 today; later releases will add, drop, and +reshape metaclasses). This schema supports **multiple releases coexisting in one database**, +and the design falls out of one observation: in an append-only store, historical commits are +immutably in the release they were written in. Whatever label a project or a branch carries, +a reader of an old commit must know *that commit's* release to interpret its payloads. The +per-commit stamp is therefore the only correct grain — everything else is derived from it: + +- **`model_version`** (§2) registers every release the database has ever stored data for. + Its id is an *ordinal* — higher is later — handed out once by the checked-in registry + (`SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/ClassKindRegistry.cs`) and + never renumbered. +- **`commit.model_version_id NOT NULL`** is the truth: the release this commit's payloads + are written in. A *branch* "is" simply the release of its head commit; there is no mutable + version field on `branch` that could disagree with history. +- **`project.target_model_version_id`** is policy, not truth: the highest release new + commits may be written in (NULL = unrestricted). An operator raises it to *allow* branches + to upgrade; the stamp records what each branch actually did. + +**Upgrading is a commit, not a migration.** A branch moves to a newer release via a +**conversion commit**: a single-parent commit that bumps the stamp and restates every element +whose shape changed between the releases (the version-diff is an impact-radius variant — the +machinery of `SysML2.NET.CodeGenerator/IMPACT-RADIUS.md` applies). The service must force a +`commit_checkpoint` on every conversion commit so folds rarely cross a release boundary. +Elements whose shape did not change are *not* restated — their old rows remain valid under +the new release, which is what makes conversion O(changed shapes), not O(model). + +**The physical schema is the superset across registered releases.** New metaclasses become +new subtype tables; new properties become nullable columns; a renamed or moved property +becomes a *new* column next to the old one (the conversion commit moves the data; the old +column keeps serving old commits). Nothing is ever dropped. Which tables and properties are +valid in which release is NOT recorded in database tables — it ships as static, per-release +generated C# (the model-version *descriptors*, section 12.2). + +Three invariants keep mixed-release history sound, enforced by `trg_commit_parent_version` +(smoke PASS 11b–11e): no commit is in an older release than a parent (downgrades are +unsupported — conversion is lossy in reverse); a single-parent commit may bump the release +(that IS the conversion commit); and a merge requires **all parents in the merge's own +release** — convert first, then merge, never both in one commit. Without the last rule a +merge would silently mix payload shapes. + +--- + +## 7. Identity: `data_identity` and the referential-integrity philosophy (§4) + +```sql +CREATE TABLE sysml2.data_identity ( + id uuid NOT NULL, + project_id uuid NOT NULL REFERENCES sysml2.project (id), + class_kind smallint NOT NULL REFERENCES sysml2.class_kind (id), -- TYPED identity + PRIMARY KEY (id), + UNIQUE (id, class_kind) +); +``` + +Three columns. This tiny table is the anchor of Axiom 1: every element-reference column in the +entire schema — ~30 single-reference columns on subtype tables, 5 reference link tables, +`element_version.owning_relationship`, `branch_head.identity_id`, and so on — is a foreign key +to `data_identity(id)`. + +**The identity is TYPED.** The metaclass of an element is invariant across its versions — an +identity is born a PartUsage and stays one — so unlike everything else about an element, the +type is a property of the *identity* and therefore FK-able. Two consumers: + +- `element_version` carries a composite FK `(identity_id, class_kind)` → + `data_identity (id, class_kind)`, making a version that claims a different metaclass than + its identity **impossible** (smoke PASS 12a); +- the generated `validate_references_at_commit()` function (below) type-checks every stored + reference against this column — including cross-project targets, because identities are + typed regardless of which project they live in. + +One maintenance rule follows: a release conversion (§6.4) that retypes an element — its +metaclass was dropped — must update `data_identity.class_kind` in the same transaction +(obligation §15.16). + +**What FKs cannot check is now checkable ON DEMAND — in two tiers.** FKs prove a reference +targets an *existing* identity; they can never prove the target is *alive at the commit +being read* (liveness is a function of (identity, commit) — there is no row to FK against), +nor that its metaclass is legal for the referencing property (an FK matches values, not type +sets). Both gaps are covered by generated functions (§14 of the schema files), one +`UNION ALL` arm per stored reference column (42 of them), each reporting `'wrong-type'` (via +the typed identity, checked for cross-project targets too) and `'dangling'` (a same-project +target absent from the commit's snapshot): + +- **`validate_references_at_commit`** — the FULL periodic audit over one commit's whole + snapshot. It materializes the snapshot into an ANALYZE'd, indexed temp table first, so the + planner knows the true cardinality and can switch to snapshot-driven PK probes on deep + histories — the pass is bounded at O(snapshot × log history), never O(history), however + large the append-only tables grow. Measured: 2.5–4.3 s on a 1M snapshot (smoke PASS + 12b/12c). +- **`validate_references_in_commit`** — the INCREMENTAL per-commit tier, O(change set): the + outgoing references of the versions the commit wrote, PLUS the reverse direction its + tombstones break — a live, *unchanged* element left referencing a deleted identity, the + case naive change-set validation misses (driven by the reverse-lookup indexes, per-target + liveness probed via `resolve_element_at_commit`). Measured: 77–86 ms for a 101-row change + set against a 1M project — fit for the synchronous commit-validation path (smoke PASS + 13a–13c). + +Deliberately *functions*, not constraints: the spec allows transiently dangling references, +and liveness of cross-project targets depends on the used-project commit (`project_usage`) — +service-layer resolution. The working protocol (obligation §15.6): the incremental tier per +commit, the full audit periodically as its backstop. + +Three more deliberate choices here: + +**The PK is the bare uuid, not `(project_id, id)`.** `ProjectUsage` lets an element in project +A reference an element in project B. A composite PK would make every cross-project reference +un-FK-able. Project scoping of references is a service-layer validation (via `project_usage`), +not an FK. The cost: `data_identity` cannot be partitioned by project like everything else. The +audit (section 14, finding R12) checked whether that matters at 10⁸ rows — it does not: two +uuid columns make a ~7 GB heap with a btree whose upper levels stay resident; every probe is +3–4 cached page reads. Read-mostly FK targets scale fine unpartitioned. + +**`element_id` (the KerML property) is `text`, not `uuid`.** Careful distinction: the *API's* +`@id` is a UUID and maps to `data_identity.id`. But KerML's `Element::elementId` is declared +`String`; only standard-*library* elements are normatively required to use name-based (v5) +UUIDs, and user models carry no format constraint at all. A `uuid` column would reject +spec-valid data. So `element_version.element_id` is `text`, and the identity row's `id` is the +uuid the API layer manages. + +**Deletion is explicit, never cascaded.** Originally the identity FKs were `ON DELETE CASCADE` +("delete a project → everything goes"). The performance audit killed this, for a mechanical +reason that generalizes: **a cascade executes per-row deletes filtered on the FK column +alone** — `DELETE FROM element_version WHERE identity_id = $1` — and *no index in this schema +leads with a bare identity column* (they all lead with `project_id`, for partition-locality). +Every cascaded identity would therefore sequentially scan the largest tables, once per +identity, a million times per project deletion. The fix is written into the schema as a +documented procedure at the `data_identity` DDL: project deletion is an *ordered, batched, +per-table* `DELETE … WHERE project_id = $1` (each statement prunes to one partition and uses a +PK prefix), finishing with `data_identity` and `project`. The remaining `NO ACTION` FKs act as +a safety net — they *block* out-of-order deletion loudly instead of scanning silently. That +trade (explicit procedure + loud guard, instead of convenient + catastrophic) is the schema's +general FK philosophy. + +--- + +## 8. Layer B — stored element state (§5, §6, §7) + +### 8.1 The core: `element_version` (§5) + +```sql +CREATE TABLE sysml2.element_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, -- the spec's DataVersion.id + identity_id uuid NOT NULL, -- composite typed-identity FK below (§7) + commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + class_kind smallint NOT NULL REFERENCES sysml2.class_kind (id), + tombstone boolean NOT NULL DEFAULT false, + + -- Element's own stored properties, folded in: + element_id text NULL, + declared_name text NULL, + declared_short_name text NULL, + is_implied_included boolean NULL, + owning_relationship uuid NULL REFERENCES sysml2.data_identity (id), + + stored_json jsonb NULL, + + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (identity_id, class_kind) + REFERENCES sysml2.data_identity (id, class_kind), -- typed identity (§7) + CONSTRAINT element_version_tombstone_empty + CHECK (NOT tombstone OR (stored_json IS NULL AND element_id IS NULL)), + CONSTRAINT element_version_payload_present + CHECK (tombstone OR (stored_json IS NOT NULL AND element_id IS NOT NULL + AND is_implied_included IS NOT NULL)) +) PARTITION BY HASH (project_id); + +CREATE UNIQUE INDEX ux_element_version_identity_commit + ON sysml2.element_version (project_id, identity_id, commit_id); +``` + +Design notes, column by column: + +- **`project_id` leads every key.** All element-scoped tables are hash-partitioned by + `project_id` with the same modulus (section 13), and their PKs lead with it. This makes + every join between them *partition-local* and lets every project-scoped query prune to one + partition at plan time. The corollary discipline — **never filter these tables on a bare + uuid without `project_id`** — turned out to matter enormously; section 14 (finding R2) shows + what happens when you forget. +- **`version_id` is the row's own identity** (the spec's `DataVersion.id`). It is + app-generated, and the audit recommends UUIDv7 (time-ordered) so each project's inserts land + on the rightmost btree leaf instead of splattering across the index (finding R8). +- **`class_kind` is a `smallint`, not a name.** 175 metaclass names are interned in the + `class_kind` catalog (section 12). On the hottest, largest table in the database, that is a + 2-byte column instead of a ~15-byte text one, times hundreds of millions of rows, times its + presence in indexes. +- **`tombstone` is the deletion marker** — the direct encoding of the spec's "a DataVersion + with a null payload is a deletion". Deletions are *rows*, because in an append-only commit + store a deletion is an event in history, not the absence of data. The two CHECK constraints + make tombstones and payload rows mutually exclusive shapes: a tombstone must be empty, a + non-tombstone must be complete. These CHECKs are the cheapest possible insurance against the + service layer writing half-formed rows. +- **The five Element columns are folded in** rather than living in a separate Element subtype + table. Rationale: *every* element has them (they are Element's own declarations, and + everything is an Element), so a separate table would add one join to every single read for + zero storage benefit. `declared_name` is also the most-filtered stored column, and having it + on the core table lets query plans avoid the join entirely. +- **`stored_json` is the read-model denormalization** — the element's stored half, + pre-serialized in exactly the API's JSON shape. The normalized columns and link tables (which + carry all the FKs and constraints) remain the system of record; `stored_json` exists so that + serving an element never requires reassembling it from up to six subtype tables and three + link tables. It is written in the same transaction as the normalized rows, so it cannot + drift. Cost: roughly doubles `element_version` storage (mitigated by lz4 — section 13). The + smoke test's PASS 4 verifies the concatenated read path produces a complete payload. +- **`ux_element_version_identity_commit`** enforces the spec invariant *"DataVersion.identity + is unique among records listed in Commit.change"* — one version of an element per commit — + and simultaneously serves as the index for "give me element X's row at commit C", which the + single-element resolver leans on. + +### 8.2 The link tables (§6) + +The census found exactly six multi-valued stored reference properties in the whole metamodel, +plus one multi-valued string. Each becomes one table; all are ordered: + +```sql +CREATE TABLE sysml2.element_owned_relationship ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + ordinal int NOT NULL, + target_identity uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id, ordinal), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_element_owned_relationship_target + ON sysml2.element_owned_relationship (project_id, target_identity); +``` + +(and likewise `relationship_owned_related_element`, `relationship_source`, +`relationship_target`, `dependency_client`, `dependency_supplier`, and `element_alias_ids` +with a `text` value column instead of a reference.) + +- `ordinal` is in the PK because every one of these properties is `isOrdered = true` in the + metamodel — order is model content, not incidental. +- Rows are keyed by **version**, not identity: the collection is part of the element's stored + state, so a new version carries its own collection rows. (This is the one place the audit + flagged real write amplification — a new version of a 100k-child package re-inserts 100k + rows even if only its name changed. Section 14, finding R7, documents the content-addressed + fix that is designed but deliberately deferred until benchmarks demand it.) +- The `target_identity` reverse index is what answers "who references element X?" — the + building block for reverse navigation, dangling-reference validation, and the derived-state + impact analysis. +- The FK back to `element_version` is a *composite* on `(project_id, version_id)` and is the + one cascade kept in this layer: deleting a version row (which only the explicit project + deletion procedure does) takes its collection rows with it, and the composite FK is + PK-prefixed on both sides so the cascade is index-backed. + +### 8.3 The subtype tables (§7) + +One table per storage-declaring metaclass — 47 of them, all with the same skeleton: + +```sql +CREATE TABLE sysml2.feature_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + direction sysml2.feature_direction_kind NULL, + is_composite boolean NOT NULL DEFAULT false, + is_constant boolean NOT NULL DEFAULT false, + is_derived boolean NOT NULL DEFAULT false, + is_end boolean NOT NULL DEFAULT false, + is_ordered boolean NOT NULL DEFAULT false, + is_portion boolean NOT NULL DEFAULT false, + is_unique boolean NOT NULL DEFAULT true, + is_variable boolean NOT NULL DEFAULT false, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); +``` + +**About the `_version` suffix:** these tables carry the *version-scoped* facet of a metaclass — +`feature_version` holds the Feature-declared columns of one element **version**, keyed by +`(project_id, version_id)` and hanging off `element_version`. The suffix deliberately matches +`element_version` and `derived_version`: the three names together read as one family of +per-version state. (Naming history: an earlier draft used a terse `_v` suffix for these +tables while the flattening views carried a `v_` prefix — `v_part_usage` vs `part_usage_v`, +same letter meaning two different things, an accident waiting to happen. Both were renamed: +the tables to `_version`, the views to `vw_` — see §12.3.) + +How an instance maps to tables: a `PartUsage` version has rows in `element_version` + +`type_version` + `feature_version` + `usage_version` + `occurrence_usage_version` — the subtype tables of exactly its +storage-declaring ancestors. A `FlowUsage` version has rows in **six** tables including *both* +`feature_version` and `relationship_version`, because `Connector` is simultaneously a Feature and a +Relationship. This is how the inheritance DAG is represented: **membership in a set of tables**, +not a chain of joins. The per-release generated descriptors (section 12.2) record the set per +metaclass so generic code never has to re-derive it. + +Details that carry intent: + +- **NOT NULL tracks the metamodel's lower bounds.** A `[1..1]` property is NOT NULL; a `[0..1]` + property (like `direction`, `member_name`, `portion_kind`) is NULL. This is safe *because* + the table only has rows for instances whose class actually declares the property — the + sparse-table design is what makes honest NOT NULLs possible at all (contrast the God-table, + where everything must be nullable). +- **DEFAULTs come from the XMI.** The generator emits a `DEFAULT` for every property whose UML + declaration carries one: the Feature booleans default false (`is_unique` true), + `Membership::visibility DEFAULT 'public'`, and — easy to get wrong by intuition — + `Import::visibility DEFAULT 'private'` (imports are private by default in KerML, unlike + memberships; a top-level import *must* be private). These defaults were cross-checked against + the metamodel knowledge base during review. +- **Reference columns FK to `data_identity`** (Axiom 1) and every one gets a reverse-lookup + index `ix_{table}_{column}`. Two of those indexes deserve a callout: + `ix_specialization_version_general` and `ix_specialization_version_specific` index the *specialization + graph* — the edges that derived properties like `Type::feature` fold over. When the service + computes the impact radius of "a supertype gained a feature", these two indexes are what make + "find all transitive specializations" affordable. +- **The four `kind` tables and four `literal_*` tables** are the visible proof of the census's + type-collision fact: `requirement_constraint_membership_version.kind` is + `sysml2.requirement_constraint_kind` while `state_subaction_membership_version.kind` is + `sysml2.state_subaction_kind`; `literal_boolean_version.value` is `boolean` while + `literal_rational_version.value` is `double precision`. One wide table cannot represent this. +- **Redefinitions have no columns.** `CollectExpression` has no subtype table at all — its only + stored property is the same-name redefinition of `operator`, which lives in + `operator_expression_version` (its ancestor's table). The property catalog records this resolution + so query code doesn't need to know it. + +### 8.4 The enum types (§1) + +```sql +CREATE TYPE sysml2.visibility_kind AS ENUM ('private', 'protected', 'public'); +``` + +Seven native enum types, one per metamodel enumeration. Native enums (vs. text + CHECK) cost 4 +bytes, validate on write, and sort in declaration order. The **labels are lowercase** for a +specific reason: they match the JSON wire format byte-for-byte — the generated serializers +write `Direction.Value.ToString().ToLower()` (see +`SysML2.NET.Serializer.Json/Core/AutoGenSerializer/FeatureSerializer.cs`), so a value can flow +from the API payload into the enum column and back out without case mapping at any layer. + +--- + +## 9. Layer C — derived element state (§8) + +```sql +CREATE TABLE sysml2.derived_version ( + project_id uuid NOT NULL, + derived_id uuid NOT NULL, + identity_id uuid NOT NULL REFERENCES sysml2.data_identity (id), + commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + + -- promoted hot derived properties (declared by Element => present on all 167 metaclasses) + owner uuid NULL REFERENCES sysml2.data_identity (id), + owning_namespace uuid NULL REFERENCES sysml2.data_identity (id), + qualified_name text NULL, + name text NULL, + short_name text NULL, + is_library_element boolean NOT NULL DEFAULT false, + + derived_json jsonb NOT NULL, -- everything else: ~325 distinct derived names + + PRIMARY KEY (project_id, derived_id) +) PARTITION BY HASH (project_id); + +CREATE UNIQUE INDEX ux_derived_version_identity_commit + ON sysml2.derived_version (project_id, identity_id, commit_id); +CREATE INDEX ix_derived_version_owner ON sysml2.derived_version (project_id, owner); +CREATE INDEX ix_derived_version_qualified_name ON sysml2.derived_version (project_id, qualified_name); +CREATE INDEX ix_derived_version_json + ON sysml2.derived_version USING gin (derived_json jsonb_path_ops); +``` + +### 9.1 Why precompute at commit time at all? + +Three strategies were on the table for full derived-property conformance: + +**Compute on read.** Zero write cost, no invalidation logic. But every element read pays the +recursive walks (`qualifiedName` = ownership chain; `feature` = specialization closure), every +*collection* read pays them per element, and — decisive — the Query service must filter and +sort on derived properties (`WHERE qualifiedName LIKE 'Vehicle::%' ORDER BY name`), which +means evaluating recursive CTEs per candidate row or building per-property materializations +anyway. Read-dominated workloads (this one, by profile) pay the computation on the hot path, +repeatedly, for values that change rarely. + +**Passthrough.** Store client-sent derived values verbatim. Cheapest to build (and the DTO +layer already round-trips derived values). Rejected as the *target* because derived values +become client-trusted data that can silently drift from the model — acceptable as an interim +conformance level, corrosive as an architecture. + +**Precompute at commit.** Writes pay for computing the change set's *impact radius*; reads pay +nothing; queries filter real columns. The existing 366 implemented `Compute*` methods in +`SysML2.NET/Extend/` are the computation engine — the .NET code already knows how to evaluate +every OCL derivation against an in-memory model; the service layer's job is to invoke them for +the affected elements at commit time and write the results here. Given the read-dominated +profile and the query requirement, this is the only strategy where the expensive thing happens +once, off the hot path. + +The honest cost: **the impact radius is unbounded in the worst case.** Renaming a namespace one +level below the root invalidates `qualifiedName` for nearly the whole model → ~1M +`derived_version` rows in one commit. This is inherent to the spec's semantics (the derived +values genuinely all changed), not to this design; the schema's job is to make the bulk write +survivable (lz4 compression, GIN pending-list tuning, async-friendly append-only shape) and +the audit budgets it as a bulk operation (section 14, finding R5). + +### 9.2 Keying: why `(identity, commit)` and why sparse + +The key is Axiom 2 made concrete: `derived_version` rows are written **only for elements whose +derived values actually changed at that commit**. A leaf edit writes one row. The rename writes +the subtree. Nothing rewrites rows for unaffected elements — resolution (section 10) finds each +element's *newest derived row at or before the commit being read*, exactly as it does for +stored versions. The two streams resolve through the same fold, which is what keeps the whole +design coherent: one resolution algorithm, two payload halves. + +`derived_id` exists (rather than using `(identity_id, commit_id)` as the PK) so that +`branch_head` and `commit_checkpoint` can point at a derived row with a single uuid, keeping +those hot tables narrow. + +### 9.3 The promoted six, and the jsonb tail + +Six derived properties get real columns; ~319 live in `derived_json`. The six are not +arbitrary: they are declared by `Element` (so they exist for every one of the 167 metaclasses, +making the columns dense, never wasted), and they are the properties a Query service filters +and sorts on constantly — `owner` (containment queries), `qualifiedName` (path lookup), `name` +(sorting/searching), `owning_namespace`, `short_name`, `is_library_element` (excluding library +content from user queries). Real columns mean real btree indexes and real per-column +statistics. + +The tail stays jsonb behind a GIN (`jsonb_path_ops`) index, because the spec's Query service +allows a `PrimitiveConstraint` on *any* property, and pre-building 319 expression indexes for +properties that may never be filtered is worse than one containment index. The audit flags the +honest weaknesses (section 14, R5): GIN insertion is the dominant write amplifier during bulk +derived writes, and the index has no `project_id` component, so a probe on a shared partition +rechecks candidates from co-located projects. The standing guidance: promoted columns first, +GIN as fallback; if production telemetry shows the filtered-property set is actually narrow, +replace the whole-document GIN with targeted expression indexes. + +### 9.4 The other two conformance levels + +Full conformance is what the schema was *optimized* for — but the schema itself is +**conformance-agnostic**. The Clause 2 level is purely a **write-path policy**: it decides who +authors `derived_version` rows and when. Nothing in the DDL changes between levels. + +**Passthrough conformance falls out of the design almost for free.** The client sends payloads +*including* derived values (the SysML2.NET DTO/serializer layer round-trips them). The service +splits the incoming payload: stored half → `element_version` + normalized columns; derived +half → a `derived_version` row at that commit, with the promoted columns simply *extracted* +from the client's payload instead of computed. Reads reproduce exactly what the client sent — +the faithful-reproduction guarantee, byte for byte — and queries on derived properties work +identically, which Clause 2 explicitly requires of passthrough providers. The only difference +from full: derived rows exist **only for change-set elements** (no impact-radius analysis +runs), so an untouched element keeps its last client-sent derived values — stale or wrong as +they may be, which is exactly passthrough semantics. The `(identity, commit)` fold does not +care *who* computed a value; the smoke test itself writes its derived rows passthrough-style +(hand-authored, never computed). + +**No conformance is trivially supported**, because derived state was made structurally +optional on purpose: never write `derived_version` rows at all. Every read function +`LEFT JOIN`s `derived_version` and `COALESCE`s `derived_json` to `'{}'`, so responses simply +contain only stored properties; `branch_head.derived_id` and `commit_checkpoint.derived_id` +are nullable by design; the `derived_version` table and its GIN index — the biggest write +amplifier — stay empty. The Query translator must then reject `PrimitiveConstraint`s whose +descriptor entry (section 12.2) routes to derived storage, consistent with the claimed level. + +**Moving between levels is a backfill, not a migration.** Because `derived_version` is a +separate append-only stream keyed `(identity, commit)`, full conformance can be adopted later +by computing derived state for the whole model and writing it *at one commit* (e.g. each +branch head); reads at or after that commit pick it up through the normal fold, and history +before it stays as it was. Caveat: checkpoints built before the backfill carry null/stale +`derived_id`s — rebuild them, or tolerate until the next cadence checkpoint. And note the +granularity mismatch: the spec declares conformance per Service Provider, but the schema would +mechanically support a different policy per project (derived rows are project-scoped like +everything else) — useful for a staged rollout, as long as the public conformance claim +reflects the weakest level actually served. + +The trade-off in one line: *no conformance* = cheapest, thinnest API; *passthrough* = full +payloads and derived queries at near-zero server cost, but derived values are client-trusted +and can silently drift from the model; *full* = correct by construction, paid for at commit +time with the impact-radius machinery — the only level with real engineering risk. + +--- + +## 10. Layer D — snapshot resolution (§9) + +This layer answers one question: **"what does the model look like at commit C / at the head of +branch B?"** — cheaply, at the profile's scale. It is where the schema earns or loses its +performance, and it went through one major redesign (the overlay) plus one empirical fix (the +registry) during the audit. The final structure has four parts. + +### 10.1 `commit_checkpoint` — materialized full folds + +```sql +CREATE TABLE sysml2.commit_checkpoint ( + project_id uuid NOT NULL, + commit_id uuid NOT NULL REFERENCES sysml2.commit (id) ON DELETE CASCADE, + identity_id uuid NOT NULL REFERENCES sysml2.data_identity (id), + version_id uuid NOT NULL, + derived_id uuid NULL, + PRIMARY KEY (project_id, commit_id, identity_id) +) PARTITION BY HASH (project_id); +``` + +A checkpoint is the spec's `versionedData` fold, fully evaluated and stored for one commit: one +row per live element, mapping identity → (version, derived row). `build_commit_checkpoint()` +constructs it from the general resolver, idempotently. Checkpoints bound how far any resolver +ever walks, and they are the *bases* that branch overlays diverge from. + +Checkpoints are O(model) each — ~1M rows, on the order of 100 MB — which drives the **cadence +policy** (written into the §9 banner, executed by the service layer): checkpoint a commit when +≥200 commits have accumulated since the nearest checkpointed ancestor *on that lineage*, or +when the cumulative change-set size since it exceeds ~25% of the model, and always at +branch-fork bases. Churn-based, not merely count-based — "every N commits" alone would +accumulate terabytes of near-identical checkpoints on a busy project. Retention: a checkpoint +that no branch bases on and that is not needed for the historical ladder gets deleted (registry +row first, then rows — both PK-prefixed, index-backed deletes). + +`build_commit_checkpoint` at 200k elements measured 2.5 s; extrapolated ~12–15 s at 1M — which +is why the banner says, in bold effect: **run it asynchronously, never on the commit path.** + +### 10.2 `branch_head` — the sparse overlay + +The naive materialization — one `(branch, identity) → version` row per element per branch — +was the original design, and the audit's arithmetic executed it: 500 branches × 1M elements = +**500M rows (~85 GB) per project**, branch creation = copying a million rows into a btree that +is already billions of entries deep, branch deletion = a million-row delete with vacuum churn. +For hundreds of routinely created and deleted branches, that is not a tuning problem; it is the +wrong data structure. The measured comparison (section 14): branch create 2,964 ms full-copy +vs **1.8 ms** overlay, at only one-fifth of target scale. + +The overlay inverts the representation: a branch stores only its **divergence** from a base +checkpoint. + +```sql +CREATE TABLE sysml2.branch_head ( + project_id uuid NOT NULL, + branch_id uuid NOT NULL REFERENCES sysml2.branch (id) ON DELETE CASCADE, + identity_id uuid NOT NULL REFERENCES sysml2.data_identity (id), + version_id uuid NOT NULL, + derived_id uuid NULL, + is_tombstone boolean NOT NULL DEFAULT false, + PRIMARY KEY (project_id, branch_id, identity_id) +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_branch_head_branch ON sysml2.branch_head (branch_id); +``` + +The semantics, precisely: + +- `branch.base_commit_id` names a **checkpointed** commit (service-enforced invariant — which + is also why the cadence policy checkpoints fork bases). +- The head state of an element on the branch is: **the overlay row if one exists, else the + base checkpoint's row.** +- A row with `is_tombstone = true` means "deleted on this branch relative to the base" — it + *masks* the checkpoint row. (It still points at the tombstone `element_version`, and the + flag denormalizes `element_version.tombstone` so set-reads can exclude masked identities + without visiting `element_version` at all.) +- `base_commit_id IS NULL` means the overlay *is* the complete head state — the bootstrap mode + for a brand-new project before its first checkpoint exists. + +Life-cycle costs under the overlay: + +| Operation | Work | +|---|---| +| Branch create at a checkpointed commit | insert one `branch` row — **zero** overlay rows | +| Branch create at a non-checkpointed commit | base = nearest checkpointed ancestor; write the (checkpoint → fork) delta into the overlay — O(delta) | +| Commit to the branch | upsert the change-set rows into the overlay (`INSERT … ON CONFLICT (project_id, branch_id, identity_id) DO UPDATE`) — O(changeset) | +| Branch delete | cascade deletes the overlay rows only — O(divergence), index-backed via `ix_branch_head_branch` | +| Compaction (service policy) | when an overlay exceeds ~10% of model or ~100k rows: checkpoint the branch head, repoint `base_commit_id`, truncate the overlay | + +Checkpoints are naturally **shared**: the hundreds of branches forked near main's head all base +on the same few checkpoints. That sharing is what fixes the storage arithmetic — total snapshot +storage is governed by the checkpoint cadence, not by branch count. + +One thing to be explicit about: the thresholds above (and the cadence of §10.1) are not +design commentary — they are **operational contracts that must be actively monitored**, with +alerts firing *before* the limits are reached. Every one of them is queryable from the schema +itself; the concrete signals, probes and alert levels are obligation §15.15. + +`ix_branch_head_branch` exists for one precise reason: the `ON DELETE CASCADE` from `branch` +filters on `branch_id` *alone*, and the PK leads with `project_id` — without this index, every +branch deletion sequentially scans every partition (audit finding R3; this is the same +mechanical trap as the identity cascades of section 7, solved oppositely because the +post-overlay table is small enough that the extra index is cheap). + +The smoke test's PASS 9a–9f sequence walks the full overlay life cycle: checkpoint build, O(1) +branch creation, read-through to base, tombstone masking, merged set-read, and +overlay-only deletion. + +### 10.3 `commit_checkpoint_registry` — a planner lesson + +```sql +CREATE TABLE sysml2.commit_checkpoint_registry ( + project_id uuid NOT NULL, + commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + created timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (project_id, commit_id) +); +``` + +One row per checkpoint (not per identity). This table exists because of a measured planner +failure worth understanding in general terms. + +The resolvers walk the commit DAG asking, at each step, *"is this commit checkpointed?"*. +Originally that probe was `EXISTS (SELECT 1 FROM commit_checkpoint WHERE project_id = … +AND commit_id = …)` — a PK-prefix probe, obviously index-friendly. Except: all ~200k rows of a +checkpoint share **one** `(project_id, commit_id)` value. The planner's statistics therefore +say `n_distinct(commit_id) ≈ 1`, its selectivity model concludes "any commit_id lookup returns +~all rows", the index scan is costed as returning 200k rows, and it **chooses a sequential +scan** — executed once per recursion step. Measured result: a 500-commit walk filtered 100 +million rows (500 × 200k) and touched 1.33M buffers; the "cheap" single-element historical +read took 3.5 seconds. + +The structural fix beats any planner coaxing: probe a table whose *shape matches the question*. +"Is this commit checkpointed?" is a question about commits, so the registry has one row per +checkpointed commit, and the probe is a one-row PK lookup no statistics model can +misunderstand. After the fix: the same read measured **1.8–4 ms** (≈1,900× improvement), and +the full-model fold went from 4,012 ms to 185 ms. + +The general lesson, worth keeping: *an EXISTS probe into a table keyed by a finer grain than +the question being asked is a statistics trap.* Registries/marker tables are cheap insurance. + +### 10.4 The resolvers + +Three SQL functions implement the spec's fold. The general one: + +```sql +CREATE FUNCTION sysml2.resolve_commit_state(p_project_id uuid, p_commit_id uuid) +RETURNS TABLE (identity_id uuid, version_id uuid, derived_id uuid) ... +``` + +Its internal CTE pipeline, in plain words: + +1. **`checkpoint`** — is the requested commit itself checkpointed? (registry probe) +2. **`ancestry`** — recursive walk over `commit_parent` from the requested commit, marking + each reached commit `at_checkpoint` via the registry, and **stopping the recursion at + checkpointed commits** (`WHERE NOT a.at_checkpoint`). The walked window is therefore + bounded by the cadence policy — ~200 commits, never the whole history. +3. **`folded`** — join the walked commits to `element_version`, take + `DISTINCT ON (identity_id) … ORDER BY identity_id, created DESC, id DESC`: for each element + changed *inside the window*, the newest version wins. +4. **`checkpoint_state` / `checkpointed`** — for every element *not* changed in the window, + take its row from the boundary checkpoint. +5. **`resolved`** — union of 3 and 4, minus tombstones. +6. **`derived_folded`** — the *same* fold, over `derived_version`, with the same window; an + element whose derived row predates the window falls back to the checkpoint's `derived_id`. + (This fallback is not decorative — without it, derived state older than the checkpoint + would silently resolve to NULL.) + +Correctness leans on the two commit invariants from section 6.1: + +- **"Newest ancestor wins" is sound because of monotonicity** — a commit is strictly newer + than everything it can reach, so for a merge commit that restates its conflict resolutions + (which the spec *requires*), the merge's own rows are the newest and win. Smoke PASS 8a–8c + verify the merge case, including that a deletion on a *non-ancestor* branch correctly does + not affect the merge's snapshot. +- **The `id DESC` tiebreaker (audit finding R13) handles what monotonicity does not**: + *sibling* commits may share a timestamp, and if a merge illegally fails to restate a + conflict, `created DESC` alone would pick between the siblings nondeterministically — + potentially a different answer per read, per plan, per replica. `id DESC` is arbitrary but + *stable*: a deterministic wrong-ish answer for an illegal input beats a nondeterministic + one, because it is testable, cacheable, and consistent across reads. Smoke PASS 10a/10b + construct the tie and assert the winner twice, through both resolvers. + +The single-element variant, `resolve_element_at_commit(project, commit, identity)`, exists +because `GET /projects/{p}/commits/{c}/elements/{e}` would otherwise fold the *entire model* +to answer for one element (audit finding R6). It runs the same ancestry walk but filters both +fold arms to one identity — `ux_element_version_identity_commit` and its derived twin make +each probe an index hit, so the cost is O(walked ancestry): measured 1.8–4 ms at 500 commits +from the checkpoint. + +And `build_commit_checkpoint(project, commit)` — `INSERT … SELECT FROM resolve_commit_state` +plus the registry row, in one statement so both become visible atomically, `ON CONFLICT DO +NOTHING` so it is idempotent and safe to re-run. + +--- + +## 11. The read path (§10) + +The functions the API layer actually calls. The design goal: **serving an element is a jsonb +concatenation over a handful of PK probes** — no per-read joins across subtype tables, no +recursion, no derived computation. + +```sql +-- the hottest query in the system +CREATE FUNCTION sysml2.get_element_at_branch_head(p_branch_id uuid, p_identity_id uuid) +RETURNS jsonb ... AS $$ + SELECT ev.stored_json || COALESCE(dv.derived_json, '{}'::jsonb) + FROM sysml2.branch b + LEFT JOIN sysml2.branch_head bh + ON bh.project_id = b.project_id AND bh.branch_id = b.id AND bh.identity_id = p_identity_id + LEFT JOIN sysml2.commit_checkpoint cc + ON cc.project_id = b.project_id AND cc.commit_id = b.base_commit_id + AND cc.identity_id = p_identity_id AND bh.identity_id IS NULL + JOIN sysml2.element_version ev + ON ev.project_id = b.project_id AND ev.version_id = COALESCE(bh.version_id, cc.version_id) + LEFT JOIN sysml2.derived_version dv + ON dv.project_id = b.project_id AND dv.derived_id = COALESCE(bh.derived_id, cc.derived_id) + WHERE b.id = p_branch_id + AND bh.is_tombstone IS NOT TRUE + AND NOT ev.tombstone; +$$; +``` + +Reading it: start from the tiny `branch` table (recovering `project_id` — see below), probe the +overlay; if no overlay row (`bh.identity_id IS NULL` gates the second join), probe the base +checkpoint; whichever won supplies the version and derived pointers; concatenate the two jsonb +halves. A tombstoned overlay row falls out of the WHERE — masking the base. Every access is a +PK probe on one partition. + +**The `project_id` discipline, learned the hard way (audit finding R2):** the original version +of this function filtered `branch_head` on `(branch_id, identity_id)` alone. Consequences on +PG16/17: no partition pruning (the predicate lacks the hash key → all 16 leaves visited), and +no PK usage (`branch_id` is the PK's *second* column, and btree skip scan only exists from +PG18). Note that even ON PG18 the rule stands: skip scan softens the index side at best — +partition pruning still requires the `project_id` predicate. The +"hottest query in the system" was, silently, the worst one. The fix — joining through `branch` +to recover `project_id` — makes runtime pruning work through the join parameter. The measured +plan shows 15 of 16 partitions "(never executed)" and 0.061 ms execution. The rule generalizes +to every entry point: **any query against a partitioned table whose only keys are bare uuids is +defective by construction in this schema.** + +The other functions: `get_elements_at_branch_head` (set read: base checkpoint minus overlaid +identities via anti-join, `UNION ALL` the live overlay — measured 1.24 s for a 200k merge), +`get_elements_at_commit` and `get_element_at_commit` (historical variants over the resolvers). + +--- + +## 12. The metamodel catalogs and the Query service (§2, §11) + +### 12.1 `model_version` and `class_kind` — the append-only registry + +```sql +CREATE TABLE sysml2.model_version ( + id smallint NOT NULL, -- ordinal: higher id == later release + name text NOT NULL, -- human label, e.g. 'sysml-2.0-beta-4' + source_fingerprint text NOT NULL, -- root-package fingerprint of the generator input + PRIMARY KEY (id), UNIQUE (name) +); + +CREATE TABLE sysml2.class_kind ( + id smallint NOT NULL, + name text NOT NULL, -- the API @type, e.g. 'PartUsage' + is_abstract boolean NOT NULL, + introduced_in smallint NOT NULL REFERENCES sysml2.model_version (id), + removed_in smallint NULL REFERENCES sysml2.model_version (id), -- first release WITHOUT it + PRIMARY KEY (id), UNIQUE (name) +); +``` + +`class_kind` interns the 175 metaclass names to a smallint. Its ids are **not positional**: +they come from the checked-in, append-only registry +(`SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/ClassKindRegistry.cs`), which is +the source of truth the seeds are emitted from — the UML model only *validates* against it. +An id is handed out once, when the metaclass first appears in a registered release, and is +frozen forever; a new release appends its newcomers after the highest existing id +(alphabetical among themselves); a dropped metaclass keeps its row, closed with `removed_in`. +The generator **fails loudly** on any drift — an unregistered class (the error message prints +the exact registry lines to append), a registration the model no longer contains, an +abstractness mismatch, or a `source_fingerprint` that no longer matches the newest registered +release. Silent renumbering — the great trap of the earlier positional design — is +impossible by construction, which is also why the seed `INSERT`s are idempotent +(`ON CONFLICT (id) DO NOTHING`, smoke PASS 11a) and safe to re-apply to a populated database. + +**The contract for every consumer:** the canonical identity of a metaclass is still its +**name** (the API `@type`); the smallint is the registry's interning of it. Never +hand-maintain a C# enum mirroring these ids — the planned `ClassKind` enum is *generated from +the same registry*, so its values are stable across releases by construction, plus a startup +assertion in the service that compares the compiled constants against the `class_kind` table +and refuses to start on drift (noted, not yet built). + +The earlier design also kept a `class_kind_table` catalog (the flattened inheritance DAG: +which subtype tables each concrete metaclass joins). It is gone from the database — nothing +in the schema ever read it, and per-release table participation now belongs to the generated +descriptors of section 12.2. + +### 12.2 The model-version descriptors — the API-to-storage bridge + +Earlier designs kept a `property_catalog` table here: 12,113 generated rows mapping every +(concrete metaclass, API property) to its storage location. It is deliberately **gone from +the database**, for three reasons that reinforce each other: + +- **Nothing in the schema reads it.** Every view, resolver, and index is already specialized + per metaclass at generation time; the catalog was passive data with zero inbound + references, purely for an external consumer. +- **The service layer is generated too.** The same generator that emits this schema emits the + service's data access; static per-metaclass C# (the codebase's standing + performance-over-reflection rule) answers the routing question without a database + round-trip. +- **A table describes one release; descriptors describe them all.** With multiple metamodel + releases in one database (section 6.4), the property→storage routing is *per release*. A + single catalog table cannot say "in release 1 this lived here, in release 2 there" without + reinventing the registry — versioned generated code carries exactly that, naturally. + +The replacement is the **model-version descriptor**: per registered release, generated C# +that enumerates the release's metaclasses, each metaclass's subtype-table set (what +`class_kind_table` used to record), and each API property's storage routing (what +`property_catalog` used to record) — plus the multiplicity/ordering metadata the Query +translator needs to validate constraint shapes. The descriptors are emitted from the same +XMI + registry inputs as the schema, so they are in lockstep by construction. (Design +sketched here; the descriptor generator lands with the service layer.) + +What the descriptor makes implementable is unchanged: the OMG **Query service**. The spec's +query model is: + +``` +Query { select: [String], where: Constraint, orderBy: [String], scope: [...] } +Constraint = PrimitiveConstraint { property, operator, value, inverse } + | CompositeConstraint { constraint: [Constraint], operator: and|or } +``` + +A `PrimitiveConstraint.property` is an API-level *name*. The query translator resolves it +through the descriptor of the commit's release: + +| Descriptor entry says | Translator emits | +|---|---| +| `('PartUsage', 'declaredName', 'column', 'element_version', 'declared_name', …)` | `ev.declared_name = $v` | +| `('PartUsage', 'isVariation', 'column', 'usage_version', 'is_variation', …)` | join `usage_version`, `u.is_variation = $v` | +| `('PartUsage', 'qualifiedName', 'derived', 'derived_version', 'qualified_name', …)` | join `derived_version`, `dv.qualified_name = $v` — an indexed column | +| `('PartUsage', 'featuringType', 'derived', 'derived_version', NULL, json_key='featuringType', …)` | `dv.derived_json @> '{"featuringType": …}'` — the GIN fallback | +| `('PartUsage', 'ownedRelationship', 'link_table', 'element_owned_relationship', …)` | `EXISTS (SELECT 1 FROM element_owned_relationship …)` | +| `('CollectExpression', 'operator', 'column', 'operator_expression_version', 'operator', …)` | the redefinition, already resolved to its storage root — the translator never learns UML redefinition rules | + +Note that derived properties are first-class query targets — which is exactly what Clause 2's +full conformance demands ("derived properties can be used in Query structures as +PrimitiveConstraint properties … query execution will consider the correctly computed and +up-to-date values"). The commit-time precomputation strategy is what makes this row-source +cheap. + +Multiplicity and ordering metadata ride along in the descriptor so the translator can also +validate constraint shapes and so API metadata endpoints can describe properties without +loading the .NET reflection model. + +### 12.3 The flattening views (§11) + +One generated view per concrete metaclass reconstructs the DTO's row shape: + +```sql +CREATE VIEW sysml2.vw_part_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, + ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, /* … */ + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 120 AND NOT ev.tombstone; -- 120 = PartUsage's frozen registry id +``` + +Naming: the `vw_` prefix stands for **view** — chosen over the more common single-letter `v_` +precisely so it can never be misread as *version*, the meaning of the `_version` suffix on +the subtype tables (§8.3). Two unmistakably different spellings for two different concepts. + +These are for the Query service and human inspection — the API element read never touches +them (it serves `stored_json`). They are pass-through views: they expose `project_id`, and the +caller's `WHERE project_id = $1` propagates through the equivalence classes of the USING joins +to prune every joined partitioned table. The audit's ops checklist (R10) applies here: verify +hot plans show pruning; watch for generic-plan flips on the 6-join views +(`plan_cache_mode = force_custom_plan` is the lever); prefer PG18 where available (btree skip +scan, AIO, `NOT VALID` FKs on partitioned tables, native `uuidv7()`), else PG17 — which scales +the fast-path lock slots +scale with `max_locks_per_transaction` — a query touching 6 partitioned relations plus indexes +can otherwise spill into the shared lock manager under high QPS. + +--- + +## 13. Partitioning and physical tuning (§12) + +**Hash partitioning by `project_id`, 16-way, co-located across all 58 element-scoped tables.** +The profile says tens-to-hundreds of projects per instance: hash-by-project spreads them across +partitions while keeping each project's rows *together* — every project-scoped query prunes to +one partition, and every `(project_id, version_id)` join between element tables is +partition-local. (For a deployment dominated by one giant project, partitioning is neutral — +one partition holds it; the design does not depend on partitioning for single-project +performance, only for multi-tenant spread. The modulus is a deployment knob.) + +**Version policy.** The floor is PostgreSQL **16** — a deployability choice, not a technical +one: nothing in the schema needs anything newer, and pinning the floor to the latest major +would exclude most real enterprise installations for zero functional gain. All verification +in this repository ran on **17** (which also scales the fast-path lock slots with +`max_locks_per_transaction`, relevant with 928 leaf partitions). **Prefer 18 where +available**: it brings four concrete wins for exactly this schema — btree skip scan (softens +the R2 failure mode, though the `project_id` rule stands: pruning still needs the predicate), +`NOT VALID` + `VALIDATE` FKs on partitioned tables (the clean R11 bulk-import path), +native `uuidv7()` (the R8 recommendation, now database-side), and asynchronous I/O (speeds up +precisely the O(model) operations: checkpoint builds, set reads, vacuum on the big leaves). + +The schema exploits 18 automatically where it can, and the rest is deployment guidance: + +- **Self-activating `uuidv7()` defaults** (implemented, §12): a version-guarded `DO` block + sets `DEFAULT uuidv7()` on every server-minted key (`version_id`, `derived_id`, and the + PIM record ids) when `server_version_num >= 180000` — a verified no-op on the 16/17 floor. + `Guid.CreateVersion7()` in the service remains the primary id source (the service needs + ids before insert); the defaults are the safety net that keeps ad-hoc and tooling inserts + time-ordered. `data_identity.id` is deliberately excluded — the spec-visible `@id` must be + supplied, never silently minted. +- **AIO tuning**: the default `io_method = worker` already helps the O(model) operations; + on Linux consider `io_method = io_uring` and raising `io_workers` for checkpoint-build and + bulk-import windows. +- **Bulk import on 18**: prefer the honest path — create the import target's FKs `NOT VALID`, + load, then `VALIDATE CONSTRAINT` (non-blocking-ish) — over the + `session_replication_role = replica` trust-me hack of R11. +- **Parallel GIN builds**: rebuilding `ix_derived_version_json` after a bulk derived write + (the R5 path) parallelizes on 18 — budget maintenance windows accordingly. +- **`pg_upgrade` keeps planner statistics** on 18 — with 928 leaf partitions that removes + the post-upgrade ANALYZE storm entirely. + +Physical decisions that came out of the audit: + +- **`max_locks_per_transaction = 4096` is a deployment requirement, not a suggestion.** + 58 partitioned tables × 16 leaves = 928 relations, and PostgreSQL clones every FK onto every + leaf (2,600+ constraints). Whole-schema DDL — install, migration, `pg_dump --schema-only` — + takes a lock per object and dies at the default 64 with `ERROR: out of shared memory`. Found + empirically: the schema does not even install without it. Hot-path queries are unaffected + (they prune to a handful of relations). +- **Differentiated autovacuum per write profile.** The partition-creation loop applies + different storage parameters: `branch_head` leaves (the only upsert-heavy table — overlay + rows update on every commit and die on compaction) get `fillfactor = 90` for HOT-update + headroom and dead-tuple-driven vacuum; the append-only leaves (`element_version`, + `derived_version`, subtype, link) get *insert*-driven vacuum + (`autovacuum_vacuum_insert_threshold = 100000` — keeping the visibility map current for + index-only scans) and analyze at 50k rows (the original blanket "analyze every 5000 rows" + would sample a 60M-row leaf continuously during imports). +- **lz4 for the jsonb columns**, set on the parents *before* the partition loop so leaves + inherit it (verified in `pg_attribute`). At this write volume, pglz compression cost is + measurable on every commit; lz4 is strictly better here. +- **UUIDv7 for app-generated keys** (`version_id`, `derived_id`): time-ordered uuids turn each + project's insert pattern from random-page btree scatter into rightmost-leaf appends. A .NET + note (`Guid.CreateVersion7()`), not a schema change. `identity_id` stays as supplied — it is + the spec-visible `@id` (and library elements are normatively v5). +- **Bulk import**: the ~3 FK probes per inserted row against a 10⁸-row `data_identity` are + real but modest; if measurement demands it, the importer path is + `SET session_replication_role = replica` plus post-import validation queries. The audit + explicitly *rejected* two tempting alternatives: `DEFERRABLE` FKs (they defer the identical + per-row work to commit time and balloon the trigger queue — not a bulk-load tool) and + `NOT VALID` + `VALIDATE` (unsupported on partitioned tables before PostgreSQL 18). + +--- + +## 14. The performance audit: war stories with numbers + +The schema was audited against the scale profile and then *measured* — a shape-faithful +synthetic dataset (200k elements, 2,000 commits, checkpoint at 1,500, 100 overlay branches, +one legacy fully-materialized branch for comparison) on PostgreSQL 17 in Docker. Three of the +findings were outright bugs that survived design review and were caught only by adversarial +audit plus empirical runs — which is the meta-lesson of this section. + +**The measured table:** + +| Operation | Legacy design | Hardened schema | +|---|---|---| +| Branch create | 2,964 ms (copy 200k rows) | **1.8 ms** (overlay) | +| Branch delete | unindexed → seq scans | 34 ms (overlay); 100 ms even for 200k rows (indexed cascade) | +| Single-element head read | all 16 partitions scanned | **0.061 ms**; 15/16 partitions "(never executed)" | +| Single-element historical read (500 commits from checkpoint) | 3,466 ms | **1.8–4 ms** | +| Full-model fold (500 commits from checkpoint) | 4,012 ms | **185 ms** | +| Branch-head set read (200k merge) | — | 1,242 ms | +| `build_commit_checkpoint` (1,500-commit fold × 200k) | — | 2,488 ms (async budget) | + +**The findings, compressed** (full table in `SysML2.NET.CodeGenerator/SQLSCHEMA.md`): + +- **R1 (SEV-1)** — materialized `branch_head` was O(branches × elements). Fixed by the overlay + (section 10.2). This was the only *architectural* rework. +- **R2 (SEV-1, bug)** — the hottest read function filtered on bare uuids → no pruning, no PK. + Fixed by joining through `branch` (section 11). Nothing in the SQL *looked* wrong; only the + plan shape revealed it. +- **R3 (SEV-1, bug)** — every `ON DELETE CASCADE` was unindexed on the cascade column. Fixed + by one index + demoting the big-table cascades to explicit procedures (sections 7, 10.2). +- **R-registry (SEV-1, found only by running)** — the checkpoint-existence probe seq-scanned + per recursion step because `n_distinct = 1` statistics defeat the index. Fixed structurally + (section 10.3). *Design review cannot catch this class of problem; only `EXPLAIN (ANALYZE, + BUFFERS)` on realistic data can.* +- **R4 (SEV-2)** — checkpoint cadence is a designed policy with a storage counterweight, not a + free knob (section 10.1). +- **R5 (SEV-2)** — the derived-burst worst case × GIN write amplification: budgeted as a bulk + operation; lz4 landed; GIN strategy documented (section 9.3). +- **R6 (SEV-2)** — single-element historical reads needed their own resolver (section 10.4). +- **R7 (SEV-3, deferred)** — link-table write amplification for huge collections; the + content-addressed collection design (digest-keyed shared collection rows, reused by pointer + when unchanged) is specified in SQLSCHEMA.md and waits for benchmark evidence, because it + reshapes generated tables and touches the generator. +- **R8/R9/R10/R11 (SEV-3)** — UUIDv7, autovacuum differentiation, plan-cache/lock-manager ops + checklist, bulk-import path (section 13). +- **R12 (refuted)** — `data_identity` unpartitioned at 10⁸ rows is fine (section 7). +- **R13 (SEV-4, silent-bug class)** — fold determinism on sibling-timestamp ties (section + 10.4). + +The follow-up gate before production, documented in SQLSCHEMA.md: a full .NET benchmark +harness — 3×1M-element projects with authentic serializer payloads sharing partitions, 20k- +commit replay, 500 branches, the root-rename burst measured *concurrently with* read latency, +a UUIDv4-vs-v7 A/B, and longevity checks (`pgstattuple` bloat, wait events, WAL per commit). + +--- + +## 15. What the service layer still owes the schema + +The schema is deliberately not self-driving. These responsibilities live above it, and the +design assumes they exist: + +1. **Impact-radius analysis** (the hard one). At commit time, compute which elements' derived + values the change set invalidates, recompute them (the `SysML2.NET/Extend/*.Compute*` + methods against the in-memory model), and write the `derived_version` rows. A leaf edit + invalidates one element; a namespace rename invalidates its subtree (`qualifiedName`, + `qualified` names of members); adding a supertype feature invalidates the specialization- + descendant closure (`feature`, `membership`, `inheritedMembership`). The reverse-lookup + indexes (`ix_*_target`, the specialization indexes) exist precisely to make these closures + computable. This is where the correctness bugs of the whole system will live — it deserves + the project's best tests. A full design sketch for this engine — the five propagation + kinds, the `derived_dependency` catalog, early cutoff, and the differential-testing + oracle — is `SysML2.NET.CodeGenerator/IMPACT-RADIUS.md`. +2. **Checkpoint cadence, retention, and overlay compaction** — the policies of sections 10.1 + and 10.2, executed asynchronously. +3. **The commit transaction discipline**: one transaction writes `commit` + `commit_parent` + (+ the trigger validates), `element_version` + subtype + link rows, `stored_json`, + `derived_version` rows, and the `branch_head` overlay upserts, then moves + `branch.head_commit_id`. Append-only tables make this a pure-insert transaction plus one + branch-row update. +4. **The base-commit invariant**: never point `branch.base_commit_id` at an uncheckpointed + commit. +5. **Project deletion** via the ordered explicit procedure (section 7) — never by deleting + `data_identity` rows first. And check `project_usage` beforehand: identities referenced + from *other* projects will (correctly, loudly) block the procedure through the NO ACTION + FKs — remove or migrate the usages first. +6. **Model-level reference validation** (dangling and wrong-type references at a commit) — + a *validation*, not an FK (Axiom 1). The generated two-tier functions (§7) are its + ready-made implementation: run `validate_references_in_commit` on every commit (O(change + set), cheap enough to gate acceptance), schedule `validate_references_at_commit` as the + periodic full audit, resolve cross-project targets through `project_usage`, and surface + the findings. +7. **Merge conflict restatement** — the spec requires merges to restate conflicting elements + in their own change set; the tiebreaker makes violations deterministic, not correct. + +The following obligations all stem from one underlying fact: **a single logical user action +usually touches more elements than the user thinks it does.** The canonical example: "add +child B to A" writes a new B, a new Membership, *and a new version of A* (A's +`ownedRelationship` list is stored, ordered state — §8.2). These couplings are invisible at +the API surface but decide whether concurrent editing feels smooth or maddening: + +8. **Three-way collection merge, with an ordering policy.** Two users adding children to the + same container both produce a new version of that container — formally a same-element + conflict on every rebase (§18.2) and every merge. Additive, disjoint collection changes + (base `[…]`, mine `[…, M_B]`, theirs `[…, M_C]`) MUST be auto-merged (`[…, M_B, M_C]`) + under a deterministic ordering policy (e.g. first-committed first), or every popular + container becomes a conflict magnet. Escalate to a human only for reorder-vs-reorder, + remove-vs-reference, and other genuinely incompatible combinations — and always run + model validation on the merged result (see item 12): a structurally clean union can still + produce duplicate member names. +9. **Ownership-quadruple coherence.** One ownership fact ("B is owned by A via M") is stored + in FOUR places: A's `ownedRelationship` list, M's `owning_related_element`, M's + `ownedRelatedElement` list, and B's `owning_relationship` back-pointer — plus the + endpoint mirrors required by the new-name redefinitions (e.g. `memberElement` alongside + `target`). Every write must keep all of them coherent within the change set; the schema + can FK-check each pointer individually but cannot cross-check their mutual agreement. +10. **Containment-aware conflict detection.** Naive conflict detection (intersect the two + change sets by identity) MISSES the delete-vs-descendant-edit case: user 1 tombstones + package A while user 2 edits a deep descendant D — disjoint identities, real conflict. + Detection must treat a tombstoned element as conflicting with every change *under* its + subtree (and with moves into it). +11. **Subtree-delete completeness.** Deleting an element means tombstoning its entire owned + closure — the element, its memberships, and all transitive children — in ONE change set. + The schema will happily store a half-deleted tree (Axiom 1: FKs check existence, not + liveness); only the service can guarantee the closure. +12. **Post-merge semantic validation, including cycle guards.** Two individually valid + branches can merge into an invalid model: duplicate names in one namespace, and — worse — + cycles that no single branch contains (user 1: B specializes C; user 2: C specializes B; + or two moves that nest A under B and B under A). Ownership cycles additionally break the + derived-computation walks (`qualifiedName` would never terminate), so the impact-radius + engine needs explicit cycle guards, and merge commits must be validated before + acceptance. +13. **Merge impact radius runs on the MERGED state.** Recomputing derived values for a merge + by unioning the two branches' derived results is wrong: cross-branch interactions (one + branch adds a Specialization, the other adds a feature to its target) produce derived + changes that neither branch ever saw. Compute against the merged snapshot. +14. **Treat the `class_kind` mapping as registry data — never hand-maintain it.** Load the + name↔id map from the `class_kind` table at startup (or use the generated `ClassKind` + enum, emitted from the same registry). The ids are frozen by the append-only registry + (§12.1), so re-applying seeds is safe and upgrades never renumber — but the registry + discipline itself (append newcomers, close dropped classes with `removed_in`, never + renumber) is a maintenance obligation on whoever regenerates the schema. +15. **Monitor the performance thresholds and alert BEFORE they bite.** The policies of + §10.1/§10.2 degrade silently when neglected — reads just get slower. Every threshold is + queryable from the schema, so measuring them is cheap; the obligation is to wire the + probes into monitoring and notify operators (and, where it explains their experience, + users) when a trend heads the wrong way. The signal set: + + | Signal | Probe | Alert when | What degrades otherwise | + |---|---|---|---| + | Overlay size per branch | `SELECT branch_id, count(*) FROM branch_head GROUP BY 1` vs the base checkpoint's row count | ≥ 50% of the compaction threshold (~10% of model / ~100k rows) | set reads and the anti-join grow; compaction is overdue | + | Checkpoint distance per branch | commits between `head_commit_id` and `base_commit_id` (walk `commit_parent`) | > 2× the cadence target (~400 commits) | resolver walks and historical reads lengthen | + | Branches with `base_commit_id IS NULL` | `SELECT count(*) FROM branch WHERE base_commit_id IS NULL AND deleted IS NULL` | > 0 outside project bootstrap | the O(model) full-overlay legacy behavior is silently back | + | Checkpoint retention backlog | checkpoints referenced by no branch and outside the historical ladder | sustained growth | storage grows by ~0.2 GB per stale 1M-element checkpoint | + | Impact radius per commit | `derived_version` rows per `commit_id` | > a few % of model size | derived bursts need the R5 bulk path (GIN pending-list tuning) | + | CAS conflict + auto-merge rate | service metrics: 409s and collection-merge rebases per branch | upward trend | hot-container contention (§18.3.6) is degrading UX | + | Seq-scan counters on element tables | `pg_stat_user_tables.seq_scan` deltas on the partitioned leaves | climbing above ~0 in steady state | an R2/R3-class planner regression has reappeared — the silent failure mode of §14 | + +16. **Release conversion (§6.4).** The schema enforces the release invariants on the commit + DAG (`trg_commit_parent_version`), but the conversion commit itself is service work: build + the version-diff between the two releases (which metaclasses/properties changed shape), + restate exactly the affected elements against the new-release descriptors, force a + `commit_checkpoint` on the conversion commit, honor `project.target_model_version_id` + before accepting the upgrade, and refuse cross-release merges with a clear + "convert first" error rather than surfacing the trigger's exception raw. When the + conversion retypes an element (its metaclass was dropped by the new release), update + `data_identity.class_kind` in the same transaction — the typed identity (§7) must keep + matching the restated versions. + +--- + +## 16. Worked examples — following data through the schema + +These mirror the smoke test; running `schema.smoke.sql` and reading its output alongside this +section is the fastest way to internalize the design. + +### 16.1 A rename ripples through derived state (Axiom 2 live) + +Setup: Package **P** ("Old") owns PartUsage **W** ("wheel"). Commit **c1** creates both. + +| Table | Rows after c1 | +|---|---| +| `element_version` | (P, p1, c1, "Old"), (W, w1, c1, "wheel") | +| `derived_version` | (P, c1, qn="Old"), (W, c1, qn="Old::wheel") | + +Commit **c2** renames P to "New". The change set is **one element**: + +| Table | New rows at c2 | +|---|---| +| `element_version` | (P, p2, c2, "New") — *nothing for W* | +| `derived_version` | (P, c2, qn="New"), **(W, c2, qn="New::wheel")** — W is in the impact radius | + +Read W at c2: the stored fold finds w1 (unchanged since c1); the derived fold finds W's c2 +row. Payload = `w1.stored_json || derived(c2).derived_json` → `"New::wheel"` with the original +stored content. (PASS 2a/2b.) + +### 16.2 A merge, and why "newest wins" is right + +History: c1 → c2 (rename P to "New") on main; c1 → c4 (rename P to "Other") on a side branch; +c5 = merge(c2, c4) resolving P to "Merged" *in its own change set*; meanwhile c3 (child of c2, +**not** an ancestor of c5) deleted W. + +Resolving c5: ancestry = {c5, c2, c4, c1}. For P, candidates are p1@c1, p2@c2, p_side@c4, +p_merge@c5 — monotonicity makes c5 the newest → "Merged" wins (PASS 8a). For W: only w1@c1 in +the ancestry — the deletion at c3 is invisible because c3 is not an ancestor (PASS 8b). This +is the OCL fold of section 6.2, executed by index. + +### 16.3 A branch's life under the overlay + +1. `build_commit_checkpoint(project, c2)` → 2 checkpoint rows + 1 registry row (PASS 9a). +2. Create branch b2 with `base_commit_id = c2` → **zero** overlay rows; reading W on b2 serves + the checkpoint row (PASS 9b/9c). +3. Delete W *on b2 only*: upsert overlay row (b2, W, → tombstone version, `is_tombstone = + true`). Reading W on b2 now returns nothing — the overlay masks the base (PASS 9d); the set + read returns 1 element (PASS 9e). Main's view of W is untouched. +4. Delete b2: the cascade removes only the overlay row; the checkpoint — shared with any other + branch based on c2 — is intact (PASS 9f). + +### 16.4 A query translation + +*"All PartUsages under `Vehicle` whose `isVariation` is true, ordered by name"* — as a spec +Query: `where = and(PrimitiveConstraint(qualifiedName, like, 'Vehicle::%'), +PrimitiveConstraint(isVariation, =, true))`, `orderBy = [name]`. The translator resolves each +property through the release's descriptor (section 12.2) and emits, over the branch-head state: + +```sql +SELECT h.identity_id +FROM sysml2.get_elements_at_branch_head($branch) h -- or the overlay-merge inline +JOIN sysml2.element_version ev USING (project_id, version_id) +JOIN sysml2.usage_version u USING (project_id, version_id) -- catalog: isVariation -> usage_version +JOIN sysml2.derived_version dv ON dv.project_id = ev.project_id AND dv.derived_id = h.derived_id +WHERE ev.class_kind = 94 -- catalog: PartUsage + AND dv.qualified_name LIKE 'Vehicle::%' -- catalog: derived, promoted column + AND u.is_variation +ORDER BY dv.name; +``` + +Every predicate landed on a real, indexed, statistics-bearing column — the payoff of promoted +derived columns plus the catalog. + +--- + +## 17. Code generation: what is emitted from the UML model and how + +The split follows volatility: **hand-written where semantics are subtle and stable +(PIM, versioning, resolvers), generated where the metamodel is large and changes with the +spec** (everything metaclass-shaped). + +| Generated section | Source of truth | Emitting helper | +|---|---|---| +| §1 enum types | UML enumerations | `WriteEnumTypes` | +| §2 registry seeds (1 release + 175 class kinds) | ClassKindRegistry (validated against the UML model) | `WriteMetamodelCatalogRows` | +| §6 link tables | multi-valued stored properties | `WriteLinkTables` | +| §7 subtype tables (47) | scalar stored declarations, bounds, XMI defaults | `WriteSubtypeTables` | +| §11 views (167) | storage-ancestor sets | `WriteFlatteningViews` | +| §12 partition list, §13 model version | table inventory, root package | `WritePartitionedTableArray`, `WriteModelVersion` | +| §14 reference validation, two tiers (42 sources; full + incremental) | stored reference columns + allowed target kinds per declared type | `WriteReferenceValidation` | + +Pipeline: `SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/SQLSchemaGenerator.cs` +reads the XMI via uml4net, renders `core-sql-schema-2.hbs` (whose hand-written sections are +kept byte-identical with `schema.golden.sql`), with the census logic in +`SysML2.NET.CodeGenerator/Extensions/SqlSchemaExtensions.cs` (snake_casing, type mapping, +declared-property computation per the two traps of section 3) and the emitters in +`SysML2.NET.CodeGenerator/HandleBarHelpers/SqlSchemaHelpers.cs`. The generator is driven by +`SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/SQLSchemaGeneratorTestFixture.cs`. + +Verification loop, end to end: run the fixture → apply `schema2.generated.sql` to PostgreSQL 18 +(`max_locks_per_transaction=4096`; both schemas are verified on 17 and 18.6, the recipe follows +the prefer-18 version policy of §13) → run `schema.smoke.sql` (30 assertions) → both the golden +and the generated schema must pass identically. + +--- + +## 18. Multi-user and concurrency + +The short version: **the schema is deliberately concurrency-friendly for readers, funnels all +writer contention into exactly one row per branch, and delegates five real multi-user +responsibilities to the service layer.** This section makes each of those statements precise — +including the one protocol that is *required but not enforced* by the schema. + +### 18.1 What the design solves by construction + +**Append-only is the concurrency strategy, not just the versioning strategy.** +`element_version`, `derived_version`, `commit`, `commit_parent`, and `commit_checkpoint` are +never UPDATEd or DELETEd in normal operation. Under PostgreSQL's MVCC that has a strong +consequence: readers never block writers, writers never block readers, and two writers can +only conflict where they write the *same row* — and immutable rows are never the same row. A +user reading a model at commit C reads data that *cannot change*: their view is perfectly +repeatable without locks, indefinitely cacheable, and consistent even if a colleague commits +mid-read. + +**All mutable state was squeezed into two places on purpose:** `branch.head_commit_id` +(+ `base_commit_id`) and the `branch_head` overlay rows of that branch. Everything else a +commit writes is a pure insert. The *entire* write-conflict surface of a project is therefore +**one `branch` row per branch**. Committers on *different* branches touch disjoint mutable +rows and cannot conflict at all; committers on the *same* branch conflict on exactly one row — +which is correct, because a branch is by definition a serial history. The database contention +mirrors the domain semantics. + +**Single-statement reads are tear-proof by construction.** The read functions of §10 join +`branch → overlay → checkpoint` in *one* SQL statement, and one statement in READ COMMITTED +sees one consistent snapshot — a reader can never observe "new base pointer + old overlay" +halfway through a compaction. This property is load-bearing: if the service ever splits that +read into two round-trips (fetch the branch row, then query the overlay), it silently loses +the guarantee. Keep such reads in one statement, or run them under REPEATABLE READ. + +**Checkpoint building coexists with everything.** `build_commit_checkpoint()` reads only +immutable history at a fixed commit and writes with `ON CONFLICT DO NOTHING`: two workers +building the same checkpoint merely waste some work; a checkpoint building while users commit +sees a frozen past that new commits cannot alter. This is why the cadence policy can run fully +asynchronously without coordination. + +**Plain READ COMMITTED is sufficient — nowhere is SERIALIZABLE needed.** That is a direct +payoff of append-only + the single-mutable-row funnel, and worth protecting when the service +is built. + +### 18.2 The required commit protocol (normative for the service layer) + +**Concurrent commits to the same branch are a lost-update bug unless the service uses +compare-and-swap on the head.** The failure: users A and B both read `head = c5`, both build +commits with parent c5, both write; the head moves twice and one user's commit becomes +unreachable from the branch — silently. The schema cannot prevent this, because "the parent I +built against" is application state. The protocol (the Git model; the OMG `createCommit` +taking `previousCommit` implies exactly this): + +```sql +BEGIN; +-- Option A (optimistic, recommended): CAS on the head +UPDATE sysml2.branch + SET head_commit_id = :new_commit + WHERE id = :branch AND head_commit_id = :expected_parent; +-- rowcount 0 => someone committed first: ROLLBACK, return 409, client rebases + +-- Option B (pessimistic): SELECT ... FOR UPDATE on the branch row at transaction +-- start, serializing committers per branch. Simpler; blocks instead of failing. + +-- then, all conflict-free pure inserts: +-- commit + commit_parent (trigger validates monotonicity), +-- element_version + subtype + link rows + stored_json, +-- derived_version rows (the impact radius), +-- branch_head overlay upserts. +COMMIT; +``` + +Touching the branch row **first** also gives every writer the same lock ordering — deadlock +prevention for free. **Compaction (§10.2) must take the same branch lock**: repointing +`base_commit_id` and clearing the overlay interleaved with a commit's overlay upserts would +leave the overlay describing divergence from the wrong base. + +**Stamp `commit.created` with `clock_timestamp()`, never transaction-start `now()`.** Under +concurrency, a transaction that began *before* the current head's transaction committed +would stamp its commit with a timestamp EARLIER than its parent's and be rejected by +`trg_commit_parent_monotonic`. `clock_timestamp()` taken after reading the expected head is +always strictly later than that head's stamp. (Surfaced by the concurrency suite below.) + +**This protocol is verified under real concurrency** by the checked-in pgbench suite +(`SysML2.NET.CodeGenerator/Sql/schema.concurrency.*.sql` — setup, hot-branch race, spread, +reader-under-write-storm, and an invariant verifier C1–C5). Measured on PG18 with 16 racing +clients on ONE branch: ~1,000 attempts/s, exactly one winner per head value, losers write +nothing (83% CAS-conflict rate at full hammer — the §15.15 signal in its worst case), heads +strictly linear, zero deadlocks; the same clients spread over 16 branches: ~2,200 commits/s +at 0% conflicts — contention is branch-local, as designed. Reads stayed at ~1.2 ms (from +0.8 ms idle) while the write storm ran: the MVCC promise of §18.1, measured. + +### 18.3 Drawbacks and open decisions + +1. **The derived-compute window stretches the critical section.** Derived values must be + computed against the exact parent snapshot. Inside the branch lock that is trivially + correct — but a root-namespace rename computes ~1M values, holding the lock for minutes and + stalling every committer on that branch. The better pattern is optimistic: compute *before* + locking, CAS, and on failure recompute the (usually tiny) difference and retry. More code, + and where the subtle bugs will live. Cross-branch there is no issue: derived rows are keyed + `(identity, commit)`, and different branches produce different commits. +2. **Pagination at HEAD is a multi-user trap.** If page 1 is served at head = c5 and a + colleague commits before page 2, "read the head again" returns a torn collection. Resolve + branch → commit once, embed the `commitId` in the page token, paginate against the + immutable commit. The schema supports this perfectly — that is what commits are *for* — but + the service must actually do it. +3. **`data_identity`'s bare-uuid PK makes `@id`s instance-global, not project-scoped** — the + deliberate price for FK-able cross-project references (§7). Consequence: two projects + cannot contain an element with the same `@id`. Non-event for random v4 ids; real for + *client-supplied* ids (kpar imports, cross-project cloning, deterministic v5 ids) — the + second insert fails with a PK violation. The service must mint fresh `@id`s when cloning + across projects and map the violation to a clear 409 Conflict. +4. **The monotonicity trigger can reject legitimate rapid commits.** `created` must be + *strictly* newer than every parent; two commits within the same microsecond on one lineage + (burst automation) are rejected — loudly, by design — so the service needs a + re-stamp-and-retry, and multi-app-server deployments should let the database assign + `created` (the `DEFAULT now()`) rather than trusting skewed application clocks. +5. **No row-level security.** Tenant isolation (who may see project X) is entirely + service-side today — by decision, not omission. PostgreSQL RLS on `project_id` composes + cleanly with this schema (every element table carries the column) and is the natural + hardening step if the database is ever exposed to less-trusted components. +6. **Hot containers are natural contention points.** Because a container's child list is + stored, *every* child addition versions the parent (§15, item 8) — so two users adding + children to the same package always collide, and popular containers (root packages, + library folders) collide constantly. The three-way collection merge of §15.8 is what + keeps this workable; without it, concurrent editing degrades into serial editing on + exactly the containers people share. (The same coupling is the R7 write-amplification + hotspot: each new container version rewrites its full collection rows.) +7. **Small print.** `UNIQUE (project_id, name)` turns concurrent same-name branch creation + into a constraint violation (map to 409, fine). GIN pending-list flushes on + `derived_version` can briefly serialize concurrent derived-heavy commits on a shared + partition (audit finding R5). `fillfactor = 90` on `branch_head` exists precisely to absorb + per-commit overlay churn from many concurrently active branches without index bloat. + +--- + +## 19. Glossary + +Appendix: every term of art used in this guide, with the shortest clear definition and — where +the guide explains it further — the section to read. Numbers in the *See* column are **guide +sections** (not the schema files' § banners); "—" means the term is used only in passing. + +| Term | Definition | See | +|---|---|---| +| **@id / @type** | The two JSON keys on every API record: the element's stable identity (uuid) and its metaclass name. | 7, 12.1 | +| **Anti-join** | "Rows in A with no match in B" (`NOT EXISTS`); how the set read subtracts overlaid identities from the checkpoint. | 11 | +| **Append-only** | Tables that only ever receive INSERTs; simultaneously the history model and the concurrency strategy. | 4, 18.1 | +| **Association-owned end** | A UML reference property owned by the association rather than the class; invisible in `OwnedAttribute` (generator trap 1). | 3 | +| **Autovacuum** | PostgreSQL's background cleanup/statistics daemon; tuned per write profile in the partition loop. | 13 | +| **Base commit** | The checkpointed commit a branch's overlay diverges from (`branch.base_commit_id`). | 10.2 | +| **Branch** | A mutable, named pointer to a head commit — the only mutable object in the versioning core. | 6.3 | +| **btree** | PostgreSQL's default ordered index type; every PK and lookup index here. | — | +| **Cadence** | The churn-based service policy deciding which commits get checkpoints. | 10.1 | +| **CAS (compare-and-swap)** | Atomically updating the branch head only if it still has the expected value; the normative commit protocol. | 18.2 | +| **Census** | The quantitative count of the metamodel (stored vs derived, types, inheritance) that drove every design decision. | 3 | +| **Change set** | The DataVersions one commit writes (`Commit.change`) — the delta. | 6.2 | +| **CHECK constraint** | A row-level validity rule (e.g. the mutually exclusive tombstone/payload shapes). | 8.1 | +| **Checkpoint** | A fully materialized fold of one commit (`commit_checkpoint`); bounds resolver walks and bases overlays. | 10.1 | +| **class_kind (interning)** | The smallint id per metaclass — interning of the canonical NAME, with ids frozen forever by the append-only registry; consumers load the map at runtime or use the registry-generated enum. | 12.1, 15 | +| **Class-kind registry** | The checked-in, append-only source of truth (`ClassKindRegistry.cs`) that freezes class_kind ids and model_version ordinals across releases; the generator validates the UML model against it and fails on drift. | 12.1 | +| **Commit** | An immutable record of the changes made at a point in time; a node in the commit DAG. | 6.1 | +| **Commit DAG** | The directed acyclic graph commits form once branching and merging are allowed. | 6.1 | +| **CommitReference** | The spec's abstract base of Branch and Tag: a named reference to a commit. | 6.3 | +| **Compaction** | Re-basing an overgrown overlay onto a fresh checkpoint at the branch head. | 10.2 | +| **Conformance (Derived Property)** | The spec's three levels — none / passthrough / full — realized here as write-path policies over one schema. | 1, 9.4 | +| **Conversion commit** | A single-parent commit that bumps the model-version stamp and restates every element whose shape changed between two releases; the only way a branch upgrades. | 6.4 | +| **CTE (recursive)** | A `WITH`-query that references itself; the resolvers' ancestry walk. | 10.4 | +| **Dangling reference** | A model-level reference to an element absent at the commit being read; a validation concern, never an FK violation (Axiom 1). | 4 | +| **data_identity** | The three-column table anchoring stable element identity (uuid, project, immutable class_kind); the FK target of every element reference. | 7 | +| **DataIdentity / DataVersion** | Spec types: the version-independent identity of data / the per-commit payload wrapper (≈ `element_version` row). | 2, 6.2 | +| **Derived property** | A metamodel property computed from other elements (77% of the metamodel); lives in `derived_version`, keyed (identity, commit) — Axiom 2. | 3, 9 | +| **derived_json / stored_json** | The pre-serialized read-model halves of an element's payload; concatenated at read time. | 8.1, 9 | +| **DISTINCT ON** | PostgreSQL's per-group-first-row selection; implements "newest version wins" in the fold. | 10.4 | +| **EAV** | Entity-attribute-value modeling (generic property rows); rejected as system of record, retained narrowly (link tables). | 5.3 | +| **element_version** | The core append-only table of stored element state; one row per (element, commit-that-changed-it). | 8.1 | +| **EXPLAIN (ANALYZE, BUFFERS)** | Plan + measured-execution inspection; the only tool that catches planner-class bugs. | 14 | +| **Fillfactor** | Page free space reserved for in-page (HOT) updates; 90 on `branch_head`. | 13 | +| **Flattened property** | A metaclass's own + inherited properties taken together (12,963 across the metamodel). | 3 | +| **Flattening view (`vw_` prefix)** | One generated view per concrete metaclass (e.g. `vw_part_usage`) reconstructing the DTO row shape; `vw_` stands for *view* — deliberately distinct from the `_version` table suffix. | 12.3 | +| **Fold** | The spec's `versionedData` computation: a commit's own changes plus everything inherited from its parents' snapshots. | 6.2 | +| **Foreign key (FK)** | A referential constraint; for element references always → `data_identity`, never → a version. | 7 | +| **Full conformance** | Derived values computed and current in every response, queryable; the design target. | 9.1, 9.4 | +| **GIN index** | An inverted index over jsonb keys/values; serves containment probes on non-promoted derived properties. | 9.3 | +| **Hash partitioning** | Splitting each table into N pieces by `hash(project_id)`; co-located across all element tables. | 13 | +| **HOT update** | A heap-only update that avoids touching indexes; requires fillfactor headroom. | 13 | +| **Identity** | The stable `@id` of an element across its whole life; a `data_identity` row; the only thing references point at (Axiom 1). | 4, 7 | +| **Impact radius** | The set of elements whose derived values a change set invalidates; determines which `derived_version` rows a commit writes. | 9.2, 15 | +| **Index-only scan** | Answering a query from the index alone; needs a current visibility map (hence insert-driven vacuum). | 13 | +| **Invariant** | A rule that must hold at all times; the four merge invariants underwrite the resolver. | 6.1 | +| **jsonb** | PostgreSQL's binary JSON column type. | — | +| **KerML** | Kernel Modeling Language — the foundation layer of SysML v2. | 1 | +| **Link table** | An ordered `(version, ordinal, target)` table for a multi-valued stored property; 7 exist. | 8.2 | +| **lz4** | Fast compression for TOASTed values; applied to both jsonb columns. | 13 | +| **max_locks_per_transaction** | Lock-table sizing; ≥ 4096 is a deployment requirement for whole-schema DDL. | 13 | +| **Merge (commit)** | A commit with two or more parents, converging branches. | 6.1 | +| **Merge invariants** | The four Clause 7.1.2 rules (monotonicity, restatement, deletion validity, uniqueness) the resolver depends on, plus this schema's own fifth: release compatibility. | 6.1, 6.4 | +| **Metaclass / metamodel** | A type in the modeling language (175 of them) / the set of all of them. | 1, 3 | +| **Model version** | A registered metamodel release (`model_version` table); every commit is stamped with the release its payloads are written in — the stamp, not the branch or project, is the truth. | 6.4, 12.1 | +| **Model-version descriptor** | Per-release generated C# that carries what the dropped catalog tables used to: each release's metaclasses, subtype-table sets, and property→storage routing for the Query translator. | 12.2 | +| **Monotonicity** | A commit is strictly newer than every parent; trigger-enforced because violations corrupt snapshots silently. | 6.1 | +| **MVCC** | Multi-version concurrency control: readers see consistent snapshots, never blocking writers. | 18.1 | +| **n_distinct** | The planner's distinct-values statistic; its collapse to 1 on checkpoint rows caused the registry redesign. | 10.3 | +| **OCL** | Object Constraint Language — the spec's formal derivation/constraint formulas. | 3, 6.2 | +| **Overlay** | The sparse `branch_head` contents: only the identities on which a branch diverges from its base checkpoint. | 10.2 | +| **Ownership chain** | The containment path from an element up to its root namespace (the `qualifiedName` walk). | 3 | +| **Ownership quadruple** | The four stored places that together record one ownership fact (parent's `ownedRelationship`, membership's `owning_related_element` + `ownedRelatedElement`, child's `owning_relationship`); the service must write them coherently — the schema cannot cross-check them. | 15 | +| **Partition pruning** | The planner skipping irrelevant partitions; requires a `project_id` predicate — the R2 lesson. | 11, 13 | +| **Passthrough** | Storing and faithfully reproducing client-sent derived values without computing; supported by the same schema. | 9.4 | +| **PIM** | Platform-Independent Model: the spec's repository machinery (Project, Commit, Branch, …); hand-written layer. | 2, 6 | +| **Promoted column** | One of the six derived properties given a real indexed column (`owner`, `qualified_name`, `name`, `short_name`, `owning_namespace`, `is_library_element`). | 9.3 | +| **Property catalog** | The former database table mapping every API property name to its physical storage; dropped in favor of the per-release model-version descriptors. | 12.2 | +| **READ COMMITTED** | PostgreSQL's default isolation level — sufficient everywhere here, by design. | 18.1 | +| **Redefinition (same-name / new-name)** | Same-name redefinitions are storage-free (resolve to the root's column); new-name redefinitions get their own storage (generator trap 2). | 3 | +| **Reference validation (generated, two-tier)** | `validate_references_in_commit()` per commit (O(change set), including the reverse direction tombstones break) + `validate_references_at_commit()` as the periodic full audit (O(snapshot × log history)); wrong-type via the typed identity, dangling via the snapshot; functions by design, never constraints. | 7 | +| **Registry** | `commit_checkpoint_registry` — one row per checkpoint, so existence probes hit a table whose grain matches the question. | 10.3 | +| **Resolver** | A SQL function that resolves an indirect reference ("commit C", "head of branch B") into the concrete snapshot: per element the mapping identity → (version_id, derived_id), by executing the fold bounded by checkpoints. Three exist: `resolve_commit_state`, `resolve_element_at_commit`, and the branch-head read path as their pre-materialized form. | 10.4 | +| **RLS** | Row-level security; absent by decision, the natural hardening step if needed. | 18.3 | +| **Sequential scan** | Reading a whole table; the silent failure mode behind findings R2/R3/registry. | 14 | +| **Sibling commits** | Commits on parallel branches sharing a parent; legally may share a timestamp — hence the tiebreaker. | 6.1, 10.4 | +| **Skip scan** | A btree scan on a non-leading index column; only from PG18, and even there partition pruning still needs `project_id` — the rule stands. | 11 | +| **Snapshot** | The full model state at one commit (`versionedData`); resolved via checkpoint + fold. | 6.2, 10 | +| **Specialization closure** | The transitive supertype/subtype set of a type; what `Type::feature` folds over. | 3 | +| **Stored property** | A non-derived metamodel property persisted in columns/link tables (2,698 flattened; 97 declarations). | 3 | +| **Storage-declaring metaclass** | A metaclass declaring ≥ 1 stored scalar of its own → gets a subtype table (47 of them). | 8.3 | +| **Subtype table** | The per-storage-declaring-metaclass table keyed `(project_id, version_id)`; DAG handled by membership, not joins. | 8.3 | +| **Superset schema** | The physical-schema policy for multi-version support: tables and columns are the union across all registered releases; nothing is ever dropped, renames become new columns. | 6.4 | +| **System of record** | The authoritative normalized columns/link tables, as opposed to the jsonb read model. | 5.4, 8.1 | +| **Tag** | An immutable, destructible named reference to a commit. | 6.3 | +| **Three-way collection merge** | Merging two changed versions of an ordered collection against their common base; additive disjoint changes auto-resolve, reorders/removals escalate to a human. | 15, 18.3 | +| **Tiebreaker (`id DESC`)** | The deterministic ordering applied when sibling commits share a timestamp. | 10.4 | +| **Tombstone** | A version row marking deletion at a commit (`payload = null` in spec terms). | 8.1 | +| **TPT (table-per-type)** | One table per class, joined along inheritance; rejected — the DAG breaks the chain. | 5.2 | +| **Typed identity** | `data_identity.class_kind`: the element's immutable metaclass on the identity row, making type FK-able where versions never are — enforced on every version by a composite FK. | 7 | +| **Upsert (`ON CONFLICT`)** | Insert-or-update in one statement; how commits maintain the overlay. | 10.2, 18.2 | +| **UUID v4 / v5 / v7** | Random / name-based / time-ordered uuids; v7 recommended for app-generated keys (self-activated as `DEFAULT uuidv7()` on PG18), v5 normative for library elementIds. | 7, 13 | +| **Version** | One element's stored state as of one commit; an `element_version` row; immutable. | 4, 8.1 | +| **WAL** | PostgreSQL's write-ahead log — the durability cost of every write. | 14 | +| **XMI** | XML Metadata Interchange — the UML files that are the metamodel's source of truth for generation. | 2, 17 | + +--- + +*Companion documents: `SysML2.NET.CodeGenerator/SQLSCHEMA.md` (compact reference, ranked audit +table, benchmark gate); the §-numbered banners inside +`SysML2.NET.CodeGenerator/Sql/schema.golden.sql` (per-object rationale in place); +`SysML2.NET.CodeGenerator/Sql/schema.smoke.sql` (the executable form of sections 4, 10 and 16).* diff --git a/SysML2.NET.CodeGenerator/SQLSCHEMA-GUIDE.nl.md b/SysML2.NET.CodeGenerator/SQLSCHEMA-GUIDE.nl.md new file mode 100644 index 000000000..2ef8957dd --- /dev/null +++ b/SysML2.NET.CodeGenerator/SQLSCHEMA-GUIDE.nl.md @@ -0,0 +1,2163 @@ +# Het SysML2.NET PostgreSQL-schema — een architectuurgids + +> **Voor wie is dit?** Je kent SQL, maar je wilt snappen *waarom* dit schema eruitziet zoals +> het eruitziet — elke tabel, elke index, elke functie, en vooral de redenering erachter. +> Dit document is de uitgebreide tegenhanger van `SysML2.NET.CodeGenerator/SQLSCHEMA.md` (de +> compacte referentie). Waar SQLSCHEMA.md beslissingen simpelweg *opsomt*, legt deze gids uit +> hoe we erop zijn uitgekomen. +> +> **Over de terminologie:** conceptuele termen (derived properties, stored state, fold, +> checkpoint, overlay, impact radius, …) laten we in het Engels staan — het is nu eenmaal het +> vocabulaire van de specificatie, de code en de schemabestanden. Alleen de lopende tekst is +> Nederlands. +> +> **De bestanden waar deze gids over gaat:** +> +> | Bestand | Rol | +> |---|---| +> | `SysML2.NET.CodeGenerator/Sql/schema.golden.sql` | Handgeschreven, geannoteerd referentieontwerp | +> | `SysML2.NET.CodeGenerator/Sql/schema2.generated.sql` | Echte generator-output (ingecheckt ter review) | +> | `SysML2.NET.CodeGenerator/Sql/schema.smoke.sql` | Functionele test met 30 assertions | +> | `SysML2.NET.CodeGenerator/Templates/Uml/core-sql-schema-2.hbs` | De Handlebars-template die het schema genereert | +> +> Sectienummers zoals **§5** verwijzen naar de genummerde banners in de schemabestanden zelf. + +--- + +## Inhoudsopgave + +1. [Het probleem dat wordt opgelost](#1-het-probleem-dat-wordt-opgelost) +2. [De twee werelden: element data en PIM-data](#2-de-twee-werelden-element-data-en-pim-data) +3. [De census: waarom 77% van het metamodel geen stored data is](#3-de-census-waarom-77-van-het-metamodel-geen-stored-data-is) +4. [De twee axioma's waar alles uit volgt](#4-de-twee-axiomas-waar-alles-uit-volgt) +5. [Verworpen alternatieven, en waarom](#5-verworpen-alternatieven-en-waarom) +6. [Laag A — het PIM: projects, commits, branches, tags (§3)](#6-laag-a--het-pim-projects-commits-branches-tags-3) +7. [Identity: `data_identity` en de filosofie achter referential integrity (§4)](#7-identity-data_identity-en-de-filosofie-achter-referential-integrity-4) +8. [Laag B — stored element state (§5, §6, §7)](#8-laag-b--stored-element-state-5-6-7) +9. [Laag C — derived element state (§8)](#9-laag-c--derived-element-state-8) +10. [Laag D — snapshot resolution (§9)](#10-laag-d--snapshot-resolution-9) +11. [Het read path (§10)](#11-het-read-path-10) +12. [De metamodel-catalogi en de Query service (§2, §11)](#12-de-metamodel-catalogi-en-de-query-service-2-11) +13. [Partitionering en fysieke tuning (§12)](#13-partitionering-en-fysieke-tuning-12) +14. [De performance-audit: praktijkverhalen met cijfers](#14-de-performance-audit-praktijkverhalen-met-cijfers) +15. [Wat de service-laag het schema nog verschuldigd is](#15-wat-de-service-laag-het-schema-nog-verschuldigd-is) +16. [Uitgewerkte voorbeelden — data volgen door het schema](#16-uitgewerkte-voorbeelden--data-volgen-door-het-schema) +17. [Codegeneratie: wat uit het UML-model wordt gegenereerd en hoe](#17-codegeneratie-wat-uit-het-uml-model-wordt-gegenereerd-en-hoe) +18. [Multi-user en concurrency](#18-multi-user-en-concurrency) +19. [Begrippenlijst](#19-begrippenlijst) + +--- + +## 1. Het probleem dat wordt opgelost + +Dit schema is de persistence-laag voor een **SysML v2-modelrepository** die de OMG-specificatie +*Systems Modeling API and Services*, versie 1.0, implementeert. In die ene zin zitten drie +harde eisen verstopt, en elk daarvan drukt een zwaarder stempel op het schema dan welke gewone +CRUD-overweging ook: + +**Eis 1 — er worden *modellen* opgeslagen, geen records.** Een SysML v2-model is een graaf van +getypeerde elementen (`PartUsage`, `Membership`, `Specialization`, …) uit een metamodel met +175 metaclasses. Die elementen verwijzen kriskras naar elkaar: ownership-bomen, +type-hiërarchieën, namespace-imports. Een "rij" is hier dus één element uit een +systems-engineering-model — en zo'n model kan er een miljoen bevatten. + +**Eis 2 — het is een *versiebeheersysteem*.** De OMG-API is met opzet op Git geënt: projects +bevatten commits, commits vormen een directed acyclic graph (een merge heeft meerdere +parents), branches zijn verschuifbare pointers in die graaf en tags zijn bevroren pointers. +Elke leesactie via de API gebeurt *op* een commit: `GET /projects/{p}/commits/{c}/elements/{e}`. +En commits zijn volgens de specificatie immutable en onverwijderbaar. Daarmee valt de +klassieke aanpak — "tabellen met de actuele stand plus een audit log" — meteen af: historie is +hier geen bijzaak voor de audit, historie *is* het datamodel. + +**Eis 3 — antwoorden moeten *derived properties* bevatten.** Dit is de eis die vrijwel +iedereen onderschat, en tegelijk de grootste drijfveer achter dit ontwerp. Het SysML +v2-metamodel definieert het merendeel van zijn properties als **derived**: berekend uit +andere elementen, via traversal-regels in OCL. De `qualifiedName` van een element? Die volgt +uit het aflopen van de ownership chain tot aan de root namespace. De `feature`-set van een +type? Die ontstaat door memberships over de complete specialization-hiërarchie samen te +vouwen. De OMG-API (Clause 2, "Derived Property Conformance") laat een server kiezen uit drie +niveaus: + +- *geen conformance* — derived properties worden nooit teruggegeven; +- *passthrough* — de server slaat op wat de client aan derived values aanlevert en geeft dat + terug, zonder zelf ooit iets te berekenen; +- **full conformance** — elk antwoord bevat correct berekende, actuele derived values, en je + kunt op derived properties filteren in query's. + +Dit schema mikt op **full conformance met precompute op het commit-moment**: derived values +worden één keer berekend — op het moment dat een commit wordt weggeschreven — en een leesactie +hoeft daarna alleen nog bytes terug te geven. Waarom die keuze (en niet compute-on-read), en +wat ze kost, lees je in sectie 9. + +Tot slot het schaalprofiel waarvoor dit schema is ontworpen (afgestemd met de +projecteigenaar): + +- **~1 miljoen elementen** per project, +- **100–500 tegelijk levende branches** per project, die routinematig worden aangemaakt en + weer weggegooid, +- **tienduizenden commits** per project (jaren van dagelijks werken), +- **tientallen tot honderden projects** op één PostgreSQL-instantie, +- leesverkeer dat wordt gedomineerd door *branch-head*-reads en queryfilters, met af en toe + een historische read. + +Houd die aantallen in je achterhoofd. Er zijn genoeg ontwerpen die bij 100k elementen en 5 +branches prima werken, maar bij dit profiel omvallen — sectie 14 laat de metingen zien. + +--- + +## 2. De twee werelden: element data en PIM-data + +De OMG-specificatie knipt haar datamodel in twee niveaus, en het schema volgt die knip. + +**Het PIM (Platform-Independent Model)** is de *machinerie van de repository*: `Project`, +`Commit`, `Branch`, `Tag`, `DataVersion`, `DataIdentity`, `Query`. Deze typen staan in Clause +7 van de API-specificatie — niet in het SysML-metamodel. Het zijn er zestien, ze zijn stabiel +(ze veranderen alleen als OMG de API herziet, dus vrijwel nooit) en hun semantiek luistert +nauw: commit-DAG's, merge-invarianten. In het schema zijn ze daarom **handgeschreven** (§3). +Zestien stabiele tabellen door een generator laten maken zou machinerie toevoegen zonder iets +op te leveren — en juist de subtiele stukken (de monotonie-trigger, de verwijderprocedure) +verdienen commentaar van een mens. + +**Element data** is de eigenlijke modelinhoud: de 175 metaclasses van KerML + SysML v2. Dat +deel wordt **gegenereerd**, uit dezelfde UML-XMI-bestanden (`Resources/KerML_only_xmi.uml`, +`Resources/SysML_only_xmi.uml`) waar ook de rest van SysML2.NET uit wordt gegenereerd: de +DTO's, de POCO's, de JSON-serializers, noem maar op. Herziet OMG de taal (en dat gebeurt met +enige regelmaat), dan draai je de generator opnieuw en heb je een schema dat exact bij het +nieuwe metamodel past — zonder 167 tabeldefinities met de hand bij te werken. Hoe die +pijplijn werkt staat in sectie 17. + +De grens tussen de twee werelden is één begrip: de **DataVersion**. In de specificatie is een +`DataVersion` de verpakking van een element-payload in de context van een commit — "element X +had déze inhoud op commit C". In het schema is dat begrip terug te vinden als de +`element_version`-rij. De PIM-tabellen administreren *welke* versions er bestaan; de +elementtabellen leggen vast *wat* elke version inhield. + +```mermaid +flowchart TB + subgraph PIM["PIM — repository-machinerie (handgeschreven, §3)"] + project --> commit + commit --> commit_parent + project --> branch + project --> tag + end + subgraph ID["Identity (§4)"] + data_identity + end + subgraph STORED["Stored element state (§5–§7, append-only)"] + element_version --> subtype["47 subtype-tabellen"] + element_version --> link["7 link-tabellen"] + end + subgraph DERIVED["Derived element state (§8, append-only)"] + derived_version + end + subgraph SNAP["Snapshot resolution (§9)"] + branch_head["branch_head (overlay)"] + commit_checkpoint + registry["commit_checkpoint_registry"] + end + commit -.->|"één version-rij per\ngewijzigd element"| element_version + commit -.->|"één derived-rij per\ngeraakt element"| derived_version + element_version -->|identity_id| data_identity + derived_version -->|identity_id| data_identity + branch --> branch_head + branch -->|base_commit_id| commit_checkpoint +``` + +--- + +## 3. De census: waarom 77% van het metamodel geen stored data is + +Voordat er ook maar één tabel op papier stond, is eerst het metamodel doorgeteld. Achteraf +was dat de belangrijkste stap van allemaal: de cijfers maken korte metten met de intuïtie +waarmee je anders aan het ontwerpen zou slaan. + +Het metamodel, zoals het in de gegenereerde code van deze repository is uitgewerkt, bevat: + +| Meting | Aantal | +|---|---| +| Metaclasses | 175 (167 concreet, 8 abstract) | +| Flattened properties over alle concrete classes (eigen + geërfd) | 12.963 | +| …waarvan **stored** (`{ get; set; }` in de DTO's) | 2.698 | +| …waarvan **derived** (`{ get; internal set; }`) | 9.582 | +| …expliciete-interface-redefinition-aliassen (geen opslag) | 683 | +| Afzonderlijke *declaraties* achter die 2.698 stored properties | **97, verdeeld over 49 metaclasses** | +| Afzonderlijke namen van stored properties | ~80 | +| Breedste stored footprint van één metaclass | **24 kolommen** (`FlowUsage` en verwanten) | +| Multi-valued stored reference properties, afzonderlijk | **6** (`ownedRelationship`, `ownedRelatedElement`, `source`, `target`, `client`, `supplier`) plus 1 multi-valued string (`aliasIds`) | +| Enumeraties | 7, met in totaal 19 literals | + +Neem die cijfers even goed in je op, want hier draait alles om: + +**Eén: het stored oppervlak is verrassend klein.** Twaalfduizend flattened properties klinkt +als een enorme berg — tot je ziet dat er maar zo'n 2.700 van worden opgeslagen, en dat die +terug te voeren zijn op slechts 97 declaraties. De vermenigvuldiging zit hem in de +overerving: `Element` declareert 7 stored properties en alle 167 concrete classes erven die — +goed voor 1.169 van de 2.698 in één klap. De stored kern van het metamodel is dus werkelijk +bescheiden: wat booleans, wat namen, hier en daar een enum, en een handjevol single-valued +references op de relationship-metaclasses. + +**Twee: het derived oppervlak is gigantisch, en het is géén franje.** 9.582 flattened derived +properties, zo'n 325 verschillende namen. En dat zijn geen extraatjes — het is het primaire +vocabulaire van de API. `owner`, `qualifiedName`, `ownedElement`, `feature`, `membership`, +`documentation`: allemaal derived, en onder full conformance worden ze allemaal in elke +API-payload verwacht. Het venijnige: juist de belangrijke zijn **recursief**: + +- `qualifiedName` loopt de ownership chain af tot aan de root, en kijkt onderweg ook nog naar + de namen van broertjes en zusjes; +- `Type::feature` en `inheritedMembership` folden over de *complete specialization closure* + van een type (een breadth-first search over `Specialization`-edges); +- `Namespace::importedMembership` wandelt recursief over imports, en `Import::isRecursive` + maakt die wandeling onbegrensd; +- `isLibraryElement` klimt via de ownership omhoog om te kijken of er een library-root boven + hangt. + +Geen van deze is in één SQL-`SELECT` uit te rekenen. Je hebt er recursieve CTE's of +gematerialiseerde closures voor nodig — of je rekent ze vooraf uit. Dat laatste is de weg die +hier is gekozen. + +**Drie: de typeconflicten in de opslag zijn echt, en ze dwingen structuur af.** Het metamodel +hergebruikt property-namen met *verschillende typen*: `LiteralBoolean::value` is een Boolean, +`LiteralInteger::value` een Integer, `LiteralRational::value` een Real en +`LiteralString::value` een String — vier onverenigbare SQL-typen achter één naam. Hetzelfde +geldt voor `kind`: op `RequirementConstraintMembership`, `StateSubactionMembership`, +`TransitionFeatureMembership` en `TriggerInvocationExpression` is het telkens een *andere* +enum. Elk ontwerp met één gedeelde `value`-kolom is daarmee bij voorbaat kansloos. Dit ene +feit veegt "één brede tabel" van tafel (sectie 5). + +**Vier: de overerving is een DAG, geen boom.** 34 metaclasses hebben meer dan één directe +supertype (tot drie aan toe: `FlowUsage` is tegelijk een `ConnectorAsUsage`, een `Flow` én een +`ActionUsage`, en daarmee zowel *Feature* als *Relationship*). Elk ontwerp dat leunt op een +lineaire "join omhoog langs de parent chain" valt dus ook af. De diepste keten telt 11 +niveaus. + +Alles in de secties 5 tot en met 9 vloeit rechtstreeks uit deze vier feiten voort. + +### Twee valkuilen die tijdens de census boven water kwamen + +De census legde ook twee eigenaardigheden van de UML-bron bloot waar een argeloze generator +over struikelt. Ze staan hier zwart op wit, omdat ze anders gegarandeerd ooit iemand bijten +die aan de generator werkt: + +**Valkuil 1 — association-owned ends.** In UML kan een reference property die bij een +association hoort eigendom zijn van de *association* zelf, niet van de class. En laat dat nu +net gelden voor de dragende reference properties van het hele metamodel: +`Membership::memberElement`, `Specialization::general`, `FeatureTyping::type` — geen van alle +te vinden in `IClass.OwnedAttribute`. Een generator die op `OwnedAttribute` leunt, levert dus +zonder één foutmelding een `membership_version`-tabel af *zonder member-element-kolom*. Dit is +tijdens de ontwikkeling echt gebeurd: 22 van de 47 subtype-tabellen kwamen er in eerste +instantie verkeerd uit. De juiste definitie van "declared door class C" luidt: *de flattened +properties van C, minus alles wat de directe generalizations van C al hebben*. Zie +`SysML2.NET.CodeGenerator/Extensions/SqlSchemaExtensions.cs`, `QueryStoredOwnProperties`. + +**Valkuil 2 — twee smaken redefinition.** UML-property-redefinition dekt twee wezenlijk +verschillende situaties. Een *same-name redefinition* (`CollectExpression::operator` +herdefinieert `OperatorExpression::operator`) is niet meer dan een aangescherpte constraint: +zelfde opslagslot, en de DTO's geven er geen eigen veld voor. Daar zijn er precies negen van. +Een *new-name redefinition* (`Membership::memberElement` herdefinieert `Relationship::target`) +is daarentegen een **nieuwe API-property met eigen opslag** — de DTO's bewaren zowel +`memberElement` als de geërfde `target`-lijst, en API-payloads bevatten ze allebei. De +opslagregel die aansluit bij de rest van SysML2.NET is dus: alleen same-name redefinitions +zijn opslagvrij en lossen transitief op naar de kolom van de root property. (Precies dezelfde +onderscheiding die `SysML2.NET.CodeGenerator/HandleBarHelpers/PropertyHelper.cs` voor de +DTO-generator hanteert.) + +--- + +## 4. De twee axioma's waar alles uit volgt + +Onthoud je verder niets van dit document, onthoud dan deze twee uitspraken. Elke structurele +keuze in het schema is op één van beide terug te voeren. + +### Axioma 1 — References wijzen naar identities, nooit naar versions + +Het `@id` van een SysML-element blijft zijn hele leven hetzelfde. Als een `FeatureTyping` +zegt "deze feature is getypeerd door element `4ace3d89-…`", dan bedoelt hij: *dat element, +wat het ook is op de commit waar jij toevallig naar kijkt* — en dus niet "versie 17 van dat +element". Die onafhankelijkheid van versies is precies de bedoeling: je kunt het doelelement +jarenlang hernoemen, hertyperen en verbouwen, en de reference blijft gewoon kloppen. + +Voor het schema betekent dat: **elke kolom die van element naar element verwijst is een +foreign key naar `data_identity(id)` — nooit naar `element_version`.** Nergens in het schema +bestaat een FK van de ene element version naar de andere. + +Dit bepaalt meteen wat referential integrity hier wél en níét kan betekenen. De FK garandeert +dat de doel-*identity in de database bestaat*. Wat hij niet kan garanderen: dat het doel ook +*bestaat op de commit die je leest*. Een element mag best verwijzen naar iets dat op deze +branch is verwijderd — dat is dan een dangling reference *in het model*, iets wat de service +als validatieprobleem rapporteert, geen schending van database-integriteit. Zou je de +database per-commit-geldigheid van references willen laten afdwingen, dan heb je FK's nodig +naar een virtuele, berekende verzameling — dat kan niet, en het zou ook onjuist zijn: de +specificatie staat uitdrukkelijk toe dat een model tussen commits in een tussentoestand +verkeert. + +### Axioma 2 — Een derived value is een functie van (identity, commit), niet van (version) + +Deze is subtieler, en de gevolgen reiken verder. Kijk even mee: + +``` +Package "Old" <- element P, version p1 + └── PartUsage "wheel" <- element W, version w1, qualifiedName = "Old::wheel" +``` + +Nu committen we een hernoeming van het package naar `"New"`. In de change set van die commit +zit **één** element: P (nieuwe version p2). Aan W is niets gebeurd — geen nieuwe version, +`w1` blijft op elke branch de actuele stored state. En tóch is W's `qualifiedName` nu ineens +`"New::wheel"`. + +Met andere woorden: de derived state van W is veranderd *zonder dat W zelf veranderde*. Een +derived value is dus geen eigenschap van een version — dezelfde version `w1` heeft op commit 1 +`qualifiedName = "Old::wheel"` en op commit 2 `"New::wheel"`. Het is een eigenschap van het +paar **(identity, snapshot)**. De OMG-specificatie zegt het met zoveel woorden (Clause 2): +*"the values of derived properties of a given Element may be affected by commits that do not +directly change that Element."* + +Voor het schema betekent dit dat derived state **onmogelijk op `element_version` kan wonen**. +Zou dat wel zo zijn, dan dwingt die ene hernoeming je om voor W — en voor élke andere +afstammeling — een nieuwe `element_version`-rij te schrijven waarvan de *stored* helft +byte-voor-byte gelijk is aan de oude. Je zou elementen versioneren die helemaal niet zijn +veranderd, het begrip "change set" uithollen, en de opslag van stored state laten exploderen +met de impact radius van elke hernoeming. + +Daarom heeft het schema **twee parallelle append-only streams**: + +- `element_version` — gesleuteld op version; één rij per *(element, commit-die-het-wijzigde)*; + immutable; het system of record voor stored state. +- `derived_version` — gesleuteld op *(identity, commit)*; één rij per *(element, + commit-die-zijn-derived-state-wijzigde)*; immutable; het read model voor derived state. + +Bij die hernoemingscommit wordt er dus geschreven: **één** nieuwe `element_version`-rij (voor +P) en **N + 1** nieuwe `derived_version`-rijen (voor P plus elk element dat door de +hernoeming werd geraakt — de "impact radius"). De stored state van W blijft onaangeroerd; de +derived state van W krijgt een nieuwe rij. + +De smoke-test met zijn 30 assertions (`SysML2.NET.CodeGenerator/Sql/schema.smoke.sql`) heeft +precies dit scenario als eerste en belangrijkste assertion-paar (PASS 2a/2b): na de +hernoeming levert W's `qualifiedName` netjes `"New::wheel"` op, *terwijl W nog steeds naar +zijn oorspronkelijke version-rij verwijst*. Wie dit schema ooit gaat verbouwen: zorg dat die +test groen blijft. Hij is de dragende muur van het ontwerp. + +--- + +## 5. Verworpen alternatieven, en waarom + +Er zijn vier voor de hand liggende architecturen overwogen en afgevallen. Als je snapt +waaróm ze afvallen, snap je meteen waarom het uiteindelijke ontwerp eruitziet zoals het +eruitziet. + +### 5.1 Eén brede tabel ("God table") + +*Eén `element`-tabel met een kolom voor elke stored property van alle metaclasses bij +elkaar.* + +Met maar ~80 verschillende stored namen klinkt dat best redelijk — 80 kolommen is niet +absurd. Maar het strandt op de typeconflicten uit de census: `value` zou tegelijk `boolean`, +`integer`, `double precision` én `text` moeten zijn, en `kind` vier verschillende enum-typen. +Dan resten er twee uitwegen, allebei lelijk. Óf je maakt getypeerde kolomfamilies +(`value_bool`, `value_int`, …, `kind_req`, `kind_state`, …) — waarmee je feitelijk +subtype-tabellen ín één tabel hebt nagebouwd, maar dan slechter: vrijwel elke cel NULL en +elke CHECK-constraint afhankelijk van `class_kind`. Óf je maakt alles `text` en strooit met +casts — en dan geef je de typeveiligheid op in precies de laag die haar hoort te leveren. + +### 5.2 Tabel-per-metaclass (volledig TPT, de "COMET-vorm") + +*Eén tabel per concrete metaclass (167), plus één link-tabel per multi-valued property +(~230); bij het lezen join je de overervingsketen weer aan elkaar.* + +Dit is de vorm van het oude `core-sql-schema.hbs`-skelet dat dit project heeft geërfd — een +port van de CDP4-COMET-server, waar deze aanpak voor een ander metamodel prima werkt. Hier +loopt hij om drie redenen spaak: + +1. **De overervings-DAG breekt de join-keten.** TPT bouwt een instantie op door de + parent-tabellen langs de keten te joinen. Maar met 34 meervoudig ervende metaclasses ís er + geen keten — een `FlowUsage`-read zou langs *twee* takken van een overervingsruit moeten + joinen. Het kan, maar dan moet elke querygenerator de DAG begrijpen. +2. **De machinerie staat niet in verhouding tot de inhoud.** 167 + ~230 tabellen om welgeteld + 97 property-declaraties te herbergen. Het overgrote deel van die tabellen zou niets anders + bevatten dan een `iid`-kolom — de meeste metaclasses declareren immers geen eigen stored + properties; ze bestaan om hun derived semantiek. En de diepste reads worden joins over elf + tabellen. +3. **De COMET-vorm gaat uit van één versie per element.** Zijn FK's wijzen naar elementrijen + en zijn `revisionNumber` is een oplopende integer — allebei onverenigbaar met een + commit-DAG en version-onafhankelijke references (axioma's 1 en 2). Geen verwijt richting + COMET, overigens: dat domein hééft lineaire revisies en verwijs-naar-actueel-semantiek. + Dit domein niet. + +### 5.3 Zuivere generieke EAV + +*Twee tabellen: `element_version(…, value_data jsonb)` en +`element_reference(version_id, property_id, ordinal, target_identity)`.* + +Het snelst te genereren, en die ene reference-tabel is oprecht aantrekkelijk voor +graaftraversal: één index beantwoordt elke "wie verwijst er naar X?"-vraag. Toch afgevallen, +omdat EAV het typesysteem platslaat tot data. Geen FK-semantiek per property, geen NOT +NULL- of enum-afdwinging per property, geen kolomstatistieken voor de planner (elke +property-lookup krijgt dezelfde generieke selectiviteit), en garanties als "`isParallel` is +een boolean" worden een kwestie van discipline in de applicatie. Het gekozen ontwerp houdt +wél een *smal* EAV-achtig randje waar dat verdedigbaar is (de 7 link-tabellen, de property +catalog), maar geeft de getypeerde kolommen voor de scalaire kern niet op. + +### 5.4 Document store (alleen jsonb) + +*Sla elke element version op als één jsonb-document en indexeer met GIN.* + +Voor het lezen is dit werkelijk prachtig — sterker nog, het gekozen ontwerp *bevat* deze +aanpak als zijn read path (`stored_json`/`derived_json`). Als *system of record* valt hij +echter af: referential integrity, getypeerde constraints, omgekeerde reference-indexen en +statistieken per kolom verdampen allemaal, en elke integriteitsgarantie van het model +verhuist naar applicatiecode. De les die hieruit is getrokken: **normaliseer om te schrijven +en te bewaken, denormaliseer om te lezen** — en houd allebei bij, in dezelfde rijen, binnen +dezelfde transactie. + +### 5.5 Wat het is geworden + +**Een element-kern + sparse subtype-tabellen + getypeerde link-tabellen + een tweede stream +voor derived state:** + +- één `element_version`-kerntabel met de identity/commit-administratie plus de 7 eigen stored + properties van `Element` (elk element heeft ze, dus apart zetten zou alleen een join voor + niets opleveren); +- **47 subtype-tabellen**, één per metaclass die zelf stored scalaire properties *declareert*, + gesleuteld op `(project_id, version_id)`. Een instantie heeft rijen in precies de + subtype-tabellen van haar storage-declarerende voorouders — een *verzameling* tabellen, geen + keten: zo wordt de DAG opgelost via lidmaatschap in plaats van joins; +- **7 link-tabellen** voor de 6 multi-valued reference properties plus `aliasIds`, allemaal + geordend (`ordinal` zit in de PK — elk van deze properties is `isOrdered` in het metamodel); +- `derived_version` als tweede stream (axioma 2); +- `stored_json` op de version-rij en `derived_json` op de derived-rij als bewuste, + transactioneel consistente lees-denormalisatie. + +Dat het er 47 zijn is overigens geen ontwerpkeuze — het rolt uit de census: 49 +storage-declarerende metaclasses, min `Element` (opgenomen in de kerntabel), min `Dependency` +(waarvan de enige stored properties de twee multi-valued zijn, en die worden link-tabellen). + +--- + +## 6. Laag A — het PIM: projects, commits, branches, tags (§3) + +### 6.1 De commit-DAG + +Eerst de term zelf. **DAG** staat voor *Directed Acyclic Graph*: een gerichte graaf zonder +cycli. Het is de vorm die een commit-historie vanzelf aanneemt zodra je branches en merges +toelaat. Zonder branches zou de historie een simpele **keten** zijn — elke commit precies één +parent: + +``` +c1 ← c2 ← c3 ← c4 (keten: lineaire historie) +``` + +Branches laten de historie *splitsen* (twee commits met dezelfde parent), en merges laten +haar weer *samenkomen* (één commit met **twee of meer parents**): + +``` + c2 ← c3 (branch "main") + ↙ ↖ +c1 c5 (merge: c5 heeft TWEE parents, c3 en c4) + ↖ ↙ + c4 (feature-branch) +``` + +*Directed*: elke pijl wijst van kind naar parent ("c5 is voortgekomen uit c3 en c4"). +*Acyclic*: wie de parent-pijlen volgt, komt nooit terug waar hij begon — een commit kan niet +zijn eigen voorouder zijn, want de parent bestond al toen het kind werd gemaakt. De vraag +"hoe zag het model eruit op c5?" beantwoord je door deze graaf vanaf c5 *terug* te bewandelen +(de recursieve `ancestry`-CTE van §9) en per element de nieuwste onderweg gevonden version te +nemen. Dat is precies hoe Git het doet — en de OMG-spec heeft dat model bewust overgenomen. + +```sql +CREATE TABLE sysml2.commit ( + id uuid NOT NULL, + project_id uuid NOT NULL REFERENCES sysml2.project (id) ON DELETE CASCADE, + created timestamptz NOT NULL DEFAULT now(), + description text NULL, + model_version_id smallint NOT NULL REFERENCES sysml2.model_version (id), -- zie 6.4 + PRIMARY KEY (id) +); + +CREATE TABLE sysml2.commit_parent ( + commit_id uuid NOT NULL REFERENCES sysml2.commit (id) ON DELETE CASCADE, + parent_commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + ordinal smallint NOT NULL, + PRIMARY KEY (commit_id, parent_commit_id) +); +``` + +De specificatie zegt onomwonden dat `Commit.previousCommit` een **verzameling** is — een +merge commit heeft twee of meer parents. Vandaar de aparte `commit_parent`-edge-tabel in +plaats van één `previous_commit_id`-kolom. `ordinal` bewaart de volgorde van de parents +("first parent" doet ertoe voor merge-semantiek, net als bij Git). De PK van de commit is de +kale uuid, omdat er van overal naar commits wordt verwezen (branches, versions, checkpoints) +en het project-verband al via `project_id` loopt. + +Twee invarianten uit de spec moet je echt paraat hebben, want de *resolvers steunen erop*: + +**Immutability.** *"Commits are immutable… Commits are not destructible"* (Clause 7.1.2). Het +schema neemt dat letterlijk: geen enkele bewerking doet ooit een UPDATE op een commit of een +`element_version`-rij. Append-only is hier geen optimalisatie, het is gewoon de semantiek van +de specificatie. + +**Monotonie.** *"Version histories must monotonically increase in time: for Commit C, the +value of C.created must be strictly newer than the value of D.created for any commit D in +C.previousCommit."* Het schema *dwingt* dit af, met een trigger: + +```sql +CREATE TRIGGER trg_commit_parent_monotonic + AFTER INSERT ON sysml2.commit_parent + FOR EACH ROW + EXECUTE FUNCTION sysml2.assert_commit_monotonic(); +``` + +Waarom afdwingen, en niet gewoon vertrouwen op de service? Omdat de snapshot resolver +(sectie 10) per element de version van de **nieuwste ancestor-commit** kiest — nieuwste +volgens `created`. Glipt er ooit een commit tussendoor met een timestamp ouder dan zijn +parent, dan geeft de resolver zonder ook maar één foutmelding het *verkeerde* snapshot terug. +Bugklassen die stilletjes verkeerde antwoorden geven, verdienen een trigger; bugklassen die +lawaai maken mag je aan de service overlaten. (Smoke-assertion PASS 6 bewijst dat de trigger +echt afgaat.) + +En let op wat monotonie *niet* regelt: de volgorde tussen **siblings**. Twee commits op +parallelle branches mogen gerust dezelfde timestamp hebben. Hoe daarmee wordt omgegaan lees +je in sectie 10.4. + +**De vier merge-invarianten op een rij.** Een *invariant* is een regel die onder alle +omstandigheden waar moet zijn — en deze vier (Clause 7.1.2) zijn geen formaliteiten: de +correctheid van de snapshot resolver hangt er rechtstreeks van af. Hier alle vier bij elkaar, +met de plek waar elk wordt geregeld: + +1. **Monotonie** — een commit is strikt nieuwer dan elk van zijn parents, langs elk pad. Dít + maakt "nieuwste ancestor wint" tot een deugdelijke resolutieregel. In het schema + afgedwongen door `trg_commit_parent_monotonic` (smoke PASS 6), omdat een schending + geruisloos verkeerde snapshots zou opleveren in plaats van fouten. +2. **Conflict-restatement** — een merge moet de oplossing van elk conflict in zijn EIGEN + change set herhalen. In combinatie met invariant 1 is de merge de nieuwste commit in zijn + eigen ancestry, dus zijn herformulering wint vanzelf van beide parents (smoke PASS 8a). + Maar monotonie ordent de *siblings* onderling niet — en dus valt een merge die de + restatement ten onrechte overslaat terug op de deterministische `id DESC`-tiebreaker + (§10.4, audit R13, smoke PASS 10a/10b). Het herformuleren zelf is een plicht van de + service-laag (§15, punt 7). +3. **Een deletie moet iets te verwijderen hebben** — een tombstone (`DataVersion` met null + payload) is alleen geldig als minstens één parent dat element nog levend in zijn snapshot + had. Validatie in de service-laag; het schema slaat de tombstone hoe dan ook op. +4. **Eén version per element per commit** — `DataVersion.identity` is uniek binnen + `Commit.change`. Afgedwongen door `ux_element_version_identity_commit` (§8.1). + +Aan deze vier spec-invarianten voegt dit schema er zelf een vijfde toe, voor multi-version +support (sectie 6.4): **release-compatibiliteit** — een commit zit nooit in een oudere +metamodel-release dan een parent, en een merge eist dat alle parents in de release van de +merge zelf zitten. Afgedwongen door `trg_commit_parent_version` (smoke PASS 11c–11e). + +In één zin: de commit-DAG is de *vorm* van de historie (splitsen en weer samenvloeien), en de +merge-invarianten zijn de *spelregels* die garanderen dat het teruglezen van die historie — +de fold van §9 — precies één welbepaald, deterministisch antwoord heeft. + +### 6.2 Hoe delta's snapshots worden — het algoritme van de spec zelf + +De specificatie definieert `Commit.change` (de delta: de DataVersions die deze commit +schreef) als stored, en `Commit.versionedData` (het volledige model-snapshot op deze commit) +als **derived** — met een OCL-algoritme dat de moeite van het lezen waard is, want de +resolver in het schema is er de rechtstreekse vertaling van: + +``` +let updatedNotDeleted = change->select(payload <> null) in +let updatedIdentities = change.identity in +let retainedWithDuplicates = + previousCommits.versionedData->select(oldData | + updatedIdentities->excludes(oldData.identity)) in +let retained = in +versionedData = updatedNotDeleted->union(retained) +``` + +In gewone taal: het snapshot van een commit bestaat uit *zijn eigen wijzigingen, aangevuld +met alles uit de snapshots van zijn parents dat hij niet zelf heeft overschreven*. De +recursie over `previousCommit` eindigt bij de root. Een deletie is een DataVersion met een +lege `payload` — in het schema terug te zien als `tombstone = true` op de version-rij. + +Het algoritme is correct, maar per leesactie uitvoeren is op schaal volstrekt kansloos: het +is een fold over de complete commit-historie. Heel §9 (sectie 10 van deze gids) bestaat om +dat betaalbaar te maken. + +### 6.3 Branches en tags + +```sql +CREATE TABLE sysml2.branch ( + id uuid NOT NULL, + project_id uuid NOT NULL REFERENCES sysml2.project (id) ON DELETE CASCADE, + name text NULL, + description text NULL, + head_commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + base_commit_id uuid NULL REFERENCES sysml2.commit (id), -- zie sectie 10.2 + created timestamptz NOT NULL DEFAULT now(), + deleted timestamptz NULL, + PRIMARY KEY (id), + UNIQUE (project_id, name) +); +``` + +De mutability-tabel van de spec is helder: branches zijn muteerbaar en verwijderbaar — het +*enige* muteerbare in de hele versioneringskern, want `head_commit_id` schuift bij elke +commit op. Tags zijn immutable maar verwijderbaar; commits geen van beide. `deleted` is een +nullable timestamp en geen harde delete, omdat de spec het verwijderen van een +CommitReference als een vastgelegde gebeurtenis behandelt. + +`base_commit_id` is puur een performancestructuur en komt niet uit de spec — hij verankert de +branch-head-*overlay*; sectie 10.2 legt hem volledig uit. + +Verder in deze laag: `tag` (zelfde vorm als branch, maar bevroren), `project_usage` +(cross-project-imports: "project A gebruikt project B op commit C", waarbij de +spec-constraint `usedProject = usedProjectCommit.owningProject` bij de service ligt), en +`project` zelf. De `default_branch_id`-FK van project wordt pas ná de branch-tabel toegevoegd +en is `DEFERRABLE INITIALLY DEFERRED`: project en default branch ontstaan in één transactie, +en de circulaire FK (project → branch → project) kan pas op het commit-moment kloppen. + +### 6.4 Model-version stamping — meerdere metamodel-releases in één database + +Het OMG-metamodel heeft zelf releases (nu Beta 4; latere releases voegen metaclasses toe, +laten ze vervallen en veranderen hun vorm). Dit schema ondersteunt **meerdere releases naast +elkaar in één database**, en het ontwerp volgt uit één observatie: in een append-only store +zitten historische commits onveranderlijk in de release waarin ze zijn geschreven. Welk label +een project of branch ook draagt — wie een oude commit leest, moet de release van *die +commit* kennen om zijn payloads te begrijpen. De stempel per commit is daarom de enige +correcte korrel; al het andere is ervan afgeleid: + +- **`model_version`** (§2) registreert elke release waarvoor deze database ooit data heeft + opgeslagen. Het id is een *ordinal* — hoger is later — dat éénmalig wordt uitgedeeld door + het ingecheckte register + (`SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/ClassKindRegistry.cs`) en + nooit hernummerd wordt. +- **`commit.model_version_id NOT NULL`** is de waarheid: de release waarin de payloads van + deze commit zijn geschreven. Een *branch* "is" simpelweg de release van zijn head-commit; + er staat geen muteerbaar versieveld op `branch` dat met de historie kan gaan liegen. +- **`project.target_model_version_id`** is beleid, geen waarheid: de hoogste release waarin + nieuwe commits geschreven mogen worden (NULL = onbeperkt). Een beheerder verhoogt hem om + branches *toe te staan* te upgraden; de stempel legt vast wat elke branch werkelijk deed. + +**Upgraden is een commit, geen migratie.** Een branch gaat naar een nieuwere release via een +**conversion commit**: een commit met één parent die de stempel ophoogt en elk element +herformuleert waarvan de vorm tussen de twee releases veranderde (de versie-diff is een +variant van de impact radius — de machinerie van +`SysML2.NET.CodeGenerator/IMPACT-RADIUS.md` is herbruikbaar). De service moet op elke +conversion commit een `commit_checkpoint` afdwingen, zodat folds vrijwel nooit een +releasegrens over hoeven. Elementen waarvan de vorm niet veranderde worden *niet* +geherformuleerd — hun oude rijen blijven onder de nieuwe release gewoon geldig, en precies +daardoor is conversie O(veranderde vormen) in plaats van O(model). + +**Het fysieke schema is de superset over alle geregistreerde releases.** Nieuwe metaclasses +worden nieuwe subtype-tabellen; nieuwe properties worden nullable kolommen; een hernoemde of +verplaatste property wordt een *nieuwe* kolom naast de oude (de conversion commit verhuist de +data; de oude kolom blijft oude commits bedienen). Er wordt nooit iets verwijderd. Welke +tabellen en properties in welke release geldig zijn, staat NIET in databasetabellen — dat +reist mee als statische, per-release gegenereerde C# (de model-version *descriptors*, +sectie 12.2). + +Drie invarianten houden een historie met gemengde releases gezond, afgedwongen door +`trg_commit_parent_version` (smoke PASS 11b–11e): geen commit zit in een oudere release dan +een parent (downgrades worden niet ondersteund — conversie is achterstevoren lossy); een +commit met één parent mag de release ophogen (dat IS de conversion commit); en een merge eist +**alle parents in de release van de merge zelf** — eerst converteren, dan mergen, nooit +allebei in één commit. Zonder die laatste regel zou een merge geruisloos payload-vormen +mengen. + +--- + +## 7. Identity: `data_identity` en de filosofie achter referential integrity (§4) + +```sql +CREATE TABLE sysml2.data_identity ( + id uuid NOT NULL, + project_id uuid NOT NULL REFERENCES sysml2.project (id), + class_kind smallint NOT NULL REFERENCES sysml2.class_kind (id), -- GETYPEERDE identity + PRIMARY KEY (id), + UNIQUE (id, class_kind) +); +``` + +Drie kolommen, meer niet — en toch is dit minitabelletje het anker van axioma 1. Elke +elementreference in het hele schema wijst hiernaartoe: de ~30 single-reference-kolommen op de +subtype-tabellen, de 5 reference-link-tabellen, `element_version.owning_relationship`, +`branch_head.identity_id`, allemaal een foreign key naar `data_identity(id)`. + +**De identity is GETYPEERD.** De metaclass van een element verandert nooit over zijn versies +heen — een identity wordt geboren als PartUsage en blijft dat — dus anders dan al het andere +aan een element is het type een eigenschap van de *identity*, en daarmee FK-baar. Twee +afnemers: + +- `element_version` draagt een composite FK `(identity_id, class_kind)` → + `data_identity (id, class_kind)`, waardoor een version die een andere metaclass claimt dan + zijn identity **onmogelijk** is (smoke PASS 12a); +- de gegenereerde functie `validate_references_at_commit()` (hieronder) controleert het type + van elke stored reference tegen deze kolom — óók cross-project-doelen, want identities zijn + getypeerd ongeacht in welk project ze wonen. + +Daar hoort één onderhoudsregel bij: een release-conversie (§6.4) die een element hertypeert — +zijn metaclass is vervallen — moet `data_identity.class_kind` in dezelfde transactie +bijwerken (verplichting §15.16). + +**Wat FK's niet kunnen controleren, is nu ON DEMAND controleerbaar — in twee lagen.** FK's +bewijzen dat een reference een *bestaande* identity raakt; ze kunnen nooit bewijzen dat het +doelwit *leeft op de gelezen commit* (liveness is een functie van (identity, commit) — er is +geen rij om tegen te FK'en), noch dat zijn metaclass legaal is voor de verwijzende property +(een FK matcht waarden, geen typesets). Beide gaten dekken de gegenereerde functies (§14 van +de schemabestanden), één `UNION ALL`-arm per stored referencekolom (42 stuks), die +`'wrong-type'` rapporteren (via de getypeerde identity, ook voor cross-project-doelen) en +`'dangling'` (een same-project-doel dat niet in het snapshot van de commit zit): + +- **`validate_references_at_commit`** — de VOLLEDIGE periodieke audit over het complete + snapshot van één commit. Hij materialiseert het snapshot eerst in een ge-ANALYZE'de, + geïndexeerde temp table, zodat de planner de echte kardinaliteit kent en bij diepe + histories kan overschakelen op snapshot-gedreven PK-probes — de pass is begrensd op + O(snapshot × log historie), nooit O(historie), hoe groot de append-only tabellen ook + worden. Gemeten: 2,5–4,3 s op een 1M-snapshot (smoke PASS 12b/12c). +- **`validate_references_in_commit`** — de INCREMENTELE laag per commit, O(change set): de + uitgaande references van de versions die de commit zelf schreef, PLUS de omgekeerde + richting die zijn tombstones breken — een levend, *ongewijzigd* element dat naar een + verwijderde identity blijft wijzen, precies het geval dat naïeve changeset-validatie mist + (gedreven door de reverse-lookup-indexen, liveness per doelwit via + `resolve_element_at_commit`). Gemeten: 77–86 ms voor een changeset van 101 rijen tegen een + 1M-project — geschikt voor het synchrone commit-validatiepad (smoke PASS 13a–13c). + +Bewust *functies*, geen constraints: de spec staat tijdelijk hangende references toe, en de +liveness van cross-project-doelen hangt af van de used-project-commit (`project_usage`) — +dat is service-resolutie. Het werkprotocol (verplichting §15.6): de incrementele laag bij +elke commit, de volledige audit periodiek als vangnet. + +Drie keuzes méér zijn hier heel bewust gemaakt: + +**De PK is de kale uuid, niet `(project_id, id)`.** Via `ProjectUsage` mag een element in +project A verwijzen naar een element in project B. Met een samengestelde PK zou zo'n +cross-project-reference niet meer als FK kunnen bestaan. Het bewaken van projectgrenzen is +daarom een taak van de service (via `project_usage`), geen FK. De keerzijde: `data_identity` +kan niet, zoals de rest, per project worden gepartitioneerd. De audit (sectie 14, bevinding +R12) heeft nagerekend of dat bij 10⁸ rijen pijn doet — en dat doet het niet: twee +uuid-kolommen geven een heap van ~7 GB met een btree waarvan de bovenste niveaus gewoon in +het geheugen blijven; elke probe kost 3–4 gecachte page reads. Een read-mostly FK-doel schaalt +prima zonder partitionering. + +**`element_id` (de KerML-property) is `text`, geen `uuid`.** Let op het verschil: het `@id` +van de *API* is een UUID en hoort bij `data_identity.id`. Maar KerML declareert +`Element::elementId` als `String`, en alleen elementen uit de standaard-*library* moeten +normatief een name-based (v5) UUID hebben — voor gebruikersmodellen geldt helemaal geen +formaateis. Een `uuid`-kolom zou dus data weigeren die volgens de spec gewoon geldig is. +Daarom is `element_version.element_id` van het type `text`, en beheert de API-laag de uuid in +de identity-rij. + +**Verwijderen gebeurt expliciet, nooit via cascades.** In het oorspronkelijke ontwerp stonden +de identity-FK's op `ON DELETE CASCADE` — project weg, alles weg. De performance-audit heeft +daar een streep door gezet, om een mechanische reden die je makkelijk onthoudt: **een cascade +voert per rij een delete uit die alléén op de FK-kolom filtert** — `DELETE FROM +element_version WHERE identity_id = $1` — en er is in dit schema *geen enkele index die met +een kale identity-kolom begint* (alles begint met `project_id`, voor partitielokaliteit). +Elke gecascadeerde identity zou dus een sequential scan over de grootste tabellen betekenen — +maal een miljoen bij het verwijderen van een project. In plaats daarvan staat er nu een +gedocumenteerde procedure bij de `data_identity`-DDL: projectverwijdering is een *geordende, +gebatchte* reeks `DELETE … WHERE project_id = $1`-statements per tabel (elk pruned naar één +partitie en gebruikt een PK-prefix), afgesloten met `data_identity` en `project`. De +overgebleven `NO ACTION`-FK's zijn het vangnet: wie in de verkeerde volgorde verwijdert, +loopt luidkeels tegen een fout aan in plaats van stilletjes tegen een tablescan. Die ruil — +expliciete procedure plus luide bewaking, in plaats van gemak plus rampspoed — is meteen de +FK-filosofie van het hele schema. + +--- + +## 8. Laag B — stored element state (§5, §6, §7) + +### 8.1 De kern: `element_version` (§5) + +```sql +CREATE TABLE sysml2.element_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, -- DataVersion.id uit de spec + identity_id uuid NOT NULL, -- composite typed-identity-FK hieronder (§7) + commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + class_kind smallint NOT NULL REFERENCES sysml2.class_kind (id), + tombstone boolean NOT NULL DEFAULT false, + + -- de eigen stored properties van Element, hier opgenomen: + element_id text NULL, + declared_name text NULL, + declared_short_name text NULL, + is_implied_included boolean NULL, + owning_relationship uuid NULL REFERENCES sysml2.data_identity (id), + + stored_json jsonb NULL, + + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (identity_id, class_kind) + REFERENCES sysml2.data_identity (id, class_kind), -- typed identity (§7) + CONSTRAINT element_version_tombstone_empty + CHECK (NOT tombstone OR (stored_json IS NULL AND element_id IS NULL)), + CONSTRAINT element_version_payload_present + CHECK (tombstone OR (stored_json IS NOT NULL AND element_id IS NOT NULL + AND is_implied_included IS NOT NULL)) +) PARTITION BY HASH (project_id); + +CREATE UNIQUE INDEX ux_element_version_identity_commit + ON sysml2.element_version (project_id, identity_id, commit_id); +``` + +De ontwerpkeuzes, kolom voor kolom: + +- **`project_id` staat voorop in elke sleutel.** Alle elementtabellen zijn met dezelfde + modulus op `project_id` gehash-partitioneerd (sectie 13), en hun PK's beginnen ermee. + Daardoor blijft elke onderlinge join *binnen één partitie*, en kan elke projectgebonden + query al bij het plannen naar één partitie prunen. Daar hoort wel discipline bij: + **filter deze tabellen nooit op een kale uuid zonder `project_id` erbij.** Hoe belangrijk + dat is, bleek pas echt tijdens de audit — zie sectie 14, bevinding R2. +- **`version_id` is de eigen identiteit van de rij** (het `DataVersion.id` uit de spec). Hij + wordt aan de applicatiekant gegenereerd, en de audit adviseert UUIDv7 (tijdgeordend), zodat + de inserts van een project netjes rechts in de btree aanhaken in plaats van kriskras door + de index te spatten (bevinding R8). +- **`class_kind` is een `smallint`, geen naam.** De 175 metaclass-namen zijn geïnterneerd in + de `class_kind`-catalogus (sectie 12). Op de heetste en grootste tabel van de database + scheelt dat 2 bytes tegenover ~15 bytes tekst — maal honderden miljoenen rijen, maal het + aantal indexen waar de kolom in zit. +- **`tombstone` markeert een deletie** — de directe vertaling van "een DataVersion met een + null payload is een deletie" uit de spec. Deleties zijn hier *rijen*, want in een + append-only commit store is een deletie een gebeurtenis in de historie, niet het ontbreken + van data. De twee CHECK-constraints houden tombstones en payload-rijen strikt gescheiden: + een tombstone moet leeg zijn, een gewone rij compleet. Goedkopere verzekering tegen een + service die halve rijen schrijft bestaat er niet. +- **De vijf Element-kolommen zitten in de kerntabel**, niet in een aparte + Element-subtype-tabel. Simpele reden: *elk* element heeft ze (het zijn Elements eigen + declaraties, en alles is een Element), dus een aparte tabel zou elke read een join kosten + zonder ook maar iets aan opslag te winnen. Bovendien is `declared_name` de stored kolom + waar het meest op wordt gefilterd — op de kerntabel kan het queryplan die join helemaal + overslaan. +- **`stored_json` is de denormalisatie voor het lezen** — de stored helft van het element, + alvast geserialiseerd in exact de JSON-vorm van de API. De genormaliseerde kolommen en + link-tabellen (met alle FK's en constraints) blijven het system of record; `stored_json` + bestaat zodat het serveren van een element nooit een reconstructie uit zes subtype- en + drie link-tabellen vergt. Hij wordt in dezelfde transactie geschreven als de + genormaliseerde rijen, dus uit de pas lopen kan niet. De prijs: `element_version` wordt er + grofweg twee keer zo groot van (lz4 dempt dat — sectie 13). PASS 4 van de smoke-test + controleert dat het samengevoegde read path een complete payload oplevert. +- **`ux_element_version_identity_commit`** dwingt de spec-invariant af — *"DataVersion.identity + is unique among records listed in Commit.change"*, oftewel één version per element per + commit — en fungeert meteen als de index achter "geef me de rij van element X op commit C", + waar de single-element-resolver zwaar op leunt. + +### 8.2 De link-tabellen (§6) + +De census vond in het hele metamodel welgeteld zes multi-valued stored reference properties, +plus één multi-valued string. Elk krijgt een eigen tabel, allemaal geordend: + +```sql +CREATE TABLE sysml2.element_owned_relationship ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + ordinal int NOT NULL, + target_identity uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id, ordinal), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_element_owned_relationship_target + ON sysml2.element_owned_relationship (project_id, target_identity); +``` + +(idem voor `relationship_owned_related_element`, `relationship_source`, +`relationship_target`, `dependency_client`, `dependency_supplier`, en `element_alias_ids` — +die laatste met een `text`-waardekolom in plaats van een reference.) + +- `ordinal` zit in de PK omdat elk van deze properties in het metamodel `isOrdered = true` + is — de volgorde is modelinhoud, geen toevalligheid. +- De rijen hangen aan een **version**, niet aan een identity: de collectie hoort bij de + stored state van het element, dus een nieuwe version brengt zijn eigen collectierijen mee. + (Dit is meteen de enige plek waar de audit echte write amplification aantrof: een nieuwe + version van een package met 100k kinderen schrijft die 100k rijen opnieuw, ook als alleen + de naam is veranderd. Bevinding R7 in sectie 14 beschrijft de content-addressed oplossing — + ontworpen, maar bewust in de la gelegd tot benchmarks erom vragen.) +- De reverse-lookup-index op `target_identity` beantwoordt "wie verwijst er naar element X?" + — de bouwsteen onder omgekeerde navigatie, het opsporen van dangling references en de + impactanalyse van derived state. +- De FK terug naar `element_version` is *samengesteld* op `(project_id, version_id)`, en dit + is de ene cascade die in deze laag mocht blijven: verwijdert de expliciete + projectverwijderprocedure een version-rij, dan gaan de collectierijen vanzelf mee — en + omdat de samengestelde FK aan beide kanten een PK-prefix is, loopt die cascade netjes via + de index. + +### 8.3 De subtype-tabellen (§7) + +Eén tabel per storage-declarerende metaclass — 47 in totaal, allemaal volgens hetzelfde +stramien: + +```sql +CREATE TABLE sysml2.feature_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + direction sysml2.feature_direction_kind NULL, + is_composite boolean NOT NULL DEFAULT false, + is_constant boolean NOT NULL DEFAULT false, + is_derived boolean NOT NULL DEFAULT false, + is_end boolean NOT NULL DEFAULT false, + is_ordered boolean NOT NULL DEFAULT false, + is_portion boolean NOT NULL DEFAULT false, + is_unique boolean NOT NULL DEFAULT true, + is_variable boolean NOT NULL DEFAULT false, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); +``` + +**Over het `_version`-achtervoegsel:** deze tabellen bevatten het *version-gebonden* deel van +een metaclass — `feature_version` draagt de door Feature gedeclareerde kolommen van één +element**version**, gesleuteld op `(project_id, version_id)` en opgehangen aan +`element_version`. Het achtervoegsel sluit bewust aan bij `element_version` en +`derived_version`: samen lezen de drie namen als één familie van per-version-state. (De +naamgevingshistorie: een eerdere versie gebruikte voor deze tabellen het kortere `_v`, terwijl +de views van §12.3 een `v_`-voorvoegsel droegen — `v_part_usage` naast `part_usage_v`, +dezelfde letter met twee betekenissen, vragen om ongelukken. Allebei hernoemd: de tabellen +naar `_version`, de views naar `vw_` — zie §12.3.) + +Hoe een instantie over de tabellen verdeeld raakt: een `PartUsage`-version heeft rijen in +`element_version` + `type_version` + `feature_version` + `usage_version` + `occurrence_usage_version` — precies de +subtype-tabellen van haar storage-declarerende voorouders. Een `FlowUsage`-version zit in +**zes** tabellen, waaronder *zowel* `feature_version` als `relationship_version`, want een `Connector` is +nu eenmaal Feature en Relationship tegelijk. Zo wordt de overervings-DAG gerepresenteerd: +als **lidmaatschap van een verzameling tabellen**, niet als een keten van joins. Welke +verzameling dat per metaclass is, staat kant-en-klaar in de per-release gegenereerde +descriptors (sectie 12.2) — generieke code hoeft het nooit zelf uit te puzzelen. + +Een paar details die de bedoeling verraden: + +- **NOT NULL volgt de lower bounds van het metamodel.** Een `[1..1]`-property is NOT NULL, + een `[0..1]`-property (`direction`, `member_name`, `portion_kind`) mag NULL zijn. Dat kan + alleen maar eerlijk, *omdat* een tabel uitsluitend rijen bevat voor instanties waarvan de + class de property echt declareert — het sparse ontwerp maakt oprechte NOT NULL's überhaupt + mogelijk (vergelijk de God table, waar noodgedwongen alles nullable is). +- **De DEFAULT's komen uit de XMI.** De generator zet een `DEFAULT` neer voor elke property + die er in de UML-declaratie één heeft: de Feature-booleans default false (`is_unique` + true), `Membership::visibility DEFAULT 'public'`, en — makkelijk fout te gokken — + `Import::visibility DEFAULT 'private'`: imports zijn in KerML standaard privé, anders dan + memberships, en een top-level import *moet* zelfs privé zijn. Deze defaults zijn tijdens de + review nagelopen tegen de metamodel-kennisbank. +- **Reference-kolommen FK'en naar `data_identity`** (axioma 1), en elk krijgt een + reverse-lookup-index `ix_{tabel}_{kolom}`. Twee daarvan verdienen het om even uit te + lichten: `ix_specialization_version_general` en `ix_specialization_version_specific` indexeren de + *specialization-graaf* — de edges waar derived properties als `Type::feature` overheen + folden. Als de service straks de impact radius van "een supertype kreeg er een feature bij" + moet bepalen, zijn déze twee indexen wat "vind alle transitieve specializations" betaalbaar + maakt. +- **De vier `kind`-tabellen en de vier `literal_*`-tabellen** zijn de typeconflicten uit de + census in het echt: `requirement_constraint_membership_version.kind` is een + `sysml2.requirement_constraint_kind`, terwijl `state_subaction_membership_version.kind` een + `sysml2.state_subaction_kind` is; `literal_boolean_version.value` is `boolean`, maar + `literal_rational_version.value` is `double precision`. In één brede tabel past dit gewoon niet. +- **Redefinitions hebben geen kolommen.** `CollectExpression` heeft zelfs helemaal geen + subtype-tabel: zijn enige stored property is de same-name redefinition van `operator`, en + die woont in `operator_expression_version` — de tabel van de voorouder. De property catalog legt + die verwijzing vast, zodat querycode er niets van hoeft te weten. + +### 8.4 De enum-typen (§1) + +```sql +CREATE TYPE sysml2.visibility_kind AS ENUM ('private', 'protected', 'public'); +``` + +Zeven native enum-typen, één per metamodel-enumeratie. Native enums (in plaats van text + +CHECK) kosten 4 bytes, valideren bij het schrijven en sorteren in declaratievolgorde. Dat de +**labels in kleine letters** staan is geen stijlkeuze: ze komen byte-voor-byte overeen met het +JSON-wire-formaat — de gegenereerde serializers schrijven +`Direction.Value.ToString().ToLower()` (zie +`SysML2.NET.Serializer.Json/Core/AutoGenSerializer/FeatureSerializer.cs`). Een waarde kan +daardoor van API-payload naar enum-kolom en weer terug zonder dat er ergens een laag +hoofdletters hoeft om te zetten. + +--- + +## 9. Laag C — derived element state (§8) + +```sql +CREATE TABLE sysml2.derived_version ( + project_id uuid NOT NULL, + derived_id uuid NOT NULL, + identity_id uuid NOT NULL REFERENCES sysml2.data_identity (id), + commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + + -- promoted hot derived properties (gedeclareerd door Element + -- => aanwezig op alle 167 metaclasses) + owner uuid NULL REFERENCES sysml2.data_identity (id), + owning_namespace uuid NULL REFERENCES sysml2.data_identity (id), + qualified_name text NULL, + name text NULL, + short_name text NULL, + is_library_element boolean NOT NULL DEFAULT false, + + derived_json jsonb NOT NULL, -- de rest: ~325 verschillende derived namen + + PRIMARY KEY (project_id, derived_id) +) PARTITION BY HASH (project_id); + +CREATE UNIQUE INDEX ux_derived_version_identity_commit + ON sysml2.derived_version (project_id, identity_id, commit_id); +CREATE INDEX ix_derived_version_owner ON sysml2.derived_version (project_id, owner); +CREATE INDEX ix_derived_version_qualified_name ON sysml2.derived_version (project_id, qualified_name); +CREATE INDEX ix_derived_version_json + ON sysml2.derived_version USING gin (derived_json jsonb_path_ops); +``` + +### 9.1 Waarom eigenlijk precompute op het commit-moment? + +Voor full derived-property conformance lagen er drie strategieën op tafel: + +**Compute-on-read.** Geen schrijfkosten, geen invalidatielogica — klinkt aanlokkelijk. Maar +dan betaalt elke element-read de recursieve wandelingen (`qualifiedName` = ownership chain, +`feature` = specialization closure), betaalt elke *collectie*-read ze per element, en — dat +geeft de doorslag — moet de Query service kunnen filteren en sorteren op derived properties +(`WHERE qualifiedName LIKE 'Vehicle::%' ORDER BY name`). Dat betekent recursieve CTE's +evalueren per kandidaatrij, of alsnog per property gaan materialiseren. Bij een +read-gedomineerde workload (en dat is deze, volgens het profiel) betaal je de rekensom dan +telkens opnieuw op het heetste pad, voor waarden die zelden veranderen. + +**Passthrough.** Gewoon opslaan wat de client aan derived values meestuurt. Verreweg het +goedkoopst om te bouwen (de DTO-laag round-tript derived values nu al). Maar als *einddoel* +afgewezen: derived values worden dan data die je van de client moet geloven, en die +geruisloos van het model kan gaan afwijken. Prima als tussenstation, corrosief als +architectuur. + +**Precompute bij de commit.** De schrijfkant betaalt — voor het uitrekenen van de impact +radius van de change set; de leeskant betaalt niets; query's filteren op echte kolommen. De +rekenmachine bestaat al: de 366 geïmplementeerde `Compute*`-methoden in `SysML2.NET/Extend/` +weten precies hoe elke OCL-derivatie tegen een in-memory-model moet worden geëvalueerd. De +service hoeft ze op het commit-moment alleen voor de geraakte elementen aan te roepen en de +uitkomsten hier weg te schrijven. Met een read-gedomineerd profiel en de query-eis erbij is +dit de enige strategie waarbij het dure werk één keer gebeurt, en dan nog buiten het hete pad +ook. + +Wel eerlijk blijven over de prijs: **in het slechtste geval is de impact radius onbegrensd.** +Hernoem een namespace vlak onder de root en de `qualifiedName` van vrijwel het hele model is +ongeldig → ~1M `derived_version`-rijen in één commit. Dat ligt niet aan dit ontwerp maar aan +de semantiek van de spec zelf (die derived values zijn ook echt allemaal veranderd); het +schema kan alleen zorgen dat zo'n bulk write te overleven valt (lz4-compressie, +GIN-pending-list-tuning, een append-only vorm die zich goed asynchroon laat verwerken). De +audit behandelt hem dan ook als bulkoperatie (sectie 14, bevinding R5). + +### 9.2 De sleutel: waarom (identity, commit), en waarom sparse + +De sleutel is axioma 2 in tabelvorm: er worden **alleen `derived_version`-rijen geschreven +voor elementen waarvan de derived values op die commit echt zijn veranderd**. Een +blad-bewerking: één rij. De hernoeming: de hele subtree. Voor onaangeraakte elementen wordt +niets herschreven — de resolutie (sectie 10) zoekt per element gewoon de *nieuwste derived-rij +op of vóór de gelezen commit*, exact zoals bij stored versions. Beide streams lossen op via +dezelfde fold, en dat houdt het ontwerp bij elkaar: één resolutie-algoritme, twee +payload-helften. + +Dat er een aparte `derived_id` bestaat (in plaats van `(identity_id, commit_id)` als PK) is +zodat `branch_head` en `commit_checkpoint` met één enkele uuid naar een derived-rij kunnen +wijzen — dat houdt die hete tabellen lekker smal. + +### 9.3 De zes uitverkorenen, en de jsonb-staart + +Zes derived properties krijgen een echte kolom; de overige ~319 wonen in `derived_json`. Die +zes zijn niet uit de lucht gegrepen: ze zijn gedeclareerd door `Element` — en bestaan dus voor +alle 167 metaclasses, waardoor de kolommen altijd gevuld zijn en nooit verspilde ruimte — en +het zijn precies de properties waar een Query service onophoudelijk op filtert en sorteert: +`owner` (containment-query's), `qualifiedName` (padopzoeking), `name` (zoeken en sorteren), +`owning_namespace`, `short_name` en `is_library_element` (library-inhoud buiten +gebruikersquery's houden). Echte kolommen betekenen echte btree-indexen en echte statistieken +per kolom. + +De staart blijft jsonb, met een GIN-index (`jsonb_path_ops`) erachter: de Query service van +de spec staat een `PrimitiveConstraint` op *elke* property toe, en 319 expression-indexen +voorbouwen voor properties waar misschien nooit op gefilterd wordt is slechter dan één +containment-index. De audit is eerlijk over de zwakke plekken (sectie 14, R5): GIN-insertie +is de grootste write amplifier bij bulk-derived-writes, en de index kent geen +`project_id`-component, dus een probe op een gedeelde partitie krijgt ook kandidaten van +buurprojecten te herchecken. De vuistregel is daarom: eerst de promoted columns, GIN als +vangnet — en blijkt uit productietelemetrie dat er maar op een handjevol properties wordt +gefilterd, vervang de hele-document-GIN dan door gerichte expression-indexen. + +### 9.4 De andere twee conformance-niveaus + +Full conformance is waar het schema voor is *geoptimaliseerd* — maar het schema zelf trekt +zich van het conformance-niveau niets aan. Het niveau uit Clause 2 is puur een +**write-path-policy**: wie schrijft de `derived_version`-rijen, en wanneer. In de DDL +verandert er tussen de niveaus helemaal niets. + +**Passthrough conformance krijg je bijna cadeau.** De client stuurt payloads *inclusief* +derived values (de DTO/serializer-laag van SysML2.NET round-tript ze). De service knipt de +binnenkomende payload in tweeën: de stored helft → `element_version` plus de genormaliseerde +kolommen; de derived helft → een `derived_version`-rij op die commit, waarbij de promoted +columns simpelweg uit de payload worden *overgenomen* in plaats van berekend. Leesacties +geven exact terug wat de client stuurde — de faithful-reproduction-garantie, byte voor byte — +en query's op derived properties werken gewoon, wat Clause 2 van passthrough-providers ook +expliciet eist. Het enige verschil met full: derived-rijen bestaan **alleen voor de elementen +in de change set** (er draait immers geen impact-radius-analyse), dus een onaangeraakt +element houdt de derived values die de client er het laatst voor instuurde — hoe verouderd of +fout ook. En dat is precies wat passthrough betekent. De `(identity, commit)`-fold maalt er +niet om *wie* een waarde heeft uitgerekend; de smoke-test schrijft zijn derived-rijen zelf +trouwens ook passthrough-stijl — met de hand, nooit berekend. + +**Geen conformance is al helemaal simpel**, want derived state is met opzet structureel +optioneel gemaakt: schrijf gewoon nooit `derived_version`-rijen. Elke leesfunctie doet een +`LEFT JOIN` op `derived_version` en een `COALESCE` van `derived_json` naar `'{}'`, dus +antwoorden bevatten dan vanzelf alleen stored properties. `branch_head.derived_id` en +`commit_checkpoint.derived_id` zijn niet voor niets nullable, en de `derived_version`-tabel +mét zijn GIN-index — de grootste write amplifier — blijft gewoon leeg. De Query-vertaler moet +dan wel `PrimitiveConstraint`s afwijzen waarvan de descriptor-entry (sectie 12.2) naar +derived opslag routeert — netjes in lijn met het geclaimde niveau. + +**Van niveau wisselen is een backfill, geen migratie.** Doordat `derived_version` een eigen +append-only stream is met sleutel `(identity, commit)`, kun je later alsnog op full +conformance overstappen: bereken de derived state van het hele model en schrijf die weg *op +één commit* (bijvoorbeeld elke branch-head). Leesacties op of ná die commit pikken de nieuwe +rijen via de gewone fold op; alles daarvóór blijft zoals het was. Eén aandachtspunt: +checkpoints van vóór de backfill dragen nog null- of verouderde `derived_id`s — herbouw ze, +of accepteer dat tot het volgende cadence-checkpoint. En let op een granulariteitsverschil: +de spec declareert conformance per Service Provider, maar mechanisch zou het schema per +project een ander beleid aankunnen (derived-rijen zijn net als al het andere +project-gebonden). Handig bij een gefaseerde uitrol — zolang de publieke claim maar het +zwakste niveau weerspiegelt dat je daadwerkelijk serveert. + +De afweging in één regel: *geen conformance* is het goedkoopst met de dunste API; +*passthrough* geeft volledige payloads en derived-query's voor bijna niets, maar de derived +values zijn client-vertrouwd en kunnen stilletjes gaan afwijken; *full* is correct per +constructie en rekent daarvoor af op het commit-moment, met de impact-radius-machinerie — het +enige niveau waar echt engineeringrisico in zit. + +--- + +## 10. Laag D — snapshot resolution (§9) + +Deze laag beantwoordt één vraag: **"hoe ziet het model eruit op commit C, of op de head van +branch B?"** — en wel goedkoop, op de schaal van het profiel. Hier wint of verliest het +schema zijn performance. De laag is tijdens de audit één keer grondig herontworpen (de +overlay) en één keer empirisch gerepareerd (de registry). Het eindresultaat bestaat uit vier +delen. + +### 10.1 `commit_checkpoint` — volledig gematerialiseerde folds + +```sql +CREATE TABLE sysml2.commit_checkpoint ( + project_id uuid NOT NULL, + commit_id uuid NOT NULL REFERENCES sysml2.commit (id) ON DELETE CASCADE, + identity_id uuid NOT NULL REFERENCES sysml2.data_identity (id), + version_id uuid NOT NULL, + derived_id uuid NULL, + PRIMARY KEY (project_id, commit_id, identity_id) +) PARTITION BY HASH (project_id); +``` + +Een checkpoint is de `versionedData`-fold uit de spec, één keer volledig uitgerekend en +opgeslagen voor één commit: per levend element één rij, van identity naar (version, +derived-rij). `build_commit_checkpoint()` bouwt hem idempotent op via de algemene resolver. +Checkpoints begrenzen hoe ver een resolver ooit hoeft te lopen, en ze zijn de *bases* waar de +branch-overlays van afwijken. + +Elk checkpoint is O(model) — zo'n 1M rijen, in de orde van 100 MB — en dat dicteert het +**cadence-beleid** (vastgelegd in de §9-banner, uitgevoerd door de service): maak een +checkpoint zodra er op die lijn ≥200 commits zijn verstreken sinds het dichtstbijzijnde +gecheckpointe voorouderpunt, óf zodra de opgetelde change-set-omvang sindsdien boven ~25% van +het model uitkomt — en altijd op branch-fork-bases. Churn-gebaseerd dus, niet domweg op +aantal: met alleen "elke N commits" stapel je op een druk project terabytes aan vrijwel +identieke checkpoints op. Retentie: een checkpoint waar geen enkele branch meer op baseert en +dat niet nodig is voor de historische ladder gaat weg — eerst de registry-rij, dan de rijen; +allebei PK-geprefixte, index-gedekte deletes. + +`build_commit_checkpoint` deed er bij 200k elementen 2,5 s over; doorgerekend is dat ~12–15 s +bij 1M. Vandaar dat de banner het in feite in vetgedrukte letters zegt: **draai dit +asynchroon, nooit op het commit-pad.** + +### 10.2 `branch_head` — de sparse overlay + +De voor de hand liggende materialisatie — per branch, per element één rij +`(branch, identity) → version` — wás het oorspronkelijke ontwerp. De audit hoefde alleen de +rekensom te maken: 500 branches × 1M elementen = **500M rijen (~85 GB) per project**; een +branch aanmaken = een miljoen rijen kopiëren naar een btree die dan al miljarden entries +groot is; een branch weggooien = een miljoen deletes plus de vacuum-nasleep. Voor honderden +branches die dagelijks komen en gaan is dat geen kwestie van tuning meer — het is de +verkeerde datastructuur. De meting (sectie 14) spreekt boekdelen: branch aanmaken kostte +2.964 ms met de volledige kopie, tegenover **1,8 ms** met de overlay — en dat op maar een +vijfde van de doelschaal. + +De overlay draait de representatie om: een branch bewaart alleen zijn **afwijking** ten +opzichte van een base checkpoint. + +```sql +CREATE TABLE sysml2.branch_head ( + project_id uuid NOT NULL, + branch_id uuid NOT NULL REFERENCES sysml2.branch (id) ON DELETE CASCADE, + identity_id uuid NOT NULL REFERENCES sysml2.data_identity (id), + version_id uuid NOT NULL, + derived_id uuid NULL, + is_tombstone boolean NOT NULL DEFAULT false, + PRIMARY KEY (project_id, branch_id, identity_id) +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_branch_head_branch ON sysml2.branch_head (branch_id); +``` + +De semantiek, op een rijtje: + +- `branch.base_commit_id` wijst naar een **gecheckpointe** commit (een invariant die de + service bewaakt — en meteen de reden dat het cadence-beleid fork-bases checkpoint). +- De head state van een element op de branch is: **de overlay-rij als die er is, anders de + rij uit het base checkpoint.** +- Een rij met `is_tombstone = true` betekent "op deze branch verwijderd ten opzichte van de + base" — hij *maskeert* de checkpoint-rij. (De rij wijst nog wel naar de + tombstone-`element_version`; de vlag is een denormalisatie van + `element_version.tombstone`, zodat set-reads gemaskeerde identities kunnen wegfilteren + zonder `element_version` erbij te halen.) +- `base_commit_id IS NULL` betekent: de overlay ís de volledige head state — de + bootstrapstand voor een kersvers project dat nog geen checkpoint heeft. + +Wat de levensloop van een branch dan kost: + +| Operatie | Werk | +|---|---| +| Branch aanmaken op een gecheckpointe commit | één `branch`-rij invoegen — **nul** overlay-rijen | +| Branch aanmaken op een niet-gecheckpointe commit | base = dichtstbijzijnde gecheckpointe voorouder; de delta (checkpoint → fork) in de overlay schrijven — O(delta) | +| Commit op de branch | de change-set-rijen in de overlay upserten (`INSERT … ON CONFLICT (project_id, branch_id, identity_id) DO UPDATE`) — O(changeset) | +| Branch verwijderen | de cascade ruimt alleen de overlay-rijen op — O(divergentie), via `ix_branch_head_branch` | +| Compaction (service-beleid) | groeit een overlay boven ~10% van het model of ~100k rijen: checkpoint de branch-head, verzet `base_commit_id`, maak de overlay leeg | + +En checkpoints worden vanzelf **gedeeld**: de honderden branches die vlak bij de head van +main worden afgetakt, baseren allemaal op dezelfde paar checkpoints. Dát is wat de +opslag-rekensom weer gezond maakt — de totale snapshot-opslag wordt bepaald door de +checkpoint-cadence, niet door het aantal branches. + +Eén ding om hier hardop te zeggen: de bovenstaande drempels (en de cadence van §10.1) zijn +geen ontwerpcommentaar — het zijn **operationele contracten die actief bewaakt moeten +worden**, met alarmen die afgaan *vóórdat* de grenzen worden bereikt. Elk ervan is uit het +schema zelf te bevragen; de concrete signalen, probes en alarmgrenzen staan in verplichting +§15.15. + +`ix_branch_head_branch` heeft één heel precieze bestaansreden: de `ON DELETE CASCADE` vanaf +`branch` filtert op alléén `branch_id`, terwijl de PK met `project_id` begint. Zonder deze +index zou elke branchverwijdering dus alle partities sequentieel doorploegen +(auditbevinding R3 — mechanisch dezelfde valkuil als bij de identity-cascades van sectie 7, +alleen hier andersom opgelost: ná de overlay is de tabel klein genoeg om die extra index +goedkoop te maken). + +De reeks PASS 9a–9f van de smoke-test doorloopt de complete overlay-levensloop: +checkpoint-opbouw, O(1)-branchcreatie, doorlezen naar de base, tombstone-maskering, de +samengevoegde set-read, en tot slot een verwijdering die alleen de overlay raakt. + +### 10.3 `commit_checkpoint_registry` — een lesje over de planner + +```sql +CREATE TABLE sysml2.commit_checkpoint_registry ( + project_id uuid NOT NULL, + commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + created timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (project_id, commit_id) +); +``` + +Eén rij per checkpoint — niet per identity. Deze tabel dankt zijn bestaan aan een gemeten +plannerfout die je het best in algemene termen onthoudt. + +De resolvers wandelen door de commit-DAG en vragen bij elke stap: *"is deze commit +gecheckpoint?"*. Aanvankelijk was die probe een `EXISTS (SELECT 1 FROM commit_checkpoint +WHERE project_id = … AND commit_id = …)` — een PK-prefix-probe, op het oog perfect +index-vriendelijk. Maar: alle ~200k rijen van een checkpoint delen **één en dezelfde** +`(project_id, commit_id)`-waarde. De statistieken zeggen dan `n_distinct(commit_id) ≈ 1`, het +selectiviteitsmodel concludeert "een commit_id-lookup levert zo ongeveer de hele tabel op", +de index-scan wordt gekosteneerd alsof hij 200k rijen teruggeeft — en de planner kiest een +sequential scan. Bij élke recursiestap opnieuw. Gemeten: een wandeling van 500 commits +filterde 100 miljoen rijen (500 × 200k) en raakte 1,33 miljoen buffers aan; de "goedkope" +historische read van één element duurde 3,5 seconde. + +De structurele oplossing wint het van elk gepruts aan de planner: laat de probe een tabel +raken waarvan de *vorm bij de vraag past*. "Is deze commit gecheckpoint?" is een vraag over +commits — dus de registry heeft één rij per gecheckpointe commit, en de probe wordt een +één-rij-PK-lookup waar geen statistiekmodel zich op kan verslikken. Na de ingreep: diezelfde +read in **1,8–4 ms** (zo'n 1.900× sneller), en de volledige model-fold van 4.012 ms naar +185 ms. + +De algemene les, om in te lijsten: *een EXISTS-probe op een tabel die fijner gesleuteld is +dan de vraag die je stelt, is een statistiekval.* Een registry- of markertabel is dan +goedkope verzekering. + +### 10.4 De resolvers + +Drie SQL-functies implementeren de fold uit de spec. De algemene: + +```sql +CREATE FUNCTION sysml2.resolve_commit_state(p_project_id uuid, p_commit_id uuid) +RETURNS TABLE (identity_id uuid, version_id uuid, derived_id uuid) ... +``` + +Zijn interne CTE-pijplijn, stap voor stap in gewone woorden: + +1. **`checkpoint`** — is de gevraagde commit zelf al gecheckpoint? (registry-probe) +2. **`ancestry`** — recursieve wandeling over `commit_parent` vanaf de gevraagde commit. + Elke bereikte commit wordt via de registry gemarkeerd als `at_checkpoint`, en de recursie + **stopt bij gecheckpointe commits** (`WHERE NOT a.at_checkpoint`). Het venster dat wordt + belopen is daarmee begrensd door het cadence-beleid — pakweg 200 commits, nooit de + volledige historie. +3. **`folded`** — join het venster met `element_version` en neem + `DISTINCT ON (identity_id) … ORDER BY identity_id, created DESC, id DESC`: voor elk + element dat *binnen het venster* is gewijzigd wint de nieuwste version. +4. **`checkpoint_state` / `checkpointed`** — elk element dat *niet* in het venster is + gewijzigd, krijgt zijn rij uit het grens-checkpoint. +5. **`resolved`** — de vereniging van 3 en 4, minus de tombstones. +6. **`derived_folded`** — exact dezelfde fold, maar dan over `derived_version` en met + hetzelfde venster; voor een element waarvan de derived-rij ouder is dan het venster geldt + de `derived_id` uit het checkpoint als terugval. (Die terugval is geen detail: zonder hem + zou derived state van vóór het checkpoint stilletjes naar NULL oplossen.) + +De correctheid steunt op de twee commit-invarianten uit sectie 6.1: + +- **"Nieuwste ancestor wint" klopt dankzij de monotonie** — een commit is strikt nieuwer dan + alles wat hij kan bereiken. Voor een merge die zijn conflictoplossingen herformuleert + (verplicht volgens de spec) betekent dat: de eigen rijen van de merge zijn de nieuwste, dus + die winnen. Smoke PASS 8a–8c toetsen het merge-geval, inclusief de controle dat een deletie + op een branch die géén voorouder is het merge-snapshot ongemoeid laat. +- **De `id DESC`-tiebreaker (auditbevinding R13) vangt op wat monotonie openlaat**: siblings + mogen een timestamp delen, en als een merge ten onrechte nalaat een conflict te + herformuleren, zou `created DESC` alleen maar lukraak tussen de siblings kiezen — met + mogelijk per read, per plan of per replica een ander antwoord. `id DESC` is willekeurig + maar *stabiel*: bij een onreglementaire invoer liever een deterministisch min-of-meer-fout + antwoord dan een wisselvallig antwoord, want het eerste is testbaar, cachebaar en + consistent. Smoke PASS 10a/10b bouwen de gelijkstand na en controleren de winnaar twee + keer, via beide resolvers. + +De variant voor één element, `resolve_element_at_commit(project, commit, identity)`, bestaat +omdat `GET /projects/{p}/commits/{c}/elements/{e}` anders het *complete model* zou moeten +folden voor één antwoord (auditbevinding R6). Zelfde ancestry-wandeling, maar beide +fold-armen gefilterd op één identity — `ux_element_version_identity_commit` en zijn +derived-evenknie maken van elke probe een indextreffer, waarmee de kosten O(belopen ancestry) +worden: gemeten 1,8–4 ms op 500 commits afstand van het checkpoint. + +En `build_commit_checkpoint(project, commit)`: een `INSERT … SELECT FROM +resolve_commit_state` plus de registry-rij, in één statement zodat beide tegelijk zichtbaar +worden, en met `ON CONFLICT DO NOTHING` zodat je hem gerust nog een keer mag draaien. + +--- + +## 11. Het read path (§10) + +Dit zijn de functies die de API-laag daadwerkelijk aanroept. Het ontwerpdoel: **een element +serveren is een jsonb-concatenatie plus een handjevol PK-probes** — geen joins over +subtype-tabellen per read, geen recursie, geen derived-rekenwerk. + +```sql +-- de heetste query van het systeem +CREATE FUNCTION sysml2.get_element_at_branch_head(p_branch_id uuid, p_identity_id uuid) +RETURNS jsonb ... AS $$ + SELECT ev.stored_json || COALESCE(dv.derived_json, '{}'::jsonb) + FROM sysml2.branch b + LEFT JOIN sysml2.branch_head bh + ON bh.project_id = b.project_id AND bh.branch_id = b.id AND bh.identity_id = p_identity_id + LEFT JOIN sysml2.commit_checkpoint cc + ON cc.project_id = b.project_id AND cc.commit_id = b.base_commit_id + AND cc.identity_id = p_identity_id AND bh.identity_id IS NULL + JOIN sysml2.element_version ev + ON ev.project_id = b.project_id AND ev.version_id = COALESCE(bh.version_id, cc.version_id) + LEFT JOIN sysml2.derived_version dv + ON dv.project_id = b.project_id AND dv.derived_id = COALESCE(bh.derived_id, cc.derived_id) + WHERE b.id = p_branch_id + AND bh.is_tombstone IS NOT TRUE + AND NOT ev.tombstone; +$$; +``` + +Hoe je hem leest: begin bij de kleine `branch`-tabel (die levert het `project_id` — zie +hieronder waarom dat zo belangrijk is), probeer de overlay; is daar geen rij +(`bh.identity_id IS NULL` bewaakt de tweede join), val terug op het base checkpoint; de +winnaar levert de version- en derived-pointers; plak de twee jsonb-helften aan elkaar. Een +getombstonede overlay-rij sneuvelt in de WHERE — en maskeert zo de base. Elke stap is een +PK-probe binnen één partitie. + +**De `project_id`-discipline — met schade en schande geleerd (auditbevinding R2):** de eerste +versie van deze functie filterde `branch_head` op alleen `(branch_id, identity_id)`. De +gevolgen op PG16/17: geen partition pruning (zonder de hash-sleutel in het predicaat worden +alle 16 leaves bezocht) én geen PK-gebruik (`branch_id` is de *tweede* kolom van de PK, en +een btree skip scan bestaat pas vanaf PG18). En let op: ook óp PG18 blijft de regel staan — +skip scan verzacht hooguit de indexkant; partition pruning heeft het `project_id`-predicaat +nog steeds nodig. Kortom: de heetste query van het systeem was +ongemerkt de slechtste. De oplossing — via `branch` joinen zodat `project_id` beschikbaar +komt — laat runtime-pruning via de joinparameter zijn werk doen. In het gemeten plan staan 15 +van de 16 partities op "(never executed)" en duurt de uitvoering 0,061 ms. De vuistregel die +hieruit volgt geldt voor elk toegangspunt: **een query op een gepartitioneerde tabel met +alleen kale uuid's als sleutel is in dit schema per definitie kapot.** + +De overige functies: `get_elements_at_branch_head` (de set-read: het base checkpoint minus de +ge-overlayde identities via een anti-join, met daarbovenop `UNION ALL` de levende overlay — +gemeten 1,24 s voor een merge van 200k), en `get_elements_at_commit` / +`get_element_at_commit` (de historische varianten, bovenop de resolvers). + +--- + +## 12. De metamodel-catalogi en de Query service (§2, §11) + +### 12.1 `model_version` en `class_kind` — het append-only register + +```sql +CREATE TABLE sysml2.model_version ( + id smallint NOT NULL, -- ordinal: hoger id == latere release + name text NOT NULL, -- leesbaar label, bv. 'sysml-2.0-beta-4' + source_fingerprint text NOT NULL, -- root-package-fingerprint van de generatorinput + PRIMARY KEY (id), UNIQUE (name) +); + +CREATE TABLE sysml2.class_kind ( + id smallint NOT NULL, + name text NOT NULL, -- het API-@type, bv. 'PartUsage' + is_abstract boolean NOT NULL, + introduced_in smallint NOT NULL REFERENCES sysml2.model_version (id), + removed_in smallint NULL REFERENCES sysml2.model_version (id), -- eerste release ZONDER de class + PRIMARY KEY (id), UNIQUE (name) +); +``` + +`class_kind` interneert de 175 metaclass-namen naar een smallint. De ids zijn **niet +positioneel**: ze komen uit het ingecheckte, append-only register +(`SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/ClassKindRegistry.cs`), dat de +bron van waarheid is waaruit de seeds worden geproduceerd — het UML-model *valideert* er +alleen tegen. Een id wordt éénmalig uitgedeeld, wanneer de metaclass voor het eerst in een +geregistreerde release verschijnt, en staat daarna voor altijd vast; een nieuwe release +appendt zijn nieuwkomers ná het hoogste bestaande id (onderling alfabetisch); een vervallen +metaclass houdt zijn rij, afgesloten met `removed_in`. De generator **faalt luid** op elke +drift — een niet-geregistreerde class (de foutmelding drukt de exacte registerregels af om +toe te voegen), een registratie die het model niet meer bevat, een abstractheidsverschil, of +een `source_fingerprint` die niet meer bij de nieuwste geregistreerde release past. Stil +hernummeren — dé valkuil van het eerdere positionele ontwerp — is per constructie onmogelijk; +precies daarom zijn de seed-`INSERT`s ook idempotent (`ON CONFLICT (id) DO NOTHING`, smoke +PASS 11a) en veilig opnieuw toe te passen op een gevulde database. + +**Het contract voor elke afnemer:** de canonieke identiteit van een metaclass is nog steeds +zijn **naam** (het API-`@type`); de smallint is de interning daarvan door het register. +Onderhoud nooit met de hand een C#-enum die deze ids naspiegelt — de geplande +`ClassKind`-enum wordt *uit hetzelfde register gegenereerd*, waardoor de waarden per +constructie stabiel zijn over releases heen, plus een startup-assertie in de service die de +gecompileerde constanten vergelijkt met de `class_kind`-tabel en bij drift weigert te starten +(genoteerd, nog niet gebouwd). + +Het eerdere ontwerp had hier ook een `class_kind_table`-catalogus (de platgeslagen +overervings-DAG: welke subtype-tabellen elke concrete metaclass joint). Die is uit de +database verdwenen — niets in het schema las hem ooit, en de tabeldeelname per release hoort +nu bij de gegenereerde descriptors van sectie 12.2. + +### 12.2 De model-version descriptors — de brug tussen API en opslag + +Eerdere ontwerpen hadden hier een `property_catalog`-tabel: 12.113 gegenereerde rijen die +elke combinatie van concrete metaclass en API-property naar haar opslaglocatie wezen. Die is +bewust **uit de database verdwenen**, om drie redenen die elkaar versterken: + +- **Niets in het schema leest hem.** Elke view, resolver en index is bij generatie al per + metaclass gespecialiseerd; de catalogus was passieve data met nul inkomende referenties, + puur voor een externe afnemer. +- **De service-laag wordt óók gegenereerd.** Dezelfde generator die dit schema produceert, + produceert de data-access van de service; statische per-metaclass C# (de vaste + performance-boven-reflectie-regel van deze codebase) beantwoordt de routeringsvraag zonder + databaserondje. +- **Een tabel beschrijft één release; descriptors beschrijven ze allemaal.** Met meerdere + metamodel-releases in één database (sectie 6.4) is de property→opslag-routering *per + release*. Eén catalogustabel kan niet zeggen "in release 1 woonde dit hier, in release 2 + daar" zonder het register opnieuw uit te vinden — geversioneerde gegenereerde code draagt + precies dat, op natuurlijke wijze. + +De vervanger is de **model-version descriptor**: per geregistreerde release gegenereerde C# +die de metaclasses van die release opsomt, per metaclass de subtype-tabellenset (wat +`class_kind_table` vroeger vastlegde), en per API-property de opslagroutering (wat +`property_catalog` vroeger vastlegde) — plus de multiplicity- en ordeningsmetadata die de +Query-vertaler nodig heeft om constraint-vormen te valideren. De descriptors worden uit +dezelfde XMI + register-inputs geproduceerd als het schema en lopen er dus per constructie +mee in lockstep. (Hier geschetst als ontwerp; de descriptor-generator landt samen met de +service-laag.) + +Wat de descriptor implementeerbaar maakt is onveranderd: de **Query service** van OMG. Het +querymodel uit de spec: + +``` +Query { select: [String], where: Constraint, orderBy: [String], scope: [...] } +Constraint = PrimitiveConstraint { property, operator, value, inverse } + | CompositeConstraint { constraint: [Constraint], operator: and|or } +``` + +Een `PrimitiveConstraint.property` is een *naam* op API-niveau. De queryvertaler zoekt die +naam op in de descriptor van de release van de commit en weet dan meteen waar hij moet zijn: + +| De descriptor-entry zegt | De vertaler maakt ervan | +|---|---| +| `('PartUsage', 'declaredName', 'column', 'element_version', 'declared_name', …)` | `ev.declared_name = $v` | +| `('PartUsage', 'isVariation', 'column', 'usage_version', 'is_variation', …)` | join `usage_version`, `u.is_variation = $v` | +| `('PartUsage', 'qualifiedName', 'derived', 'derived_version', 'qualified_name', …)` | join `derived_version`, `dv.qualified_name = $v` — een geïndexeerde kolom | +| `('PartUsage', 'featuringType', 'derived', 'derived_version', NULL, json_key='featuringType', …)` | `dv.derived_json @> '{"featuringType": …}'` — het GIN-vangnet | +| `('PartUsage', 'ownedRelationship', 'link_table', 'element_owned_relationship', …)` | `EXISTS (SELECT 1 FROM element_owned_relationship …)` | +| `('CollectExpression', 'operator', 'column', 'operator_expression_version', 'operator', …)` | de redefinition, al doorverwezen naar haar storage-root — de vertaler hoeft geen UML-redefinitionregels te kennen | + +Merk op dat derived properties hier volwaardige querydoelen zijn — exact wat full conformance +in Clause 2 verlangt ("derived properties can be used in Query structures as +PrimitiveConstraint properties … query execution will consider the correctly computed and +up-to-date values"). Dat deze rijbron goedkoop is, is de verdienste van de +precompute-op-commit-strategie. + +Multiplicity- en ordeningsmetadata reizen mee in de descriptor, zodat de vertaler ook de vorm +van een constraint kan valideren en metadata-endpoints van de API properties kunnen +beschrijven zonder het .NET-reflectiemodel te laden. + +### 12.3 De flattening views (§11) + +Per concrete metaclass wordt één view gegenereerd die de rijvorm van de DTO reconstrueert: + +```sql +CREATE VIEW sysml2.vw_part_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, + ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, /* … */ + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 120 AND NOT ev.tombstone; -- 120 = het bevroren register-id van PartUsage +``` + +Over de naam: het voorvoegsel `vw_` staat voor **view** — bewust gekozen boven het gangbaardere +losse `v_`, juist zodat het nooit als *version* gelezen kan worden, de betekenis van het +`_version`-achtervoegsel op de subtype-tabellen (§8.3). Twee onmiskenbaar verschillende +schrijfwijzen voor twee verschillende begrippen. + +Deze views zijn er voor de Query service en voor mensen die willen rondneuzen — de gewone +API-element-read komt er nooit langs (die serveert `stored_json`). Het zijn doorgeefviews: ze +tonen `project_id`, en de `WHERE project_id = $1` van de aanroeper sijpelt via de +equivalentieklassen van de USING-joins door naar elke gejoinde gepartitioneerde tabel, zodat +er netjes wordt gepruned. De ops-checklist uit de audit (R10) hoort hierbij: controleer dat +hete plannen daadwerkelijk prunen; houd de 6-join-views in de gaten voor +generic-plan-omslagen (`plan_cache_mode = force_custom_plan` is dan de knop); en kies bij +voorkeur PG18 waar beschikbaar (btree skip scan, AIO, `NOT VALID`-FK's op gepartitioneerde +tabellen, native `uuidv7()`), en anders PG17, waar de fast-path-lockslots meegroeien met +`max_locks_per_transaction` — een +query die zes gepartitioneerde relaties plus indexen aanraakt kan onder hoge druk anders in +de gedeelde lock manager belanden. + +--- + +## 13. Partitionering en fysieke tuning (§12) + +**Hash-partitionering op `project_id`, 16-voudig, over alle 58 elementtabellen op dezelfde +manier.** Het profiel spreekt van tientallen tot honderden projects per instantie: +hash-op-project verdeelt ze over de partities en houdt tegelijk alles van één project *bij +elkaar*. Elke projectgebonden query pruned naar één partitie, en elke +`(project_id, version_id)`-join tussen elementtabellen blijft binnen die partitie. (Wordt een +deployment gedomineerd door één reusachtig project, dan is de partitionering daarvoor +neutraal — dat project past in zijn geheel in één partitie. Het ontwerp heeft partitionering +ook niet nodig voor single-project-performance, alleen voor de spreiding over tenants. De +modulus is een instelknop per deployment.) + +**Versiebeleid.** De vloer is PostgreSQL **16** — een deployability-keuze, geen technische: +niets in het schema heeft iets nieuwers nodig, en de vloer op de nieuwste major leggen zou +het gros van de echte enterprise-installaties uitsluiten voor nul functionele winst. Alle +verificatie in deze repository draaide op **17** (dat bovendien de fast-path-lockslots laat +meegroeien met `max_locks_per_transaction` — relevant met 928 leaf-partities). **Prefereer 18 +waar beschikbaar**: dat brengt vier concrete voordelen voor precies dit schema — btree skip +scan (verzacht de R2-faalmodus, al blijft de `project_id`-regel staan: pruning heeft het +predicaat nog steeds nodig), `NOT VALID` + `VALIDATE`-FK's op gepartitioneerde tabellen (het +nette R11-bulk-importpad), native `uuidv7()` (de R8-aanbeveling, nu ook database-zijdig), en +asynchrone I/O (versnelt juist de O(model)-operaties: checkpoint-builds, set-reads, vacuum op +de grote leaves). + +Het schema benut 18 automatisch waar dat kan; de rest is deployment-advies: + +- **Zelf-activerende `uuidv7()`-defaults** (geïmplementeerd, §12): een versie-gegarde + `DO`-block zet `DEFAULT uuidv7()` op elke server-gemunte sleutel (`version_id`, + `derived_id` en de PIM-record-ids) zodra `server_version_num >= 180000` — geverifieerd een + no-op op de 16/17-vloer. `Guid.CreateVersion7()` in de service blijft de primaire bron (de + service heeft de ids vóór de insert nodig); de defaults zijn het vangnet dat ook ad-hoc- + en tooling-inserts tijdgeordend houdt. `data_identity.id` is bewust uitgezonderd — het + spec-zichtbare `@id` moet worden aangeleverd, nooit stilletjes gemunt. +- **AIO-tuning**: de standaard `io_method = worker` helpt de O(model)-operaties al; overweeg + op Linux `io_method = io_uring` en verhoog `io_workers` tijdens checkpoint-build- en + bulk-importvensters. +- **Bulk import op 18**: kies het nette pad — de FK's van het importdoel als `NOT VALID` + aanmaken, laden, dan `VALIDATE CONSTRAINT` (vrijwel niet-blokkerend) — boven de + vertrouw-me-truc `session_replication_role = replica` uit R11. +- **Parallelle GIN-builds**: het herbouwen van `ix_derived_version_json` na een bulk derived + write (het R5-pad) parallelliseert op 18 — plan onderhoudsvensters daarop in. +- **`pg_upgrade` behoudt plannerstatistieken** op 18 — met 928 leaf-partities verdwijnt de + ANALYZE-storm na een major-upgrade daarmee volledig. + +De fysieke keuzes die uit de audit zijn gerold: + +- **`max_locks_per_transaction = 4096` is een keiharde deployment-eis.** 58 gepartitioneerde + tabellen × 16 leaves = 928 relaties, en PostgreSQL kopieert elke FK naar elk leaf (ruim + 2.600 constraints). Schema-brede DDL — installatie, migratie, `pg_dump --schema-only` — + pakt per object een lock en loopt op de default van 64 stuk met `ERROR: out of shared + memory`. Dit is proefondervindelijk vastgesteld: zonder deze instelling installeert het + schema niet eens. Het hete pad merkt er niets van (dat pruned naar een handvol relaties). +- **Autovacuum afgestemd op het schrijfprofiel.** De lus die de partities aanmaakt geeft ze + verschillende storage-parameters mee. De `branch_head`-leaves — als enige upsert-zwaar: + overlay-rijen worden bij elke commit bijgewerkt en verdwijnen bij compaction — krijgen + `fillfactor = 90` (ruimte voor HOT-updates) en het gewone dead-tuple-gedreven vacuum. De + append-only leaves (`element_version`, `derived_version`, subtype, link) krijgen juist + *insert*-gedreven vacuum (`autovacuum_vacuum_insert_threshold = 100000`, zodat de + visibility map bijblijft voor index-only scans) en analyze pas bij 50k rijen — het + oorspronkelijke vlakke "analyze om de 5000 rijen" zou een leaf van 60M rijen tijdens een + import onafgebroken aan het bemonsteren houden. +- **lz4 voor de jsonb-kolommen**, ingesteld op de parents *vóórdat* de partitielus draait, + zodat de leaves het overerven (gecontroleerd in `pg_attribute`). Bij dit schrijfvolume is + het compressiewerk van pglz op elke commit merkbaar; lz4 is hier gewoon in alles beter. +- **UUIDv7 voor app-gegenereerde sleutels** (`version_id`, `derived_id`): met tijdgeordende + uuid's haken de inserts van een project rechts in de btree aan, in plaats van als hagel + over de index. Eén regel in .NET (`Guid.CreateVersion7()`), geen schemawijziging. + `identity_id` blijft zoals hij binnenkomt — dat is het spec-zichtbare `@id`, en + library-elementen zijn normatief v5. +- **Bulk import**: de ~3 FK-probes per rij tegen een `data_identity` van 10⁸ rijen zijn reëel + maar overzichtelijk. Wordt het volgens metingen toch te veel, dan is het importpad + `SET session_replication_role = replica` met validatiequery's achteraf. Twee verleidelijke + alternatieven zijn in de audit uitdrukkelijk *afgewezen*: `DEFERRABLE`-FK's (die schuiven + exact hetzelfde werk per rij door naar commit-tijd en laten de triggerwachtrij opzwellen — + geen bulk-load-instrument) en `NOT VALID` + `VALIDATE` (op gepartitioneerde tabellen pas + vanaf PostgreSQL 18). + +--- + +## 14. De performance-audit: praktijkverhalen met cijfers + +Het schema is tegen het schaalprofiel geauditeerd en daarna ook echt *gemeten*: een +vormgetrouwe synthetische dataset (200k elementen, 2.000 commits, checkpoint op 1.500, 100 +overlay-branches en één ouderwets volledig gematerialiseerde branch ter vergelijking) op +PostgreSQL 17 in Docker. Drie bevindingen bleken regelrechte bugs die de ontwerpreview +gewoon hadden overleefd — ze kwamen pas boven bij adversariële audit plus echte runs. Dat is +meteen de moraal van deze sectie. + +**De meettabel:** + +| Operatie | Legacy-ontwerp | Gehard schema | +|---|---|---| +| Branch aanmaken | 2.964 ms (200k rijen kopiëren) | **1,8 ms** (overlay) | +| Branch verwijderen | ongeïndexeerd → seq scans | 34 ms (overlay); 100 ms zelfs bij 200k rijen (geïndexeerde cascade) | +| Single-element-head-read | alle 16 partities gescand | **0,061 ms**; 15/16 partities "(never executed)" | +| Single-element-historische-read (500 commits van checkpoint) | 3.466 ms | **1,8–4 ms** | +| Volledige-model-fold (500 commits van checkpoint) | 4.012 ms | **185 ms** | +| Branch-head-set-read (200k-overlay-merge) | — | 1.242 ms | +| `build_commit_checkpoint` (fold van 1.500 commits × 200k) | — | 2.488 ms (async-budget) | + +**De bevindingen in het kort** (de volledige tabel staat in +`SysML2.NET.CodeGenerator/SQLSCHEMA.md`): + +- **R1 (SEV-1)** — de gematerialiseerde `branch_head` was O(branches × elementen). Opgelost + met de overlay (sectie 10.2). Dit was de enige echt *architecturale* verbouwing. +- **R2 (SEV-1, bug)** — de heetste leesfunctie filterde op kale uuid's → geen pruning, geen + PK. Opgelost door via `branch` te joinen (sectie 11). Aan de SQL was niets verdachts te + zien; alleen de planvorm verraadde het. +- **R3 (SEV-1, bug)** — elke `ON DELETE CASCADE` miste een index op de cascadekolom. + Opgelost met één index plus het terugbrengen van de groot-tabel-cascades naar expliciete + procedures (secties 7 en 10.2). +- **R-registry (SEV-1, alleen door draaien gevonden)** — de checkpoint-bestaansprobe deed per + recursiestap een seq scan, omdat `n_distinct = 1`-statistieken de index onderuithalen. + Structureel opgelost (sectie 10.3). *Deze klasse problemen vang je niet met een + ontwerpreview — alleen met `EXPLAIN (ANALYZE, BUFFERS)` op realistische data.* +- **R4 (SEV-2)** — checkpoint-cadence is een ontworpen beleid met een opslag-tegenwicht, geen + vrije knop (sectie 10.1). +- **R5 (SEV-2)** — de worst-case derived burst × GIN-write-amplification: als bulkoperatie + gebudgetteerd; lz4 doorgevoerd; GIN-strategie gedocumenteerd (sectie 9.3). +- **R6 (SEV-2)** — historische reads van één element verdienden een eigen resolver (sectie + 10.4). +- **R7 (SEV-3, geparkeerd)** — write amplification in de link-tabellen bij enorme collecties; + het content-addressed ontwerp (digest-gesleutelde gedeelde collectierijen die bij + ongewijzigde inhoud via een pointer worden hergebruikt) staat uitgewerkt in SQLSCHEMA.md en + wacht op benchmarkbewijs — het hervormt gegenereerde tabellen en raakt de generator. +- **R8/R9/R10/R11 (SEV-3)** — UUIDv7, autovacuum-differentiatie, de + plan-cache/lock-manager-checklist en het bulk-importpad (sectie 13). +- **R12 (weerlegd)** — `data_identity` ongepartitioneerd op 10⁸ rijen kan prima (sectie 7). +- **R13 (SEV-4, stille-bugklasse)** — fold-determinisme bij timestamp-gelijkstanden tussen + siblings (sectie 10.4). + +De poort die vóór productie nog genomen moet worden (hier bewust niet gebouwd): het volledige +.NET-benchmarkharnas — drie projecten van 1M elementen met authentieke serializer-payloads op +gedeelde partities, een replay van 20k commits, 500 branches, de root-hernoemingsburst +gemeten *terwijl* de leeslatentie wordt bewaakt, een A/B van UUIDv4 tegen v7, en +levensduurcontroles (`pgstattuple`-bloat, wait events, WAL per commit). + +--- + +## 15. Wat de service-laag het schema nog verschuldigd is + +Het schema is met opzet niet zelfrijdend. De volgende verantwoordelijkheden liggen erboven, +en het ontwerp rekent erop dat ze worden ingevuld: + +1. **De impact-radius-analyse** (de moeilijkste). Bepaal bij elke commit welke elementen hun + derived values kwijtraken door de change set, reken ze opnieuw uit (de + `SysML2.NET/Extend/*.Compute*`-methoden tegen het in-memory-model) en schrijf de + `derived_version`-rijen. Een blad-bewerking raakt één element; een namespace-hernoeming + zijn hele subtree (`qualifiedName` van alle leden); een feature toevoegen aan een + supertype raakt de complete specialization-afstammelingen-closure (`feature`, + `membership`, `inheritedMembership`). De reverse-lookup-indexen (`ix_*_target`, de + specialization-indexen) bestaan precies om die closures betaalbaar te maken. Hier gaan de + correctheidsbugs van het hele systeem wonen — dit stuk verdient de beste tests van het + project. Een volledige ontwerpschets voor deze engine — de vijf voortplantingssoorten, de + `derived_dependency`-catalogus, early cutoff en het differentiële test-orakel — staat in + `SysML2.NET.CodeGenerator/IMPACT-RADIUS.md`. +2. **Checkpoint-cadence, retentie en overlay-compaction** — het beleid van secties 10.1 en + 10.2, asynchroon uitgevoerd. +3. **De discipline van de committransactie**: één transactie schrijft `commit` + + `commit_parent` (de trigger valideert), de `element_version`- + subtype- + link-rijen, + `stored_json`, de `derived_version`-rijen en de `branch_head`-overlay-upserts, en verzet + daarna `branch.head_commit_id`. Dankzij append-only is dat een zuivere insert-transactie + plus één update op de branch-rij. +4. **De base-commit-invariant**: laat `branch.base_commit_id` nooit wijzen naar een commit + zonder checkpoint. +5. **Projectverwijdering** via de geordende expliciete procedure (sectie 7) — en dus nooit + door eerst `data_identity`-rijen weg te gooien. Controleer bovendien eerst + `project_usage`: identities waar *andere* projecten naar verwijzen blokkeren de procedure + (terecht, en luidruchtig) via de NO ACTION-FK's — ruim die usages eerst op of migreer ze. +6. **Referencevalidatie op modelniveau** (dangling én wrong-type references op een commit): + nadrukkelijk een *validatie*, geen FK (axioma 1). De gegenereerde two-tier functies (§7) + zijn er de kant-en-klare implementatie van: draai `validate_references_in_commit` bij + elke commit (O(change set), goedkoop genoeg om acceptatie op te poorten), plan + `validate_references_at_commit` als periodieke volledige audit, resolve + cross-project-doelen via `project_usage`, en presenteer de bevindingen. +7. **Merge-conflictherformulering** — de spec eist dat een merge conflicterende elementen in + zijn eigen change set herformuleert; de tiebreaker maakt overtredingen hooguit + deterministisch, niet correct. + +De volgende verplichtingen komen allemaal voort uit één onderliggend feit: **één logische +gebruikersactie raakt doorgaans méér elementen dan de gebruiker denkt.** Het schoolvoorbeeld: +"voeg kind B toe aan A" schrijft een nieuwe B, een nieuwe Membership, *én een nieuwe version +van A* — A's `ownedRelationship`-lijst is immers stored, geordende state (§8.2). Aan de +API-oppervlakte zie je deze koppelingen niet, maar ze bepalen wel of gelijktijdig werken +soepel aanvoelt of om gek van te worden is: + +8. **Three-way collection merge, met een ordeningsbeleid.** Twee gebruikers die een kind aan + dezelfde container hangen, maken allebei een nieuwe version van die container — formeel + een same-element-conflict bij elke rebase (§18.2) en elke merge. Additieve, disjuncte + collectiewijzigingen (basis `[…]`, de mijne `[…, M_B]`, de jouwe `[…, M_C]`) MOETEN + automatisch worden samengevoegd (`[…, M_B, M_C]`) met een deterministisch ordeningsbeleid + (bv. wie het eerst commitde staat voorop) — anders wordt elke populaire container een + conflictmagneet. Alleen echt onverenigbare combinaties (herordenen-tegen-herordenen, + verwijderen-tegen-verwijzen) gaan naar een mens — en valideer het samengevoegde resultaat + altijd op modelniveau (zie punt 12): een structureel nette union kan alsnog dubbele namen + opleveren. +9. **Coherentie van het ownership-quadruple.** Eén eigendomsfeit ("B hoort bij A via M") + staat op VIER plekken opgeslagen: A's `ownedRelationship`-lijst, M's + `owning_related_element`, M's `ownedRelatedElement`-lijst en B's + `owning_relationship`-terugverwijzing — plus de endpoint-spiegels van de new-name + redefinitions (bv. `memberElement` naast `target`). Elke write moet ze binnen de change + set alle vier kloppend houden; het schema kan elke pointer afzonderlijk FK-checken, maar + hun onderlinge overeenstemming niet. +10. **Containment-bewuste conflictdetectie.** Naïeve detectie (snijd de twee change sets op + identity) MIST het geval verwijderen-tegen-afstammeling-bewerken: gebruiker 1 tombstonet + package A terwijl gebruiker 2 diep daaronder element D bewerkt — disjuncte identities, + maar een echt conflict. De detectie moet een getombstoned element laten conflicteren met + élke wijziging *onder* zijn subtree (en met verplaatsingen erin). +11. **Volledigheid van subtree-deletes.** Een element verwijderen betekent: zijn complete + owned closure tombstonen — het element, zijn memberships en alle transitieve kinderen — + in ÉÉN change set. Het schema slaat een half verwijderde boom zonder morren op (axioma + 1: FK's toetsen bestaan, niet levendheid); alleen de service kan de closure garanderen. +12. **Semantische validatie ná de merge, inclusief cyclusbewaking.** Twee elk-voor-zich + geldige branches kunnen samen een ongeldig model opleveren: dubbele namen in één + namespace, en — erger — cycli die op geen van beide branches bestonden (gebruiker 1: B + specialiseert C; gebruiker 2: C specialiseert B; of twee verplaatsingen die A onder B én + B onder A hangen). Ownership-cycli breken bovendien de derived-berekeningen + (`qualifiedName` zou nooit stoppen met wandelen), dus de impact-radius-engine heeft + expliciete cyclusbewaking nodig, en een merge-commit wordt pas geaccepteerd na validatie. +13. **De merge-impact-radius draait op de SAMENGEVOEGDE toestand.** Derived values voor een + merge herberekenen door de resultaten van de twee branches te verenigen is fout: + kruisinteracties (de ene branch voegt een Specialization toe, de andere een feature op + het doel ervan) veroorzaken derived wijzigingen die geen van beide branches ooit heeft + gezien. Reken tegen het samengevoegde snapshot. +14. **Behandel de `class_kind`-mapping als registerdata — nooit met de hand bijhouden.** Laad + de naam↔id-mapping bij het opstarten uit de `class_kind`-tabel (of gebruik de + gegenereerde `ClassKind`-enum, geproduceerd uit hetzelfde register). De ids liggen vast + door het append-only register (§12.1), dus seeds opnieuw toepassen is veilig en upgrades + hernummeren nooit — maar de registerdiscipline zelf (nieuwkomers appenden, vervallen + classes afsluiten met `removed_in`, nooit hernummeren) is een onderhoudsplicht voor wie + het schema regenereert. +15. **Bewaak de performance-drempels en alarmeer VOORDAT ze pijn doen.** Het beleid van + §10.1/§10.2 degradeert geruisloos wanneer het wordt verwaarloosd — reads worden gewoon + langzamer. Elke drempel is uit het schema zelf te bevragen, dus meten is goedkoop; de + verplichting is om de probes in de monitoring te hangen en operators (en, waar het hun + ervaring verklaart, gebruikers) te waarschuwen zodra een trend de verkeerde kant op + gaat. De signaalset: + + | Signaal | Probe | Alarm bij | Wat er anders degradeert | + |---|---|---|---| + | Overlay-grootte per branch | `SELECT branch_id, count(*) FROM branch_head GROUP BY 1` versus het rijaantal van het base checkpoint | ≥ 50% van de compaction-drempel (~10% van het model / ~100k rijen) | set-reads en de anti-join groeien; compaction is achterstallig | + | Checkpoint-afstand per branch | commits tussen `head_commit_id` en `base_commit_id` (wandel `commit_parent`) | > 2× het cadence-doel (~400 commits) | resolver-wandelingen en historische reads worden langer | + | Branches met `base_commit_id IS NULL` | `SELECT count(*) FROM branch WHERE base_commit_id IS NULL AND deleted IS NULL` | > 0 buiten project-bootstrap | het O(model)-full-overlay-gedrag van vroeger is stilletjes terug | + | Checkpoint-retentiebacklog | checkpoints waar geen branch op baseert en die buiten de historische ladder vallen | aanhoudende groei | opslag groeit met ~0,2 GB per achtergebleven 1M-element-checkpoint | + | Impact radius per commit | `derived_version`-rijen per `commit_id` | > enkele % van de modelgrootte | derived bursts hebben het R5-bulkpad nodig (GIN-pending-list-tuning) | + | CAS-conflict- + auto-merge-ratio | service-metrics: 409's en collection-merge-rebases per branch | stijgende trend | hot-container-contentie (§18.3.6) holt de gebruikerservaring uit | + | Seq-scan-tellers op elementtabellen | `pg_stat_user_tables.seq_scan`-delta's op de gepartitioneerde leaves | klimt boven ~0 in stabiele toestand | een planner-regressie van de R2/R3-klasse is terug — de stille faalmodus van §14 | + +16. **Release-conversie (§6.4).** Het schema dwingt de release-invarianten op de commit-DAG + af (`trg_commit_parent_version`), maar de conversion commit zelf is servicewerk: bouw de + versie-diff tussen de twee releases (welke metaclasses/properties van vorm veranderden), + herformuleer exact de geraakte elementen tegen de descriptors van de nieuwe release, + dwing een `commit_checkpoint` af op de conversion commit, honoreer + `project.target_model_version_id` vóór je de upgrade accepteert, en wijs cross-release + merges af met een duidelijke "eerst converteren"-fout in plaats van de exception van de + trigger rauw door te geven. Hertypeert de conversie een element (zijn metaclass is door + de nieuwe release vervallen), werk dan `data_identity.class_kind` in dezelfde transactie + bij — de typed identity (§7) moet blijven kloppen met de geherformuleerde versions. + +--- + +## 16. Uitgewerkte voorbeelden — data volgen door het schema + +Deze voorbeelden lopen gelijk op met de smoke-test. `schema.smoke.sql` draaien en de uitvoer +naast deze sectie leggen is dan ook de snelste manier om het ontwerp echt in de vingers te +krijgen. + +### 16.1 Een hernoeming golft door de derived state (axioma 2 in actie) + +De opstelling: Package **P** ("Old") bezit PartUsage **W** ("wheel"). Commit **c1** maakt ze +allebei aan. + +| Tabel | Rijen na c1 | +|---|---| +| `element_version` | (P, p1, c1, "Old"), (W, w1, c1, "wheel") | +| `derived_version` | (P, c1, qn="Old"), (W, c1, qn="Old::wheel") | + +Commit **c2** hernoemt P naar "New". In de change set zit **één element**: + +| Tabel | Nieuwe rijen op c2 | +|---|---| +| `element_version` | (P, p2, c2, "New") — *niets voor W* | +| `derived_version` | (P, c2, qn="New"), **(W, c2, qn="New::wheel")** — W zit in de impact radius | + +Lees W op c2: de stored fold vindt w1 (ongewijzigd sinds c1), de derived fold vindt de +c2-rij van W. De payload wordt `w1.stored_json || derived(c2).derived_json` → +`"New::wheel"`, met de oorspronkelijke stored inhoud. (PASS 2a/2b.) + +### 16.2 Een merge, en waarom "nieuwste wint" klopt + +De historie: c1 → c2 (P wordt "New") op main; c1 → c4 (P wordt "Other") op een zijbranch; +c5 = merge(c2, c4), die P *in zijn eigen change set* op "Merged" zet. En ondertussen heeft c3 +(een kind van c2, **géén** voorouder van c5) element W verwijderd. + +Resolutie van c5: de ancestry is {c5, c2, c4, c1}. Voor P zijn de kandidaten p1@c1, p2@c2, +p_zij@c4 en p_merge@c5 — dankzij de monotonie is c5 de nieuwste, dus "Merged" wint +(PASS 8a). Voor W staat alleen w1@c1 in de ancestry: de deletie op c3 is onzichtbaar, want c3 +is geen voorouder (PASS 8b). Dit is niets anders dan de OCL-fold van sectie 6.2, uitgevoerd +via indexen. + +### 16.3 Het leven van een branch onder de overlay + +1. `build_commit_checkpoint(project, c2)` → 2 checkpoint-rijen + 1 registry-rij (PASS 9a). +2. Maak branch b2 aan met `base_commit_id = c2` → **nul** overlay-rijen; wie W op b2 leest, + krijgt de checkpoint-rij (PASS 9b/9c). +3. Verwijder W *alleen op b2*: er komt een overlay-rij (b2, W, → tombstone-version, + `is_tombstone = true`). W lezen op b2 levert nu niets meer op — de overlay maskeert de + base (PASS 9d) — en de set-read telt nog 1 element (PASS 9e). Op main is er met W niets + gebeurd. +4. Gooi b2 weg: de cascade ruimt alleen de overlay-rij op; het checkpoint — gedeeld met elke + andere branch die op c2 baseert — blijft onaangetast (PASS 9f). + +### 16.4 Een query vertaald + +*"Alle PartUsages onder `Vehicle` waarvan `isVariation` waar is, gesorteerd op naam"* — als +spec-Query: `where = and(PrimitiveConstraint(qualifiedName, like, 'Vehicle::%'), +PrimitiveConstraint(isVariation, =, true))`, `orderBy = [name]`. De vertaler zoekt elke +property op in de descriptor van de release (sectie 12.2) en produceert, over de +branch-head-toestand: + +```sql +SELECT h.identity_id +FROM sysml2.get_elements_at_branch_head($branch) h -- of de overlay-merge inline +JOIN sysml2.element_version ev USING (project_id, version_id) +JOIN sysml2.usage_version u USING (project_id, version_id) -- catalogus: isVariation -> usage_version +JOIN sysml2.derived_version dv ON dv.project_id = ev.project_id AND dv.derived_id = h.derived_id +WHERE ev.class_kind = 94 -- catalogus: PartUsage + AND dv.qualified_name LIKE 'Vehicle::%' -- catalogus: derived, promoted column + AND u.is_variation +ORDER BY dv.name; +``` + +Elk predicaat komt terecht op een echte, geïndexeerde kolom met eigen statistieken — en dat +is precies de winst van promoted derived columns plus de catalogus. + +--- + +## 17. Codegeneratie: wat uit het UML-model wordt gegenereerd en hoe + +De verdeling volgt de veranderlijkheid: **handgeschreven waar de semantiek subtiel en +stabiel is (PIM, versionering, resolvers), gegenereerd waar het metamodel groot is en met de +spec meebeweegt** (alles wat de vorm van een metaclass heeft). + +| Gegenereerde sectie | Bron van waarheid | Emitterende helper | +|---|---|---| +| §1 enum-typen | UML-enumeraties | `WriteEnumTypes` | +| §2 register-seeds (1 release + 175 class kinds) | ClassKindRegistry (gevalideerd tegen het UML-model) | `WriteMetamodelCatalogRows` | +| §6 link-tabellen | multi-valued stored properties | `WriteLinkTables` | +| §7 subtype-tabellen (47) | scalaire stored declaraties, bounds, XMI-defaults | `WriteSubtypeTables` | +| §11 views (167) | storage-ancestor-verzamelingen | `WriteFlatteningViews` | +| §12 partitielijst, §13 modelversie | tabelinventaris, root package | `WritePartitionedTableArray`, `WriteModelVersion` | +| §14 referencevalidatie, twee lagen (42 bronnen; volledig + incrementeel) | stored referencekolommen + toegestane doeltypen per declared type | `WriteReferenceValidation` | + +De pijplijn: `SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/SQLSchemaGenerator.cs` +leest de XMI via uml4net en rendert `core-sql-schema-2.hbs` (waarvan de handgeschreven +secties byte-voor-byte gelijk worden gehouden aan `schema.golden.sql`). De census-logica zit +in `SysML2.NET.CodeGenerator/Extensions/SqlSchemaExtensions.cs` (snake_casing, typemapping, +en het bepalen van declared properties volgens de twee valkuilen van sectie 3); de emitters +in `SysML2.NET.CodeGenerator/HandleBarHelpers/SqlSchemaHelpers.cs`. De generator wordt +aangestuurd vanuit +`SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/SQLSchemaGeneratorTestFixture.cs`. + +De verificatielus, van begin tot eind: draai de fixture → zet `schema2.generated.sql` op een +PostgreSQL 18 (`max_locks_per_transaction=4096`; beide schema's zijn geverifieerd op 17 én +18.6, het recept volgt het prefereer-18-versiebeleid van §13) → draai `schema.smoke.sql` +(30 assertions) → en zowel het golden als het gegenereerde schema moet daar identiek doorheen +komen. + +--- + +## 18. Multi-user en concurrency + +De korte versie: **het schema is voor readers uitgesproken concurrency-vriendelijk, bundelt +alle schrijfcontentie in precies één rij per branch, en legt vijf echte +multi-user-verantwoordelijkheden bij de service-laag.** Deze sectie maakt elk van die drie +beweringen concreet — inclusief het ene protocol dat het schema wél *vereist* maar niet zelf +kan *afdwingen*. + +### 18.1 Wat het ontwerp uit zichzelf al oplost + +**Append-only is hier de concurrency-strategie, niet alleen de versioneringsstrategie.** +`element_version`, `derived_version`, `commit`, `commit_parent` en `commit_checkpoint` +krijgen in de normale gang van zaken nooit een UPDATE of DELETE te zien. Onder PostgreSQL's +MVCC heeft dat een krachtig gevolg: readers blokkeren nooit writers, writers blokkeren nooit +readers, en twee writers kunnen elkaar alleen in de weg zitten als ze *dezelfde rij* willen +schrijven — en immutable rijen zijn nooit dezelfde rij. Wie een model op commit C leest, +leest data die *niet meer kan veranderen*: dat beeld is zonder ook maar één lock herhaalbaar, +onbeperkt te cachen, en blijft kloppen ook als een collega midden in je leesactie commit. + +**Alle muteerbare state is bewust op twee plekken geconcentreerd:** `branch.head_commit_id` +(plus `base_commit_id`) en de `branch_head`-overlay-rijen van die branch. Al het overige dat +een commit schrijft is een kale insert. Daarmee is het *totale* schrijfconflict-oppervlak van +een project: **één `branch`-rij per branch**. Twee committers op *verschillende* branches +raken disjuncte muteerbare rijen en kunnen elkaar per definitie niet raken; twee committers +op *dezelfde* branch botsen op precies één rij — en dat hoort ook zo, want een branch is per +definitie een seriële geschiedenis. De contentie in de database weerspiegelt exact de +semantiek van het domein. + +**Single-statement reads kunnen niet scheuren.** De leesfuncties van §10 joinen +`branch → overlay → checkpoint` in *één* SQL-statement, en één statement onder READ COMMITTED +ziet één consistente snapshot — een reader kan dus nooit halverwege een compaction "nieuwe +base-pointer met oude overlay" te zien krijgen. Deze eigenschap is dragend: knipt de service +die read ooit op in twee rondgangen (eerst de branch-rij ophalen, dan de overlay bevragen), +dan is de garantie geruisloos weg. Houd zulke reads in één statement, of draai ze onder +REPEATABLE READ. + +**Checkpoint-opbouw verdraagt alles naast zich.** `build_commit_checkpoint()` leest +uitsluitend immutable historie op een vaste commit en schrijft met `ON CONFLICT DO NOTHING`. +Bouwen twee workers per ongeluk hetzelfde checkpoint, dan is dat hooguit wat verspilde +moeite; wordt er gecommit terwijl een checkpoint in aanbouw is, dan kijkt die bouw naar een +bevroren verleden waar nieuwe commits niets aan kunnen veranderen. Precies daarom kan het +cadence-beleid volledig asynchroon draaien, zonder enige coördinatie. + +**Gewoon READ COMMITTED volstaat — SERIALIZABLE is nergens nodig.** Dat is rechtstreekse +winst van append-only plus de één-muteerbare-rij-trechter, en iets om te koesteren wanneer de +service wordt gebouwd. + +### 18.2 Het verplichte commit-protocol (normatief voor de service-laag) + +**Gelijktijdige commits op dezelfde branch zijn een lost-update-bug in wording, tenzij de +service compare-and-swap op de head toepast.** Het faalscenario: gebruikers A en B lezen +allebei `head = c5`, bouwen allebei een commit met parent c5, en schrijven allebei weg. De +head verschuift twee keer, en de commit van één van beiden bungelt onbereikbaar buiten de +branch — zonder dat iemand iets merkt. Het schema kán dit niet voorkomen, want "de parent +waar ik tegenaan heb gebouwd" is applicatiestate. Het protocol (het Git-model; dat de +OMG-`createCommit` een `previousCommit` meekrijgt zegt het eigenlijk al): + +```sql +BEGIN; +-- Optie A (optimistisch, aanbevolen): CAS op de head +UPDATE sysml2.branch + SET head_commit_id = :new_commit + WHERE id = :branch AND head_commit_id = :expected_parent; +-- rowcount 0 => iemand anders was je voor: ROLLBACK, geef 409, client rebased + +-- Optie B (pessimistisch): SELECT ... FOR UPDATE op de branch-rij aan het begin +-- van de transactie; committers op die branch staan dan netjes in de rij. +-- Eenvoudiger; blokkeert in plaats van te falen. + +-- daarna, allemaal conflictvrije kale inserts: +-- commit + commit_parent (de trigger valideert de monotonie), +-- element_version + subtype- + link-rijen + stored_json, +-- derived_version-rijen (de impact radius), +-- branch_head-overlay-upserts. +COMMIT; +``` + +De branch-rij als **eerste** aanraken heeft nog een prettige bijvangst: elke writer hanteert +dan dezelfde lock-volgorde, en deadlockpreventie krijg je er gratis bij. **Ook compaction +(§10.2) moet diezelfde branch-lock nemen**: als het verzetten van `base_commit_id` en het +legen van de overlay dwars door de overlay-upserts van een commit heen lopen, beschrijft de +overlay daarna zijn afwijking ten opzichte van de verkeerde base. + +**Stempel `commit.created` met `clock_timestamp()`, nooit met het transactie-start-`now()`.** +Onder concurrency zou een transactie die begon *vóórdat* de transactie van de huidige head +committe, haar commit een timestamp geven die VROEGER is dan die van haar parent — en dan +weigert `trg_commit_parent_monotonic`. Een `clock_timestamp()` genomen ná het lezen van de +verwachte head is altijd strikt later dan de stempel van die head. (Boven water gekomen door +de concurrency-suite hieronder.) + +**Dit protocol is onder echte gelijktijdigheid geverifieerd** door de ingecheckte +pgbench-suite (`SysML2.NET.CodeGenerator/Sql/schema.concurrency.*.sql` — setup, +hot-branch-race, spread, reader-onder-schrijfstorm, en een invariantenverificatie C1–C5). +Gemeten op PG18 met 16 racende clients op ÉÉN branch: ~1.000 pogingen/s, exact één winnaar +per head-waarde, verliezers schrijven niets (83% CAS-conflictrate bij vol hameren — het +§15.15-signaal in zijn slechtste geval), heads strikt lineair, nul deadlocks; dezelfde +clients gespreid over 16 branches: ~2.200 commits/s bij 0% conflicten — contentie is +branch-lokaal, zoals ontworpen. Reads bleven op ~1,2 ms (vanaf 0,8 ms in rust) terwijl de +schrijfstorm liep: de MVCC-belofte van §18.1, gemeten. + +### 18.3 Nadelen en open keuzes + +1. **Het derived-rekenvenster rekt de kritieke sectie op.** Derived values moeten worden + berekend tegen exact de parent-snapshot. Binnen de branch-lock is dat vanzelf correct — + maar een hernoeming bij de root betekent ~1M waarden uitrekenen, minutenlang de lock + vasthouden en elke andere committer op die branch laten wachten. Het betere patroon is + optimistisch: reken *vóór* het locken, doe dan de CAS, en als de head intussen is + verschoven: reken het (meestal kleine) verschil bij en probeer opnieuw. Meer code — en de + plek waar de subtiele bugs zullen zitten. Tussen branches onderling speelt dit niet: + derived-rijen zijn gesleuteld op `(identity, commit)`, en verschillende branches maken + verschillende commits. +2. **Pagineren op HEAD is een klassieke multi-user-valkuil.** Serveer pagina 1 op head = c5, + laat een collega committen, en "lees de head opnieuw" geeft voor pagina 2 een gescheurde + verzameling terug. De remedie: los branch → commit één keer op, stop het `commitId` in het + page token, en pagineer tegen de immutable commit. Het schema is er klaar voor — daar zíjn + commits voor — maar de service moet het wel echt zo doen. +3. **De kale-uuid-PK van `data_identity` maakt `@id`s globaal per instantie, niet per + project** — de bewust betaalde prijs voor FK-bare cross-project-references (sectie 7). + Gevolg: twee projecten kunnen niet allebei een element met hetzelfde `@id` hebben. Voor + random v4-ids maakt dat niets uit; voor *door de client aangeleverde* ids (kpar-imports, + klonen tussen projecten, deterministische v5-ids) is het reëel — de tweede insert knalt op + de PK. De service moet bij klonen over projectgrenzen dus verse `@id`s uitgeven, en de + PK-schending vertalen naar een nette 409 Conflict. +4. **De monotonie-trigger kan legitieme snelle commits afkeuren.** `created` moet *strikt* + nieuwer zijn dan elke parent; twee commits binnen dezelfde microseconde op één lijn + (burst-automatisering) worden geweigerd — luidruchtig, en dat is de bedoeling. De service + heeft dus een herstempel-en-opnieuw nodig, en bij meerdere app-servers laat je `created` + bij voorkeur door de database zetten (de `DEFAULT now()`) in plaats van te vertrouwen op + klokken die uit de pas lopen. +5. **Geen row-level security.** Wie project X mag zien is vandaag volledig een zaak van de + service — een keuze, geen vergissing. PostgreSQL-RLS op `project_id` past naadloos op dit + schema (elke elementtabel heeft de kolom) en is de logische verhardingsstap zodra de + database ooit aan minder vertrouwde componenten wordt blootgesteld. +6. **Populaire containers zijn natuurlijke botspunten.** Omdat de kindlijst van een + container stored state is, levert *elke* kind-toevoeging een nieuwe version van de parent + op (§15, punt 8) — twee gebruikers die kinderen aan hetzelfde package hangen botsen dus + altijd, en veelgebruikte containers (root-packages, library-mappen) botsen aan de lopende + band. De three-way collection merge van §15.8 is wat dit werkbaar houdt; zonder die + auto-merge verwordt gelijktijdig werken tot om-de-beurt werken op precies de containers + die iedereen deelt. (Dezelfde koppeling is ook de R7-write-amplification-hotspot: elke + nieuwe container-version herschrijft zijn volledige collectierijen.) +7. **De kleine lettertjes.** `UNIQUE (project_id, name)` maakt van twee gelijktijdige + branches met dezelfde naam een constraint-fout (vertaal naar 409, klaar). De + GIN-pending-list-flushes op `derived_version` kunnen gelijktijdige derived-zware commits + op een gedeelde partitie heel even serialiseren (auditbevinding R5). En `fillfactor = 90` + op `branch_head` is er precies om de overlay-churn van veel tegelijk actieve branches op + te vangen zonder dat de index opzwelt. + +--- + +## 19. Begrippenlijst + +Bijlage: elke vakterm die in deze gids voorkomt, met de kortst mogelijke maar duidelijke +omschrijving en — waar de gids de term verder uitlegt — de sectie om te lezen. De nummers in +de *Zie*-kolom zijn **gidssecties** (niet de §-banners van de schemabestanden); "—" betekent +dat de term alleen terloops voorkomt. + +| Term | Omschrijving | Zie | +|---|---|---| +| **@id / @type** | De twee JSON-sleutels op elk API-record: de stabiele identity van het element (uuid) en zijn metaclass-naam. | 7, 12.1 | +| **Anti-join** | "Rijen in A zonder match in B" (`NOT EXISTS`); zo trekt de set-read de ge-overlayde identities van het checkpoint af. | 11 | +| **Append-only** | Tabellen waar alleen ooit INSERT's in gaan; tegelijk het historiemodel en de concurrency-strategie. | 4, 18.1 | +| **Association-owned end** | Een UML-reference-property die eigendom is van de association in plaats van de class; onzichtbaar in `OwnedAttribute` (generatorvalkuil 1). | 3 | +| **Autovacuum** | PostgreSQL's achtergrondproces voor opruimen en statistieken; per schrijfprofiel getuned in de partitielus. | 13 | +| **Base commit** | De gecheckpointe commit waar de overlay van een branch van afwijkt (`branch.base_commit_id`). | 10.2 | +| **Branch** | Een muteerbare, benoemde pointer naar een head commit — het enige muteerbare object in de versioneringskern. | 6.3 | +| **btree** | PostgreSQL's standaard geordende indextype; elke PK en lookup-index hier. | — | +| **Cadence** | Het churn-gebaseerde servicebeleid dat bepaalt welke commits een checkpoint krijgen. | 10.1 | +| **CAS (compare-and-swap)** | De branch-head alleen atomair bijwerken als hij nog de verwachte waarde heeft; het normatieve commit-protocol. | 18.2 | +| **Census** | De kwantitatieve telling van het metamodel (stored vs derived, typen, overerving) waar elke ontwerpkeuze op is gebaseerd. | 3 | +| **Change set** | De DataVersions die één commit schrijft (`Commit.change`) — de delta. | 6.2 | +| **CHECK constraint** | Een geldigheidregel op rijniveau (bv. de elkaar uitsluitende tombstone/payload-vormen). | 8.1 | +| **Checkpoint** | Een volledig gematerialiseerde fold van één commit (`commit_checkpoint`); begrenst resolver-wandelingen en fundeert overlays. | 10.1 | +| **class_kind (interning)** | Het smallint-id per metaclass — interning van de canonieke NAAM, met ids die voor altijd vastliggen door het append-only register; afnemers laden de mapping at runtime of gebruiken de uit het register gegenereerde enum. | 12.1, 15 | +| **Class-kind register** | De ingecheckte, append-only bron van waarheid (`ClassKindRegistry.cs`) die class_kind-ids en model_version-ordinals over releases heen bevriest; de generator valideert het UML-model ertegen en faalt bij drift. | 12.1 | +| **Commit** | Een immutable vastlegging van de wijzigingen op één moment; een knoop in de commit-DAG. | 6.1 | +| **Commit-DAG** | De directed acyclic graph die commits vormen zodra branchen en mergen mag. | 6.1 | +| **CommitReference** | De abstracte spec-basis van Branch en Tag: een benoemde verwijzing naar een commit. | 6.3 | +| **Compaction** | Een uitgegroeide overlay opnieuw funderen op een vers checkpoint op de branch-head. | 10.2 | +| **Conformance (Derived Property)** | De drie spec-niveaus — none / passthrough / full — hier gerealiseerd als write-path-policies op één en hetzelfde schema. | 1, 9.4 | +| **Conversion commit** | Een commit met één parent die de model-version-stempel ophoogt en elk element herformuleert waarvan de vorm tussen twee releases veranderde; de enige manier waarop een branch upgradet. | 6.4 | +| **CTE (recursief)** | Een `WITH`-query die naar zichzelf verwijst; de ancestry-wandeling van de resolvers. | 10.4 | +| **Dangling reference** | Een verwijzing in het model naar een element dat op de gelezen commit niet bestaat; een validatiekwestie, nooit een FK-schending (axioma 1). | 4 | +| **data_identity** | De driekoloms tabel die de stabiele element-identity verankert (uuid, project, onveranderlijke class_kind); het FK-doel van elke element-reference. | 7 | +| **DataIdentity / DataVersion** | Spec-typen: de version-onafhankelijke identity van data / de payload-verpakking per commit (≈ `element_version`-rij). | 2, 6.2 | +| **Derived property** | Een metamodel-property die uit andere elementen wordt berekend (77% van het metamodel); woont in `derived_version`, gesleuteld op (identity, commit) — axioma 2. | 3, 9 | +| **derived_json / stored_json** | De voorgeserialiseerde helften van een element-payload; bij het lezen aan elkaar geplakt. | 8.1, 9 | +| **DISTINCT ON** | PostgreSQL's eerste-rij-per-groep-selectie; zo werkt "nieuwste version wint" in de fold. | 10.4 | +| **EAV** | Entity-attribute-value-modellering (generieke property-rijen); afgevallen als system of record, smal behouden (link-tabellen). | 5.3 | +| **element_version** | De centrale append-only tabel met stored element state; één rij per (element, commit-die-het-wijzigde). | 8.1 | +| **EXPLAIN (ANALYZE, BUFFERS)** | Plan- én uitvoeringsmeting; het enige gereedschap dat bugs van de planner-klasse vangt. | 14 | +| **Fillfactor** | Vrije ruimte die per pagina wordt gereserveerd voor in-page (HOT-)updates; 90 op `branch_head`. | 13 | +| **Flattened property** | De eigen en geërfde properties van een metaclass bij elkaar (12.963 over het hele metamodel). | 3 | +| **Flattening view (`vw_`-prefix)** | Per concrete metaclass één gegenereerde view (bv. `vw_part_usage`) die de DTO-rijvorm reconstrueert; `vw_` staat voor *view* — bewust anders gespeld dan het `_version`-tabelachtervoegsel. | 12.3 | +| **Fold** | De `versionedData`-berekening uit de spec: de eigen wijzigingen van een commit plus alles wat hij uit de snapshots van zijn parents erft. | 6.2 | +| **Foreign key (FK)** | Een referentiële constraint; voor element-references altijd → `data_identity`, nooit → een version. | 7 | +| **Full conformance** | Derived values berekend en actueel in elk antwoord, en querybaar; het ontwerpdoel. | 9.1, 9.4 | +| **GIN-index** | Een inverted index over jsonb-sleutels en -waarden; bedient containment-probes op niet-gepromote derived properties. | 9.3 | +| **Hash-partitionering** | Elke tabel opknippen in N stukken via `hash(project_id)`; over alle elementtabellen gelijk. | 13 | +| **HOT update** | Een heap-only update die de indexen ongemoeid laat; vraagt om fillfactor-ruimte. | 13 | +| **Identity** | Het stabiele `@id` van een element, zijn leven lang; een `data_identity`-rij; het enige waar references naar wijzen (axioma 1). | 4, 7 | +| **Impact radius** | De verzameling elementen waarvan een change set de derived values ongeldig maakt; bepaalt welke `derived_version`-rijen een commit schrijft. | 9.2, 15 | +| **Index-only scan** | Een query beantwoorden vanuit alleen de index; vraagt om een bijgewerkte visibility map (vandaar insert-gedreven vacuum). | 13 | +| **Invariant** | Een regel die altijd waar moet zijn; de vier merge-invarianten schragen de resolver. | 6.1 | +| **jsonb** | PostgreSQL's binaire JSON-kolomtype. | — | +| **KerML** | Kernel Modeling Language — de fundamentlaag onder SysML v2. | 1 | +| **Link-tabel** | Een geordende `(version, ordinal, target)`-tabel voor een multi-valued stored property; er zijn er 7. | 8.2 | +| **lz4** | Snelle compressie voor geTOASTe waarden; toegepast op beide jsonb-kolommen. | 13 | +| **max_locks_per_transaction** | De maat van de locktabel; ≥ 4096 is een deployment-eis voor schema-brede DDL. | 13 | +| **Merge (commit)** | Een commit met twee of meer parents, waar branches samenkomen. | 6.1 | +| **Merge-invarianten** | De vier regels uit Clause 7.1.2 (monotonie, restatement, deletiegeldigheid, uniciteit) waar de resolver op steunt, plus de eigen vijfde van dit schema: release-compatibiliteit. | 6.1, 6.4 | +| **Metaclass / metamodel** | Een type in de modelleertaal (175 stuks) / het geheel daarvan. | 1, 3 | +| **Model version** | Een geregistreerde metamodel-release (`model_version`-tabel); elke commit draagt de stempel van de release waarin zijn payloads zijn geschreven — die stempel, niet de branch of het project, is de waarheid. | 6.4, 12.1 | +| **Model-version descriptor** | Per-release gegenereerde C# die draagt wat de vervallen catalogustabellen vastlegden: de metaclasses van elke release, hun subtype-tabellensets en de property→opslag-routering voor de Query-vertaler. | 12.2 | +| **Monotonie** | Een commit is strikt nieuwer dan elk van zijn parents; met een trigger afgedwongen, omdat schendingen snapshots geruisloos corrumperen. | 6.1 | +| **MVCC** | Multi-version concurrency control: readers zien consistente snapshots en blokkeren writers nooit. | 18.1 | +| **n_distinct** | De distinct-waarden-statistiek van de planner; dat die op checkpoint-rijen naar 1 klapt, leidde tot het registry-herontwerp. | 10.3 | +| **OCL** | Object Constraint Language — de formele derivatie- en constraint-formules van de spec. | 3, 6.2 | +| **Overlay** | De sparse inhoud van `branch_head`: alleen de identities waarop een branch afwijkt van zijn base checkpoint. | 10.2 | +| **Ownership chain** | Het containment-pad van een element omhoog tot zijn root namespace (de wandeling achter `qualifiedName`). | 3 | +| **Ownership quadruple** | De vier opslagplekken die samen één eigendomsfeit vastleggen (parents `ownedRelationship`, memberships `owning_related_element` + `ownedRelatedElement`, kinds `owning_relationship`); de service moet ze samenhangend schrijven — het schema kan ze niet onderling controleren. | 15 | +| **Partition pruning** | De planner die irrelevante partities links laat liggen; vereist een `project_id`-predicaat — de les van R2. | 11, 13 | +| **Passthrough** | Door de client aangeleverde derived values opslaan en getrouw teruggeven, zonder zelf te rekenen; door hetzelfde schema gedragen. | 9.4 | +| **PIM** | Platform-Independent Model: de repository-machinerie van de spec (Project, Commit, Branch, …); de handgeschreven laag. | 2, 6 | +| **Promoted column** | Eén van de zes derived properties met een echte geïndexeerde kolom (`owner`, `qualified_name`, `name`, `short_name`, `owning_namespace`, `is_library_element`). | 9.3 | +| **Property catalog** | De voormalige databasetabel die elke API-property-naam naar zijn fysieke opslag wees; vervallen ten gunste van de per-release model-version descriptors. | 12.2 | +| **READ COMMITTED** | PostgreSQL's standaard isolatieniveau — hier overal toereikend, en dat is geen toeval. | 18.1 | +| **Redefinition (same-name / new-name)** | Same-name redefinitions zijn opslagvrij (ze wijzen door naar de kolom van de root); new-name redefinitions krijgen eigen opslag (generatorvalkuil 2). | 3 | +| **Referencevalidatie (gegenereerd, two-tier)** | `validate_references_in_commit()` per commit (O(change set), inclusief de omgekeerde richting die tombstones breken) + `validate_references_at_commit()` als periodieke volledige audit (O(snapshot × log historie)); wrong-type via de typed identity, dangling via het snapshot; bewust functies, nooit constraints. | 7 | +| **Registry** | `commit_checkpoint_registry` — één rij per checkpoint, zodat bestaansprobes een tabel raken waarvan de korrel bij de vraag past. | 10.3 | +| **Resolver** | Een SQL-functie die een indirecte verwijzing ("commit C", "head van branch B") omzet in het concrete snapshot: per element de mapping identity → (version_id, derived_id), via de fold begrensd door checkpoints. Er zijn er drie: `resolve_commit_state`, `resolve_element_at_commit`, en het branch-head-leespad als hun voorgematerialiseerde vorm. | 10.4 | +| **RLS** | Row-level security; bewust afwezig, en de logische verhardingsstap zodra die nodig wordt. | 18.3 | +| **Sequential scan** | Een hele tabel doorlezen; de stille faalmodus achter de bevindingen R2, R3 en de registry. | 14 | +| **Sibling commits** | Commits op parallelle branches met dezelfde parent; mogen legaal een timestamp delen — vandaar de tiebreaker. | 6.1, 10.4 | +| **Skip scan** | Een btree-scan op een niet-leidende indexkolom; bestaat pas vanaf PG18, en ook dáár heeft partition pruning nog steeds `project_id` nodig — de regel blijft dus staan. | 11 | +| **Snapshot** | De volledige modeltoestand op één commit (`versionedData`); opgelost via checkpoint + fold. | 6.2, 10 | +| **Specialization closure** | De transitieve verzameling super-/subtypen van een type; waar `Type::feature` overheen foldt. | 3 | +| **Stored property** | Een niet-derived metamodel-property die echt wordt opgeslagen, in kolommen of link-tabellen (2.698 flattened; 97 declaraties). | 3 | +| **Storage-declarerende metaclass** | Een metaclass die ≥ 1 eigen stored scalar declareert → krijgt een subtype-tabel (47 stuks). | 8.3 | +| **Subtype-tabel** | De tabel per storage-declarerende metaclass, gesleuteld op `(project_id, version_id)`; de DAG geregeld via lidmaatschap, niet via joins. | 8.3 | +| **Superset schema** | Het fysieke-schema-beleid voor multi-version support: tabellen en kolommen zijn de unie over alle geregistreerde releases; er wordt nooit iets verwijderd, hernoemingen worden nieuwe kolommen. | 6.4 | +| **System of record** | De gezaghebbende genormaliseerde kolommen en link-tabellen, tegenover het jsonb-leesmodel. | 5.4, 8.1 | +| **Tag** | Een immutable maar verwijderbare benoemde verwijzing naar een commit. | 6.3 | +| **Three-way collection merge** | Twee gewijzigde versies van een geordende collectie samenvoegen tegen hun gemeenschappelijke basis; additieve disjuncte wijzigingen lossen automatisch op, herordeningen/verwijderingen gaan naar een mens. | 15, 18.3 | +| **Tiebreaker (`id DESC`)** | De deterministische ordening voor als sibling commits een timestamp delen. | 10.4 | +| **Tombstone** | Een version-rij die een deletie op een commit markeert (`payload = null` in spec-termen). | 8.1 | +| **TPT (table-per-type)** | Eén tabel per class, samengevoegd langs de overerving; afgevallen — de DAG breekt de keten. | 5.2 | +| **Typed identity** | `data_identity.class_kind`: de onveranderlijke metaclass van het element op de identity-rij, waardoor het type FK-baar wordt waar versions dat nooit zijn — op elke version afgedwongen via een composite FK. | 7 | +| **Upsert (`ON CONFLICT`)** | Invoegen-of-bijwerken in één statement; zo houden commits de overlay bij. | 10.2, 18.2 | +| **UUID v4 / v5 / v7** | Random / name-based / tijdgeordende uuid's; v7 aanbevolen voor app-gegenereerde sleutels (op PG18 zelf-geactiveerd als `DEFAULT uuidv7()`), v5 normatief voor library-elementIds. | 7, 13 | +| **Version** | De stored state van één element per één commit; een `element_version`-rij; immutable. | 4, 8.1 | +| **WAL** | PostgreSQL's write-ahead log — de duurzaamheidsprijs van elke write. | 14 | +| **XMI** | XML Metadata Interchange — de UML-bestanden die voor de generatie de bron van waarheid van het metamodel zijn. | 2, 17 | + +--- + +*Verwante documenten: `SysML2.NET.CodeGenerator/SQLSCHEMA.md` (compacte referentie, +gerangschikte audittabel, benchmarkpoort); de §-genummerde banners in +`SysML2.NET.CodeGenerator/Sql/schema.golden.sql` (rationale per object, ter plekke); +`SysML2.NET.CodeGenerator/Sql/schema.smoke.sql` (de uitvoerbare vorm van secties 4, 10 en 16). +De Engelstalige versie van deze gids: `SysML2.NET.CodeGenerator/SQLSCHEMA-GUIDE.md`.* diff --git a/SysML2.NET.CodeGenerator/SQLSCHEMA.md b/SysML2.NET.CodeGenerator/SQLSCHEMA.md new file mode 100644 index 000000000..9a364793f --- /dev/null +++ b/SysML2.NET.CodeGenerator/SQLSCHEMA.md @@ -0,0 +1,372 @@ +# SQLSCHEMA.md — PostgreSQL persistence schema for SysML v2 models + +This document is the compact design reference for the SQL schema code-generation pipeline. +For the long-form architectural guide — the full reasoning chain behind every table, function, +and index, with worked examples and the performance war stories — see +`SysML2.NET.CodeGenerator/SQLSCHEMA-GUIDE.md` (Dutch translation: +`SysML2.NET.CodeGenerator/SQLSCHEMA-GUIDE.nl.md`). Guide section map: §1–§5 problem, census +and rejected alternatives; §6–§13 layer-by-layer walkthrough (§6.4: multi-version support — +commit-stamped metamodel releases, the append-only class-kind registry, and conversion +commits; §9.4: how all three Derived Property Conformance levels — none / passthrough / +full — map onto the same schema as write-path policies); §14 performance audit; §15 +service-layer obligations; §16 worked +examples; §17 code generation; **§18 multi-user and concurrency — including the NORMATIVE +compare-and-swap commit protocol (§18.2) that every service implementation must follow**; +§19 glossary. + +The pipeline artifacts: + +| Artifact | Role | +|---|---| +| `SysML2.NET.CodeGenerator/Sql/schema.golden.sql` | Hand-written, annotated reference design. Carries the rationale comments. | +| `SysML2.NET.CodeGenerator/Sql/schema2.generated.sql` | The actual generator output, checked in for review. Supersedes the golden's `[GENERATED]` excerpts. | +| `SysML2.NET.CodeGenerator/Sql/schema.smoke.sql` | Functional test. 30 assertions; raises on any wrong answer. Runs against golden AND generated schema. | +| `SysML2.NET.CodeGenerator/Sql/schema.concurrency.{setup,hot,spread,read,verify}.sql` | Multi-user suite: pgbench scenarios racing the §18.2 CAS protocol (hot branch / spread / reads-under-write-storm) + invariant verifier C1–C5 (linear chains, losers write nothing, overlay coherence). | +| `SysML2.NET.CodeGenerator/Templates/Uml/core-sql-schema-2.hbs` | Handlebars template: hand-written sections verbatim, `[GENERATED]` sections via helpers. | +| `SysML2.NET.CodeGenerator/HandleBarHelpers/SqlSchemaHelpers.cs` | The eight `uml_template.SQL2.*` helpers. | +| `SysML2.NET.CodeGenerator/Extensions/SqlSchemaExtensions.cs` | Naming, type mapping, and the stored-property census logic. | +| `SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/SQLSchemaGenerator.cs` | The generator (emits `schema2.sql`). | +| `SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/ClassKindRegistry.cs` | The checked-in APPEND-ONLY registry freezing `class_kind` ids and `model_version` ordinals across metamodel releases; the generator validates the UML model against it and fails on drift. | +| `SysML2.NET.CodeGenerator/IMPACT-RADIUS.md` | Design sketch for the derived-property impact-radius engine (obligation §15.1): propagation kinds, `derived_dependency` catalog, early cutoff, differential-testing oracle. | + +The old `SysML2.NET.CodeGenerator/Templates/Uml/core-sql-schema.hbs` and the fully commented-out +COMET-style helpers it referenced are superseded by this pipeline and kept only for comparison. + +## Why this shape + +The metamodel realised in this repo has 175 metaclasses (167 concrete) and 12,963 flattened +properties — but only ~2,700 of them are stored (`{ get; set; }` in `SysML2.NET/Core/AutoGenDto/`); +the other ~9,600 (77%) are derived. The stored surface collapses to under 100 property +declarations across 49 metaclasses, with the widest metaclass at 24 stored columns. Two facts +force everything else: + +1. **Derived values are functions of (version, snapshot), not of the version alone.** Renaming a + Namespace changes every descendant's `qualifiedName` without any descendant's stored state + changing. The OMG API spec (Systems Modeling API and Services v1.0, Clause 2) warns of exactly + this: derived properties of an element may be affected by commits that do not directly change + that element. +2. **References name identities, not versions.** A SysML2 reference targets a stable element + `@id`. Under commits/branches, referential integrity can only be enforced against the identity + (`data_identity` — TYPED: it carries the element's immutable `class_kind`, so a version can + never contradict its identity's metaclass); "does the target exist at commit C" is a + validation query, not an FK — generated as `validate_references_at_commit()` (guide §7). + +## Architecture (four layers) + +1. **PIM / versioning** (hand-written): `project`, `commit`, `commit_parent` (the DAG — a commit + has a SET of parents; merges are real), `branch`, `tag`, `project_usage`, `data_identity`. + A trigger enforces the spec's monotonic-commit-timestamp invariant because the snapshot + resolver depends on it ("newest ancestor wins"). **Multi-version support** lives here too: + every commit carries `model_version_id` (the metamodel release its payloads are written in; + guide §6.4), `project.target_model_version_id` is the upgrade policy, and a second trigger + (`trg_commit_parent_version`) forbids downgrades and mixed-release merges — a branch + upgrades via a single-parent CONVERSION COMMIT. +2. **Stored element state** (append-only): `element_version` (core; Element's own six stored + properties folded in; `tombstone` = `DataVersion.payload = null`; `stored_json` = the + pre-serialized stored half of the payload) + 47 generated subtype tables (one per + storage-introducing metaclass, keyed `(project_id, version_id)`) + 7 generated ordered link + tables for the multi-valued stored properties. +3. **Derived element state** (append-only, the second stream): `derived_version` keyed by + `(identity, commit)` — a row is written only for elements whose derived values actually + changed at a commit (the change set's impact radius). Six hot derived properties are promoted + to real columns (`owner`, `owning_namespace`, `qualified_name`, `name`, `short_name`, + `is_library_element`) so the Query service can filter/ORDER BY them; the remaining ~325 + distinct derived names live in `derived_json` behind a GIN index. +4. **Snapshot resolution + read path**: materialized `branch_head` (updated incrementally, + O(changeset)), `commit_checkpoint` for historical commits, and `resolve_commit_state()` as the + general recursive-CTE fold. `GET .../elements/{id}` is `stored_json || derived_json` — one + jsonb concat, no joins, no recursion, no derived computation at read time. + +Derived-property conformance level targeted: **Full Conformance, precomputed at commit time** +(the existing 366 implemented `SysML2.NET/Extend/*.Compute*` methods are the computation engine; +the impact-radius analysis lives in .NET, not SQL). The schema itself is conformance-agnostic — +the level is a write-path policy, and passthrough / no-conformance are supported by the same +DDL (guide §9.4). + +## What is generated vs hand-written + +Generated from the UML XMI (`Resources/KerML_only_xmi.uml` + `Resources/SysML_only_xmi.uml`): + +- the 7 enum types (labels = lowercase C# literals, matching the JSON wire format); +- the `model_version` + `class_kind` registry seeds — emitted from the checked-in append-only + `ClassKindRegistry.cs`, NOT computed from the model: **ids are frozen forever once assigned** + (a new release appends after the highest id; dropped classes close with `removed_in`), the + generator fails on any model↔registry drift, and the seeds are idempotent + (`ON CONFLICT (id) DO NOTHING` — safe to re-apply; the old fresh-installs-only trap is gone; + see guide §12.1/§15.14). The former `class_kind_table` and `property_catalog` tables are + REMOVED: nothing in the schema read them, and their content becomes per-release generated C# + (model-version descriptors, guide §12.2); +- the 7 link tables and 47 subtype tables, with NULL-ability from the property's lower bound, + `DEFAULT` clauses from the XMI-declared default values, FKs to `data_identity`, and a + reverse-lookup index on every reference column; +- 167 per-metaclass flattening views (`vw_part_usage`, …) reconstructing the DTO row shape; +- the TWO-TIER reference validation, one `UNION ALL` arm per stored reference column (42), + reporting `'wrong-type'` (via the TYPED identity: `data_identity.class_kind` plus the + composite FK from `element_version`, so a version can never contradict its identity's + metaclass) and `'dangling'` (same-project target not alive in the snapshot): + `validate_references_at_commit` (the full periodic audit, snapshot in an ANALYZE'd temp + table — O(snapshot × log history)) and `validate_references_in_commit` (the incremental + per-commit check — outgoing change-set references plus the reverse direction tombstones + break; O(change set)). Deliberately functions, not constraints — the spec allows + transiently dangling references, and liveness is per-commit (guide §7); +- the partition list and the model-version function. + +Everything else (PIM, `element_version`, `derived_version`, snapshot functions) is hand-written +and lives verbatim in the template. + +## Census rules (the part that was non-obvious) + +- **Declared properties must NOT be read from `IClass.OwnedAttribute`.** Reference properties + that are association ends (e.g. `Membership::memberElement`) are owned by the association, not + the class. The generator computes declared = `QueryAllProperties(class)` minus the union of + `QueryAllProperties(directGenerals)`. +- **Only SAME-NAME redefinitions are storage-free** (`CollectExpression::operator`, + `ConnectionDefinition::isSufficient`, `Expose::visibility`, the four `kind`s, … — 9 in total). + They resolve transitively to the root property's column. A redefinition under a NEW name + (`memberElement` redefines `target`) is a distinct API property with storage of its own — + exactly as in the generated DTOs, which store both. +- A metaclass gets a subtype table iff it declares ≥1 single-valued stored property and is not + Element. Multi-valued stored properties become link tables (`{class}_{property}` snake_case). + `value` being 4 different SQL types on the `Literal*` classes and `kind` being 4 different + enums is what rules out a single wide table. + +## Performance at scale + +Audited against the confirmed profile: **~1M elements/project, 100–500 live branches/project, +tens of thousands of commits/project, tens-to-hundreds of projects per instance.** Findings +ranked; all SEV-1/2 fixes are implemented in the schema, the policy items are specified here. + +| # | Sev | Finding | Resolution | +|---|---|---|---| +| R1 | 1 | `branch_head` was O(branches × elements): 500 × 1M = 500M rows/project (~85 GB); branch create copied the model | **Fixed**: sparse overlay — `branch.base_commit_id` (a checkpointed commit) + `branch_head` holds only divergence (`is_tombstone` masks deletions). Branch create/delete are O(divergence). | +| R2 | 1 | `get_element_at_branch_head()` filtered on bare uuids → no partition pruning, no PK use (no btree skip scan before PG18; and pruning needs `project_id` on every version) | **Fixed**: every read function resolves `project_id` through `branch` first. Verified: 15/16 partitions "(never executed)", 0.061 ms execution. | +| R3 | 1 | Every `ON DELETE CASCADE` was unindexed → branch/project deletion seq-scanned the largest tables | **Fixed**: `ix_branch_head_branch` for the branch cascade; all `data_identity` cascades demoted to NO ACTION with an explicit ordered per-table project-deletion procedure (documented at the `data_identity` DDL). | +| R4 | 2 | No checkpoint cadence; resolvers degrade linearly with distance to the nearest checkpoint — but checkpoints are O(model) each | **Policy** (service layer): checkpoint when ≥200 commits since the nearest checkpointed ancestor on the lineage OR cumulative changeset ≥25% of model; always at branch-fork bases; retention drops checkpoints no branch bases on (registry row first). `build_commit_checkpoint()` provided; run async, never on the commit path. | +| — | 1* | *(found empirically)* the resolvers' checkpoint-existence probe seq-scanned the checkpoint partition per recursion step — `n_distinct=1` on `(project_id, commit_id)` makes the planner reject the index | **Fixed**: `commit_checkpoint_registry` (one row per checkpoint) serves all existence probes. Measured: single-element historical read 3,466 ms → 1.8–4 ms; full fold 4,012 ms → 185 ms. | +| R5 | 2 | Worst-case derived burst (root rename ≈ 1M multi-KB jsonb rows) × whole-document GIN | **Partially fixed**: lz4 on `stored_json`/`derived_json` (verified inherited by leaves). Bulk derived writes: raise `gin_pending_list_limit` (≥64MB) for the session + `gin_clean_pending_list()` after. GIN stays (spec requires arbitrary-property constraints); revisit as expression indexes if the Query service's filtered-property set proves narrow. | +| R6 | 2 | Single-element historical read folded the whole model | **Fixed**: `resolve_element_at_commit()` / `get_element_at_commit()` — O(walked ancestry). | +| R7 | 3 | Link-table write amplification: a new version re-inserts the element's whole collection (pathological for 100k-child packages) | **Documented, conditional**: content-addressed collections (`collection_id` digest on `element_version`, shared ordered rows) if benchmarks fail; reshapes generated tables. | +| R8 | 3 | Random UUID PKs fragment per-project index ranges | **.NET note**: generate `version_id`/`derived_id` as UUIDv7 (`Guid.CreateVersion7()`); on PG18 the schema additionally self-activates `DEFAULT uuidv7()` on all server-minted keys (version-guarded DO block, no-op on 16/17). `identity_id` is spec-visible, stays as supplied (library elements are normatively v5). | +| R9 | 3 | One-size autovacuum for opposite write profiles | **Fixed**: `branch_head` leaves get `fillfactor 90` + analyze@50k; append-only leaves get insert-driven vacuum (100k) + analyze@50k. | +| R10 | 3 | Generic-plan flips and fast-path lock exhaustion on the 6-join views | **Ops checklist**: prefer PG18 where available (btree skip scan, AIO, `NOT VALID` FKs on partitioned tables, native `uuidv7()`), else PG17 (fast-path slots scale with `max_locks_per_transaction`); verify hot plans show "Subplans Removed: 15"; pin `plan_cache_mode = force_custom_plan` on Query-service pools if generic plans misbehave. | +| R11 | 3 | Bulk-import FK probes (~3M/1M elements). `DEFERRABLE` REJECTED (defers, doesn't reduce); `NOT VALID` needs PG18 on partitioned tables | **Documented**: measure first; importer may use `SET session_replication_role = replica` + post-import validation queries; or raise the floor to PG18 for `NOT VALID`+`VALIDATE`. | +| R12 | 4 | `data_identity` at 10⁸ rows unpartitioned | **Refuted**: narrow, read-mostly FK target; healthy. Hazards route through R3's explicit deletion. | +| R13 | 4 | Fold nondeterminism on sibling-commit timestamp ties (silent wrong answers) | **Fixed**: `id DESC` tiebreaker in every fold; smoke-tested (PASS 10a/10b). | + +Measured on PostgreSQL 17 (docker, 200k elements, 2,000 commits, checkpoint at 1,500, 100 +overlay branches — shape-faithful, scale ~5× below target): + +| Operation | Legacy design | Hardened schema | +|---|---|---| +| Branch create | 2,964 ms (copy 200k rows) | **1.8 ms** (overlay) | +| Branch delete | unindexed seq scans | 34 ms overlay / 100 ms even at 200k rows (indexed cascade) | +| Single-element head read | all 16 partitions scanned | **0.061 ms** exec; 15/16 partitions never executed | +| Single-element historical read (500 commits from checkpoint) | 3,466 ms | **1.8–4 ms** | +| Full-model fold (500 commits from checkpoint) | 4,012 ms | **185 ms** | +| Branch-head set read (200k overlay merge) | — | 1,242 ms | +| `build_commit_checkpoint` (fold 1,500 commits × 200k) | — | 2,488 ms (async budget) | + +**Extreme-scale validation** (PostgreSQL 17, docker, `shared_buffers=2GB`): a 1M-element +"giant" project (2,000 commits, checkpoints at 1,500 and 1,900, 200 overlay branches) plus +**40 co-tenant projects (20k elements each) sharing its hash partitions** — ~1.84M +`element_version`, ~1.84M `derived_version`, 2.8M `commit_checkpoint` rows, 2.6 GB total: + +| Operation | @200k (single tenant) | @1M (41 tenants) | Verdict | +|---|---|---|---| +| Single-element head read (overlay / checkpoint-fallback) | 3–9 ms | 3.9–4.8 / 2.9–3.7 ms | **flat — log-N as designed** | +| Same read on a small tenant sharing the giant's partitions | — | 3.4–3.9 ms | **no cross-tenant interference** | +| Single-element historical read | 1.8–4 ms | 2.1–2.8 ms | flat | +| Full-model fold (from checkpoint) | 185 ms | 1,100 ms | linear O(model), as documented | +| Branch-head set read | 1,242 ms | 1,124 ms | O(model); **API pagination is mandatory at 1M** | +| GIN probe, non-promoted property, shared partition | — | 1.4–3.1 ms; recheck removed only 10 foreign-tenant rows | R5 cross-tenant concern minor at this shape | +| `qualified_name` equality / LIKE-prefix | — | 0.3 ms / 48 ms | prefix queries: revisit `text_pattern_ops` if hot | +| 100-row commit transaction (ev + dv + overlay upsert) | < 100 ms | ~18 ms | ✓ | +| Branch create / delete (overlay) | 1.8 / 34 ms | 1.2 / 1.9 ms | **O(1) confirmed at 1M** | +| `build_commit_checkpoint` at 1M | extrapolated 12–15 s | **14.9 s measured** | async-budget claim confirmed; the "incremental" build at 1,900 also cost ~17 s — checkpoint builds are O(model) regardless of ancestry | +| 5-join flattening-view planning (928 leaves in catalog) | — | 3.0 ms | R10 fine | +| GIN insert overhead during 1M bulk derived write | — | ~30k rows/s (vs ~31k/s without GIN on element_version) | modest with small `derived_json`; re-measure with production-size (~2–4 KB) documents | + +Extrapolation to the edge profile (250 projects: 5×1M + 20×250k + 225×20k ≈ 14.5M identities, +~35M element_version, ~120M derived_version, ~30M checkpoint rows): the single-element paths +stay flat (all probes are PK/index lookups — 100× more rows adds one or two btree levels); +the O(model) operations scale linearly per project, not with instance size; storage lands at +roughly 15 GB element_version + 6 GB checkpoints + a `derived_json`-dominated derived stream +(~145 GB at realistic 1–1.5 KB compressed documents — the synthetic test used ~300 B docs, so +multiply its 1.1 GB accordingly). The instance-level totals are governed by two levers: +**checkpoint retention** (each retained giant checkpoint is ~0.2 GB + 1M rows) and +**derived_json size** (the only column measured in kilobytes). + +**Typed-identity / validation / trigger round** (PostgreSQL 18, docker, `shared_buffers=2GB`, +1M elements — 500k Package + 500k OwningMembership — in one project, **~1 KB jsonb payloads** +this time, single import commit + 20k chained commits). A/B against a "legacy-shape" database +(same schema, composite typed-identity FK swapped back to the single-column FK, the +`UNIQUE (id, class_kind)` index and `trg_commit_parent_version` dropped): + +Two full runs (fresh containers); both values shown where they differ materially — the +spread IS a finding (limitation 4 below): + +| Operation | Typed (current) | Legacy shape | Verdict | +|---|---|---|---| +| 1M `data_identity` inserts | 17.3 s / 14.8 s | 15.5 s / 15.3 s | the A/B delta **flipped sign between runs** — the second index's cost is smaller than Docker-run noise (±15%); treat it as ~free | +| 1M `element_version` inserts (1 KB jsonb) | 40.0 s / 33.5 s | 39.9 s / 41.4 s | **composite FK is free** on the hot path (one index probe either way); same noise caveat | +| 20k commit + parent-edge inserts | 1.02 s / 0.93 s | 0.74 s / 0.71 s | version trigger ≈ **+11–14 µs/commit**, consistent across runs — noise in absolute terms | +| `build_commit_checkpoint` at 1M | 12.4–13.3 s | — | consistent with the earlier 14.9 s | +| `validate_references_at_commit` (temp-table form), clean 1M snapshot | 2.5–4.3 s across runs | — | was 6.9–7.0 s as a plain SQL function; **async-only budget**, never on the commit path | +| same, dirty (1,000 injected problems, full pass) | 3.3–5.8 s, exact counts | — | precision confirmed at scale | +| `validate_references_in_commit`, 101-row change set vs 1M project | **77–86 ms**, exact counts | — | fit for the synchronous commit-validation path | +| single-element `resolve_element_at_commit` | 0.9–2.4 ms | — | consistent with earlier 2–5 ms | +| implied element-write throughput at 1 KB payloads | ~25–30k rows/s | — | the earlier ~30k rows/s was measured at ~300 B docs | + +A structural caveat surfaced by reading the plans — the validation arms joined *the full +(project-pruned) table history* against the snapshot, so validate cost grew with **total +history size** — and was then **fixed with a two-tier redesign**, re-measured on the same +1M dataset: + +| Operation | Measured | Notes | +|---|---|---| +| Full pass, temp-table variant (`validate_references_at_commit`), clean 1M snapshot | **2.5–4.3 s across runs** (was 6.9–7.0 s as a plain SQL function) | the snapshot is materialized into an ANALYZE'd, indexed temp table, so the planner knows its true cardinality and can pick snapshot-driven PK probes on deep histories — bounding the pass at O(snapshot × log history), never O(history) | +| Incremental tier (`validate_references_in_commit`), 101-row change set against the 1M project | **77–86 ms**, exactly the 2 injected problems | outgoing references of the change set + the REVERSE direction its tombstones break (a live, unchanged holder left dangling — caught via the reverse-lookup indexes and per-target `resolve_element_at_commit` probes); O(change set), fit for the synchronous commit-validation path | + +Working protocol: the incremental tier runs per commit; the full pass remains the periodic +audit (checkpoint cadence is a natural rhythm) that backstops it. + +**Multi-user round** (PostgreSQL 18, docker, pgbench, 1,000-element project, 16 branches; +the first measurements under REAL concurrency — this partially retires limitation 2 below +for the SQL layer; service-level concurrency remains for the .NET harness): + +| Scenario | Result | Invariants (verifier C1–C5) | +|---|---|---| +| HOT: 16 clients racing the full commit protocol on ONE branch, 20 s | 984 attempts/s, 166 winning commits/s, **83.2% CAS-conflict rate**, 0 errors, 0 deadlocks | all PASS: one winner per head value, strictly linear chain, losers wrote nothing | +| SPREAD: same 16 clients on 16 branches | **2,182 commits/s, 0% conflicts** | all PASS — contention is branch-local, as designed (§18.3.6) | +| READS during the hot write storm (8 clients) | 1.21 ms avg (0.79 ms idle baseline), 6,633 reads/s | MVCC promise measured: readers never block on writers | + +Protocol notes the suite surfaced (both now normative in guide §18.2): stamp `commit.created` +with `clock_timestamp()`, not transaction-start `now()` — otherwise a transaction that began +before the current head committed stamps its commit earlier than its parent and trips the +monotonicity trigger; and the suite's CAS-FIRST ordering (lock-then-verify) is contention- +equivalent to §18.2's optimistic ordering, which pgbench cannot express (documented in the +setup script). The 83% conflict rate is the deliberate worst case — zero think time on one +branch; real editing sessions sit far below it, and §15.15's CAS-conflict-rate signal now +has a measured ceiling to calibrate against. + +**Known limitations of ALL the measured numbers above** — read before quoting them: + +1. **Payload realism.** The R1–R13 and extreme-scale rounds used ~300 B synthetic + `stored_json`/`derived_json`; production documents are 1–4 KB. The typed-identity round + moved to 1 KB (element-write throughput dropped from ~30k to ~25k rows/s — that ratio is + the correction factor to keep in mind for the older write numbers). TOAST and GIN costs + at 2–4 KB derived documents remain unmeasured. +2. **Warm cache; concurrency only at the SQL layer.** The load/read numbers are warm-cache, + zero-concurrency measurements. The multi-user round above now covers CAS contention, + deadlock-freedom, and read latency under write storms at the SQL layer — but on a tiny + (1k-element) model with synthetic payloads; cold-partition p99s, WAL flush pressure at + production payload sizes, autovacuum interference at sustained commit rates, and all + service-level concurrency (rebase flows, merge conflicts) remain for the .NET harness + (below). +3. **Docker-on-Windows I/O.** All rounds ran against Docker Desktop's virtualized filesystem; + fsync and I/O latency differ from a tuned Linux host. Treat the *relative* comparisons + (before/after, A/B, flat-vs-linear) as the reliable signal and the absolute milliseconds + as indicative only. +4. **Single-shot timings.** Most figures are 1–3 runs, no variance reported. The flat-vs- + linear verdicts are robust to that; the single-digit-millisecond figures are not. +5. **Statistics freshness was hand-managed.** Every round ran `ANALYZE` right after loading; + production relies on autovacuum keeping statistics current, and the `n_distinct` incident + proves the plans are sensitive to stale stats — which is why the §15.15 monitoring + signals include seq-scan counters. + +Follow-up gate before production (not built here): the full .NET benchmark harness — 3×1M-element +projects with authentic serializer payloads sharing hash partitions, 20k-commit history replay, +500 branches, root-rename burst with concurrent read-latency measurement, UUIDv4-vs-v7 A/B, and +`pgstattuple`/wait-event longevity checks. + +Also noted for the build-out phase (not built here): a **generated `ClassKind` C# enum** as an +additional `SQLSchemaGenerator` output (emitted from `ClassKindRegistry.cs`, so its values are +frozen across releases by construction) plus a service startup assertion against the +`class_kind` table, turning the name-is-the-contract rule of guide §12.1/§15.14 into a +fail-fast check. + +## Operational requirements + +- **`max_locks_per_transaction >= 4096`.** 58 partitioned tables × 16 hash partitions = 928 + leaves; Postgres clones every FK onto every leaf (2,600+ constraints). Whole-schema DDL + (install, drop, migrate, `pg_dump --schema-only`) exhausts the default 64. Verified + empirically on PostgreSQL 17: install fails without it. Hot-path queries are unaffected + (partition pruning at plan time). +- Branch creation is O(model) (`branch_head` copy). Rare and COPY-speed, but with heavy branch + fan-out on 1M-element projects, switch `branch_head` to a base-commit + delta overlay. +- `element_version` / `derived_version` never receive UPDATEs; autovacuum on their leaf + partitions is driven by absolute thresholds, set inside the partition-creation loop. + +## C# PIM alignment (fixed alongside this schema) + +`SysML2.NET/PIM/DTO/` (and the mirroring, currently-unconsumed `SysML2.NET/PIM/POCO/`) now match +the Clause 7.1/7.2 model this schema implements: + +- `Commit.PreviousCommit` is an ordered `List` (multi-parent merges — `commit_parent`), + and `Commit.Change` carries the `DataVersion` delta; a deletion is a `DataVersion` with a null + `Payload` (→ `element_version.tombstone`). +- `PrimitiveConstraint : Constraint`, and `Query` has `OrderBy` — `Query.Where` / + `CompositeConstraint` compose as the spec requires. +- `CommitReference` has `ReferencedCommit`; `Branch.Head` and `Tag.TaggedCommit` redefine it + (same underlying value); `Deleted` is `DateTime?`. +- `ProjectUsage : Record` with `UsedProjectCommit` (spec name → `used_project_commit_id`); + `DataIdentity` carries the derived `CreatedAt`/`DeletedAt` commit references. +- The JSON serializers (`SysML2.NET.Serializer.Json/PIM/`) round-trip all of it, including the + single-object `previousCommit` form emitted by the Intercax reference implementation. + +Deliberately NOT DTO properties: `Commit.versionedData` (derived, unbounded — resolved by +`branch_head` / `resolve_commit_state()`), and `Project.branch`/`tag`/`usage`/`identifiedData` +(derived subsets, absent from API payloads — modeled here as reverse FKs on `branch`/`tag`/ +`project_usage`/`data_identity`). + +## Verification + +```bash +# generate (runs the generator against the checked-in XMI) +dotnet test SysML2.NET.CodeGenerator.Tests/SysML2.NET.CodeGenerator.Tests.csproj \ + --filter "FullyQualifiedName~SQLSchemaGeneratorTestFixture" + +# install + functional smoke (30 assertions) against a real PostgreSQL 18 +# (both schemas verified on 17 AND 18.6; the recipe follows the prefer-18 version policy) +docker run -d --name sysml2pg -e POSTGRES_PASSWORD=pg postgres:18 -c max_locks_per_transaction=4096 +docker cp sysml2pg:/tmp/schema.sql +docker cp SysML2.NET.CodeGenerator/Sql/schema.smoke.sql sysml2pg:/tmp/smoke.sql +docker exec sysml2pg psql -U postgres -v ON_ERROR_STOP=1 -f /tmp/schema.sql +docker exec sysml2pg psql -U postgres -v ON_ERROR_STOP=1 -f /tmp/smoke.sql + +# multi-user suite (scratch database; setup expects the generated schema installed first) +docker exec sysml2pg psql -U postgres -v ON_ERROR_STOP=1 -f /tmp/conc-setup.sql # schema.concurrency.setup.sql +docker exec sysml2pg pgbench -n -U postgres -c 16 -j 4 -T 20 -f /tmp/conc-hot.sql # or conc-spread.sql / conc-read.sql +docker exec sysml2pg psql -U postgres -v ON_ERROR_STOP=1 -f /tmp/conc-verify.sql # invariants C1–C5, PASS/FAIL + +# OR: the .NET route — the same smoke + concurrency suites as NUnit fixtures on a +# PostgreSQL 18 Testcontainer (Docker required; skips cleanly when unavailable). +# The fixtures generate the schema IN-PROCESS from the UML model, so they also catch +# generator/registry drift; the smoke PASS count is read from the script itself. +# Fixtures: SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/ +# SqlSchemaSmokeTestFixture.cs, SqlSchemaConcurrencyTestFixture.cs (TESTING.md §10) +dotnet test SysML2.NET.CodeGenerator.Tests/SysML2.NET.CodeGenerator.Tests.csproj \ + --filter "TestCategory=Integration" +``` + +Note: the smoke file runs UNCHANGED against both schemas. It seeds `model_version` and +`class_kind` itself, idempotently (`ON CONFLICT (id) DO NOTHING`) and **with the frozen +registry ids** (OwningMembership=116, Package=117, PartUsage=120 — identical to the generated +seeds, a no-op there), and references kinds everywhere else by name lookup +(`SELECT id FROM sysml2.class_kind WHERE name = '…'`). No adaptation step exists anymore. + +The smoke test's load-bearing assertions: a Package rename changes the child's derived +`qualifiedName` while the child still resolves to its ORIGINAL version row (PASS 2a/2b — the +reason derived state is a second stream); a two-parent merge commit resolves to the merge's +own conflict resolution while elements deleted on non-ancestor branches stay alive (PASS +8a–8c); the branch_head overlay life cycle — O(1) branch creation at a checkpoint, +read-through to the base, tombstone masking, and overlay-only deletion (PASS 9a–9f); +deterministic fold resolution on sibling-commit timestamp ties, through both resolvers +(PASS 10a/10b); the multi-version rules — idempotent registry seeds, the conversion +commit as the only way up, and rejection of downgrades, mixed-release merges, and +convert+merge combos (PASS 11a–11e); the typed identity plus reference validation — +the composite FK rejecting a version whose class_kind contradicts its identity, a clean +snapshot validating clean, and wrong-type/dangling references being reported precisely +(PASS 12a–12c); and the incremental tier — the change set's own problems, a healthy +commit validating clean, and a tombstone's reverse-direction dangling reference caught +in agreement with the full audit (PASS 13a–13c). diff --git a/SysML2.NET.CodeGenerator/Sql/schema.concurrency.hot.sql b/SysML2.NET.CodeGenerator/Sql/schema.concurrency.hot.sql new file mode 100644 index 000000000..49638b3af --- /dev/null +++ b/SysML2.NET.CodeGenerator/Sql/schema.concurrency.hot.sql @@ -0,0 +1,4 @@ +-- pgbench script — HOT-BRANCH scenario: every client races the commit protocol on branch 1. +-- Run: pgbench -n -c 16 -j 4 -T 30 -f schema.concurrency.hot.sql +\set seed random(1, 1000000000) +SELECT sysml2.bench_try_commit(1, :seed); diff --git a/SysML2.NET.CodeGenerator/Sql/schema.concurrency.read.sql b/SysML2.NET.CodeGenerator/Sql/schema.concurrency.read.sql new file mode 100644 index 000000000..ab5d8031d --- /dev/null +++ b/SysML2.NET.CodeGenerator/Sql/schema.concurrency.read.sql @@ -0,0 +1,6 @@ +-- pgbench script — READER scenario: single-element branch-head reads on the hot branch. +-- Run CONCURRENTLY with the hot scenario to measure the MVCC promise: readers never block +-- on writers, and read latency stays flat under a write storm. +-- Run: pgbench -n -c 8 -j 2 -T 30 -f schema.concurrency.read.sql +\set n random(0, 999) +SELECT sysml2.bench_read(:n); diff --git a/SysML2.NET.CodeGenerator/Sql/schema.concurrency.setup.sql b/SysML2.NET.CodeGenerator/Sql/schema.concurrency.setup.sql new file mode 100644 index 000000000..44c110cf8 --- /dev/null +++ b/SysML2.NET.CodeGenerator/Sql/schema.concurrency.setup.sql @@ -0,0 +1,120 @@ +-- Concurrency-suite SETUP for the SysML2 PostgreSQL schema — run AFTER installing the +-- generated schema (SysML2.NET.CodeGenerator/Sql/schema2.generated.sql) on a scratch database. +-- +-- Seeds one project with 1,000 elements, a checkpointed base commit and 16 branches, and +-- installs two TEST-ONLY helper functions the pgbench scripts drive: +-- +-- * sysml2.bench_try_commit(branch_index, seed) — one full commit-protocol attempt against +-- branch N. Returns true when the compare-and-swap won, false when it lost (the 409 case). +-- * sysml2.bench_read(n) — one branch-head element read, the read path under write load. +-- +-- PROTOCOL NOTE (deliberate deviation from guide §18.2's optimistic ordering): pgbench cannot +-- express "roll back my own writes on CAS failure", so bench_try_commit performs the CAS +-- FIRST. Under READ COMMITTED this is the lock-then-verify variant: the winner holds the +-- branch-row lock while writing its (tiny) change set; a loser blocks on the row lock, then +-- re-evaluates the WHERE against the new head and updates 0 rows. The CONTENTION semantics — +-- exactly one winner per head value, losers detect atomically — are identical to §18.2; only +-- the lock-hold window differs (production computes derived state BEFORE locking). +-- +-- TIMESTAMP NOTE (normative for services, guide §18.2): commit.created is stamped with +-- clock_timestamp(), NOT the transaction-start now(). Under concurrency a transaction that +-- started BEFORE the current head committed would otherwise stamp its commit EARLIER than +-- its parent and be rejected by trg_commit_parent_monotonic. + +SET search_path = sysml2, public; + +INSERT INTO sysml2.project (id, name) VALUES + ('11111111-0000-0000-0000-00000000cc01', 'ConcurrencyBench'); + +INSERT INTO sysml2.commit (id, project_id, created, description, model_version_id) VALUES + ('c0000000-0000-0000-0000-00000000cc01', '11111111-0000-0000-0000-00000000cc01', + '2026-01-01T00:00:00Z', 'seed', 1); + +INSERT INTO sysml2.data_identity (id, project_id, class_kind) +SELECT md5('cid' || i)::uuid, '11111111-0000-0000-0000-00000000cc01', + (SELECT id FROM sysml2.class_kind WHERE name = 'Package') +FROM generate_series(0, 999) i; + +INSERT INTO sysml2.element_version + (project_id, version_id, identity_id, commit_id, class_kind, tombstone, + element_id, declared_name, is_implied_included, stored_json) +SELECT '11111111-0000-0000-0000-00000000cc01', md5('v0' || i)::uuid, md5('cid' || i)::uuid, + 'c0000000-0000-0000-0000-00000000cc01', + (SELECT id FROM sysml2.class_kind WHERE name = 'Package'), false, + md5('cid' || i), 'seed_' || i, false, + jsonb_build_object('@id', md5('cid' || i), '@type', 'Package', 'declaredName', 'seed_' || i) +FROM generate_series(0, 999) i; + +SELECT sysml2.build_commit_checkpoint('11111111-0000-0000-0000-00000000cc01', + 'c0000000-0000-0000-0000-00000000cc01'); + +-- 16 branches, all based on the checkpointed seed commit (empty overlays). +INSERT INTO sysml2.branch (id, project_id, name, head_commit_id, base_commit_id) +SELECT md5('branch' || i)::uuid, '11111111-0000-0000-0000-00000000cc01', + 'bench-' || i, + 'c0000000-0000-0000-0000-00000000cc01', 'c0000000-0000-0000-0000-00000000cc01' +FROM generate_series(1, 16) i; + +ANALYZE; + +-- TEST-ONLY: one commit-protocol attempt. Not part of the production schema. +CREATE OR REPLACE FUNCTION sysml2.bench_try_commit(p_branch_index int, p_seed bigint) +RETURNS boolean +LANGUAGE plpgsql +AS $$ +DECLARE + proj constant uuid := '11111111-0000-0000-0000-00000000cc01'; + v_branch uuid := md5('branch' || p_branch_index)::uuid; -- v_ prefix: plain names shadow column names in ON CONFLICT + expected uuid; + new_commit uuid := gen_random_uuid(); + v_identity uuid := md5('cid' || (p_seed % 1000))::uuid; + new_ver uuid := gen_random_uuid(); + won int; +BEGIN + SELECT head_commit_id INTO expected FROM sysml2.branch WHERE id = v_branch; + + -- The new commit must exist before the branch FK can point at it. A LOSING attempt + -- therefore leaves one parentless orphan commit behind — the verify script counts + -- orphans as the loss tally, so no extra contention point is needed for stats. + INSERT INTO sysml2.commit (id, project_id, created, description, model_version_id) + VALUES (new_commit, proj, clock_timestamp(), 'bench', 1); + + UPDATE sysml2.branch + SET head_commit_id = new_commit + WHERE id = v_branch AND head_commit_id = expected; + + GET DIAGNOSTICS won = ROW_COUNT; + + IF won = 0 THEN + RETURN false; -- the 409 path: caller rebases (next pgbench iteration) + END IF; + + INSERT INTO sysml2.commit_parent (commit_id, parent_commit_id, ordinal) + VALUES (new_commit, expected, 0); + + INSERT INTO sysml2.element_version + (project_id, version_id, identity_id, commit_id, class_kind, tombstone, + element_id, declared_name, is_implied_included, stored_json) + VALUES + (proj, new_ver, v_identity, new_commit, + (SELECT id FROM sysml2.class_kind WHERE name = 'Package'), false, + v_identity::text, 'r' || p_seed, false, + jsonb_build_object('@id', v_identity::text, '@type', 'Package', 'declaredName', 'r' || p_seed)); + + INSERT INTO sysml2.branch_head (project_id, branch_id, identity_id, version_id, derived_id, is_tombstone) + VALUES (proj, v_branch, v_identity, new_ver, NULL, false) + ON CONFLICT (project_id, branch_id, identity_id) + DO UPDATE SET version_id = EXCLUDED.version_id, derived_id = NULL, is_tombstone = false; + + RETURN true; +END; +$$; + +-- TEST-ONLY: one branch-head read against branch 1 (the hot branch). +CREATE OR REPLACE FUNCTION sysml2.bench_read(p_n int) +RETURNS jsonb +LANGUAGE sql +STABLE +AS $$ + SELECT sysml2.get_element_at_branch_head(md5('branch1')::uuid, md5('cid' || p_n)::uuid); +$$; diff --git a/SysML2.NET.CodeGenerator/Sql/schema.concurrency.spread.sql b/SysML2.NET.CodeGenerator/Sql/schema.concurrency.spread.sql new file mode 100644 index 000000000..e264af32b --- /dev/null +++ b/SysML2.NET.CodeGenerator/Sql/schema.concurrency.spread.sql @@ -0,0 +1,6 @@ +-- pgbench script — SPREAD scenario: each client commits on its own branch (contention is +-- branch-local by design; this measures the uncontended ceiling of the same protocol). +-- Run: pgbench -n -c 16 -j 4 -T 30 -f schema.concurrency.spread.sql +\set seed random(1, 1000000000) +\set bidx :client_id % 16 + 1 +SELECT sysml2.bench_try_commit(:bidx, :seed); diff --git a/SysML2.NET.CodeGenerator/Sql/schema.concurrency.verify.sql b/SysML2.NET.CodeGenerator/Sql/schema.concurrency.verify.sql new file mode 100644 index 000000000..60ebff60d --- /dev/null +++ b/SysML2.NET.CodeGenerator/Sql/schema.concurrency.verify.sql @@ -0,0 +1,112 @@ +-- Concurrency-suite VERIFICATION — run after any pgbench scenario. Asserts the invariants +-- the CAS protocol (guide §18.2) promises under real concurrency, and reports the tallies. +-- Raises on any violation; prints PASS notices like schema.smoke.sql. + +DO $$ +DECLARE + proj constant uuid := '11111111-0000-0000-0000-00000000cc01'; + seed_commit constant uuid := 'c0000000-0000-0000-0000-00000000cc01'; + chain_total bigint; + rooted bigint; + parented bigint; + attempts bigint; + orphans bigint; + multi_parents bigint; + changed_rows bigint; + stray_overlay bigint; +BEGIN + -- C1. Every bench commit has AT MOST one parent: the protocol never accidentally merges. + SELECT count(*) INTO multi_parents + FROM (SELECT cp.commit_id + FROM sysml2.commit_parent cp + JOIN sysml2.commit c ON c.id = cp.commit_id + WHERE c.project_id = proj + GROUP BY cp.commit_id + HAVING count(*) > 1) m; + + IF multi_parents <> 0 THEN + RAISE EXCEPTION 'FAIL C1: % commits have multiple parents', multi_parents; + END IF; + RAISE NOTICE 'PASS C1: no accidental merges — every parented commit has exactly one parent'; + + -- Materialize the 16 head-to-root chains once; every remaining check reads this table. + CREATE TEMP TABLE bench_chains ON COMMIT DROP AS + WITH RECURSIVE chain AS ( + SELECT b.id AS branch_id, b.head_commit_id AS commit_id + FROM sysml2.branch b + WHERE b.project_id = '11111111-0000-0000-0000-00000000cc01' + + UNION ALL + + SELECT chain.branch_id, cp.parent_commit_id + FROM chain + JOIN sysml2.commit_parent cp ON cp.commit_id = chain.commit_id + ) + SELECT branch_id, commit_id FROM chain; + + -- C2. Per branch: the head walks back to the seed commit over a strictly linear chain. + SELECT count(DISTINCT branch_id) INTO rooted + FROM bench_chains WHERE commit_id = seed_commit; + + IF rooted <> 16 THEN + RAISE EXCEPTION 'FAIL C2: only % of 16 branch heads reach the seed commit', rooted; + END IF; + RAISE NOTICE 'PASS C2: all 16 heads walk back to the seed commit'; + + -- C3. No lost updates and no cross-links: the chains partition the parented commits — + -- the summed chain length (minus the 16 seed entries) equals the parent-edge count. + SELECT count(*) - 16 INTO chain_total FROM bench_chains; + + SELECT count(*) INTO parented + FROM sysml2.commit_parent cp + JOIN sysml2.commit c ON c.id = cp.commit_id + WHERE c.project_id = proj; + + IF chain_total <> parented THEN + RAISE EXCEPTION 'FAIL C3: % parent edges but % chain positions — a commit was lost or cross-linked', parented, chain_total; + END IF; + RAISE NOTICE 'PASS C3: chains partition all % winning commits — no lost updates', parented; + + -- C4. Exactly the winners wrote data: one element_version per won CAS; losers left only + -- a parentless orphan commit (the loss tally) and nothing else. + SELECT count(*) - 1000 INTO changed_rows + FROM sysml2.element_version ev WHERE ev.project_id = proj; + + SELECT count(*) INTO attempts + FROM sysml2.commit c WHERE c.project_id = proj AND c.description = 'bench'; + + SELECT count(*) INTO orphans + FROM sysml2.commit c + WHERE c.project_id = proj AND c.id <> seed_commit + AND NOT EXISTS (SELECT 1 FROM sysml2.commit_parent cp WHERE cp.commit_id = c.id); + + IF changed_rows <> parented THEN + RAISE EXCEPTION 'FAIL C4: % element writes for % winning commits — a loser wrote data or a winner did not', changed_rows, parented; + END IF; + + IF attempts <> parented + orphans THEN + RAISE EXCEPTION 'FAIL C4: attempts (%) <> wins (%) + losses (%)', attempts, parented, orphans; + END IF; + + RAISE NOTICE 'PASS C4: attempts % = wins % + CAS losses % (conflict rate %%%); losers wrote nothing', + attempts, parented, orphans, round(100.0 * orphans / GREATEST(attempts, 1), 1); + + -- C5. Overlay coherence: every overlay row's version was written by a commit on its own + -- branch's chain. + SELECT count(*) INTO stray_overlay + FROM sysml2.branch_head bh + JOIN sysml2.element_version ev + ON ev.project_id = bh.project_id AND ev.version_id = bh.version_id + WHERE bh.project_id = proj + AND NOT EXISTS (SELECT 1 + FROM bench_chains bc + WHERE bc.branch_id = bh.branch_id AND bc.commit_id = ev.commit_id); + + IF stray_overlay <> 0 THEN + RAISE EXCEPTION 'FAIL C5: % overlay rows point at versions outside their branch chain', stray_overlay; + END IF; + RAISE NOTICE 'PASS C5: every overlay row belongs to its own branch''s chain'; + + DROP TABLE bench_chains; +END; +$$; diff --git a/SysML2.NET.CodeGenerator/Sql/schema.golden.sql b/SysML2.NET.CodeGenerator/Sql/schema.golden.sql new file mode 100644 index 000000000..283626803 --- /dev/null +++ b/SysML2.NET.CodeGenerator/Sql/schema.golden.sql @@ -0,0 +1,3205 @@ +------------------------------------------------------------------------------------------------ +-- SysML2.NET — PostgreSQL persistence schema (golden reference) +-- +-- This file is the HAND-WRITTEN GOLDEN for the schema that +-- SysML2.NET.CodeGenerator/Templates/Uml/core-sql-schema-2.hbs must emit. +-- The generator's output is diffed against this file. Where a section is marked +-- [GENERATED], the template emits it from Resources/KerML_only_xmi.uml + +-- Resources/SysML_only_xmi.uml; a representative excerpt is shown here. +-- +-- Target: PostgreSQL 16+ (declarative partitioning, FKs to partitioned tables, +-- jsonb, MERGE, WITH RECURSIVE). +-- +-- Design summary (see SysML2.NET.CodeGenerator/SQLSCHEMA.md for the rationale): +-- +-- * The metamodel has 175 metaclasses / 12,963 flattened properties, but only +-- 2,698 of those are STORED (`{ get; set; }`); the other 9,582 are DERIVED +-- (`{ get; internal set; }`). The stored surface reduces to 97 declarations +-- across 49 metaclasses. +-- * Stored state is APPEND-ONLY and immutable (commits are immutable per +-- OMG Systems Modeling API & Services v1.0 Clause 7.1.2). +-- * A derived value is a function of (version, snapshot) — NOT of the version +-- alone: renaming a Namespace changes a child's qualifiedName without +-- changing the child's version. Derived state therefore lives in a SECOND +-- append-only stream keyed by (identity, commit). +-- * Referential integrity targets DATA IDENTITIES, never versions. A SysML2 +-- reference points at a stable element @id, not at a particular version of it. +-- * MULTIPLE METAMODEL RELEASES coexist in one database: every commit is stamped +-- with the model_version its payloads were written in (§2/§3), class_kind ids +-- are frozen in an append-only registry, and the physical schema is the +-- superset across the registered releases. A branch upgrades via a CONVERSION +-- COMMIT; merges require all parents to be in the same release. +------------------------------------------------------------------------------------------------ + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +CREATE SCHEMA IF NOT EXISTS sysml2; + +SET search_path = sysml2, public; + +------------------------------------------------------------------------------------------------ +-- 1. ENUM TYPES [GENERATED] +-- +-- Labels are the lowercase form of the C# literal, matching the JSON wire format: +-- SysML2.NET.Serializer.Json/Core/AutoGenSerializer/FeatureSerializer.cs:162 writes +-- `Direction.Value.ToString().ToLower()`. Deserialization is case-insensitive. +------------------------------------------------------------------------------------------------ + +CREATE TYPE sysml2.feature_direction_kind AS ENUM ('in', 'inout', 'out'); +CREATE TYPE sysml2.portion_kind AS ENUM ('timeslice', 'snapshot'); +CREATE TYPE sysml2.requirement_constraint_kind AS ENUM ('assumption', 'requirement'); +CREATE TYPE sysml2.state_subaction_kind AS ENUM ('entry', 'do', 'exit'); +CREATE TYPE sysml2.transition_feature_kind AS ENUM ('trigger', 'guard', 'effect'); +CREATE TYPE sysml2.trigger_kind AS ENUM ('when', 'at', 'after'); +CREATE TYPE sysml2.visibility_kind AS ENUM ('private', 'protected', 'public'); + +------------------------------------------------------------------------------------------------ +-- 2. METAMODEL CATALOGS [GENERATED] +-- +-- model_version registers every metamodel release this database has ever stored data for. +-- The id is an ORDINAL — a higher id is a later release — handed out once by the checked-in +-- registry (SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/ClassKindRegistry.cs) +-- and never renumbered. Every commit is stamped with the release its payloads are written in +-- (commit.model_version_id, §3); that stamp, not the branch or the project, is what a reader +-- consults to know the shape of a historical payload. +-- +-- class_kind interns the metaclass names to a smallint. Every element row carries the +-- smallint, not a VARCHAR(100) — at 1M+ elements that is the difference between a 2-byte and +-- a ~20-byte column on the hottest table in the database. +-- +-- !! THE IDS ARE AN APPEND-ONLY REGISTRY — FROZEN FOREVER ONCE ASSIGNED !! +-- A new metamodel release appends its new metaclasses AFTER the highest existing id (the +-- newcomers alphabetical among themselves); existing ids never change, so persisted +-- element_version.class_kind values and the generated C# ClassKind enum stay valid across +-- upgrades. A metaclass dropped by a release keeps its row, closed with removed_in. The +-- generator FAILS on any drift between the UML model and the registry (unregistered class, +-- stale registration, fingerprint mismatch) instead of silently renumbering, and the seed +-- INSERTs are idempotent (ON CONFLICT DO NOTHING) — safe to re-apply to a populated database. +-- +-- The property->storage routing that earlier designs kept in a property_catalog table is +-- deliberately NOT in the database: the generated service layer carries it as static +-- per-release C# (model-version descriptors). Nothing in this schema reads such a catalog — +-- the views and resolvers are already specialized per metaclass at generation time — and only +-- versioned generated code can describe EVERY registered release at once. +------------------------------------------------------------------------------------------------ + +CREATE TABLE sysml2.model_version ( + id smallint NOT NULL, -- ordinal: higher id == later release + name text NOT NULL, -- human-readable release label, e.g. 'sysml-2.0-beta-4' + source_fingerprint text NOT NULL, -- root-package fingerprint of the generator input + PRIMARY KEY (id), + UNIQUE (name) +); + +CREATE TABLE sysml2.class_kind ( + id smallint NOT NULL, + name text NOT NULL, -- the API @type value, e.g. 'PartUsage' + is_abstract boolean NOT NULL, + introduced_in smallint NOT NULL REFERENCES sysml2.model_version (id), + removed_in smallint NULL REFERENCES sysml2.model_version (id), -- first release WITHOUT the class; NULL = current + PRIMARY KEY (id), + UNIQUE (name) +); + +-- [GENERATED] Representative rows — the template emits every registered release and all 175 +-- registered metaclasses from ClassKindRegistry, idempotently: +-- +-- INSERT INTO sysml2.model_version (id, name, source_fingerprint) VALUES +-- (1, 'sysml-2.0-beta-4', 'SysML:_mczcUFn3EfG_XZTXp4TXuA') +-- ON CONFLICT (id) DO NOTHING; +-- +-- INSERT INTO sysml2.class_kind (id, name, is_abstract, introduced_in, removed_in) VALUES +-- (1, 'AcceptActionUsage', false, 1, NULL), ..., (120, 'PartUsage', false, 1, NULL), ... +-- ON CONFLICT (id) DO NOTHING; + +------------------------------------------------------------------------------------------------ +-- 3. PIM — PROJECTS, COMMITS, BRANCHES, TAGS [HAND-WRITTEN] +-- +-- Models Clause 7.1.1/7.1.2 of OMG Systems Modeling API and Services v1.0. +-- +-- The C# PIM DTOs (SysML2.NET/PIM/DTO/) match this model: Commit carries `Change` and a +-- multi-valued ordered `PreviousCommit`; Branch/Tag redefine CommitReference.ReferencedCommit; +-- CommitReference.Deleted is nullable. `VersionedData` is deliberately NOT a DTO property — +-- it is derived, unbounded, and resolved here by branch_head / resolve_commit_state (§9). +------------------------------------------------------------------------------------------------ + +CREATE TABLE sysml2.project ( + id uuid NOT NULL, + name text NULL, + description text NULL, + resource_identifier text NULL, + created timestamptz NOT NULL DEFAULT now(), + default_branch_id uuid NULL, -- FK added after branch exists (circular) + + -- Upgrade POLICY, not truth: the highest model_version (§2) new commits may be written + -- in. NULL = no restriction (any registered release). The release a commit actually IS + -- in lives on the commit itself. + target_model_version_id smallint NULL REFERENCES sysml2.model_version (id), + + PRIMARY KEY (id) +); + +CREATE TABLE sysml2.commit ( + id uuid NOT NULL, + project_id uuid NOT NULL REFERENCES sysml2.project (id) ON DELETE CASCADE, + created timestamptz NOT NULL DEFAULT now(), + description text NULL, + + -- The metamodel release (§2) this commit's payloads are written in. Append-only history + -- makes the per-commit stamp the only correct grain: a historical commit stays in the + -- release it was written in forever. A branch upgrades via a CONVERSION COMMIT — a + -- single-parent commit that bumps this stamp and restates every element whose shape + -- changed between the releases (service obligation; force a commit_checkpoint on it so + -- folds rarely cross the release boundary). Readers resolve payload shape from this + -- stamp, never from the branch or the project. + model_version_id smallint NOT NULL REFERENCES sysml2.model_version (id), + + PRIMARY KEY (id) +); + +CREATE INDEX ix_commit_project_created ON sysml2.commit (project_id, created DESC); + +-- Commit.previousCommit is a SET — merges have multiple parents. The DAG lives here. +CREATE TABLE sysml2.commit_parent ( + commit_id uuid NOT NULL REFERENCES sysml2.commit (id) ON DELETE CASCADE, + parent_commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + ordinal smallint NOT NULL, + PRIMARY KEY (commit_id, parent_commit_id) +); + +CREATE INDEX ix_commit_parent_parent ON sysml2.commit_parent (parent_commit_id); + +-- Clause 7.1.2 invariant: "Version histories must monotonically increase in time" — for commit C, +-- C.created is strictly newer than D.created for any D in C.previousCommit. The snapshot resolver +-- in §8 RELIES on this: it picks the version from the newest ancestor commit. Enforce it, because +-- a violation silently produces the wrong snapshot rather than an error. +CREATE OR REPLACE FUNCTION sysml2.assert_commit_monotonic() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +DECLARE + child_created timestamptz; + parent_created timestamptz; +BEGIN + SELECT created INTO child_created FROM sysml2.commit WHERE id = NEW.commit_id; + SELECT created INTO parent_created FROM sysml2.commit WHERE id = NEW.parent_commit_id; + + IF child_created <= parent_created THEN + RAISE EXCEPTION + 'commit % (created %) is not strictly newer than its parent % (created %)', + NEW.commit_id, child_created, NEW.parent_commit_id, parent_created + USING ERRCODE = 'check_violation'; + END IF; + + RETURN NEW; +END; +$$; + +CREATE TRIGGER trg_commit_parent_monotonic + AFTER INSERT ON sysml2.commit_parent + FOR EACH ROW + EXECUTE FUNCTION sysml2.assert_commit_monotonic(); + +-- Model-version invariants on the commit DAG (multi-version support, §2): +-- * a commit is never in an OLDER release than a parent — downgrades are unsupported; +-- * a SINGLE-parent commit MAY bump the release: that is a conversion commit; +-- * a MERGE commit (2+ parents) must have ALL parents in its own release — convert first, +-- then merge. Without this check a merge would silently mix payload shapes. +-- The count-based re-check makes the guard insertion-order independent: the moment a second +-- parent row lands, every already-inserted parent is re-validated against the child's release. +CREATE OR REPLACE FUNCTION sysml2.assert_commit_version_compatible() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +DECLARE + child_version smallint; + parent_version smallint; + parent_count int; + mixed_parents int; +BEGIN + SELECT model_version_id INTO child_version FROM sysml2.commit WHERE id = NEW.commit_id; + SELECT model_version_id INTO parent_version FROM sysml2.commit WHERE id = NEW.parent_commit_id; + + IF child_version < parent_version THEN + RAISE EXCEPTION + 'commit % (model_version %) cannot have parent % (model_version %): downgrades are not supported', + NEW.commit_id, child_version, NEW.parent_commit_id, parent_version + USING ERRCODE = 'check_violation'; + END IF; + + SELECT count(*) INTO parent_count + FROM sysml2.commit_parent + WHERE commit_id = NEW.commit_id; + + IF parent_count >= 2 THEN + + SELECT count(*) INTO mixed_parents + FROM sysml2.commit_parent cp + JOIN sysml2.commit parent ON parent.id = cp.parent_commit_id + WHERE cp.commit_id = NEW.commit_id + AND parent.model_version_id <> child_version; + + IF mixed_parents > 0 THEN + RAISE EXCEPTION + 'merge commit % (model_version %) has a parent in a different model_version: convert every branch to the target release before merging', + NEW.commit_id, child_version + USING ERRCODE = 'check_violation'; + END IF; + + END IF; + + RETURN NEW; +END; +$$; + +CREATE TRIGGER trg_commit_parent_version + AFTER INSERT ON sysml2.commit_parent + FOR EACH ROW + EXECUTE FUNCTION sysml2.assert_commit_version_compatible(); + +-- Branch and Tag are both CommitReference. Branch is mutable + destructible; Tag is immutable + +-- destructible (Clause 7.1.2 mutability table). +CREATE TABLE sysml2.branch ( + id uuid NOT NULL, + project_id uuid NOT NULL REFERENCES sysml2.project (id) ON DELETE CASCADE, + name text NULL, + description text NULL, + head_commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + + -- The base of the branch_head OVERLAY (see §9): a commit for which a commit_checkpoint has + -- been materialized (service-enforced invariant). branch_head then stores ONLY the + -- identities that DIVERGE from that checkpoint, making branch creation and deletion + -- O(divergence) instead of O(model). NULL means the overlay is the complete head state + -- (bootstrap / small-project mode). + base_commit_id uuid NULL REFERENCES sysml2.commit (id), + + created timestamptz NOT NULL DEFAULT now(), + deleted timestamptz NULL, + PRIMARY KEY (id), + UNIQUE (project_id, name) +); + +ALTER TABLE sysml2.project + ADD CONSTRAINT project_default_branch_fk + FOREIGN KEY (default_branch_id) REFERENCES sysml2.branch (id) DEFERRABLE INITIALLY DEFERRED; + +CREATE TABLE sysml2.tag ( + id uuid NOT NULL, + project_id uuid NOT NULL REFERENCES sysml2.project (id) ON DELETE CASCADE, + name text NULL, + description text NULL, + tagged_commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + created timestamptz NOT NULL DEFAULT now(), + deleted timestamptz NULL, + PRIMARY KEY (id), + UNIQUE (project_id, name) +); + +CREATE TABLE sysml2.project_usage ( + id uuid NOT NULL, + project_id uuid NOT NULL REFERENCES sysml2.project (id) ON DELETE CASCADE, + used_project_id uuid NOT NULL REFERENCES sysml2.project (id), + used_project_commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + PRIMARY KEY (id) +); + +------------------------------------------------------------------------------------------------ +-- 4. DATA IDENTITY +-- +-- DataIdentity is the version-independent identity of an element — the stable @id that every +-- SysML2 reference points at. +-- +-- This table is deliberately NOT partitioned and its PK is the bare uuid: ProjectUsage lets an +-- element in project A reference an element in project B, so a (project_id, id) composite key +-- would make every cross-project reference un-FK-able. Project scoping is enforced by the +-- service layer via project_usage, not by the FK. +------------------------------------------------------------------------------------------------ + +-- DELETION IS EXPLICIT, NOT CASCADED. At scale (~1M identities per project), FK cascades are a +-- trap: a cascade executes per-row deletes filtered on the FK column ALONE, which no index in +-- this schema leads with — every cascaded identity would trigger scans of the largest tables. +-- Project deletion is therefore an ordered, batched, per-table procedure (each statement prunes +-- to one partition and uses a PK prefix): +-- +-- DELETE FROM sysml2. WHERE project_id = $1; +-- DELETE FROM sysml2.derived_version WHERE project_id = $1; +-- DELETE FROM sysml2.element_version WHERE project_id = $1; +-- DELETE FROM sysml2.branch_head WHERE project_id = $1; +-- DELETE FROM sysml2.commit_checkpoint WHERE project_id = $1; +-- DELETE FROM sysml2.data_identity WHERE project_id = $1; +-- DELETE FROM sysml2.project WHERE id = $1; -- cascades only PIM rows +-- +-- The remaining NO ACTION FKs guarantee the procedure cannot leave dangling references — they +-- block out-of-order deletion instead of silently scanning. +CREATE TABLE sysml2.data_identity ( + id uuid NOT NULL, + project_id uuid NOT NULL REFERENCES sysml2.project (id), + + -- TYPED IDENTITY. The metaclass of an element is invariant across its versions — an + -- identity is born a PartUsage and stays one — so the type is a property of the IDENTITY + -- and therefore FK-able, unlike everything else about an element. Two consumers: + -- * element_version's composite FK (identity_id, class_kind) makes a version that + -- claims a different metaclass than its identity IMPOSSIBLE; + -- * validate_references_at_commit (§14) type-checks every stored reference against + -- this column — including cross-project targets, because identities are typed + -- regardless of which project they live in. + -- A release conversion that retypes an element (e.g. its metaclass was dropped) must + -- update this column in the same transaction (service obligation, guide §15.16). + class_kind smallint NOT NULL REFERENCES sysml2.class_kind (id), + + PRIMARY KEY (id), + UNIQUE (id, class_kind) -- FK target for element_version's type-consistency check +); + +CREATE INDEX ix_data_identity_project ON sysml2.data_identity (project_id); + +------------------------------------------------------------------------------------------------ +-- 5. STORED ELEMENT STATE — append-only [HAND-WRITTEN] +-- +-- One row per (element, commit-in-which-it-changed). Never UPDATEd, never DELETEd. This is the +-- write hot path: a commit is a pure COPY of new rows. +-- +-- Element's own six stored properties are folded in here rather than into a separate table — +-- every element has them, so a join would be pure overhead. +-- +-- stored_json is a DELIBERATE DENORMALIZATION: the pre-serialized JSON of the element's stored +-- half. The normalized columns and link tables remain the system of record (and carry the FKs); +-- stored_json exists so that a read never has to reassemble an element from six tables. See §9. +------------------------------------------------------------------------------------------------ + +CREATE TABLE sysml2.element_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, -- DataVersion.id + identity_id uuid NOT NULL, -- FK via the composite typed-identity check below + commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + class_kind smallint NOT NULL REFERENCES sysml2.class_kind (id), + + -- tombstone == true is DataVersion.payload = null, i.e. a deletion (Clause 7.1.2) + tombstone boolean NOT NULL DEFAULT false, + + -- Element (the only metaclass whose stored properties are folded into the core table) + -- element_id is text, NOT uuid, deliberately: KerML declares Element::elementId : String. + -- Only standard-library elements are normatively required to use name-based (version 5, + -- SHA-1) UUIDs; user-model elementIds carry no format constraint, so uuid would reject + -- spec-valid data. + element_id text NULL, + declared_name text NULL, + declared_short_name text NULL, + is_implied_included boolean NULL, + owning_relationship uuid NULL REFERENCES sysml2.data_identity (id), + + stored_json jsonb NULL, + + PRIMARY KEY (project_id, version_id), + + -- TYPED IDENTITY (§4): one composite FK both anchors the identity AND makes a version + -- that claims a different metaclass than its identity impossible. + FOREIGN KEY (identity_id, class_kind) REFERENCES sysml2.data_identity (id, class_kind), + + CONSTRAINT element_version_tombstone_empty + CHECK (NOT tombstone OR (stored_json IS NULL AND element_id IS NULL)), + CONSTRAINT element_version_payload_present + CHECK (tombstone OR (stored_json IS NOT NULL AND element_id IS NOT NULL AND is_implied_included IS NOT NULL)) +) PARTITION BY HASH (project_id); + +-- Clause 7.1.2: "DataVersion.identity is unique among records listed in Commit.change." +CREATE UNIQUE INDEX ux_element_version_identity_commit + ON sysml2.element_version (project_id, identity_id, commit_id); + +-- lz4 beats the pglz default by a wide margin at this write volume; set BEFORE the partitions +-- are created (§12) so every leaf inherits it. +ALTER TABLE sysml2.element_version ALTER COLUMN stored_json SET COMPRESSION lz4; + +CREATE INDEX ix_element_version_commit ON sysml2.element_version (project_id, commit_id); +CREATE INDEX ix_element_version_class_kind ON sysml2.element_version (project_id, class_kind); +CREATE INDEX ix_element_version_owning_rel ON sysml2.element_version (project_id, owning_relationship) + WHERE owning_relationship IS NOT NULL; + +------------------------------------------------------------------------------------------------ +-- 6. LINK TABLES — ordered multi-valued properties [GENERATED] +-- +-- The entire metamodel has only SIX distinct multi-valued reference properties and ONE +-- multi-valued value property. Every one of them is isOrdered=true, so `ordinal` is part of +-- the key rather than a set. +-- +-- Element::ownedRelationship (composite) × 167 classes +-- Relationship::ownedRelatedElement (composite) × 62 classes +-- Relationship::source × 62 classes +-- Relationship::target × 62 classes +-- Dependency::client × 1 class +-- Dependency::supplier × 1 class +-- Element::aliasIds (string) × 167 classes +-- +-- target_identity FKs to data_identity, NOT to element_version — a reference names an element, +-- not a version of it. +-- +-- GENERATOR NOTE: core-sql-schema-2.hbs derives these mechanically. Every reference-valued link +-- table gets an ix_{table}_target reverse-lookup index (this file shows the same rule). +------------------------------------------------------------------------------------------------ + +CREATE TABLE sysml2.element_owned_relationship ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + ordinal int NOT NULL, + target_identity uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id, ordinal), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_element_owned_relationship_target + ON sysml2.element_owned_relationship (project_id, target_identity); + +CREATE TABLE sysml2.element_alias_ids ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + ordinal int NOT NULL, + value text NOT NULL, + PRIMARY KEY (project_id, version_id, ordinal), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.relationship_owned_related_element ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + ordinal int NOT NULL, + target_identity uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id, ordinal), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_relationship_owned_related_element_target + ON sysml2.relationship_owned_related_element (project_id, target_identity); + +CREATE TABLE sysml2.relationship_source ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + ordinal int NOT NULL, + target_identity uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id, ordinal), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_relationship_source_target + ON sysml2.relationship_source (project_id, target_identity); + +CREATE TABLE sysml2.relationship_target ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + ordinal int NOT NULL, + target_identity uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id, ordinal), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_relationship_target_target + ON sysml2.relationship_target (project_id, target_identity); + +CREATE TABLE sysml2.dependency_client ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + ordinal int NOT NULL, + target_identity uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id, ordinal), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_dependency_client_target + ON sysml2.dependency_client (project_id, target_identity); + +CREATE TABLE sysml2.dependency_supplier ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + ordinal int NOT NULL, + target_identity uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id, ordinal), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_dependency_supplier_target + ON sysml2.dependency_supplier (project_id, target_identity); + +------------------------------------------------------------------------------------------------ +-- 7. SUBTYPE TABLES — one per storage-introducing metaclass [GENERATED] +-- +-- 47 tables. A metaclass gets a table iff it DECLARES at least one stored scalar or single +-- reference property of its own. Redeclarations (`new` in C#, redefinition in UML) do NOT get a +-- column — they resolve to the ancestor's column. There are 9 of them: +-- +-- CollectExpression::operator, SelectExpression::operator, FeatureChainExpression::operator, +-- IndexExpression::operator -> operator_expression_version.operator +-- ConnectionDefinition::isSufficient -> type_version.is_sufficient +-- EnumerationDefinition::isVariation -> definition_version.is_variation +-- Expose::isImportAll, Expose::visibility -> import_version.is_import_all / import_version.visibility +-- FramedConcernMembership::kind, +-- RequirementVerificationMembership::kind -> requirement_constraint_membership_version.kind +-- +-- Every table is keyed by (project_id, version_id) and co-partitioned with element_version, so +-- the join is partition-local. NOT NULL is safe on [1..1] properties: a row only exists here if +-- the element's class actually inherits this metaclass. +-- +-- Dependency introduces no scalar properties (client/supplier are link tables), so it has no +-- subtype table. +-- +-- GENERATOR NOTE: core-sql-schema-2.hbs derives these tables mechanically and emits two things +-- this hand-curated file only shows selectively: +-- * a DEFAULT clause for every column whose UML property declares a default value in the XMI +-- (e.g. the Feature booleans DEFAULT false, Membership::visibility DEFAULT 'public', +-- Import::visibility DEFAULT 'private'); +-- * an ix_{table}_{column} reverse-lookup index on EVERY data_identity-referencing column, +-- not just the four hot ones annotated below. The specialization graph indexes remain the +-- load-bearing ones for derived-property impact analysis. +------------------------------------------------------------------------------------------------ + +-- Root: KerML Core +CREATE TABLE sysml2.relationship_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + is_implied boolean NOT NULL, + owning_related_element uuid NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.type_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + is_abstract boolean NOT NULL, + is_sufficient boolean NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.feature_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + direction sysml2.feature_direction_kind NULL, + is_composite boolean NOT NULL, + is_constant boolean NOT NULL, + is_derived boolean NOT NULL, + is_end boolean NOT NULL, + is_ordered boolean NOT NULL, + is_portion boolean NOT NULL, + is_unique boolean NOT NULL, + is_variable boolean NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.membership_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + member_element uuid NOT NULL REFERENCES sysml2.data_identity (id), + member_name text NULL, + member_short_name text NULL, + -- KerML: Membership::visibility defaults to public + visibility sysml2.visibility_kind NOT NULL DEFAULT 'public', + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_membership_version_member_element ON sysml2.membership_version (project_id, member_element); + +CREATE TABLE sysml2.import_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + is_import_all boolean NOT NULL DEFAULT false, + is_recursive boolean NOT NULL DEFAULT false, + -- KerML: Import::visibility defaults to PRIVATE (unlike Membership), and a top-level + -- Import owned by a root Namespace MUST be private. + visibility sysml2.visibility_kind NOT NULL DEFAULT 'private', + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.membership_import_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + imported_membership uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.namespace_import_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + imported_namespace uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +-- Specialization and its 8 refinements. Each carries its own pair of endpoint references, which +-- SUBSET Relationship::source/target but are stored independently (isDerived=false in the model). +CREATE TABLE sysml2.specialization_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + general uuid NOT NULL REFERENCES sysml2.data_identity (id), + specific uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +-- The specialization graph is walked by Type::allSupertypes / inheritedMembership / feature. +-- These two indexes are what make the derived-property impact analysis (§10) affordable. +CREATE INDEX ix_specialization_version_general ON sysml2.specialization_version (project_id, general); +CREATE INDEX ix_specialization_version_specific ON sysml2.specialization_version (project_id, specific); + +CREATE TABLE sysml2.subclassification_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + subclassifier uuid NOT NULL REFERENCES sysml2.data_identity (id), + superclassifier uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.subsetting_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + subsetted_feature uuid NOT NULL REFERENCES sysml2.data_identity (id), + subsetting_feature uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.redefinition_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + redefined_feature uuid NOT NULL REFERENCES sysml2.data_identity (id), + redefining_feature uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.reference_subsetting_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + referenced_feature uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.cross_subsetting_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + crossed_feature uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.feature_typing_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + type uuid NOT NULL REFERENCES sysml2.data_identity (id), + typed_feature uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_feature_typing_version_type ON sysml2.feature_typing_version (project_id, type); + +CREATE TABLE sysml2.conjugated_port_typing_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + conjugated_port_definition uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.conjugation_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + conjugated_type uuid NOT NULL REFERENCES sysml2.data_identity (id), + original_type uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.port_conjugation_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + original_port_definition uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.disjoining_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + disjoining_type uuid NOT NULL REFERENCES sysml2.data_identity (id), + type_disjoined uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.differencing_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + differencing_type uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.intersecting_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + intersecting_type uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.unioning_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + unioning_type uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.feature_chaining_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + chaining_feature uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.feature_inverting_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + feature_inverted uuid NOT NULL REFERENCES sysml2.data_identity (id), + inverting_feature uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.type_featuring_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + feature_of_type uuid NOT NULL REFERENCES sysml2.data_identity (id), + featuring_type uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.feature_value_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + is_default boolean NOT NULL, + is_initial boolean NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.annotation_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + annotated_element uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_annotation_version_annotated_element + ON sysml2.annotation_version (project_id, annotated_element); + +CREATE TABLE sysml2.comment_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + body text NOT NULL, + locale text NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.textual_representation_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + body text NOT NULL, + language text NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.library_package_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + is_standard boolean NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +-- Expressions. Note that `operator` is declared ONCE on OperatorExpression; CollectExpression, +-- SelectExpression, FeatureChainExpression and IndexExpression merely redeclare it. +CREATE TABLE sysml2.operator_expression_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + operator text NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +-- `value` is a DIFFERENT TYPE on each of the four Literal metaclasses. This is the single +-- clearest reason the schema cannot collapse to one wide table. +CREATE TABLE sysml2.literal_boolean_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + value boolean NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.literal_integer_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + value integer NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.literal_rational_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + value double precision NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.literal_string_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + value text NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.invariant_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + is_negated boolean NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +-- SysML layer +CREATE TABLE sysml2.definition_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + is_variation boolean NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.usage_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + is_variation boolean NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +-- isIndividual is declared independently on OccurrenceDefinition and OccurrenceUsage — they sit +-- on the two parallel Definition/Usage branches and neither inherits from the other. +CREATE TABLE sysml2.occurrence_definition_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + is_individual boolean NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.occurrence_usage_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + is_individual boolean NOT NULL, + portion_kind sysml2.portion_kind NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.state_definition_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + is_parallel boolean NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.state_usage_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + is_parallel boolean NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.requirement_definition_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + req_id text NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.requirement_usage_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + req_id text NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +-- `kind` is a DIFFERENT ENUM TYPE on each of these four. Same argument as Literal::value. +CREATE TABLE sysml2.requirement_constraint_membership_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + kind sysml2.requirement_constraint_kind NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.state_subaction_membership_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + kind sysml2.state_subaction_kind NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.transition_feature_membership_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + kind sysml2.transition_feature_kind NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.trigger_invocation_expression_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + kind sysml2.trigger_kind NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +------------------------------------------------------------------------------------------------ +-- 8. DERIVED ELEMENT STATE — the second append-only stream [HAND-WRITTEN] +-- +-- THE KEY INSIGHT OF THIS SCHEMA. +-- +-- A derived value is a function of (version, SNAPSHOT), not of the version alone. Rename a +-- Namespace and every descendant's qualifiedName changes, even though no descendant's version +-- row changed. So derived state CANNOT hang off element_version — it is keyed by +-- (identity, commit), exactly like a version, and resolves through the SAME fold. +-- +-- A row is written only for elements whose derived values ACTUALLY CHANGED at that commit — +-- the "impact radius" of the change set. A leaf edit writes one row. A Namespace rename writes +-- one row per descendant. Adding a supertype to a widely-specialized Type writes one row per +-- member of its specialization-descendant closure. This is inherent, not a flaw in the design; +-- OMG Clause 2 warns of exactly this: "the values of derived properties of a given Element may +-- be affected by commits that do not directly change that Element." +-- +-- The six hot derived properties are promoted to real columns so the Query service can filter +-- and ORDER BY them (Clause 2, Derived Property Full Conformance: derived properties "can be +-- used in Query structures as PrimitiveConstraint properties"). The remaining ~325 distinct +-- derived property names live in derived_json. +------------------------------------------------------------------------------------------------ + +CREATE TABLE sysml2.derived_version ( + project_id uuid NOT NULL, + derived_id uuid NOT NULL, + identity_id uuid NOT NULL REFERENCES sysml2.data_identity (id), + commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + + -- promoted hot derived properties (all 167 metaclasses declare these via Element) + owner uuid NULL REFERENCES sysml2.data_identity (id), + owning_namespace uuid NULL REFERENCES sysml2.data_identity (id), + qualified_name text NULL, + name text NULL, + short_name text NULL, + is_library_element boolean NOT NULL DEFAULT false, + + -- everything else: ~325 distinct derived property names + derived_json jsonb NOT NULL, + + PRIMARY KEY (project_id, derived_id) +) PARTITION BY HASH (project_id); + +ALTER TABLE sysml2.derived_version ALTER COLUMN derived_json SET COMPRESSION lz4; + +CREATE UNIQUE INDEX ux_derived_version_identity_commit + ON sysml2.derived_version (project_id, identity_id, commit_id); + +CREATE INDEX ix_derived_version_commit ON sysml2.derived_version (project_id, commit_id); +CREATE INDEX ix_derived_version_owner ON sysml2.derived_version (project_id, owner); +CREATE INDEX ix_derived_version_qualified_name ON sysml2.derived_version (project_id, qualified_name); + +-- Ad-hoc PrimitiveConstraint filtering on a derived property that did NOT get promoted to a +-- column falls back to a jsonb containment probe. GIN keeps that from being a sequential scan. +CREATE INDEX ix_derived_version_json ON sysml2.derived_version USING gin (derived_json jsonb_path_ops); + +------------------------------------------------------------------------------------------------ +-- 9. SNAPSHOT RESOLUTION [HAND-WRITTEN] +-- +-- Commit.versionedData is DERIVED: fold `change` over the transitive previousCommit closure +-- (Clause 7.1.2). Doing that fold on every read does not scale, so: +-- +-- * branch_head is a sparse OVERLAY: it stores ONLY the identities that DIVERGE from the +-- branch's base checkpoint (branch.base_commit_id, §3). The head state of a branch is +-- overlay-row-if-present, else checkpoint-row. Updated incrementally on commit with the +-- change-set rows: O(changeset). Branch creation from a checkpointed commit writes ZERO +-- rows; branch deletion deletes only the divergence. At the design scale (100-500 live +-- branches x 1M elements) a fully-materialized per-branch head would be ~500M rows per +-- project — the overlay is what makes hundreds of branches affordable. +-- A deletion on the branch is masked by an overlay row with is_tombstone = true (it also +-- points at the tombstone element_version row). base_commit_id NULL = the overlay IS the +-- complete head state (bootstrap / small-project mode). +-- COMPACTION (service policy): when an overlay exceeds ~10% of the model or ~100k rows, +-- materialize a checkpoint at the branch head, repoint base_commit_id, delete the overlay. +-- +-- * commit_checkpoint materializes the full fold for selected commits. It serves two masters: +-- the base of every branch overlay, and the bound on how far resolve_commit_state() walks. +-- CADENCE (service policy): checkpoint a commit when EITHER ~200 commits have passed since +-- the nearest checkpointed ancestor on that lineage OR the cumulative change-set size since +-- it exceeds ~25% of the model — plus always at branch-fork bases. Checkpoints are O(model) +-- rows each, so cadence must be churn-based, never "every N commits" alone. Retention: drop +-- checkpoints referenced by no branch.base_commit_id (delete the registry row FIRST, then +-- the rows), keeping a sparse historical ladder. +-- +-- * resolve_commit_state() is the general fallback for an arbitrary commit; +-- resolve_element_at_commit() is the single-element variant (O(ancestry), not O(model)). +------------------------------------------------------------------------------------------------ + +CREATE TABLE sysml2.branch_head ( + project_id uuid NOT NULL, + branch_id uuid NOT NULL REFERENCES sysml2.branch (id) ON DELETE CASCADE, + identity_id uuid NOT NULL REFERENCES sysml2.data_identity (id), + version_id uuid NOT NULL, + derived_id uuid NULL, + + -- true = the element is DELETED on this branch relative to the base checkpoint; the row + -- masks the checkpoint row on read. Denormalizes element_version.tombstone so the set-read + -- anti-join never has to visit element_version for masked identities. + is_tombstone boolean NOT NULL DEFAULT false, + + PRIMARY KEY (project_id, branch_id, identity_id) +) PARTITION BY HASH (project_id); + +-- Supports the ON DELETE CASCADE from branch: a cascade filters on branch_id ALONE, and the PK +-- leads with project_id — without this index every branch deletion would sequentially scan all +-- partitions. (Cheap to maintain now that the table only holds divergence.) +CREATE INDEX ix_branch_head_branch ON sysml2.branch_head (branch_id); + +-- One row per CHECKPOINT (not per identity): the resolvers' "is this commit checkpointed?" +-- probe hits this tiny table, never commit_checkpoint itself. Probing commit_checkpoint for +-- existence is a planner trap: all of a checkpoint's ~1M rows share one (project_id, commit_id) +-- value, so n_distinct estimates make the index look useless and the EXISTS degenerates into a +-- repeated sequential scan of the whole partition (measured: 500 probes = 100M rows filtered). +CREATE TABLE sysml2.commit_checkpoint_registry ( + project_id uuid NOT NULL, + commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + created timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (project_id, commit_id) +); + +CREATE TABLE sysml2.commit_checkpoint ( + project_id uuid NOT NULL, + commit_id uuid NOT NULL REFERENCES sysml2.commit (id) ON DELETE CASCADE, + identity_id uuid NOT NULL REFERENCES sysml2.data_identity (id), + version_id uuid NOT NULL, + derived_id uuid NULL, + PRIMARY KEY (project_id, commit_id, identity_id) +) PARTITION BY HASH (project_id); + +-- The general resolver. Walks the commit DAG back from :commit_id (stopping at the nearest +-- checkpoint) and picks, for each identity, the version from the NEWEST ancestor commit. +-- +-- "Newest" is well-defined only because Clause 7.1.2 guarantees monotonically increasing commit +-- timestamps along every parent edge — which trg_commit_parent_monotonic enforces. For a merge +-- commit, the merge itself carries the conflict resolution in its own change set, so it is +-- correctly the newest and wins. +-- +-- SIBLING commits (parallel branches, later merged) are NOT ordered by the monotonicity +-- invariant and may share a timestamp; the `id DESC` tiebreaker makes the fold DETERMINISTIC +-- in that case (an arbitrary-but-stable winner beats a nondeterministic one). A merge that +-- restates its conflicts — as Clause 7.1.2 requires — never reaches the tiebreaker. +CREATE OR REPLACE FUNCTION sysml2.resolve_commit_state(p_project_id uuid, p_commit_id uuid) +RETURNS TABLE (identity_id uuid, version_id uuid, derived_id uuid) +LANGUAGE sql +STABLE +AS $$ + WITH RECURSIVE + checkpoint AS ( + SELECT ccr.commit_id + FROM sysml2.commit_checkpoint_registry ccr + WHERE ccr.project_id = p_project_id AND ccr.commit_id = p_commit_id + ), + ancestry AS ( + SELECT c.id, c.created, (SELECT count(*) > 0 FROM checkpoint) AS at_checkpoint + FROM sysml2.commit c + WHERE c.id = p_commit_id + + UNION + + SELECT parent.id, parent.created, + EXISTS (SELECT 1 FROM sysml2.commit_checkpoint_registry ccr + WHERE ccr.project_id = p_project_id AND ccr.commit_id = parent.id) + FROM ancestry a + JOIN sysml2.commit_parent cp ON cp.commit_id = a.id + JOIN sysml2.commit parent ON parent.id = cp.parent_commit_id + WHERE NOT a.at_checkpoint + ), + folded AS ( + SELECT DISTINCT ON (ev.identity_id) + ev.identity_id, + ev.version_id, + ev.tombstone + FROM ancestry a + JOIN sysml2.element_version ev + ON ev.project_id = p_project_id AND ev.commit_id = a.id + ORDER BY ev.identity_id, a.created DESC, a.id DESC + ), + checkpoint_state AS ( + SELECT cc.identity_id, cc.version_id, cc.derived_id + FROM sysml2.commit_checkpoint cc + JOIN ancestry a ON a.id = cc.commit_id AND a.at_checkpoint + WHERE cc.project_id = p_project_id + ), + checkpointed AS ( + SELECT cs.identity_id, cs.version_id + FROM checkpoint_state cs + WHERE NOT EXISTS (SELECT 1 FROM folded f WHERE f.identity_id = cs.identity_id) + ), + resolved AS ( + SELECT f.identity_id, f.version_id FROM folded f WHERE NOT f.tombstone + UNION ALL + SELECT c.identity_id, c.version_id FROM checkpointed c + ), + -- Derived state folds over the SAME walked ancestry, in one pass (no per-row probe). + -- An identity whose latest derived row predates the walked window falls back to the + -- checkpoint's derived_id — without that fallback, derived state older than the + -- checkpoint would silently resolve to NULL. + derived_folded AS ( + SELECT DISTINCT ON (dv.identity_id) + dv.identity_id, + dv.derived_id + FROM ancestry a + JOIN sysml2.derived_version dv + ON dv.project_id = p_project_id AND dv.commit_id = a.id + ORDER BY dv.identity_id, a.created DESC, a.id DESC + ) + SELECT r.identity_id, + r.version_id, + COALESCE(df.derived_id, cs.derived_id) AS derived_id + FROM resolved r + LEFT JOIN derived_folded df ON df.identity_id = r.identity_id + LEFT JOIN checkpoint_state cs ON cs.identity_id = r.identity_id; +$$; + +-- Single-element variant of the resolver: same ancestry walk, but the fold arms are filtered to +-- one identity, so the cost is O(walked ancestry) index probes — NOT O(model). This is the +-- backing for GET /projects/{p}/commits/{c}/elements/{e}; without it a one-element historical +-- read would fold the entire model. +CREATE OR REPLACE FUNCTION sysml2.resolve_element_at_commit(p_project_id uuid, p_commit_id uuid, p_identity_id uuid) +RETURNS TABLE (identity_id uuid, version_id uuid, derived_id uuid) +LANGUAGE sql +STABLE +AS $$ + WITH RECURSIVE + checkpoint AS ( + SELECT ccr.commit_id + FROM sysml2.commit_checkpoint_registry ccr + WHERE ccr.project_id = p_project_id AND ccr.commit_id = p_commit_id + ), + ancestry AS ( + SELECT c.id, c.created, (SELECT count(*) > 0 FROM checkpoint) AS at_checkpoint + FROM sysml2.commit c + WHERE c.id = p_commit_id + + UNION + + SELECT parent.id, parent.created, + EXISTS (SELECT 1 FROM sysml2.commit_checkpoint_registry ccr + WHERE ccr.project_id = p_project_id AND ccr.commit_id = parent.id) + FROM ancestry a + JOIN sysml2.commit_parent cp ON cp.commit_id = a.id + JOIN sysml2.commit parent ON parent.id = cp.parent_commit_id + WHERE NOT a.at_checkpoint + ), + folded AS ( + SELECT ev.identity_id, ev.version_id, ev.tombstone + FROM ancestry a + JOIN sysml2.element_version ev + ON ev.project_id = p_project_id AND ev.commit_id = a.id AND ev.identity_id = p_identity_id + ORDER BY a.created DESC, a.id DESC + LIMIT 1 + ), + checkpointed AS ( + SELECT cc.identity_id, cc.version_id, cc.derived_id + FROM sysml2.commit_checkpoint cc + JOIN ancestry a ON a.id = cc.commit_id AND a.at_checkpoint + WHERE cc.project_id = p_project_id AND cc.identity_id = p_identity_id + ), + resolved AS ( + SELECT f.identity_id, f.version_id FROM folded f WHERE NOT f.tombstone + UNION ALL + SELECT c.identity_id, c.version_id FROM checkpointed c + WHERE NOT EXISTS (SELECT 1 FROM folded) + ), + derived_folded AS ( + SELECT dv.derived_id + FROM ancestry a + JOIN sysml2.derived_version dv + ON dv.project_id = p_project_id AND dv.commit_id = a.id AND dv.identity_id = p_identity_id + ORDER BY a.created DESC, a.id DESC + LIMIT 1 + ) + SELECT r.identity_id, + r.version_id, + COALESCE((SELECT df.derived_id FROM derived_folded df), + (SELECT c.derived_id FROM checkpointed c)) AS derived_id + FROM resolved r; +$$; + +-- Materializes the full fold of a commit as a checkpoint (idempotent). O(model) by design — +-- run it ASYNCHRONOUSLY per the cadence policy in the §9 banner, never on the commit path. +-- Returns the number of rows written. +CREATE OR REPLACE FUNCTION sysml2.build_commit_checkpoint(p_project_id uuid, p_commit_id uuid) +RETURNS bigint +LANGUAGE sql +VOLATILE +AS $$ + WITH inserted AS ( + INSERT INTO sysml2.commit_checkpoint (project_id, commit_id, identity_id, version_id, derived_id) + SELECT p_project_id, p_commit_id, r.identity_id, r.version_id, r.derived_id + FROM sysml2.resolve_commit_state(p_project_id, p_commit_id) r + ON CONFLICT (project_id, commit_id, identity_id) DO NOTHING + RETURNING 1 + ), + -- same statement, so checkpoint rows + registry row become visible atomically; the EXISTS + -- dependency also skips registering a checkpoint that materialized zero rows + registered AS ( + INSERT INTO sysml2.commit_checkpoint_registry (project_id, commit_id) + SELECT p_project_id, p_commit_id + WHERE EXISTS (SELECT 1 FROM inserted) + ON CONFLICT (project_id, commit_id) DO NOTHING + ) + SELECT count(*) FROM inserted; +$$; + +------------------------------------------------------------------------------------------------ +-- 10. READ PATH [HAND-WRITTEN] +-- +-- GET /projects/{p}/commits/{c}/elements/{e} reduces to ONE jsonb concat over a handful of PK +-- lookups. No joins across 6 subtype tables, no recursion, no derived computation. That is the +-- entire point of splitting stored_json from derived_json. +-- +-- EVERY function here resolves project_id by joining through `branch` FIRST. Filtering a +-- partitioned table on a bare uuid (branch_id / identity_id / version_id) without project_id +-- defeats partition pruning (all 16 leaves visited) AND cannot use the PKs (project_id-leading, +-- and PG16/17 has no btree skip scan) — the hottest query would silently become the worst one. +-- +-- The normalized columns of §5-§7 are the system of record and carry the referential integrity; +-- stored_json / derived_json are the read model built from them at commit time. +------------------------------------------------------------------------------------------------ + +-- Branch-head single-element read over the OVERLAY: the overlay row wins (a tombstoned overlay +-- row masks the base); otherwise fall back to the base checkpoint row. +CREATE OR REPLACE FUNCTION sysml2.get_element_at_branch_head(p_branch_id uuid, p_identity_id uuid) +RETURNS jsonb +LANGUAGE sql +STABLE +AS $$ + SELECT ev.stored_json || COALESCE(dv.derived_json, '{}'::jsonb) + FROM sysml2.branch b + LEFT JOIN sysml2.branch_head bh + ON bh.project_id = b.project_id AND bh.branch_id = b.id AND bh.identity_id = p_identity_id + LEFT JOIN sysml2.commit_checkpoint cc + ON cc.project_id = b.project_id AND cc.commit_id = b.base_commit_id + AND cc.identity_id = p_identity_id AND bh.identity_id IS NULL + JOIN sysml2.element_version ev + ON ev.project_id = b.project_id AND ev.version_id = COALESCE(bh.version_id, cc.version_id) + LEFT JOIN sysml2.derived_version dv + ON dv.project_id = b.project_id AND dv.derived_id = COALESCE(bh.derived_id, cc.derived_id) + WHERE b.id = p_branch_id + AND bh.is_tombstone IS NOT TRUE + AND NOT ev.tombstone; +$$; + +-- Branch-head set read: base checkpoint minus overlaid identities, plus the live overlay. +CREATE OR REPLACE FUNCTION sysml2.get_elements_at_branch_head(p_branch_id uuid) +RETURNS TABLE (identity_id uuid, payload jsonb) +LANGUAGE sql +STABLE +AS $$ + WITH head AS ( + SELECT b.project_id, bh.identity_id, bh.version_id, bh.derived_id + FROM sysml2.branch b + JOIN sysml2.branch_head bh + ON bh.project_id = b.project_id AND bh.branch_id = b.id + WHERE b.id = p_branch_id + AND NOT bh.is_tombstone + + UNION ALL + + SELECT b.project_id, cc.identity_id, cc.version_id, cc.derived_id + FROM sysml2.branch b + JOIN sysml2.commit_checkpoint cc + ON cc.project_id = b.project_id AND cc.commit_id = b.base_commit_id + WHERE b.id = p_branch_id + AND NOT EXISTS (SELECT 1 + FROM sysml2.branch_head masked + WHERE masked.project_id = cc.project_id + AND masked.branch_id = b.id + AND masked.identity_id = cc.identity_id) + ) + SELECT h.identity_id, + ev.stored_json || COALESCE(dv.derived_json, '{}'::jsonb) + FROM head h + JOIN sysml2.element_version ev + ON ev.project_id = h.project_id AND ev.version_id = h.version_id + LEFT JOIN sysml2.derived_version dv + ON dv.project_id = h.project_id AND dv.derived_id = h.derived_id + WHERE NOT ev.tombstone; +$$; + +CREATE OR REPLACE FUNCTION sysml2.get_elements_at_commit(p_project_id uuid, p_commit_id uuid) +RETURNS TABLE (identity_id uuid, payload jsonb) +LANGUAGE sql +STABLE +AS $$ + SELECT r.identity_id, + ev.stored_json || COALESCE(dv.derived_json, '{}'::jsonb) + FROM sysml2.resolve_commit_state(p_project_id, p_commit_id) r + JOIN sysml2.element_version ev + ON ev.project_id = p_project_id AND ev.version_id = r.version_id + LEFT JOIN sysml2.derived_version dv + ON dv.project_id = p_project_id AND dv.derived_id = r.derived_id; +$$; + +CREATE OR REPLACE FUNCTION sysml2.get_element_at_commit(p_project_id uuid, p_commit_id uuid, p_identity_id uuid) +RETURNS jsonb +LANGUAGE sql +STABLE +AS $$ + SELECT ev.stored_json || COALESCE(dv.derived_json, '{}'::jsonb) + FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, p_identity_id) r + JOIN sysml2.element_version ev + ON ev.project_id = p_project_id AND ev.version_id = r.version_id + LEFT JOIN sysml2.derived_version dv + ON dv.project_id = p_project_id AND dv.derived_id = r.derived_id; +$$; + +------------------------------------------------------------------------------------------------ +-- 11. PER-METACLASS FLATTENING VIEWS [GENERATED] +-- +-- One view per concrete metaclass (167), reconstructing the full DTO row shape by LEFT JOINing +-- exactly the subtype tables in that metaclass's supertype closure. These serve the Query service +-- and any consumer that wants columns rather than jsonb. +-- +-- Representative excerpt — the template emits all 167: +-- +-- CREATE VIEW sysml2.vw_part_usage AS +-- SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, +-- ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, +-- ev.owning_relationship, +-- t.is_abstract, t.is_sufficient, +-- f.direction, f.is_composite, f.is_constant, f.is_derived, f.is_end, +-- f.is_ordered, f.is_portion, f.is_unique, f.is_variable, +-- u.is_variation, +-- ou.is_individual, ou.portion_kind +-- FROM sysml2.element_version ev +-- JOIN sysml2.type_version t USING (project_id, version_id) +-- JOIN sysml2.feature_version f USING (project_id, version_id) +-- JOIN sysml2.usage_version u USING (project_id, version_id) +-- JOIN sysml2.occurrence_usage_version ou USING (project_id, version_id) +-- WHERE ev.class_kind = 120 AND NOT ev.tombstone; -- 120 = PartUsage's FROZEN registry id (§2) +-- +-- CREATE VIEW sysml2.vw_flow_usage AS ... -- SIX subtype tables: Connector is both a Feature +-- -- and a Relationship, so relationship_version joins too. + +------------------------------------------------------------------------------------------------ +-- 12. PARTITIONS +-- +-- Every element-scoped table is PARTITION BY HASH (project_id) with the same modulus, so they are +-- co-located: a join between element_version and a subtype table stays partition-local, and every +-- project-scoped API call prunes to a single partition. +-- +-- 16 is a starting point. The generator emits this loop; tune the modulus to the deployment. +-- +-- !! OPERATIONAL REQUIREMENT — max_locks_per_transaction !! +-- +-- 58 partitioned tables x 16 partitions = 928 leaf partitions, and Postgres CLONES every foreign +-- key onto every leaf: 2,629 FK constraints in total. Any single transaction that touches the +-- whole schema (CREATE SCHEMA, DROP SCHEMA CASCADE, a migration, pg_dump --schema-only) needs a +-- lock per object and will fail on the default max_locks_per_transaction = 64 with: +-- +-- ERROR: out of shared memory +-- HINT: You might need to increase "max_locks_per_transaction". +-- +-- Deploy with at least: +-- max_locks_per_transaction = 4096 +-- +-- This does NOT affect the hot path: a project-scoped query prunes to one partition at plan time +-- and locks a handful of objects. It is a DDL/administration constraint only. Verified against +-- PostgreSQL 17 — the schema fails to install without it. +-- +-- If raising the setting is not an option, halve the modulus (16 -> 8) and/or drop partitioning +-- from the 47 subtype tables, keeping it only on element_version, derived_version, branch_head, +-- commit_checkpoint and the 7 link tables. That trades partition-local subtype joins for a far +-- smaller catalog. +------------------------------------------------------------------------------------------------ + +DO $$ +DECLARE + partitioned_table text; + partition_index int; + partition_count constant int := 16; +BEGIN + FOREACH partitioned_table IN ARRAY ARRAY[ + 'element_version', 'derived_version', 'branch_head', 'commit_checkpoint', + 'element_owned_relationship', 'element_alias_ids', + 'relationship_owned_related_element', 'relationship_source', 'relationship_target', + 'dependency_client', 'dependency_supplier', + 'relationship_version', 'type_version', 'feature_version', 'membership_version', 'import_version', + 'membership_import_version', 'namespace_import_version', 'specialization_version', 'subclassification_version', + 'subsetting_version', 'redefinition_version', 'reference_subsetting_version', 'cross_subsetting_version', + 'feature_typing_version', 'conjugated_port_typing_version', 'conjugation_version', 'port_conjugation_version', + 'disjoining_version', 'differencing_version', 'intersecting_version', 'unioning_version', + 'feature_chaining_version', 'feature_inverting_version', 'type_featuring_version', 'feature_value_version', + 'annotation_version', 'comment_version', 'textual_representation_version', 'library_package_version', + 'operator_expression_version', 'literal_boolean_version', 'literal_integer_version', 'literal_rational_version', + 'literal_string_version', 'invariant_version', + 'definition_version', 'usage_version', 'occurrence_definition_version', 'occurrence_usage_version', + 'state_definition_version', 'state_usage_version', 'requirement_definition_version', 'requirement_usage_version', + 'requirement_constraint_membership_version', 'state_subaction_membership_version', + 'transition_feature_membership_version', 'trigger_invocation_expression_version' + ] + LOOP + FOR partition_index IN 0 .. partition_count - 1 LOOP + EXECUTE format( + 'CREATE TABLE sysml2.%I PARTITION OF sysml2.%I FOR VALUES WITH (MODULUS %s, REMAINDER %s)', + partitioned_table || '_p' || partition_index, + partitioned_table, + partition_count, + partition_index); + + -- Storage parameters cannot be set on a partitioned parent — only on leaf partitions. + -- The write profiles differ and so must the tuning: + -- + -- * branch_head is UPSERT-heavy (overlay rows are updated on every commit to the + -- branch and deleted on compaction/branch-delete): fillfactor 90 leaves HOT + -- headroom so the per-commit updates don't bloat the PK, and dead-tuple-driven + -- vacuum applies. + -- + -- * everything else is APPEND-ONLY: dead-tuple thresholds never fire usefully, so + -- drive vacuum off INSERT counts (keeps the visibility map current for index-only + -- scans) and analyze off a threshold sized for bulk commit traffic — absolute-5000 + -- analyze on a 60M-row leaf would sample continuously during imports. + IF partitioned_table = 'branch_head' THEN + + EXECUTE format( + 'ALTER TABLE sysml2.%I SET (' + || 'fillfactor = 90, ' + || 'autovacuum_analyze_scale_factor = 0.0, ' + || 'autovacuum_analyze_threshold = 50000)', + partitioned_table || '_p' || partition_index); + + ELSE + + EXECUTE format( + 'ALTER TABLE sysml2.%I SET (' + || 'autovacuum_vacuum_insert_scale_factor = 0.0, ' + || 'autovacuum_vacuum_insert_threshold = 100000, ' + || 'autovacuum_analyze_scale_factor = 0.0, ' + || 'autovacuum_analyze_threshold = 50000)', + partitioned_table || '_p' || partition_index); + + END IF; + END LOOP; + END LOOP; +END; +$$; + +-- PG18 OPPORTUNISTIC DEFAULTS (version-guarded; a no-op on the PostgreSQL 16/17 floor). +-- +-- On PostgreSQL 18+ the schema self-activates native uuidv7() defaults on every +-- SERVER-MINTED key. Time-ordered ids turn each project's insert pattern into rightmost +-- btree appends instead of random-page scatter (audit finding R8). The application-side +-- Guid.CreateVersion7() remains the PRIMARY id source — the service usually needs the ids +-- before insert (to wire derived_id into branch_head etc.); these defaults are the safety +-- net that keeps ad-hoc/tooling inserts time-ordered too. +-- +-- Deliberately NOT defaulted: data_identity.id (the spec-visible @id is supplied by the +-- API layer or the client — a silent server default would mask missing-id bugs). +DO $$ +BEGIN + + IF current_setting('server_version_num')::int >= 180000 THEN + + ALTER TABLE sysml2.element_version ALTER COLUMN version_id SET DEFAULT uuidv7(); + ALTER TABLE sysml2.derived_version ALTER COLUMN derived_id SET DEFAULT uuidv7(); + ALTER TABLE sysml2.project ALTER COLUMN id SET DEFAULT uuidv7(); + ALTER TABLE sysml2.commit ALTER COLUMN id SET DEFAULT uuidv7(); + ALTER TABLE sysml2.branch ALTER COLUMN id SET DEFAULT uuidv7(); + ALTER TABLE sysml2.tag ALTER COLUMN id SET DEFAULT uuidv7(); + + END IF; +END; +$$; + +------------------------------------------------------------------------------------------------ +-- 13. MODEL VERSION [GENERATED] +------------------------------------------------------------------------------------------------ + +CREATE OR REPLACE FUNCTION sysml2.query_model_version() +RETURNS text +LANGUAGE sql +IMMUTABLE +AS $$ + SELECT '{{model-version}}'::text; +$$; + +------------------------------------------------------------------------------------------------ +-- 14. REFERENCE VALIDATION — two tiers [GENERATED] +-- +-- The ON-DEMAND half of referential integrity (guide §7): the FKs prove that every stored +-- reference targets an EXISTING identity; these functions check what FKs cannot — +-- * 'dangling' — a same-project target that is not alive in the snapshot of the given +-- commit (absent or tombstoned); +-- * 'wrong-type' — a target whose metaclass is illegal for the referencing property, +-- checked via the TYPED IDENTITY (data_identity.class_kind, §4) — this +-- check applies to cross-project targets too. +-- Liveness of cross-project targets is deliberately NOT checked here: it depends on the +-- used-project commit (project_usage), which is service-layer resolution. +-- +-- TIER 1 — validate_references_at_commit: the FULL periodic audit over one commit's whole +-- snapshot. It materializes and ANALYZEs the snapshot in a temp table first, so the planner +-- knows the true cardinality and bounds the pass at O(snapshot x log history) — never +-- O(history) — however deep the append-only tables grow. Run asynchronously (checkpoint +-- cadence is a natural rhythm). +-- +-- TIER 2 — validate_references_in_commit: the INCREMENTAL per-commit check, O(change set): +-- the outgoing references of the versions the commit wrote, PLUS the reverse direction its +-- tombstones break — a live, unchanged element left referencing a deleted identity (driven +-- by the reverse-lookup indexes; per-target liveness probed via resolve_element_at_commit). +-- Cheap enough for the synchronous commit-validation path; tier 1 backstops it. +-- +-- Deliberately FUNCTIONS, not constraints: the spec allows transiently dangling references, +-- and liveness is a function of (identity, commit) — unenforceable by FK. One UNION ALL arm +-- per stored reference column, emitted from the UML model with the allowed target +-- metaclasses resolved through ClassKindRegistry. +------------------------------------------------------------------------------------------------ + +CREATE OR REPLACE FUNCTION sysml2.validate_references_at_commit( + p_project_id uuid, + p_commit_id uuid +) +RETURNS TABLE ( + source_table text, + source_column text, + source_identity uuid, + target_identity uuid, + problem text +) +LANGUAGE plpgsql +AS $$ +BEGIN + -- Materialize + ANALYZE the snapshot so the planner knows its TRUE cardinality and + -- can choose per arm between hashing the source (young history) and snapshot-driven + -- PK probes (deep history) — bounding the pass at O(snapshot x log history) instead + -- of O(history). A bare function CTE would be estimated at ~1000 rows. + CREATE TEMP TABLE IF NOT EXISTS validation_snapshot ( + identity_id uuid NOT NULL, + version_id uuid NOT NULL + ) ON COMMIT DROP; + + TRUNCATE validation_snapshot; + + INSERT INTO validation_snapshot (identity_id, version_id) + SELECT r.identity_id, r.version_id + FROM sysml2.resolve_commit_state(p_project_id, p_commit_id) r; + + CREATE INDEX IF NOT EXISTS ix_validation_snapshot_version ON validation_snapshot (version_id); + CREATE INDEX IF NOT EXISTS ix_validation_snapshot_identity ON validation_snapshot (identity_id); + + ANALYZE validation_snapshot; + + RETURN QUERY + SELECT 'annotation_version'::text, 'annotated_element'::text, + snap.identity_id, src.annotated_element, 'dangling'::text + FROM sysml2.annotation_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.annotated_element + LEFT JOIN validation_snapshot live ON live.identity_id = src.annotated_element + WHERE src.project_id = p_project_id + AND src.annotated_element IS NOT NULL + AND ti.project_id = p_project_id + AND live.identity_id IS NULL + UNION ALL + SELECT 'conjugated_port_typing_version'::text, 'conjugated_port_definition'::text, + snap.identity_id, src.conjugated_port_definition, + CASE WHEN ti.class_kind NOT IN (31) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.conjugated_port_typing_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.conjugated_port_definition + LEFT JOIN validation_snapshot live ON live.identity_id = src.conjugated_port_definition + WHERE src.project_id = p_project_id + AND src.conjugated_port_definition IS NOT NULL + AND (ti.class_kind NOT IN (31) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'conjugation_version'::text, 'conjugated_type'::text, + snap.identity_id, src.conjugated_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.conjugation_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.conjugated_type + LEFT JOIN validation_snapshot live ON live.identity_id = src.conjugated_type + WHERE src.project_id = p_project_id + AND src.conjugated_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'conjugation_version'::text, 'original_type'::text, + snap.identity_id, src.original_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.conjugation_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.original_type + LEFT JOIN validation_snapshot live ON live.identity_id = src.original_type + WHERE src.project_id = p_project_id + AND src.original_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'cross_subsetting_version'::text, 'crossed_feature'::text, + snap.identity_id, src.crossed_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.cross_subsetting_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.crossed_feature + LEFT JOIN validation_snapshot live ON live.identity_id = src.crossed_feature + WHERE src.project_id = p_project_id + AND src.crossed_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'dependency_client'::text, 'target_identity'::text, + snap.identity_id, src.target_identity, 'dangling'::text + FROM sysml2.dependency_client src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.target_identity + LEFT JOIN validation_snapshot live ON live.identity_id = src.target_identity + WHERE src.project_id = p_project_id + AND src.target_identity IS NOT NULL + AND ti.project_id = p_project_id + AND live.identity_id IS NULL + UNION ALL + SELECT 'dependency_supplier'::text, 'target_identity'::text, + snap.identity_id, src.target_identity, 'dangling'::text + FROM sysml2.dependency_supplier src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.target_identity + LEFT JOIN validation_snapshot live ON live.identity_id = src.target_identity + WHERE src.project_id = p_project_id + AND src.target_identity IS NOT NULL + AND ti.project_id = p_project_id + AND live.identity_id IS NULL + UNION ALL + SELECT 'differencing_version'::text, 'differencing_type'::text, + snap.identity_id, src.differencing_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.differencing_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.differencing_type + LEFT JOIN validation_snapshot live ON live.identity_id = src.differencing_type + WHERE src.project_id = p_project_id + AND src.differencing_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'disjoining_version'::text, 'disjoining_type'::text, + snap.identity_id, src.disjoining_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.disjoining_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.disjoining_type + LEFT JOIN validation_snapshot live ON live.identity_id = src.disjoining_type + WHERE src.project_id = p_project_id + AND src.disjoining_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'disjoining_version'::text, 'type_disjoined'::text, + snap.identity_id, src.type_disjoined, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.disjoining_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.type_disjoined + LEFT JOIN validation_snapshot live ON live.identity_id = src.type_disjoined + WHERE src.project_id = p_project_id + AND src.type_disjoined IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'element_version'::text, 'owning_relationship'::text, + snap.identity_id, src.owning_relationship, + CASE WHEN ti.class_kind NOT IN (4, 5, 6, 10, 13, 14, 18, 19, 32, 33, 34, 35, 36, 42, 46, 47, 48, 51, 52, 61, 62, 63, 65, 66, 67, 68, 70, 73, 80, 81, 82, 83, 97, 98, 99, 109, 110, 112, 116, 118, 123, 127, 128, 133, 136, 137, 138, 142, 143, 145, 149, 150, 151, 152, 153, 154, 155, 158, 162, 163, 167, 171) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.owning_relationship + LEFT JOIN validation_snapshot live ON live.identity_id = src.owning_relationship + WHERE src.project_id = p_project_id + AND src.owning_relationship IS NOT NULL + AND (ti.class_kind NOT IN (4, 5, 6, 10, 13, 14, 18, 19, 32, 33, 34, 35, 36, 42, 46, 47, 48, 51, 52, 61, 62, 63, 65, 66, 67, 68, 70, 73, 80, 81, 82, 83, 97, 98, 99, 109, 110, 112, 116, 118, 123, 127, 128, 133, 136, 137, 138, 142, 143, 145, 149, 150, 151, 152, 153, 154, 155, 158, 162, 163, 167, 171) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'element_owned_relationship'::text, 'target_identity'::text, + snap.identity_id, src.target_identity, + CASE WHEN ti.class_kind NOT IN (4, 5, 6, 10, 13, 14, 18, 19, 32, 33, 34, 35, 36, 42, 46, 47, 48, 51, 52, 61, 62, 63, 65, 66, 67, 68, 70, 73, 80, 81, 82, 83, 97, 98, 99, 109, 110, 112, 116, 118, 123, 127, 128, 133, 136, 137, 138, 142, 143, 145, 149, 150, 151, 152, 153, 154, 155, 158, 162, 163, 167, 171) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_owned_relationship src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.target_identity + LEFT JOIN validation_snapshot live ON live.identity_id = src.target_identity + WHERE src.project_id = p_project_id + AND src.target_identity IS NOT NULL + AND (ti.class_kind NOT IN (4, 5, 6, 10, 13, 14, 18, 19, 32, 33, 34, 35, 36, 42, 46, 47, 48, 51, 52, 61, 62, 63, 65, 66, 67, 68, 70, 73, 80, 81, 82, 83, 97, 98, 99, 109, 110, 112, 116, 118, 123, 127, 128, 133, 136, 137, 138, 142, 143, 145, 149, 150, 151, 152, 153, 154, 155, 158, 162, 163, 167, 171) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'feature_chaining_version'::text, 'chaining_feature'::text, + snap.identity_id, src.chaining_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.feature_chaining_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.chaining_feature + LEFT JOIN validation_snapshot live ON live.identity_id = src.chaining_feature + WHERE src.project_id = p_project_id + AND src.chaining_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'feature_inverting_version'::text, 'feature_inverted'::text, + snap.identity_id, src.feature_inverted, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.feature_inverting_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.feature_inverted + LEFT JOIN validation_snapshot live ON live.identity_id = src.feature_inverted + WHERE src.project_id = p_project_id + AND src.feature_inverted IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'feature_inverting_version'::text, 'inverting_feature'::text, + snap.identity_id, src.inverting_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.feature_inverting_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.inverting_feature + LEFT JOIN validation_snapshot live ON live.identity_id = src.inverting_feature + WHERE src.project_id = p_project_id + AND src.inverting_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'feature_typing_version'::text, 'type'::text, + snap.identity_id, src.type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.feature_typing_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.type + LEFT JOIN validation_snapshot live ON live.identity_id = src.type + WHERE src.project_id = p_project_id + AND src.type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'feature_typing_version'::text, 'typed_feature'::text, + snap.identity_id, src.typed_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.feature_typing_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.typed_feature + LEFT JOIN validation_snapshot live ON live.identity_id = src.typed_feature + WHERE src.project_id = p_project_id + AND src.typed_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'intersecting_version'::text, 'intersecting_type'::text, + snap.identity_id, src.intersecting_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.intersecting_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.intersecting_type + LEFT JOIN validation_snapshot live ON live.identity_id = src.intersecting_type + WHERE src.project_id = p_project_id + AND src.intersecting_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'membership_version'::text, 'member_element'::text, + snap.identity_id, src.member_element, 'dangling'::text + FROM sysml2.membership_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.member_element + LEFT JOIN validation_snapshot live ON live.identity_id = src.member_element + WHERE src.project_id = p_project_id + AND src.member_element IS NOT NULL + AND ti.project_id = p_project_id + AND live.identity_id IS NULL + UNION ALL + SELECT 'membership_import_version'::text, 'imported_membership'::text, + snap.identity_id, src.imported_membership, + CASE WHEN ti.class_kind NOT IN (4, 51, 52, 63, 66, 73, 97, 112, 116, 118, 133, 136, 137, 138, 143, 145, 150, 158, 167, 171) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.membership_import_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.imported_membership + LEFT JOIN validation_snapshot live ON live.identity_id = src.imported_membership + WHERE src.project_id = p_project_id + AND src.imported_membership IS NOT NULL + AND (ti.class_kind NOT IN (4, 51, 52, 63, 66, 73, 97, 112, 116, 118, 133, 136, 137, 138, 143, 145, 150, 158, 167, 171) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'namespace_import_version'::text, 'imported_namespace'::text, + snap.identity_id, src.imported_namespace, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 108, 111, 113, 114, 115, 117, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.namespace_import_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.imported_namespace + LEFT JOIN validation_snapshot live ON live.identity_id = src.imported_namespace + WHERE src.project_id = p_project_id + AND src.imported_namespace IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 108, 111, 113, 114, 115, 117, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'port_conjugation_version'::text, 'original_port_definition'::text, + snap.identity_id, src.original_port_definition, + CASE WHEN ti.class_kind NOT IN (31, 124) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.port_conjugation_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.original_port_definition + LEFT JOIN validation_snapshot live ON live.identity_id = src.original_port_definition + WHERE src.project_id = p_project_id + AND src.original_port_definition IS NOT NULL + AND (ti.class_kind NOT IN (31, 124) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'redefinition_version'::text, 'redefined_feature'::text, + snap.identity_id, src.redefined_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.redefinition_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.redefined_feature + LEFT JOIN validation_snapshot live ON live.identity_id = src.redefined_feature + WHERE src.project_id = p_project_id + AND src.redefined_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'redefinition_version'::text, 'redefining_feature'::text, + snap.identity_id, src.redefining_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.redefinition_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.redefining_feature + LEFT JOIN validation_snapshot live ON live.identity_id = src.redefining_feature + WHERE src.project_id = p_project_id + AND src.redefining_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'reference_subsetting_version'::text, 'referenced_feature'::text, + snap.identity_id, src.referenced_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.reference_subsetting_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.referenced_feature + LEFT JOIN validation_snapshot live ON live.identity_id = src.referenced_feature + WHERE src.project_id = p_project_id + AND src.referenced_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'relationship_version'::text, 'owning_related_element'::text, + snap.identity_id, src.owning_related_element, 'dangling'::text + FROM sysml2.relationship_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.owning_related_element + LEFT JOIN validation_snapshot live ON live.identity_id = src.owning_related_element + WHERE src.project_id = p_project_id + AND src.owning_related_element IS NOT NULL + AND ti.project_id = p_project_id + AND live.identity_id IS NULL + UNION ALL + SELECT 'relationship_owned_related_element'::text, 'target_identity'::text, + snap.identity_id, src.target_identity, 'dangling'::text + FROM sysml2.relationship_owned_related_element src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.target_identity + LEFT JOIN validation_snapshot live ON live.identity_id = src.target_identity + WHERE src.project_id = p_project_id + AND src.target_identity IS NOT NULL + AND ti.project_id = p_project_id + AND live.identity_id IS NULL + UNION ALL + SELECT 'relationship_source'::text, 'target_identity'::text, + snap.identity_id, src.target_identity, 'dangling'::text + FROM sysml2.relationship_source src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.target_identity + LEFT JOIN validation_snapshot live ON live.identity_id = src.target_identity + WHERE src.project_id = p_project_id + AND src.target_identity IS NOT NULL + AND ti.project_id = p_project_id + AND live.identity_id IS NULL + UNION ALL + SELECT 'relationship_target'::text, 'target_identity'::text, + snap.identity_id, src.target_identity, 'dangling'::text + FROM sysml2.relationship_target src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.target_identity + LEFT JOIN validation_snapshot live ON live.identity_id = src.target_identity + WHERE src.project_id = p_project_id + AND src.target_identity IS NOT NULL + AND ti.project_id = p_project_id + AND live.identity_id IS NULL + UNION ALL + SELECT 'specialization_version'::text, 'general'::text, + snap.identity_id, src.general, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.specialization_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.general + LEFT JOIN validation_snapshot live ON live.identity_id = src.general + WHERE src.project_id = p_project_id + AND src.general IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'specialization_version'::text, 'specific'::text, + snap.identity_id, src.specific, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.specialization_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.specific + LEFT JOIN validation_snapshot live ON live.identity_id = src.specific + WHERE src.project_id = p_project_id + AND src.specific IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'subclassification_version'::text, 'subclassifier'::text, + snap.identity_id, src.subclassifier, + CASE WHEN ti.class_kind NOT IN (2, 5, 7, 13, 14, 15, 17, 21, 23, 25, 26, 29, 31, 34, 38, 43, 45, 53, 68, 74, 80, 81, 86, 101, 103, 113, 119, 124, 126, 131, 134, 144, 148, 165, 168, 170, 173) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.subclassification_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.subclassifier + LEFT JOIN validation_snapshot live ON live.identity_id = src.subclassifier + WHERE src.project_id = p_project_id + AND src.subclassifier IS NOT NULL + AND (ti.class_kind NOT IN (2, 5, 7, 13, 14, 15, 17, 21, 23, 25, 26, 29, 31, 34, 38, 43, 45, 53, 68, 74, 80, 81, 86, 101, 103, 113, 119, 124, 126, 131, 134, 144, 148, 165, 168, 170, 173) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'subclassification_version'::text, 'superclassifier'::text, + snap.identity_id, src.superclassifier, + CASE WHEN ti.class_kind NOT IN (2, 5, 7, 13, 14, 15, 17, 21, 23, 25, 26, 29, 31, 34, 38, 43, 45, 53, 68, 74, 80, 81, 86, 101, 103, 113, 119, 124, 126, 131, 134, 144, 148, 165, 168, 170, 173) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.subclassification_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.superclassifier + LEFT JOIN validation_snapshot live ON live.identity_id = src.superclassifier + WHERE src.project_id = p_project_id + AND src.superclassifier IS NOT NULL + AND (ti.class_kind NOT IN (2, 5, 7, 13, 14, 15, 17, 21, 23, 25, 26, 29, 31, 34, 38, 43, 45, 53, 68, 74, 80, 81, 86, 101, 103, 113, 119, 124, 126, 131, 134, 144, 148, 165, 168, 170, 173) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'subsetting_version'::text, 'subsetted_feature'::text, + snap.identity_id, src.subsetted_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.subsetting_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.subsetted_feature + LEFT JOIN validation_snapshot live ON live.identity_id = src.subsetted_feature + WHERE src.project_id = p_project_id + AND src.subsetted_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'subsetting_version'::text, 'subsetting_feature'::text, + snap.identity_id, src.subsetting_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.subsetting_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.subsetting_feature + LEFT JOIN validation_snapshot live ON live.identity_id = src.subsetting_feature + WHERE src.project_id = p_project_id + AND src.subsetting_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'type_featuring_version'::text, 'feature_of_type'::text, + snap.identity_id, src.feature_of_type, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.type_featuring_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.feature_of_type + LEFT JOIN validation_snapshot live ON live.identity_id = src.feature_of_type + WHERE src.project_id = p_project_id + AND src.feature_of_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'type_featuring_version'::text, 'featuring_type'::text, + snap.identity_id, src.featuring_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.type_featuring_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.featuring_type + LEFT JOIN validation_snapshot live ON live.identity_id = src.featuring_type + WHERE src.project_id = p_project_id + AND src.featuring_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'unioning_version'::text, 'unioning_type'::text, + snap.identity_id, src.unioning_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.unioning_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.unioning_type + LEFT JOIN validation_snapshot live ON live.identity_id = src.unioning_type + WHERE src.project_id = p_project_id + AND src.unioning_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)); +END; +$$; + +-- The INCREMENTAL tier: validates only commit p_commit_id's change set — outgoing +-- references of its new versions, plus the reverse direction its tombstones break +-- (a live, UNCHANGED element left referencing a deleted identity). O(change set), +-- independent of history and snapshot size; the full pass above remains the +-- periodic audit that backstops it. +CREATE OR REPLACE FUNCTION sysml2.validate_references_in_commit( + p_project_id uuid, + p_commit_id uuid +) +RETURNS TABLE ( + source_table text, + source_column text, + source_identity uuid, + target_identity uuid, + problem text +) +LANGUAGE sql +STABLE +AS $$ + SELECT DISTINCT findings.source_table, findings.source_column, + findings.source_identity, findings.target_identity, findings.problem + FROM ( + SELECT 'annotation_version'::text, 'annotated_element'::text, + changed.identity_id, src.annotated_element, + 'dangling'::text + FROM sysml2.element_version changed + JOIN sysml2.annotation_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.annotated_element + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.annotated_element IS NOT NULL + AND (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.annotated_element))) + UNION ALL + SELECT 'conjugated_port_typing_version'::text, 'conjugated_port_definition'::text, + changed.identity_id, src.conjugated_port_definition, + CASE WHEN ti.class_kind NOT IN (31) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.conjugated_port_typing_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.conjugated_port_definition + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.conjugated_port_definition IS NOT NULL + AND (ti.class_kind NOT IN (31) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.conjugated_port_definition)))) + UNION ALL + SELECT 'conjugation_version'::text, 'conjugated_type'::text, + changed.identity_id, src.conjugated_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.conjugation_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.conjugated_type + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.conjugated_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.conjugated_type)))) + UNION ALL + SELECT 'conjugation_version'::text, 'original_type'::text, + changed.identity_id, src.original_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.conjugation_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.original_type + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.original_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.original_type)))) + UNION ALL + SELECT 'cross_subsetting_version'::text, 'crossed_feature'::text, + changed.identity_id, src.crossed_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.cross_subsetting_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.crossed_feature + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.crossed_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.crossed_feature)))) + UNION ALL + SELECT 'dependency_client'::text, 'target_identity'::text, + changed.identity_id, src.target_identity, + 'dangling'::text + FROM sysml2.element_version changed + JOIN sysml2.dependency_client src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.target_identity + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.target_identity IS NOT NULL + AND (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.target_identity))) + UNION ALL + SELECT 'dependency_supplier'::text, 'target_identity'::text, + changed.identity_id, src.target_identity, + 'dangling'::text + FROM sysml2.element_version changed + JOIN sysml2.dependency_supplier src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.target_identity + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.target_identity IS NOT NULL + AND (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.target_identity))) + UNION ALL + SELECT 'differencing_version'::text, 'differencing_type'::text, + changed.identity_id, src.differencing_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.differencing_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.differencing_type + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.differencing_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.differencing_type)))) + UNION ALL + SELECT 'disjoining_version'::text, 'disjoining_type'::text, + changed.identity_id, src.disjoining_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.disjoining_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.disjoining_type + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.disjoining_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.disjoining_type)))) + UNION ALL + SELECT 'disjoining_version'::text, 'type_disjoined'::text, + changed.identity_id, src.type_disjoined, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.disjoining_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.type_disjoined + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.type_disjoined IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.type_disjoined)))) + UNION ALL + SELECT 'element_version'::text, 'owning_relationship'::text, + changed.identity_id, changed.owning_relationship, + CASE WHEN ti.class_kind NOT IN (4, 5, 6, 10, 13, 14, 18, 19, 32, 33, 34, 35, 36, 42, 46, 47, 48, 51, 52, 61, 62, 63, 65, 66, 67, 68, 70, 73, 80, 81, 82, 83, 97, 98, 99, 109, 110, 112, 116, 118, 123, 127, 128, 133, 136, 137, 138, 142, 143, 145, 149, 150, 151, 152, 153, 154, 155, 158, 162, 163, 167, 171) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.data_identity ti ON ti.id = changed.owning_relationship + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND changed.owning_relationship IS NOT NULL + AND (ti.class_kind NOT IN (4, 5, 6, 10, 13, 14, 18, 19, 32, 33, 34, 35, 36, 42, 46, 47, 48, 51, 52, 61, 62, 63, 65, 66, 67, 68, 70, 73, 80, 81, 82, 83, 97, 98, 99, 109, 110, 112, 116, 118, 123, 127, 128, 133, 136, 137, 138, 142, 143, 145, 149, 150, 151, 152, 153, 154, 155, 158, 162, 163, 167, 171) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, changed.owning_relationship)))) + UNION ALL + SELECT 'element_owned_relationship'::text, 'target_identity'::text, + changed.identity_id, src.target_identity, + CASE WHEN ti.class_kind NOT IN (4, 5, 6, 10, 13, 14, 18, 19, 32, 33, 34, 35, 36, 42, 46, 47, 48, 51, 52, 61, 62, 63, 65, 66, 67, 68, 70, 73, 80, 81, 82, 83, 97, 98, 99, 109, 110, 112, 116, 118, 123, 127, 128, 133, 136, 137, 138, 142, 143, 145, 149, 150, 151, 152, 153, 154, 155, 158, 162, 163, 167, 171) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.element_owned_relationship src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.target_identity + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.target_identity IS NOT NULL + AND (ti.class_kind NOT IN (4, 5, 6, 10, 13, 14, 18, 19, 32, 33, 34, 35, 36, 42, 46, 47, 48, 51, 52, 61, 62, 63, 65, 66, 67, 68, 70, 73, 80, 81, 82, 83, 97, 98, 99, 109, 110, 112, 116, 118, 123, 127, 128, 133, 136, 137, 138, 142, 143, 145, 149, 150, 151, 152, 153, 154, 155, 158, 162, 163, 167, 171) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.target_identity)))) + UNION ALL + SELECT 'feature_chaining_version'::text, 'chaining_feature'::text, + changed.identity_id, src.chaining_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.feature_chaining_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.chaining_feature + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.chaining_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.chaining_feature)))) + UNION ALL + SELECT 'feature_inverting_version'::text, 'feature_inverted'::text, + changed.identity_id, src.feature_inverted, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.feature_inverting_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.feature_inverted + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.feature_inverted IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.feature_inverted)))) + UNION ALL + SELECT 'feature_inverting_version'::text, 'inverting_feature'::text, + changed.identity_id, src.inverting_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.feature_inverting_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.inverting_feature + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.inverting_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.inverting_feature)))) + UNION ALL + SELECT 'feature_typing_version'::text, 'type'::text, + changed.identity_id, src.type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.feature_typing_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.type + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.type)))) + UNION ALL + SELECT 'feature_typing_version'::text, 'typed_feature'::text, + changed.identity_id, src.typed_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.feature_typing_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.typed_feature + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.typed_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.typed_feature)))) + UNION ALL + SELECT 'intersecting_version'::text, 'intersecting_type'::text, + changed.identity_id, src.intersecting_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.intersecting_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.intersecting_type + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.intersecting_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.intersecting_type)))) + UNION ALL + SELECT 'membership_version'::text, 'member_element'::text, + changed.identity_id, src.member_element, + 'dangling'::text + FROM sysml2.element_version changed + JOIN sysml2.membership_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.member_element + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.member_element IS NOT NULL + AND (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.member_element))) + UNION ALL + SELECT 'membership_import_version'::text, 'imported_membership'::text, + changed.identity_id, src.imported_membership, + CASE WHEN ti.class_kind NOT IN (4, 51, 52, 63, 66, 73, 97, 112, 116, 118, 133, 136, 137, 138, 143, 145, 150, 158, 167, 171) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.membership_import_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.imported_membership + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.imported_membership IS NOT NULL + AND (ti.class_kind NOT IN (4, 51, 52, 63, 66, 73, 97, 112, 116, 118, 133, 136, 137, 138, 143, 145, 150, 158, 167, 171) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.imported_membership)))) + UNION ALL + SELECT 'namespace_import_version'::text, 'imported_namespace'::text, + changed.identity_id, src.imported_namespace, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 108, 111, 113, 114, 115, 117, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.namespace_import_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.imported_namespace + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.imported_namespace IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 108, 111, 113, 114, 115, 117, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.imported_namespace)))) + UNION ALL + SELECT 'port_conjugation_version'::text, 'original_port_definition'::text, + changed.identity_id, src.original_port_definition, + CASE WHEN ti.class_kind NOT IN (31, 124) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.port_conjugation_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.original_port_definition + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.original_port_definition IS NOT NULL + AND (ti.class_kind NOT IN (31, 124) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.original_port_definition)))) + UNION ALL + SELECT 'redefinition_version'::text, 'redefined_feature'::text, + changed.identity_id, src.redefined_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.redefinition_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.redefined_feature + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.redefined_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.redefined_feature)))) + UNION ALL + SELECT 'redefinition_version'::text, 'redefining_feature'::text, + changed.identity_id, src.redefining_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.redefinition_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.redefining_feature + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.redefining_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.redefining_feature)))) + UNION ALL + SELECT 'reference_subsetting_version'::text, 'referenced_feature'::text, + changed.identity_id, src.referenced_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.reference_subsetting_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.referenced_feature + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.referenced_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.referenced_feature)))) + UNION ALL + SELECT 'relationship_version'::text, 'owning_related_element'::text, + changed.identity_id, src.owning_related_element, + 'dangling'::text + FROM sysml2.element_version changed + JOIN sysml2.relationship_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.owning_related_element + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.owning_related_element IS NOT NULL + AND (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.owning_related_element))) + UNION ALL + SELECT 'relationship_owned_related_element'::text, 'target_identity'::text, + changed.identity_id, src.target_identity, + 'dangling'::text + FROM sysml2.element_version changed + JOIN sysml2.relationship_owned_related_element src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.target_identity + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.target_identity IS NOT NULL + AND (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.target_identity))) + UNION ALL + SELECT 'relationship_source'::text, 'target_identity'::text, + changed.identity_id, src.target_identity, + 'dangling'::text + FROM sysml2.element_version changed + JOIN sysml2.relationship_source src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.target_identity + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.target_identity IS NOT NULL + AND (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.target_identity))) + UNION ALL + SELECT 'relationship_target'::text, 'target_identity'::text, + changed.identity_id, src.target_identity, + 'dangling'::text + FROM sysml2.element_version changed + JOIN sysml2.relationship_target src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.target_identity + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.target_identity IS NOT NULL + AND (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.target_identity))) + UNION ALL + SELECT 'specialization_version'::text, 'general'::text, + changed.identity_id, src.general, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.specialization_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.general + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.general IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.general)))) + UNION ALL + SELECT 'specialization_version'::text, 'specific'::text, + changed.identity_id, src.specific, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.specialization_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.specific + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.specific IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.specific)))) + UNION ALL + SELECT 'subclassification_version'::text, 'subclassifier'::text, + changed.identity_id, src.subclassifier, + CASE WHEN ti.class_kind NOT IN (2, 5, 7, 13, 14, 15, 17, 21, 23, 25, 26, 29, 31, 34, 38, 43, 45, 53, 68, 74, 80, 81, 86, 101, 103, 113, 119, 124, 126, 131, 134, 144, 148, 165, 168, 170, 173) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.subclassification_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.subclassifier + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.subclassifier IS NOT NULL + AND (ti.class_kind NOT IN (2, 5, 7, 13, 14, 15, 17, 21, 23, 25, 26, 29, 31, 34, 38, 43, 45, 53, 68, 74, 80, 81, 86, 101, 103, 113, 119, 124, 126, 131, 134, 144, 148, 165, 168, 170, 173) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.subclassifier)))) + UNION ALL + SELECT 'subclassification_version'::text, 'superclassifier'::text, + changed.identity_id, src.superclassifier, + CASE WHEN ti.class_kind NOT IN (2, 5, 7, 13, 14, 15, 17, 21, 23, 25, 26, 29, 31, 34, 38, 43, 45, 53, 68, 74, 80, 81, 86, 101, 103, 113, 119, 124, 126, 131, 134, 144, 148, 165, 168, 170, 173) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.subclassification_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.superclassifier + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.superclassifier IS NOT NULL + AND (ti.class_kind NOT IN (2, 5, 7, 13, 14, 15, 17, 21, 23, 25, 26, 29, 31, 34, 38, 43, 45, 53, 68, 74, 80, 81, 86, 101, 103, 113, 119, 124, 126, 131, 134, 144, 148, 165, 168, 170, 173) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.superclassifier)))) + UNION ALL + SELECT 'subsetting_version'::text, 'subsetted_feature'::text, + changed.identity_id, src.subsetted_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.subsetting_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.subsetted_feature + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.subsetted_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.subsetted_feature)))) + UNION ALL + SELECT 'subsetting_version'::text, 'subsetting_feature'::text, + changed.identity_id, src.subsetting_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.subsetting_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.subsetting_feature + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.subsetting_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.subsetting_feature)))) + UNION ALL + SELECT 'type_featuring_version'::text, 'feature_of_type'::text, + changed.identity_id, src.feature_of_type, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.type_featuring_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.feature_of_type + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.feature_of_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.feature_of_type)))) + UNION ALL + SELECT 'type_featuring_version'::text, 'featuring_type'::text, + changed.identity_id, src.featuring_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.type_featuring_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.featuring_type + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.featuring_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.featuring_type)))) + UNION ALL + SELECT 'unioning_version'::text, 'unioning_type'::text, + changed.identity_id, src.unioning_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.unioning_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.unioning_type + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.unioning_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.unioning_type)))) + UNION ALL + SELECT 'annotation_version'::text, 'annotated_element'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.annotation_version src + ON src.project_id = dead.project_id AND src.annotated_element = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'conjugated_port_typing_version'::text, 'conjugated_port_definition'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.conjugated_port_typing_version src + ON src.project_id = dead.project_id AND src.conjugated_port_definition = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'conjugation_version'::text, 'conjugated_type'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.conjugation_version src + ON src.project_id = dead.project_id AND src.conjugated_type = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'conjugation_version'::text, 'original_type'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.conjugation_version src + ON src.project_id = dead.project_id AND src.original_type = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'cross_subsetting_version'::text, 'crossed_feature'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.cross_subsetting_version src + ON src.project_id = dead.project_id AND src.crossed_feature = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'dependency_client'::text, 'target_identity'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.dependency_client src + ON src.project_id = dead.project_id AND src.target_identity = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'dependency_supplier'::text, 'target_identity'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.dependency_supplier src + ON src.project_id = dead.project_id AND src.target_identity = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'differencing_version'::text, 'differencing_type'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.differencing_version src + ON src.project_id = dead.project_id AND src.differencing_type = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'disjoining_version'::text, 'disjoining_type'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.disjoining_version src + ON src.project_id = dead.project_id AND src.disjoining_type = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'disjoining_version'::text, 'type_disjoined'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.disjoining_version src + ON src.project_id = dead.project_id AND src.type_disjoined = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'element_version'::text, 'owning_relationship'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.element_version holder + ON holder.project_id = dead.project_id AND holder.owning_relationship = dead.identity_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'element_owned_relationship'::text, 'target_identity'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.element_owned_relationship src + ON src.project_id = dead.project_id AND src.target_identity = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'feature_chaining_version'::text, 'chaining_feature'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.feature_chaining_version src + ON src.project_id = dead.project_id AND src.chaining_feature = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'feature_inverting_version'::text, 'feature_inverted'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.feature_inverting_version src + ON src.project_id = dead.project_id AND src.feature_inverted = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'feature_inverting_version'::text, 'inverting_feature'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.feature_inverting_version src + ON src.project_id = dead.project_id AND src.inverting_feature = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'feature_typing_version'::text, 'type'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.feature_typing_version src + ON src.project_id = dead.project_id AND src.type = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'feature_typing_version'::text, 'typed_feature'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.feature_typing_version src + ON src.project_id = dead.project_id AND src.typed_feature = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'intersecting_version'::text, 'intersecting_type'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.intersecting_version src + ON src.project_id = dead.project_id AND src.intersecting_type = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'membership_version'::text, 'member_element'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.membership_version src + ON src.project_id = dead.project_id AND src.member_element = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'membership_import_version'::text, 'imported_membership'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.membership_import_version src + ON src.project_id = dead.project_id AND src.imported_membership = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'namespace_import_version'::text, 'imported_namespace'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.namespace_import_version src + ON src.project_id = dead.project_id AND src.imported_namespace = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'port_conjugation_version'::text, 'original_port_definition'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.port_conjugation_version src + ON src.project_id = dead.project_id AND src.original_port_definition = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'redefinition_version'::text, 'redefined_feature'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.redefinition_version src + ON src.project_id = dead.project_id AND src.redefined_feature = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'redefinition_version'::text, 'redefining_feature'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.redefinition_version src + ON src.project_id = dead.project_id AND src.redefining_feature = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'reference_subsetting_version'::text, 'referenced_feature'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.reference_subsetting_version src + ON src.project_id = dead.project_id AND src.referenced_feature = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'relationship_version'::text, 'owning_related_element'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.relationship_version src + ON src.project_id = dead.project_id AND src.owning_related_element = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'relationship_owned_related_element'::text, 'target_identity'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.relationship_owned_related_element src + ON src.project_id = dead.project_id AND src.target_identity = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'relationship_source'::text, 'target_identity'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.relationship_source src + ON src.project_id = dead.project_id AND src.target_identity = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'relationship_target'::text, 'target_identity'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.relationship_target src + ON src.project_id = dead.project_id AND src.target_identity = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'specialization_version'::text, 'general'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.specialization_version src + ON src.project_id = dead.project_id AND src.general = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'specialization_version'::text, 'specific'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.specialization_version src + ON src.project_id = dead.project_id AND src.specific = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'subclassification_version'::text, 'subclassifier'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.subclassification_version src + ON src.project_id = dead.project_id AND src.subclassifier = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'subclassification_version'::text, 'superclassifier'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.subclassification_version src + ON src.project_id = dead.project_id AND src.superclassifier = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'subsetting_version'::text, 'subsetted_feature'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.subsetting_version src + ON src.project_id = dead.project_id AND src.subsetted_feature = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'subsetting_version'::text, 'subsetting_feature'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.subsetting_version src + ON src.project_id = dead.project_id AND src.subsetting_feature = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'type_featuring_version'::text, 'feature_of_type'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.type_featuring_version src + ON src.project_id = dead.project_id AND src.feature_of_type = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'type_featuring_version'::text, 'featuring_type'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.type_featuring_version src + ON src.project_id = dead.project_id AND src.featuring_type = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'unioning_version'::text, 'unioning_type'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.unioning_version src + ON src.project_id = dead.project_id AND src.unioning_type = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + ) AS findings (source_table, source_column, source_identity, target_identity, problem); +$$; diff --git a/SysML2.NET.CodeGenerator/Sql/schema.smoke.sql b/SysML2.NET.CodeGenerator/Sql/schema.smoke.sql new file mode 100644 index 000000000..311d6613b --- /dev/null +++ b/SysML2.NET.CodeGenerator/Sql/schema.smoke.sql @@ -0,0 +1,753 @@ +-- Smoke test for SysML2.NET.CodeGenerator/Sql/schema.golden.sql +-- +-- Scenario, exercising the load-bearing claim of the design: +-- +-- c1: create Package "Old" and a PartUsage "wheel" owned by it. +-- c2: RENAME the package to "New". The PartUsage is NOT touched — no new element_version row +-- for it — yet its derived qualifiedName MUST change from Old::wheel to New::wheel. +-- This is the case that proves derived state cannot live on element_version. +-- c3: delete the PartUsage (tombstone). +-- +-- Everything below runs on a clean database and RAISE EXCEPTIONs on any wrong answer. + +SET search_path = sysml2, public; + +INSERT INTO sysml2.model_version (id, name, source_fingerprint) VALUES + (1, 'smoke-release-1', 'smoke:v1'), + (2, 'smoke-release-2', 'smoke:v2') +ON CONFLICT (id) DO NOTHING; + +-- The FROZEN registry ids of the three metaclasses this test uses (see ClassKindRegistry) — +-- identical to the generated schema's seeds, so this INSERT is a no-op there and every +-- hard-coded id in validate_references_at_commit() means the same thing in both contexts. +INSERT INTO sysml2.class_kind (id, name, is_abstract, introduced_in) VALUES + (116, 'OwningMembership', false, 1), + (117, 'Package', false, 1), + (120, 'PartUsage', false, 1) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO sysml2.project (id, name, created) VALUES + ('11111111-0000-0000-0000-000000000000', 'SmokeProject', '2026-01-01T00:00:00Z'); + +INSERT INTO sysml2.commit (id, project_id, created, description, model_version_id) VALUES + ('c1111111-0000-0000-0000-000000000000', '11111111-0000-0000-0000-000000000000', '2026-01-01T10:00:00Z', 'create', 1), + ('c2222222-0000-0000-0000-000000000000', '11111111-0000-0000-0000-000000000000', '2026-01-01T11:00:00Z', 'rename package', 1), + ('c3333333-0000-0000-0000-000000000000', '11111111-0000-0000-0000-000000000000', '2026-01-01T12:00:00Z', 'delete wheel', 1); + +INSERT INTO sysml2.commit_parent (commit_id, parent_commit_id, ordinal) VALUES + ('c2222222-0000-0000-0000-000000000000', 'c1111111-0000-0000-0000-000000000000', 0), + ('c3333333-0000-0000-0000-000000000000', 'c2222222-0000-0000-0000-000000000000', 0); + +INSERT INTO sysml2.branch (id, project_id, name, head_commit_id) VALUES + ('b1111111-0000-0000-0000-000000000000', '11111111-0000-0000-0000-000000000000', 'main', + 'c2222222-0000-0000-0000-000000000000'); + +UPDATE sysml2.project SET default_branch_id = 'b1111111-0000-0000-0000-000000000000'; + +-- Identities: the stable @id of each element, independent of version — TYPED (§4): the +-- metaclass is invariant across versions, so it lives on the identity. +INSERT INTO sysml2.data_identity (id, project_id, class_kind) VALUES + ('e1111111-0000-0000-0000-000000000000', '11111111-0000-0000-0000-000000000000', + (SELECT id FROM sysml2.class_kind WHERE name = 'Package')), + ('e2222222-0000-0000-0000-000000000000', '11111111-0000-0000-0000-000000000000', + (SELECT id FROM sysml2.class_kind WHERE name = 'PartUsage')); + +---------------------------------------------------------------------------------------------- +-- c1 — create both elements +---------------------------------------------------------------------------------------------- + +INSERT INTO sysml2.element_version + (project_id, version_id, identity_id, commit_id, class_kind, tombstone, + element_id, declared_name, is_implied_included, stored_json) +VALUES + ('11111111-0000-0000-0000-000000000000', 'a1111111-0000-0000-0000-000000000000', + 'e1111111-0000-0000-0000-000000000000', 'c1111111-0000-0000-0000-000000000000', (SELECT id FROM sysml2.class_kind WHERE name = 'Package'), false, + 'e1111111-0000-0000-0000-000000000000', 'Old', false, + '{"@id":"e1111111-0000-0000-0000-000000000000","@type":"Package","declaredName":"Old"}'), + + ('11111111-0000-0000-0000-000000000000', 'a2222222-0000-0000-0000-000000000000', + 'e2222222-0000-0000-0000-000000000000', 'c1111111-0000-0000-0000-000000000000', (SELECT id FROM sysml2.class_kind WHERE name = 'PartUsage'), false, + 'e2222222-0000-0000-0000-000000000000', 'wheel', false, + '{"@id":"e2222222-0000-0000-0000-000000000000","@type":"PartUsage","declaredName":"wheel"}'); + +-- PartUsage participates in type_version / feature_version / usage_version / occurrence_usage_version +INSERT INTO sysml2.type_version VALUES ('11111111-0000-0000-0000-000000000000', 'a2222222-0000-0000-0000-000000000000', false, false); +INSERT INTO sysml2.feature_version VALUES ('11111111-0000-0000-0000-000000000000', 'a2222222-0000-0000-0000-000000000000', + NULL, false, false, false, false, false, false, true, false); +INSERT INTO sysml2.usage_version VALUES ('11111111-0000-0000-0000-000000000000', 'a2222222-0000-0000-0000-000000000000', false); +INSERT INTO sysml2.occurrence_usage_version VALUES ('11111111-0000-0000-0000-000000000000', 'a2222222-0000-0000-0000-000000000000', false, NULL); + +INSERT INTO sysml2.derived_version + (project_id, derived_id, identity_id, commit_id, owner, qualified_name, name, derived_json) +VALUES + ('11111111-0000-0000-0000-000000000000', 'd1111111-0000-0000-0000-000000000000', + 'e1111111-0000-0000-0000-000000000000', 'c1111111-0000-0000-0000-000000000000', + NULL, 'Old', 'Old', '{"qualifiedName":"Old","name":"Old","owner":null}'), + + ('11111111-0000-0000-0000-000000000000', 'd2222222-0000-0000-0000-000000000000', + 'e2222222-0000-0000-0000-000000000000', 'c1111111-0000-0000-0000-000000000000', + 'e1111111-0000-0000-0000-000000000000', 'Old::wheel', 'wheel', + '{"qualifiedName":"Old::wheel","name":"wheel","owner":"e1111111-0000-0000-0000-000000000000"}'); + +---------------------------------------------------------------------------------------------- +-- c2 — rename the Package ONLY. No new element_version row for the PartUsage. +-- But the PartUsage DOES get a new derived_version row: it is in the impact radius. +---------------------------------------------------------------------------------------------- + +INSERT INTO sysml2.element_version + (project_id, version_id, identity_id, commit_id, class_kind, tombstone, + element_id, declared_name, is_implied_included, stored_json) +VALUES + ('11111111-0000-0000-0000-000000000000', 'a3333333-0000-0000-0000-000000000000', + 'e1111111-0000-0000-0000-000000000000', 'c2222222-0000-0000-0000-000000000000', (SELECT id FROM sysml2.class_kind WHERE name = 'Package'), false, + 'e1111111-0000-0000-0000-000000000000', 'New', false, + '{"@id":"e1111111-0000-0000-0000-000000000000","@type":"Package","declaredName":"New"}'); + +INSERT INTO sysml2.derived_version + (project_id, derived_id, identity_id, commit_id, owner, qualified_name, name, derived_json) +VALUES + ('11111111-0000-0000-0000-000000000000', 'd3333333-0000-0000-0000-000000000000', + 'e1111111-0000-0000-0000-000000000000', 'c2222222-0000-0000-0000-000000000000', + NULL, 'New', 'New', '{"qualifiedName":"New","name":"New","owner":null}'), + + -- the impact radius: the child's derived values changed although the child did not + ('11111111-0000-0000-0000-000000000000', 'd4444444-0000-0000-0000-000000000000', + 'e2222222-0000-0000-0000-000000000000', 'c2222222-0000-0000-0000-000000000000', + 'e1111111-0000-0000-0000-000000000000', 'New::wheel', 'wheel', + '{"qualifiedName":"New::wheel","name":"wheel","owner":"e1111111-0000-0000-0000-000000000000"}'); + +INSERT INTO sysml2.branch_head (project_id, branch_id, identity_id, version_id, derived_id) VALUES + ('11111111-0000-0000-0000-000000000000', 'b1111111-0000-0000-0000-000000000000', + 'e1111111-0000-0000-0000-000000000000', 'a3333333-0000-0000-0000-000000000000', 'd3333333-0000-0000-0000-000000000000'), + ('11111111-0000-0000-0000-000000000000', 'b1111111-0000-0000-0000-000000000000', + 'e2222222-0000-0000-0000-000000000000', 'a2222222-0000-0000-0000-000000000000', 'd4444444-0000-0000-0000-000000000000'); + +---------------------------------------------------------------------------------------------- +-- c3 — delete the PartUsage (DataVersion.payload = null) +---------------------------------------------------------------------------------------------- + +INSERT INTO sysml2.element_version + (project_id, version_id, identity_id, commit_id, class_kind, tombstone) +VALUES + ('11111111-0000-0000-0000-000000000000', 'a4444444-0000-0000-0000-000000000000', + 'e2222222-0000-0000-0000-000000000000', 'c3333333-0000-0000-0000-000000000000', (SELECT id FROM sysml2.class_kind WHERE name = 'PartUsage'), true); + +---------------------------------------------------------------------------------------------- +-- ASSERTIONS +---------------------------------------------------------------------------------------------- + +DO $$ +DECLARE + proj constant uuid := '11111111-0000-0000-0000-000000000000'; + wheel constant uuid := 'e2222222-0000-0000-0000-000000000000'; + c1 constant uuid := 'c1111111-0000-0000-0000-000000000000'; + c2 constant uuid := 'c2222222-0000-0000-0000-000000000000'; + c3 constant uuid := 'c3333333-0000-0000-0000-000000000000'; + actual text; + actual_id uuid; + row_count int; +BEGIN + -- 1. At c1 the wheel's qualifiedName is Old::wheel + SELECT dv.qualified_name INTO actual + FROM sysml2.resolve_commit_state(proj, c1) r + JOIN sysml2.derived_version dv ON dv.project_id = proj AND dv.derived_id = r.derived_id + WHERE r.identity_id = wheel; + + IF actual IS DISTINCT FROM 'Old::wheel' THEN + RAISE EXCEPTION 'FAIL 1: qualifiedName at c1 = %, expected Old::wheel', actual; + END IF; + RAISE NOTICE 'PASS 1: qualifiedName at c1 = Old::wheel'; + + -- 2. At c2 the qualifiedName is New::wheel -- WITHOUT the wheel having a new version + SELECT dv.qualified_name, r.version_id INTO actual, actual_id + FROM sysml2.resolve_commit_state(proj, c2) r + JOIN sysml2.derived_version dv ON dv.project_id = proj AND dv.derived_id = r.derived_id + WHERE r.identity_id = wheel; + + IF actual IS DISTINCT FROM 'New::wheel' THEN + RAISE EXCEPTION 'FAIL 2a: qualifiedName at c2 = %, expected New::wheel', actual; + END IF; + RAISE NOTICE 'PASS 2a: qualifiedName at c2 = New::wheel (derived changed)'; + + IF actual_id IS DISTINCT FROM 'a2222222-0000-0000-0000-000000000000'::uuid THEN + RAISE EXCEPTION 'FAIL 2b: wheel version at c2 = %, expected the ORIGINAL a2222222 (unchanged)', actual_id; + END IF; + RAISE NOTICE 'PASS 2b: wheel still resolves to its ORIGINAL version a2222222 (stored unchanged)'; + + -- 3. At c3 the wheel is gone (tombstone), and only the Package survives + SELECT count(*) INTO row_count + FROM sysml2.resolve_commit_state(proj, c3) r + WHERE r.identity_id = wheel; + + IF row_count <> 0 THEN + RAISE EXCEPTION 'FAIL 3a: wheel still present at c3 (tombstone not honoured)'; + END IF; + + SELECT count(*) INTO row_count FROM sysml2.resolve_commit_state(proj, c3); + IF row_count <> 1 THEN + RAISE EXCEPTION 'FAIL 3b: expected 1 surviving element at c3, got %', row_count; + END IF; + RAISE NOTICE 'PASS 3: wheel tombstoned at c3; 1 element survives'; + + -- 4. Branch-head read path returns stored || derived as one merged payload + SELECT sysml2.get_element_at_branch_head('b1111111-0000-0000-0000-000000000000', wheel) ->> 'qualifiedName' + INTO actual; + + IF actual IS DISTINCT FROM 'New::wheel' THEN + RAISE EXCEPTION 'FAIL 4a: branch-head payload qualifiedName = %, expected New::wheel', actual; + END IF; + + SELECT sysml2.get_element_at_branch_head('b1111111-0000-0000-0000-000000000000', wheel) ->> '@type' + INTO actual; + + IF actual IS DISTINCT FROM 'PartUsage' THEN + RAISE EXCEPTION 'FAIL 4b: branch-head payload @type = %, expected PartUsage', actual; + END IF; + RAISE NOTICE 'PASS 4: branch-head read merges stored_json (@type) with derived_json (qualifiedName)'; + + -- 5. get_elements_at_commit returns merged payloads for the whole snapshot + SELECT count(*) INTO row_count FROM sysml2.get_elements_at_commit(proj, c2); + IF row_count <> 2 THEN + RAISE EXCEPTION 'FAIL 5: expected 2 elements at c2, got %', row_count; + END IF; + RAISE NOTICE 'PASS 5: get_elements_at_commit(c2) returns 2 merged payloads'; +END; +$$; + +-- 6. The monotonic-commit trigger must reject a parent edge that goes backwards in time. +-- The snapshot resolver orders by commit.created, so a violation would silently produce the +-- WRONG snapshot rather than an error. Prove the trigger fires. +DO $$ +BEGIN + INSERT INTO sysml2.commit (id, project_id, created, model_version_id) + VALUES ('c9999999-0000-0000-0000-000000000000', '11111111-0000-0000-0000-000000000000', + '2020-01-01T00:00:00Z', 1); -- older than its parent-to-be + + BEGIN + INSERT INTO sysml2.commit_parent (commit_id, parent_commit_id, ordinal) + VALUES ('c9999999-0000-0000-0000-000000000000', 'c1111111-0000-0000-0000-000000000000', 0); + + RAISE EXCEPTION 'FAIL 6: non-monotonic commit parent was ACCEPTED'; + EXCEPTION WHEN check_violation THEN + RAISE NOTICE 'PASS 6: non-monotonic commit parent rejected by trg_commit_parent_monotonic'; + END; +END; +$$; + +-- 7. Referential integrity targets IDENTITIES, not versions: a reference to an unknown element +-- must be rejected. +DO $$ +BEGIN + BEGIN + INSERT INTO sysml2.element_owned_relationship (project_id, version_id, ordinal, target_identity) + VALUES ('11111111-0000-0000-0000-000000000000', 'a1111111-0000-0000-0000-000000000000', 0, + 'dddddddd-dead-dead-dead-dddddddddddd'); + + RAISE EXCEPTION 'FAIL 7: dangling reference to a non-existent data_identity was ACCEPTED'; + EXCEPTION WHEN foreign_key_violation THEN + RAISE NOTICE 'PASS 7: dangling element reference rejected by FK to data_identity'; + END; +END; +$$; + +---------------------------------------------------------------------------------------------- +-- MERGE SCENARIO — the DAG fold's hardest case. +-- +-- c1 ──> c2 (rename Package to "New") ──> c3 (delete wheel) +-- \ \ +-- \ ──> c5 (MERGE: parents c2 + c4, +-- ──> c4 (rename Package to "Other") resolves Package to "Merged") +-- +-- Clause 7.1.2: "A Commit must resolve all conflicts in its parent Commits" — the merge carries +-- the resolution in its OWN change set, which is exactly why newest-ancestor-wins is correct. +-- Note c3 is NOT an ancestor of c5, so the wheel deleted at c3 must still be ALIVE at c5. +---------------------------------------------------------------------------------------------- + +INSERT INTO sysml2.commit (id, project_id, created, description, model_version_id) VALUES + ('c4444444-0000-0000-0000-000000000000', '11111111-0000-0000-0000-000000000000', '2026-01-01T10:30:00Z', 'concurrent rename on side branch', 1), + ('c5555555-0000-0000-0000-000000000000', '11111111-0000-0000-0000-000000000000', '2026-01-01T13:00:00Z', 'merge c2 + c4', 1); + +INSERT INTO sysml2.commit_parent (commit_id, parent_commit_id, ordinal) VALUES + ('c4444444-0000-0000-0000-000000000000', 'c1111111-0000-0000-0000-000000000000', 0), + ('c5555555-0000-0000-0000-000000000000', 'c2222222-0000-0000-0000-000000000000', 0), + ('c5555555-0000-0000-0000-000000000000', 'c4444444-0000-0000-0000-000000000000', 1); + +INSERT INTO sysml2.element_version + (project_id, version_id, identity_id, commit_id, class_kind, tombstone, + element_id, declared_name, is_implied_included, stored_json) +VALUES + -- c4: the side branch renames the Package to "Other" (conflicts with c2's "New") + ('11111111-0000-0000-0000-000000000000', 'a5555555-0000-0000-0000-000000000000', + 'e1111111-0000-0000-0000-000000000000', 'c4444444-0000-0000-0000-000000000000', (SELECT id FROM sysml2.class_kind WHERE name = 'Package'), false, + 'e1111111-0000-0000-0000-000000000000', 'Other', false, + '{"@id":"e1111111-0000-0000-0000-000000000000","@type":"Package","declaredName":"Other"}'), + + -- c5: the merge resolves the conflict to "Merged" in its own change set + ('11111111-0000-0000-0000-000000000000', 'a6666666-0000-0000-0000-000000000000', + 'e1111111-0000-0000-0000-000000000000', 'c5555555-0000-0000-0000-000000000000', (SELECT id FROM sysml2.class_kind WHERE name = 'Package'), false, + 'e1111111-0000-0000-0000-000000000000', 'Merged', false, + '{"@id":"e1111111-0000-0000-0000-000000000000","@type":"Package","declaredName":"Merged"}'); + +DO $$ +DECLARE + proj constant uuid := '11111111-0000-0000-0000-000000000000'; + package constant uuid := 'e1111111-0000-0000-0000-000000000000'; + wheel constant uuid := 'e2222222-0000-0000-0000-000000000000'; + c5 constant uuid := 'c5555555-0000-0000-0000-000000000000'; + actual text; + actual_id uuid; + row_count int; +BEGIN + -- 8a. At the merge, the Package resolves to the MERGE's own resolution, not either parent's + SELECT ev.declared_name, r.version_id INTO actual, actual_id + FROM sysml2.resolve_commit_state(proj, c5) r + JOIN sysml2.element_version ev ON ev.project_id = proj AND ev.version_id = r.version_id + WHERE r.identity_id = package; + + IF actual IS DISTINCT FROM 'Merged' + OR actual_id IS DISTINCT FROM 'a6666666-0000-0000-0000-000000000000'::uuid THEN + RAISE EXCEPTION 'FAIL 8a: Package at merge = % (version %), expected Merged (a6666666)', actual, actual_id; + END IF; + RAISE NOTICE 'PASS 8a: merge resolution wins over both parents (Package = Merged)'; + + -- 8b. The wheel was deleted on c3, but c3 is NOT an ancestor of the merge — it must be + -- alive at c5, still at its original version from c1. + SELECT r.version_id INTO actual_id + FROM sysml2.resolve_commit_state(proj, c5) r + WHERE r.identity_id = wheel; + + IF actual_id IS DISTINCT FROM 'a2222222-0000-0000-0000-000000000000'::uuid THEN + RAISE EXCEPTION 'FAIL 8b: wheel at merge = %, expected original a2222222 (c3 is not an ancestor)', actual_id; + END IF; + RAISE NOTICE 'PASS 8b: wheel alive at merge (deletion on c3 correctly out of scope)'; + + -- 8c. Exactly 2 elements at the merge; and the wheel still carries its newest derived row + -- (d4444444, written at c2 — reachable through the c2 parent). + SELECT count(*) INTO row_count FROM sysml2.resolve_commit_state(proj, c5); + IF row_count <> 2 THEN + RAISE EXCEPTION 'FAIL 8c: expected 2 elements at merge, got %', row_count; + END IF; + + SELECT r.derived_id INTO actual_id + FROM sysml2.resolve_commit_state(proj, c5) r + WHERE r.identity_id = wheel; + + IF actual_id IS DISTINCT FROM 'd4444444-0000-0000-0000-000000000000'::uuid THEN + RAISE EXCEPTION 'FAIL 8c: wheel derived at merge = %, expected d4444444 (from c2 side)', actual_id; + END IF; + RAISE NOTICE 'PASS 8c: merge snapshot complete; derived state folds across the DAG'; +END; +$$; +---------------------------------------------------------------------------------------------- +-- OVERLAY SCENARIO — branch_head as sparse divergence over a base checkpoint. +-- +-- Checkpoint c2, fork branch b2 from it with ZERO overlay rows, then diverge by deleting the +-- wheel on b2 only. Reads must merge overlay-over-checkpoint; branch deletion must remove only +-- the overlay. +---------------------------------------------------------------------------------------------- + +DO $$ +DECLARE + proj constant uuid := '11111111-0000-0000-0000-000000000000'; + c2 constant uuid := 'c2222222-0000-0000-0000-000000000000'; + b2 constant uuid := 'b2222222-0000-0000-0000-000000000000'; + wheel constant uuid := 'e2222222-0000-0000-0000-000000000000'; + row_count bigint; + actual text; +BEGIN + row_count := sysml2.build_commit_checkpoint(proj, c2); + + IF row_count <> 2 THEN + RAISE EXCEPTION 'FAIL 9a: checkpoint at c2 materialized % rows, expected 2', row_count; + END IF; + RAISE NOTICE 'PASS 9a: build_commit_checkpoint(c2) materialized 2 rows'; + + INSERT INTO sysml2.branch (id, project_id, name, head_commit_id, base_commit_id) + VALUES (b2, proj, 'overlay-branch', c2, c2); + + -- zero overlay rows written: branch creation is O(1), reads come from the checkpoint + SELECT sysml2.get_element_at_branch_head(b2, wheel) ->> 'qualifiedName' INTO actual; + + IF actual IS DISTINCT FROM 'New::wheel' THEN + RAISE EXCEPTION 'FAIL 9b: empty-overlay read = %, expected New::wheel from the base checkpoint', actual; + END IF; + RAISE NOTICE 'PASS 9b: empty overlay reads through to the base checkpoint'; + + SELECT count(*) INTO row_count FROM sysml2.get_elements_at_branch_head(b2); + + IF row_count <> 2 THEN + RAISE EXCEPTION 'FAIL 9c: set read over empty overlay returned %, expected 2', row_count; + END IF; + RAISE NOTICE 'PASS 9c: set read over empty overlay returns the full checkpoint state'; + + -- diverge: delete the wheel ON THIS BRANCH ONLY (tombstone overlay row pointing at the + -- tombstone element_version written at c3) + INSERT INTO sysml2.branch_head (project_id, branch_id, identity_id, version_id, derived_id, is_tombstone) + VALUES (proj, b2, wheel, 'a4444444-0000-0000-0000-000000000000', NULL, true); + + IF sysml2.get_element_at_branch_head(b2, wheel) IS NOT NULL THEN + RAISE EXCEPTION 'FAIL 9d: tombstoned overlay row did not mask the checkpoint'; + END IF; + RAISE NOTICE 'PASS 9d: overlay tombstone masks the checkpoint row'; + + SELECT count(*) INTO row_count FROM sysml2.get_elements_at_branch_head(b2); + + IF row_count <> 1 THEN + RAISE EXCEPTION 'FAIL 9e: set read after overlay tombstone returned %, expected 1', row_count; + END IF; + RAISE NOTICE 'PASS 9e: set read merges overlay divergence over the checkpoint'; + + -- branch deletion removes ONLY the overlay; the shared checkpoint stays + DELETE FROM sysml2.branch WHERE id = b2; + + SELECT count(*) INTO row_count FROM sysml2.branch_head bh WHERE bh.project_id = proj AND bh.branch_id = b2; + + IF row_count <> 0 THEN + RAISE EXCEPTION 'FAIL 9f: branch deletion left % overlay rows behind', row_count; + END IF; + + SELECT count(*) INTO row_count FROM sysml2.commit_checkpoint cc WHERE cc.project_id = proj AND cc.commit_id = c2; + + IF row_count <> 2 THEN + RAISE EXCEPTION 'FAIL 9f: branch deletion damaged the shared checkpoint (% rows left)', row_count; + END IF; + RAISE NOTICE 'PASS 9f: branch deletion removed only the overlay; shared checkpoint intact'; +END; +$$; + +---------------------------------------------------------------------------------------------- +-- DETERMINISM SCENARIO — sibling commits sharing a timestamp. +-- +-- The monotonicity invariant orders parent edges only; siblings c6/c7 legitimately share +-- created. A merge that (illegally) fails to restate the conflict must still resolve +-- DETERMINISTICALLY: the id DESC tiebreaker picks c7 (greater id) on every run. +---------------------------------------------------------------------------------------------- + +INSERT INTO sysml2.commit (id, project_id, created, description, model_version_id) VALUES + ('c6666666-0000-0000-0000-000000000000', '11111111-0000-0000-0000-000000000000', '2026-01-01T11:30:00Z', 'sibling A', 1), + ('c7777777-0000-0000-0000-000000000000', '11111111-0000-0000-0000-000000000000', '2026-01-01T11:30:00Z', 'sibling B', 1), + ('c8888888-0000-0000-0000-000000000000', '11111111-0000-0000-0000-000000000000', '2026-01-01T14:00:00Z', 'merge without restating', 1); + +INSERT INTO sysml2.commit_parent (commit_id, parent_commit_id, ordinal) VALUES + ('c6666666-0000-0000-0000-000000000000', 'c1111111-0000-0000-0000-000000000000', 0), + ('c7777777-0000-0000-0000-000000000000', 'c1111111-0000-0000-0000-000000000000', 0), + ('c8888888-0000-0000-0000-000000000000', 'c6666666-0000-0000-0000-000000000000', 0), + ('c8888888-0000-0000-0000-000000000000', 'c7777777-0000-0000-0000-000000000000', 1); + +INSERT INTO sysml2.element_version + (project_id, version_id, identity_id, commit_id, class_kind, tombstone, + element_id, declared_name, is_implied_included, stored_json) +VALUES + ('11111111-0000-0000-0000-000000000000', 'a7777777-0000-0000-0000-000000000000', + 'e1111111-0000-0000-0000-000000000000', 'c6666666-0000-0000-0000-000000000000', (SELECT id FROM sysml2.class_kind WHERE name = 'Package'), false, + 'e1111111-0000-0000-0000-000000000000', 'SiblingA', false, + '{"@id":"e1111111-0000-0000-0000-000000000000","@type":"Package","declaredName":"SiblingA"}'), + + ('11111111-0000-0000-0000-000000000000', 'a8888888-0000-0000-0000-000000000000', + 'e1111111-0000-0000-0000-000000000000', 'c7777777-0000-0000-0000-000000000000', (SELECT id FROM sysml2.class_kind WHERE name = 'Package'), false, + 'e1111111-0000-0000-0000-000000000000', 'SiblingB', false, + '{"@id":"e1111111-0000-0000-0000-000000000000","@type":"Package","declaredName":"SiblingB"}'); + +DO $$ +DECLARE + proj constant uuid := '11111111-0000-0000-0000-000000000000'; + package constant uuid := 'e1111111-0000-0000-0000-000000000000'; + c8 constant uuid := 'c8888888-0000-0000-0000-000000000000'; + first_id uuid; + second_id uuid; +BEGIN + SELECT r.version_id INTO first_id + FROM sysml2.resolve_commit_state(proj, c8) r + WHERE r.identity_id = package; + + SELECT r.version_id INTO second_id + FROM sysml2.resolve_commit_state(proj, c8) r + WHERE r.identity_id = package; + + IF first_id IS DISTINCT FROM 'a8888888-0000-0000-0000-000000000000'::uuid + OR second_id IS DISTINCT FROM first_id THEN + RAISE EXCEPTION 'FAIL 10a: timestamp tie resolved to % then % — expected a8888888 (c7, greater id) both times', first_id, second_id; + END IF; + RAISE NOTICE 'PASS 10a: sibling-timestamp tie resolves deterministically (id DESC winner)'; + + SELECT r.version_id INTO first_id + FROM sysml2.resolve_element_at_commit(proj, c8, package) r; + + IF first_id IS DISTINCT FROM 'a8888888-0000-0000-0000-000000000000'::uuid THEN + RAISE EXCEPTION 'FAIL 10b: resolve_element_at_commit tie winner = %, expected a8888888', first_id; + END IF; + RAISE NOTICE 'PASS 10b: single-element resolver agrees with the full fold on the tie'; +END; +$$; + +---------------------------------------------------------------------------------------------- +-- MULTI-VERSION SCENARIO — commit-stamped metamodel releases. +-- +-- The registry seeds are idempotent; a CONVERSION COMMIT (single parent, higher release) is +-- the only way up; downgrades and mixed-release merges must be rejected by +-- trg_commit_parent_version rather than silently mixing payload shapes. +---------------------------------------------------------------------------------------------- + +DO $$ +DECLARE + proj constant uuid := '11111111-0000-0000-0000-000000000000'; + c2 constant uuid := 'c2222222-0000-0000-0000-000000000000'; + c3 constant uuid := 'c3333333-0000-0000-0000-000000000000'; + c4 constant uuid := 'c4444444-0000-0000-0000-000000000000'; + conv constant uuid := 'ca111111-0000-0000-0000-000000000000'; -- conversion commit, release 2 + down constant uuid := 'ca222222-0000-0000-0000-000000000000'; -- downgrade attempt, release 1 + mixed constant uuid := 'ca333333-0000-0000-0000-000000000000'; -- mixed-release merge attempt + combo constant uuid := 'ca444444-0000-0000-0000-000000000000'; -- convert+merge combo attempt + count_before int; + count_after int; +BEGIN + -- 11a. The registry seeds are idempotent: re-applying them to a populated database is a + -- no-op, not a corruption (the old FRESH-INSTALLS-ONLY trap is gone by construction). + SELECT count(*) INTO count_before FROM sysml2.class_kind; + + INSERT INTO sysml2.class_kind (id, name, is_abstract, introduced_in) VALUES + (116, 'OwningMembership', false, 1), + (117, 'Package', false, 1), + (120, 'PartUsage', false, 1) + ON CONFLICT (id) DO NOTHING; + + SELECT count(*) INTO count_after FROM sysml2.class_kind; + + IF count_after <> count_before THEN + RAISE EXCEPTION 'FAIL 11a: seed re-apply changed class_kind from % to % rows', count_before, count_after; + END IF; + RAISE NOTICE 'PASS 11a: class_kind seed re-apply is idempotent'; + + -- 11b. A conversion commit — single parent, HIGHER release — is accepted. + INSERT INTO sysml2.commit (id, project_id, created, description, model_version_id) + VALUES (conv, proj, '2026-01-01T15:00:00Z', 'conversion to release 2', 2); + + INSERT INTO sysml2.commit_parent (commit_id, parent_commit_id, ordinal) + VALUES (conv, c3, 0); + + RAISE NOTICE 'PASS 11b: conversion commit (release 1 -> 2, single parent) accepted'; + + -- 11c. A DOWNGRADE — child in an older release than its parent — is rejected. + INSERT INTO sysml2.commit (id, project_id, created, description, model_version_id) + VALUES (down, proj, '2026-01-01T15:30:00Z', 'illegal downgrade', 1); + + BEGIN + INSERT INTO sysml2.commit_parent (commit_id, parent_commit_id, ordinal) + VALUES (down, conv, 0); + + RAISE EXCEPTION 'FAIL 11c: downgrade commit parent was ACCEPTED'; + EXCEPTION WHEN check_violation THEN + RAISE NOTICE 'PASS 11c: downgrade rejected by trg_commit_parent_version'; + END; + + -- 11d. A MERGE across releases — parents 2 and 1 — is rejected the moment the second + -- parent edge lands, regardless of insertion order. + INSERT INTO sysml2.commit (id, project_id, created, description, model_version_id) + VALUES (mixed, proj, '2026-01-01T16:00:00Z', 'illegal mixed-release merge', 2); + + BEGIN + INSERT INTO sysml2.commit_parent (commit_id, parent_commit_id, ordinal) + VALUES (mixed, conv, 0), + (mixed, c2, 1); + + RAISE EXCEPTION 'FAIL 11d: mixed-release merge was ACCEPTED'; + EXCEPTION WHEN check_violation THEN + RAISE NOTICE 'PASS 11d: mixed-release merge rejected (convert first, then merge)'; + END; + + -- 11e. A convert+merge COMBO — both parents in release 1, child claiming release 2 — is + -- also rejected: the conversion must be its own single-parent commit. + INSERT INTO sysml2.commit (id, project_id, created, description, model_version_id) + VALUES (combo, proj, '2026-01-01T16:30:00Z', 'illegal convert+merge combo', 2); + + BEGIN + INSERT INTO sysml2.commit_parent (commit_id, parent_commit_id, ordinal) + VALUES (combo, c2, 0), + (combo, c4, 1); + + RAISE EXCEPTION 'FAIL 11e: convert+merge combo was ACCEPTED'; + EXCEPTION WHEN check_violation THEN + RAISE NOTICE 'PASS 11e: convert+merge combo rejected (conversion must be single-parent)'; + END; +END; +$$; + +---------------------------------------------------------------------------------------------- +-- TYPED-IDENTITY & REFERENCE-VALIDATION SCENARIO +-- +-- The composite FK makes a version that claims a different metaclass than its identity +-- impossible; validate_references_at_commit() reports what FKs cannot: 'wrong-type' (via the +-- typed identity) and 'dangling' (target not alive in the commit's snapshot). +---------------------------------------------------------------------------------------------- + +DO $$ +DECLARE + proj constant uuid := '11111111-0000-0000-0000-000000000000'; + e1 constant uuid := 'e1111111-0000-0000-0000-000000000000'; + e2 constant uuid := 'e2222222-0000-0000-0000-000000000000'; + c2 constant uuid := 'c2222222-0000-0000-0000-000000000000'; + c12 constant uuid := 'cb111111-0000-0000-0000-000000000000'; -- parentless probe commit + e3 constant uuid := 'e3333333-0000-0000-0000-000000000000'; -- Package, refs e2 (wrong type) + e4 constant uuid := 'e4444444-0000-0000-0000-000000000000'; -- Package, refs e5 (dangling) + e5 constant uuid := 'e5555555-0000-0000-0000-000000000000'; -- OwningMembership, never given a version + row_count int; +BEGIN + INSERT INTO sysml2.commit (id, project_id, created, description, model_version_id) + VALUES (c12, proj, '2026-01-01T17:00:00Z', 'reference-validation probe', 1); + + INSERT INTO sysml2.data_identity (id, project_id, class_kind) VALUES + (e3, proj, (SELECT id FROM sysml2.class_kind WHERE name = 'Package')), + (e4, proj, (SELECT id FROM sysml2.class_kind WHERE name = 'Package')), + (e5, proj, (SELECT id FROM sysml2.class_kind WHERE name = 'OwningMembership')); + + -- 12a. The composite FK rejects a version claiming a different metaclass than its identity + -- (e1 is a Package identity; the row claims PartUsage). + BEGIN + INSERT INTO sysml2.element_version + (project_id, version_id, identity_id, commit_id, class_kind, tombstone, + element_id, declared_name, is_implied_included, stored_json) + VALUES + (proj, 'ab111111-0000-0000-0000-000000000000', e1, c12, + (SELECT id FROM sysml2.class_kind WHERE name = 'PartUsage'), false, + 'e1111111-0000-0000-0000-000000000000', 'Imposter', false, '{}'); + + RAISE EXCEPTION 'FAIL 12a: version with a different class_kind than its identity was ACCEPTED'; + EXCEPTION WHEN foreign_key_violation THEN + RAISE NOTICE 'PASS 12a: typed identity rejects a version whose class_kind contradicts its identity'; + END; + + -- 12b. The healthy snapshot at c2 validates clean. + SELECT count(*) INTO row_count FROM sysml2.validate_references_at_commit(proj, c2); + + IF row_count <> 0 THEN + RAISE EXCEPTION 'FAIL 12b: healthy snapshot reported % reference problems', row_count; + END IF; + RAISE NOTICE 'PASS 12b: healthy snapshot validates clean'; + + -- 12c. A wrong-type reference (owning_relationship -> a PartUsage, not a Relationship) and + -- a dangling reference (owning_relationship -> a Membership identity with no live + -- version at c12) are both reported, and nothing else. + INSERT INTO sysml2.element_version + (project_id, version_id, identity_id, commit_id, class_kind, tombstone, + element_id, declared_name, is_implied_included, owning_relationship, stored_json) + VALUES + (proj, 'ab222222-0000-0000-0000-000000000000', e3, c12, + (SELECT id FROM sysml2.class_kind WHERE name = 'Package'), false, + 'e3333333-0000-0000-0000-000000000000', 'WrongTypedRef', false, e2, '{}'), + + (proj, 'ab333333-0000-0000-0000-000000000000', e4, c12, + (SELECT id FROM sysml2.class_kind WHERE name = 'Package'), false, + 'e4444444-0000-0000-0000-000000000000', 'DanglingRef', false, e5, '{}'); + + SELECT count(*) INTO row_count FROM sysml2.validate_references_at_commit(proj, c12); + + IF row_count <> 2 THEN + RAISE EXCEPTION 'FAIL 12c: expected exactly 2 reference problems at c12, got %', row_count; + END IF; + + SELECT count(*) INTO row_count + FROM sysml2.validate_references_at_commit(proj, c12) validation + WHERE (validation.source_identity = e3 AND validation.target_identity = e2 AND validation.problem = 'wrong-type') + OR (validation.source_identity = e4 AND validation.target_identity = e5 AND validation.problem = 'dangling'); + + IF row_count <> 2 THEN + RAISE EXCEPTION 'FAIL 12c: the 2 problems are not the expected wrong-type/dangling pair'; + END IF; + RAISE NOTICE 'PASS 12c: validate_references_at_commit reports wrong-type and dangling, nothing else'; +END; +$$; + +---------------------------------------------------------------------------------------------- +-- INCREMENTAL-VALIDATION SCENARIO — the O(change set) tier. +-- +-- c13 (parent c2): adds membership e7 and package e6 referencing it — a HEALTHY commit. +-- c14 (parent c13): tombstones e7 ONLY. The incremental tier must catch the reverse +-- direction: live, UNCHANGED e6 is left dangling — the case naive change-set validation +-- misses — and must agree with the full audit pass. +---------------------------------------------------------------------------------------------- + +DO $$ +DECLARE + proj constant uuid := '11111111-0000-0000-0000-000000000000'; + c2 constant uuid := 'c2222222-0000-0000-0000-000000000000'; + c12 constant uuid := 'cb111111-0000-0000-0000-000000000000'; + c13 constant uuid := 'cc111111-0000-0000-0000-000000000000'; + c14 constant uuid := 'cd111111-0000-0000-0000-000000000000'; + e3 constant uuid := 'e3333333-0000-0000-0000-000000000000'; + e4 constant uuid := 'e4444444-0000-0000-0000-000000000000'; + e5 constant uuid := 'e5555555-0000-0000-0000-000000000000'; + e6 constant uuid := 'e6666666-0000-0000-0000-000000000000'; -- Package, refs e7 + e7 constant uuid := 'e7777777-0000-0000-0000-000000000000'; -- OwningMembership, tombstoned at c14 + row_count int; +BEGIN + -- 13a. The incremental tier reports c12's own outgoing problems — and exactly those. + SELECT count(*) INTO row_count + FROM sysml2.validate_references_in_commit(proj, c12) validation + WHERE (validation.source_identity = e3 AND validation.target_identity = 'e2222222-0000-0000-0000-000000000000' AND validation.problem = 'wrong-type') + OR (validation.source_identity = e4 AND validation.target_identity = e5 AND validation.problem = 'dangling'); + + IF row_count <> 2 + OR (SELECT count(*) FROM sysml2.validate_references_in_commit(proj, c12)) <> 2 THEN + RAISE EXCEPTION 'FAIL 13a: incremental tier does not report exactly c12''s wrong-type/dangling pair'; + END IF; + RAISE NOTICE 'PASS 13a: incremental tier catches the change set''s outgoing problems'; + + -- 13b. A healthy commit validates clean incrementally. + INSERT INTO sysml2.commit (id, project_id, created, description, model_version_id) + VALUES (c13, proj, '2026-01-01T18:00:00Z', 'add membership + referencing package', 1); + + INSERT INTO sysml2.commit_parent (commit_id, parent_commit_id, ordinal) VALUES (c13, c2, 0); + + INSERT INTO sysml2.data_identity (id, project_id, class_kind) VALUES + (e6, proj, (SELECT id FROM sysml2.class_kind WHERE name = 'Package')), + (e7, proj, (SELECT id FROM sysml2.class_kind WHERE name = 'OwningMembership')); + + INSERT INTO sysml2.element_version + (project_id, version_id, identity_id, commit_id, class_kind, tombstone, + element_id, declared_name, is_implied_included, owning_relationship, stored_json) + VALUES + (proj, 'ac111111-0000-0000-0000-000000000000', e7, c13, + (SELECT id FROM sysml2.class_kind WHERE name = 'OwningMembership'), false, + 'e7777777-0000-0000-0000-000000000000', 'm', false, NULL, '{}'), + + (proj, 'ac222222-0000-0000-0000-000000000000', e6, c13, + (SELECT id FROM sysml2.class_kind WHERE name = 'Package'), false, + 'e6666666-0000-0000-0000-000000000000', 'Owned', false, e7, '{}'); + + SELECT count(*) INTO row_count FROM sysml2.validate_references_in_commit(proj, c13); + + IF row_count <> 0 THEN + RAISE EXCEPTION 'FAIL 13b: healthy commit reported % incremental problems', row_count; + END IF; + RAISE NOTICE 'PASS 13b: healthy commit validates clean incrementally'; + + -- 13c. Tombstoning e7 breaks UNCHANGED e6's reference: the incremental tier must catch + -- the reverse direction, and must agree with the full audit pass. + INSERT INTO sysml2.commit (id, project_id, created, description, model_version_id) + VALUES (c14, proj, '2026-01-01T19:00:00Z', 'delete the membership only', 1); + + INSERT INTO sysml2.commit_parent (commit_id, parent_commit_id, ordinal) VALUES (c14, c13, 0); + + INSERT INTO sysml2.element_version + (project_id, version_id, identity_id, commit_id, class_kind, tombstone) + VALUES + (proj, 'ac333333-0000-0000-0000-000000000000', e7, c14, + (SELECT id FROM sysml2.class_kind WHERE name = 'OwningMembership'), true); + + SELECT count(*) INTO row_count + FROM sysml2.validate_references_in_commit(proj, c14) validation + WHERE validation.source_identity = e6 AND validation.target_identity = e7 AND validation.problem = 'dangling'; + + IF row_count <> 1 + OR (SELECT count(*) FROM sysml2.validate_references_in_commit(proj, c14)) <> 1 THEN + RAISE EXCEPTION 'FAIL 13c: tombstone''s reverse-direction dangling reference not reported exactly once'; + END IF; + + SELECT count(*) INTO row_count FROM sysml2.validate_references_at_commit(proj, c14); + + IF row_count <> 1 THEN + RAISE EXCEPTION 'FAIL 13c: full audit pass disagrees with the incremental tier (% rows)', row_count; + END IF; + RAISE NOTICE 'PASS 13c: incremental tier catches the reverse direction and agrees with the full pass'; +END; +$$; diff --git a/SysML2.NET.CodeGenerator/Sql/schema2.generated.sql b/SysML2.NET.CodeGenerator/Sql/schema2.generated.sql new file mode 100644 index 000000000..29336fe02 --- /dev/null +++ b/SysML2.NET.CodeGenerator/Sql/schema2.generated.sql @@ -0,0 +1,5440 @@ +------------------------------------------------------------------------------------------------ +--------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +------------------------------------------------------------------------------------------------ +-- SysML2.NET — PostgreSQL persistence schema, generated from the KerML/SysML v2 UML metamodel. +-- +-- Generated by SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/SQLSchemaGenerator.cs +-- from SysML2.NET.CodeGenerator/Templates/Uml/core-sql-schema-2.hbs. +-- Reference design and rationale: SysML2.NET.CodeGenerator/Sql/schema.golden.sql. +-- +-- Target: PostgreSQL 16+. Deploy with max_locks_per_transaction >= 4096 (see the PARTITIONS +-- section for why). +------------------------------------------------------------------------------------------------ + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +CREATE SCHEMA IF NOT EXISTS sysml2; + +SET search_path = sysml2, public; + +------------------------------------------------------------------------------------------------ +-- 1. ENUM TYPES [GENERATED] +-- +-- Labels are the lowercase form of the C# literal, matching the JSON wire format: +-- SysML2.NET.Serializer.Json/Core/AutoGenSerializer/FeatureSerializer.cs:162 writes +-- `Direction.Value.ToString().ToLower()`. Deserialization is case-insensitive. +------------------------------------------------------------------------------------------------ + +CREATE TYPE sysml2.feature_direction_kind AS ENUM ('in', 'inout', 'out'); +CREATE TYPE sysml2.portion_kind AS ENUM ('timeslice', 'snapshot'); +CREATE TYPE sysml2.requirement_constraint_kind AS ENUM ('assumption', 'requirement'); +CREATE TYPE sysml2.state_subaction_kind AS ENUM ('entry', 'do', 'exit'); +CREATE TYPE sysml2.transition_feature_kind AS ENUM ('trigger', 'guard', 'effect'); +CREATE TYPE sysml2.trigger_kind AS ENUM ('when', 'at', 'after'); +CREATE TYPE sysml2.visibility_kind AS ENUM ('private', 'protected', 'public'); +------------------------------------------------------------------------------------------------ +-- 2. METAMODEL CATALOGS [GENERATED] +-- +-- model_version registers every metamodel release this database has ever stored data for. +-- The id is an ORDINAL — a higher id is a later release — handed out once by the checked-in +-- registry (SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/ClassKindRegistry.cs) +-- and never renumbered. Every commit is stamped with the release its payloads are written in +-- (commit.model_version_id, §3); that stamp, not the branch or the project, is what a reader +-- consults to know the shape of a historical payload. +-- +-- class_kind interns the metaclass names to a smallint. Every element row carries the +-- smallint, not a VARCHAR(100) — at 1M+ elements that is the difference between a 2-byte and +-- a ~20-byte column on the hottest table in the database. +-- +-- !! THE IDS ARE AN APPEND-ONLY REGISTRY — FROZEN FOREVER ONCE ASSIGNED !! +-- A new metamodel release appends its new metaclasses AFTER the highest existing id (the +-- newcomers alphabetical among themselves); existing ids never change, so persisted +-- element_version.class_kind values and the generated C# ClassKind enum stay valid across +-- upgrades. A metaclass dropped by a release keeps its row, closed with removed_in. The +-- generator FAILS on any drift between the UML model and the registry (unregistered class, +-- stale registration, fingerprint mismatch) instead of silently renumbering, and the seed +-- INSERTs are idempotent (ON CONFLICT DO NOTHING) — safe to re-apply to a populated database. +-- +-- The property->storage routing that earlier designs kept in a property_catalog table is +-- deliberately NOT in the database: the generated service layer carries it as static +-- per-release C# (model-version descriptors). Nothing in this schema reads such a catalog — +-- the views and resolvers are already specialized per metaclass at generation time — and only +-- versioned generated code can describe EVERY registered release at once. +------------------------------------------------------------------------------------------------ + +CREATE TABLE sysml2.model_version ( + id smallint NOT NULL, -- ordinal: higher id == later release + name text NOT NULL, -- human-readable release label, e.g. 'sysml-2.0-beta-4' + source_fingerprint text NOT NULL, -- root-package fingerprint of the generator input + PRIMARY KEY (id), + UNIQUE (name) +); + +CREATE TABLE sysml2.class_kind ( + id smallint NOT NULL, + name text NOT NULL, -- the API @type value, e.g. 'PartUsage' + is_abstract boolean NOT NULL, + introduced_in smallint NOT NULL REFERENCES sysml2.model_version (id), + removed_in smallint NULL REFERENCES sysml2.model_version (id), -- first release WITHOUT the class; NULL = current + PRIMARY KEY (id), + UNIQUE (name) +); + +-- The append-only registry seeds, emitted from ClassKindRegistry (NOT from the UML model — +-- the model only validates against the registry). Idempotent by construction. + +INSERT INTO sysml2.model_version (id, name, source_fingerprint) VALUES + (1, 'sysml-2.0-beta-4', 'SysML:_mczcUFn3EfG_XZTXp4TXuA') +ON CONFLICT (id) DO NOTHING; + +INSERT INTO sysml2.class_kind (id, name, is_abstract, introduced_in, removed_in) VALUES + (1, 'AcceptActionUsage', false, 1, NULL), + (2, 'ActionDefinition', false, 1, NULL), + (3, 'ActionUsage', false, 1, NULL), + (4, 'ActorMembership', false, 1, NULL), + (5, 'AllocationDefinition', false, 1, NULL), + (6, 'AllocationUsage', false, 1, NULL), + (7, 'AnalysisCaseDefinition', false, 1, NULL), + (8, 'AnalysisCaseUsage', false, 1, NULL), + (9, 'AnnotatingElement', false, 1, NULL), + (10, 'Annotation', false, 1, NULL), + (11, 'AssertConstraintUsage', false, 1, NULL), + (12, 'AssignmentActionUsage', false, 1, NULL), + (13, 'Association', false, 1, NULL), + (14, 'AssociationStructure', false, 1, NULL), + (15, 'AttributeDefinition', false, 1, NULL), + (16, 'AttributeUsage', false, 1, NULL), + (17, 'Behavior', false, 1, NULL), + (18, 'BindingConnector', false, 1, NULL), + (19, 'BindingConnectorAsUsage', false, 1, NULL), + (20, 'BooleanExpression', false, 1, NULL), + (21, 'CalculationDefinition', false, 1, NULL), + (22, 'CalculationUsage', false, 1, NULL), + (23, 'CaseDefinition', false, 1, NULL), + (24, 'CaseUsage', false, 1, NULL), + (25, 'Class', false, 1, NULL), + (26, 'Classifier', false, 1, NULL), + (27, 'CollectExpression', false, 1, NULL), + (28, 'Comment', false, 1, NULL), + (29, 'ConcernDefinition', false, 1, NULL), + (30, 'ConcernUsage', false, 1, NULL), + (31, 'ConjugatedPortDefinition', false, 1, NULL), + (32, 'ConjugatedPortTyping', false, 1, NULL), + (33, 'Conjugation', false, 1, NULL), + (34, 'ConnectionDefinition', false, 1, NULL), + (35, 'ConnectionUsage', false, 1, NULL), + (36, 'Connector', false, 1, NULL), + (37, 'ConnectorAsUsage', true, 1, NULL), + (38, 'ConstraintDefinition', false, 1, NULL), + (39, 'ConstraintUsage', false, 1, NULL), + (40, 'ConstructorExpression', false, 1, NULL), + (41, 'ControlNode', true, 1, NULL), + (42, 'CrossSubsetting', false, 1, NULL), + (43, 'DataType', false, 1, NULL), + (44, 'DecisionNode', false, 1, NULL), + (45, 'Definition', false, 1, NULL), + (46, 'Dependency', false, 1, NULL), + (47, 'Differencing', false, 1, NULL), + (48, 'Disjoining', false, 1, NULL), + (49, 'Documentation', false, 1, NULL), + (50, 'Element', true, 1, NULL), + (51, 'ElementFilterMembership', false, 1, NULL), + (52, 'EndFeatureMembership', false, 1, NULL), + (53, 'EnumerationDefinition', false, 1, NULL), + (54, 'EnumerationUsage', false, 1, NULL), + (55, 'EventOccurrenceUsage', false, 1, NULL), + (56, 'ExhibitStateUsage', false, 1, NULL), + (57, 'Expose', true, 1, NULL), + (58, 'Expression', false, 1, NULL), + (59, 'Feature', false, 1, NULL), + (60, 'FeatureChainExpression', false, 1, NULL), + (61, 'FeatureChaining', false, 1, NULL), + (62, 'FeatureInverting', false, 1, NULL), + (63, 'FeatureMembership', false, 1, NULL), + (64, 'FeatureReferenceExpression', false, 1, NULL), + (65, 'FeatureTyping', false, 1, NULL), + (66, 'FeatureValue', false, 1, NULL), + (67, 'Flow', false, 1, NULL), + (68, 'FlowDefinition', false, 1, NULL), + (69, 'FlowEnd', false, 1, NULL), + (70, 'FlowUsage', false, 1, NULL), + (71, 'ForLoopActionUsage', false, 1, NULL), + (72, 'ForkNode', false, 1, NULL), + (73, 'FramedConcernMembership', false, 1, NULL), + (74, 'Function', false, 1, NULL), + (75, 'IfActionUsage', false, 1, NULL), + (76, 'Import', true, 1, NULL), + (77, 'IncludeUseCaseUsage', false, 1, NULL), + (78, 'IndexExpression', false, 1, NULL), + (79, 'InstantiationExpression', true, 1, NULL), + (80, 'Interaction', false, 1, NULL), + (81, 'InterfaceDefinition', false, 1, NULL), + (82, 'InterfaceUsage', false, 1, NULL), + (83, 'Intersecting', false, 1, NULL), + (84, 'Invariant', false, 1, NULL), + (85, 'InvocationExpression', false, 1, NULL), + (86, 'ItemDefinition', false, 1, NULL), + (87, 'ItemUsage', false, 1, NULL), + (88, 'JoinNode', false, 1, NULL), + (89, 'LibraryPackage', false, 1, NULL), + (90, 'LiteralBoolean', false, 1, NULL), + (91, 'LiteralExpression', false, 1, NULL), + (92, 'LiteralInfinity', false, 1, NULL), + (93, 'LiteralInteger', false, 1, NULL), + (94, 'LiteralRational', false, 1, NULL), + (95, 'LiteralString', false, 1, NULL), + (96, 'LoopActionUsage', true, 1, NULL), + (97, 'Membership', false, 1, NULL), + (98, 'MembershipExpose', false, 1, NULL), + (99, 'MembershipImport', false, 1, NULL), + (100, 'MergeNode', false, 1, NULL), + (101, 'Metaclass', false, 1, NULL), + (102, 'MetadataAccessExpression', false, 1, NULL), + (103, 'MetadataDefinition', false, 1, NULL), + (104, 'MetadataFeature', false, 1, NULL), + (105, 'MetadataUsage', false, 1, NULL), + (106, 'Multiplicity', false, 1, NULL), + (107, 'MultiplicityRange', false, 1, NULL), + (108, 'Namespace', false, 1, NULL), + (109, 'NamespaceExpose', false, 1, NULL), + (110, 'NamespaceImport', false, 1, NULL), + (111, 'NullExpression', false, 1, NULL), + (112, 'ObjectiveMembership', false, 1, NULL), + (113, 'OccurrenceDefinition', false, 1, NULL), + (114, 'OccurrenceUsage', false, 1, NULL), + (115, 'OperatorExpression', false, 1, NULL), + (116, 'OwningMembership', false, 1, NULL), + (117, 'Package', false, 1, NULL), + (118, 'ParameterMembership', false, 1, NULL), + (119, 'PartDefinition', false, 1, NULL), + (120, 'PartUsage', false, 1, NULL), + (121, 'PayloadFeature', false, 1, NULL), + (122, 'PerformActionUsage', false, 1, NULL), + (123, 'PortConjugation', false, 1, NULL), + (124, 'PortDefinition', false, 1, NULL), + (125, 'PortUsage', false, 1, NULL), + (126, 'Predicate', false, 1, NULL), + (127, 'Redefinition', false, 1, NULL), + (128, 'ReferenceSubsetting', false, 1, NULL), + (129, 'ReferenceUsage', false, 1, NULL), + (130, 'Relationship', true, 1, NULL), + (131, 'RenderingDefinition', false, 1, NULL), + (132, 'RenderingUsage', false, 1, NULL), + (133, 'RequirementConstraintMembership', false, 1, NULL), + (134, 'RequirementDefinition', false, 1, NULL), + (135, 'RequirementUsage', false, 1, NULL), + (136, 'RequirementVerificationMembership', false, 1, NULL), + (137, 'ResultExpressionMembership', false, 1, NULL), + (138, 'ReturnParameterMembership', false, 1, NULL), + (139, 'SatisfyRequirementUsage', false, 1, NULL), + (140, 'SelectExpression', false, 1, NULL), + (141, 'SendActionUsage', false, 1, NULL), + (142, 'Specialization', false, 1, NULL), + (143, 'StakeholderMembership', false, 1, NULL), + (144, 'StateDefinition', false, 1, NULL), + (145, 'StateSubactionMembership', false, 1, NULL), + (146, 'StateUsage', false, 1, NULL), + (147, 'Step', false, 1, NULL), + (148, 'Structure', false, 1, NULL), + (149, 'Subclassification', false, 1, NULL), + (150, 'SubjectMembership', false, 1, NULL), + (151, 'Subsetting', false, 1, NULL), + (152, 'Succession', false, 1, NULL), + (153, 'SuccessionAsUsage', false, 1, NULL), + (154, 'SuccessionFlow', false, 1, NULL), + (155, 'SuccessionFlowUsage', false, 1, NULL), + (156, 'TerminateActionUsage', false, 1, NULL), + (157, 'TextualRepresentation', false, 1, NULL), + (158, 'TransitionFeatureMembership', false, 1, NULL), + (159, 'TransitionUsage', false, 1, NULL), + (160, 'TriggerInvocationExpression', false, 1, NULL), + (161, 'Type', false, 1, NULL), + (162, 'TypeFeaturing', false, 1, NULL), + (163, 'Unioning', false, 1, NULL), + (164, 'Usage', false, 1, NULL), + (165, 'UseCaseDefinition', false, 1, NULL), + (166, 'UseCaseUsage', false, 1, NULL), + (167, 'VariantMembership', false, 1, NULL), + (168, 'VerificationCaseDefinition', false, 1, NULL), + (169, 'VerificationCaseUsage', false, 1, NULL), + (170, 'ViewDefinition', false, 1, NULL), + (171, 'ViewRenderingMembership', false, 1, NULL), + (172, 'ViewUsage', false, 1, NULL), + (173, 'ViewpointDefinition', false, 1, NULL), + (174, 'ViewpointUsage', false, 1, NULL), + (175, 'WhileLoopActionUsage', false, 1, NULL) +ON CONFLICT (id) DO NOTHING; +------------------------------------------------------------------------------------------------ +-- 3. PIM — PROJECTS, COMMITS, BRANCHES, TAGS [HAND-WRITTEN] +-- +-- Models Clause 7.1.1/7.1.2 of OMG Systems Modeling API and Services v1.0. +-- +-- The C# PIM DTOs (SysML2.NET/PIM/DTO/) match this model: Commit carries `Change` and a +-- multi-valued ordered `PreviousCommit`; Branch/Tag redefine CommitReference.ReferencedCommit; +-- CommitReference.Deleted is nullable. `VersionedData` is deliberately NOT a DTO property — +-- it is derived, unbounded, and resolved here by branch_head / resolve_commit_state (§9). +------------------------------------------------------------------------------------------------ + +CREATE TABLE sysml2.project ( + id uuid NOT NULL, + name text NULL, + description text NULL, + resource_identifier text NULL, + created timestamptz NOT NULL DEFAULT now(), + default_branch_id uuid NULL, -- FK added after branch exists (circular) + + -- Upgrade POLICY, not truth: the highest model_version (§2) new commits may be written + -- in. NULL = no restriction (any registered release). The release a commit actually IS + -- in lives on the commit itself. + target_model_version_id smallint NULL REFERENCES sysml2.model_version (id), + + PRIMARY KEY (id) +); + +CREATE TABLE sysml2.commit ( + id uuid NOT NULL, + project_id uuid NOT NULL REFERENCES sysml2.project (id) ON DELETE CASCADE, + created timestamptz NOT NULL DEFAULT now(), + description text NULL, + + -- The metamodel release (§2) this commit's payloads are written in. Append-only history + -- makes the per-commit stamp the only correct grain: a historical commit stays in the + -- release it was written in forever. A branch upgrades via a CONVERSION COMMIT — a + -- single-parent commit that bumps this stamp and restates every element whose shape + -- changed between the releases (service obligation; force a commit_checkpoint on it so + -- folds rarely cross the release boundary). Readers resolve payload shape from this + -- stamp, never from the branch or the project. + model_version_id smallint NOT NULL REFERENCES sysml2.model_version (id), + + PRIMARY KEY (id) +); + +CREATE INDEX ix_commit_project_created ON sysml2.commit (project_id, created DESC); + +-- Commit.previousCommit is a SET — merges have multiple parents. The DAG lives here. +CREATE TABLE sysml2.commit_parent ( + commit_id uuid NOT NULL REFERENCES sysml2.commit (id) ON DELETE CASCADE, + parent_commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + ordinal smallint NOT NULL, + PRIMARY KEY (commit_id, parent_commit_id) +); + +CREATE INDEX ix_commit_parent_parent ON sysml2.commit_parent (parent_commit_id); + +-- Clause 7.1.2 invariant: "Version histories must monotonically increase in time" — for commit C, +-- C.created is strictly newer than D.created for any D in C.previousCommit. The snapshot resolver +-- in §8 RELIES on this: it picks the version from the newest ancestor commit. Enforce it, because +-- a violation silently produces the wrong snapshot rather than an error. +CREATE OR REPLACE FUNCTION sysml2.assert_commit_monotonic() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +DECLARE + child_created timestamptz; + parent_created timestamptz; +BEGIN + SELECT created INTO child_created FROM sysml2.commit WHERE id = NEW.commit_id; + SELECT created INTO parent_created FROM sysml2.commit WHERE id = NEW.parent_commit_id; + + IF child_created <= parent_created THEN + RAISE EXCEPTION + 'commit % (created %) is not strictly newer than its parent % (created %)', + NEW.commit_id, child_created, NEW.parent_commit_id, parent_created + USING ERRCODE = 'check_violation'; + END IF; + + RETURN NEW; +END; +$$; + +CREATE TRIGGER trg_commit_parent_monotonic + AFTER INSERT ON sysml2.commit_parent + FOR EACH ROW + EXECUTE FUNCTION sysml2.assert_commit_monotonic(); + +-- Model-version invariants on the commit DAG (multi-version support, §2): +-- * a commit is never in an OLDER release than a parent — downgrades are unsupported; +-- * a SINGLE-parent commit MAY bump the release: that is a conversion commit; +-- * a MERGE commit (2+ parents) must have ALL parents in its own release — convert first, +-- then merge. Without this check a merge would silently mix payload shapes. +-- The count-based re-check makes the guard insertion-order independent: the moment a second +-- parent row lands, every already-inserted parent is re-validated against the child's release. +CREATE OR REPLACE FUNCTION sysml2.assert_commit_version_compatible() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +DECLARE + child_version smallint; + parent_version smallint; + parent_count int; + mixed_parents int; +BEGIN + SELECT model_version_id INTO child_version FROM sysml2.commit WHERE id = NEW.commit_id; + SELECT model_version_id INTO parent_version FROM sysml2.commit WHERE id = NEW.parent_commit_id; + + IF child_version < parent_version THEN + RAISE EXCEPTION + 'commit % (model_version %) cannot have parent % (model_version %): downgrades are not supported', + NEW.commit_id, child_version, NEW.parent_commit_id, parent_version + USING ERRCODE = 'check_violation'; + END IF; + + SELECT count(*) INTO parent_count + FROM sysml2.commit_parent + WHERE commit_id = NEW.commit_id; + + IF parent_count >= 2 THEN + + SELECT count(*) INTO mixed_parents + FROM sysml2.commit_parent cp + JOIN sysml2.commit parent ON parent.id = cp.parent_commit_id + WHERE cp.commit_id = NEW.commit_id + AND parent.model_version_id <> child_version; + + IF mixed_parents > 0 THEN + RAISE EXCEPTION + 'merge commit % (model_version %) has a parent in a different model_version: convert every branch to the target release before merging', + NEW.commit_id, child_version + USING ERRCODE = 'check_violation'; + END IF; + + END IF; + + RETURN NEW; +END; +$$; + +CREATE TRIGGER trg_commit_parent_version + AFTER INSERT ON sysml2.commit_parent + FOR EACH ROW + EXECUTE FUNCTION sysml2.assert_commit_version_compatible(); + +-- Branch and Tag are both CommitReference. Branch is mutable + destructible; Tag is immutable + +-- destructible (Clause 7.1.2 mutability table). +CREATE TABLE sysml2.branch ( + id uuid NOT NULL, + project_id uuid NOT NULL REFERENCES sysml2.project (id) ON DELETE CASCADE, + name text NULL, + description text NULL, + head_commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + + -- The base of the branch_head OVERLAY (see §9): a commit for which a commit_checkpoint has + -- been materialized (service-enforced invariant). branch_head then stores ONLY the + -- identities that DIVERGE from that checkpoint, making branch creation and deletion + -- O(divergence) instead of O(model). NULL means the overlay is the complete head state + -- (bootstrap / small-project mode). + base_commit_id uuid NULL REFERENCES sysml2.commit (id), + + created timestamptz NOT NULL DEFAULT now(), + deleted timestamptz NULL, + PRIMARY KEY (id), + UNIQUE (project_id, name) +); + +ALTER TABLE sysml2.project + ADD CONSTRAINT project_default_branch_fk + FOREIGN KEY (default_branch_id) REFERENCES sysml2.branch (id) DEFERRABLE INITIALLY DEFERRED; + +CREATE TABLE sysml2.tag ( + id uuid NOT NULL, + project_id uuid NOT NULL REFERENCES sysml2.project (id) ON DELETE CASCADE, + name text NULL, + description text NULL, + tagged_commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + created timestamptz NOT NULL DEFAULT now(), + deleted timestamptz NULL, + PRIMARY KEY (id), + UNIQUE (project_id, name) +); + +CREATE TABLE sysml2.project_usage ( + id uuid NOT NULL, + project_id uuid NOT NULL REFERENCES sysml2.project (id) ON DELETE CASCADE, + used_project_id uuid NOT NULL REFERENCES sysml2.project (id), + used_project_commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + PRIMARY KEY (id) +); + +------------------------------------------------------------------------------------------------ +-- 4. DATA IDENTITY +-- +-- DataIdentity is the version-independent identity of an element — the stable @id that every +-- SysML2 reference points at. +-- +-- This table is deliberately NOT partitioned and its PK is the bare uuid: ProjectUsage lets an +-- element in project A reference an element in project B, so a (project_id, id) composite key +-- would make every cross-project reference un-FK-able. Project scoping is enforced by the +-- service layer via project_usage, not by the FK. +------------------------------------------------------------------------------------------------ + +-- DELETION IS EXPLICIT, NOT CASCADED. At scale (~1M identities per project), FK cascades are a +-- trap: a cascade executes per-row deletes filtered on the FK column ALONE, which no index in +-- this schema leads with — every cascaded identity would trigger scans of the largest tables. +-- Project deletion is therefore an ordered, batched, per-table procedure (each statement prunes +-- to one partition and uses a PK prefix): +-- +-- DELETE FROM sysml2. WHERE project_id = $1; +-- DELETE FROM sysml2.derived_version WHERE project_id = $1; +-- DELETE FROM sysml2.element_version WHERE project_id = $1; +-- DELETE FROM sysml2.branch_head WHERE project_id = $1; +-- DELETE FROM sysml2.commit_checkpoint WHERE project_id = $1; +-- DELETE FROM sysml2.data_identity WHERE project_id = $1; +-- DELETE FROM sysml2.project WHERE id = $1; -- cascades only PIM rows +-- +-- The remaining NO ACTION FKs guarantee the procedure cannot leave dangling references — they +-- block out-of-order deletion instead of silently scanning. +CREATE TABLE sysml2.data_identity ( + id uuid NOT NULL, + project_id uuid NOT NULL REFERENCES sysml2.project (id), + + -- TYPED IDENTITY. The metaclass of an element is invariant across its versions — an + -- identity is born a PartUsage and stays one — so the type is a property of the IDENTITY + -- and therefore FK-able, unlike everything else about an element. Two consumers: + -- * element_version's composite FK (identity_id, class_kind) makes a version that + -- claims a different metaclass than its identity IMPOSSIBLE; + -- * validate_references_at_commit (§14) type-checks every stored reference against + -- this column — including cross-project targets, because identities are typed + -- regardless of which project they live in. + -- A release conversion that retypes an element (e.g. its metaclass was dropped) must + -- update this column in the same transaction (service obligation, guide §15.16). + class_kind smallint NOT NULL REFERENCES sysml2.class_kind (id), + + PRIMARY KEY (id), + UNIQUE (id, class_kind) -- FK target for element_version's type-consistency check +); + +CREATE INDEX ix_data_identity_project ON sysml2.data_identity (project_id); + +------------------------------------------------------------------------------------------------ +-- 5. STORED ELEMENT STATE — append-only [HAND-WRITTEN] +-- +-- One row per (element, commit-in-which-it-changed). Never UPDATEd, never DELETEd. This is the +-- write hot path: a commit is a pure COPY of new rows. +-- +-- Element's own six stored properties are folded in here rather than into a separate table — +-- every element has them, so a join would be pure overhead. +-- +-- stored_json is a DELIBERATE DENORMALIZATION: the pre-serialized JSON of the element's stored +-- half. The normalized columns and link tables remain the system of record (and carry the FKs); +-- stored_json exists so that a read never has to reassemble an element from six tables. See §9. +------------------------------------------------------------------------------------------------ + +CREATE TABLE sysml2.element_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, -- DataVersion.id + identity_id uuid NOT NULL, -- FK via the composite typed-identity check below + commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + class_kind smallint NOT NULL REFERENCES sysml2.class_kind (id), + + -- tombstone == true is DataVersion.payload = null, i.e. a deletion (Clause 7.1.2) + tombstone boolean NOT NULL DEFAULT false, + + -- Element (the only metaclass whose stored properties are folded into the core table) + -- element_id is text, NOT uuid, deliberately: KerML declares Element::elementId : String. + -- Only standard-library elements are normatively required to use name-based (version 5, + -- SHA-1) UUIDs; user-model elementIds carry no format constraint, so uuid would reject + -- spec-valid data. + element_id text NULL, + declared_name text NULL, + declared_short_name text NULL, + is_implied_included boolean NULL, + owning_relationship uuid NULL REFERENCES sysml2.data_identity (id), + + stored_json jsonb NULL, + + PRIMARY KEY (project_id, version_id), + + -- TYPED IDENTITY (§4): one composite FK both anchors the identity AND makes a version + -- that claims a different metaclass than its identity impossible. + FOREIGN KEY (identity_id, class_kind) REFERENCES sysml2.data_identity (id, class_kind), + + CONSTRAINT element_version_tombstone_empty + CHECK (NOT tombstone OR (stored_json IS NULL AND element_id IS NULL)), + CONSTRAINT element_version_payload_present + CHECK (tombstone OR (stored_json IS NOT NULL AND element_id IS NOT NULL AND is_implied_included IS NOT NULL)) +) PARTITION BY HASH (project_id); + +-- Clause 7.1.2: "DataVersion.identity is unique among records listed in Commit.change." +CREATE UNIQUE INDEX ux_element_version_identity_commit + ON sysml2.element_version (project_id, identity_id, commit_id); + +-- lz4 beats the pglz default by a wide margin at this write volume; set BEFORE the partitions +-- are created (§12) so every leaf inherits it. +ALTER TABLE sysml2.element_version ALTER COLUMN stored_json SET COMPRESSION lz4; + +CREATE INDEX ix_element_version_commit ON sysml2.element_version (project_id, commit_id); +CREATE INDEX ix_element_version_class_kind ON sysml2.element_version (project_id, class_kind); +CREATE INDEX ix_element_version_owning_rel ON sysml2.element_version (project_id, owning_relationship) + WHERE owning_relationship IS NOT NULL; + +------------------------------------------------------------------------------------------------ +-- 6. LINK TABLES — ordered multi-valued properties [GENERATED] +-- +-- The entire metamodel has only SIX distinct multi-valued reference properties and ONE +-- multi-valued value property. Every one of them is isOrdered=true, so `ordinal` is part of +-- the key rather than a set. +-- +-- Element::ownedRelationship (composite) × 167 classes +-- Relationship::ownedRelatedElement (composite) × 62 classes +-- Relationship::source × 62 classes +-- Relationship::target × 62 classes +-- Dependency::client × 1 class +-- Dependency::supplier × 1 class +-- Element::aliasIds (string) × 167 classes +-- +-- target_identity FKs to data_identity, NOT to element_version — a reference names an element, +-- not a version of it. +-- +-- GENERATOR NOTE: core-sql-schema-2.hbs derives these mechanically. Every reference-valued link +-- table gets an ix_{table}_target reverse-lookup index (this file shows the same rule). +------------------------------------------------------------------------------------------------ + +CREATE TABLE sysml2.dependency_client ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + ordinal int NOT NULL, + target_identity uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id, ordinal), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_dependency_client_target + ON sysml2.dependency_client (project_id, target_identity); + +CREATE TABLE sysml2.dependency_supplier ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + ordinal int NOT NULL, + target_identity uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id, ordinal), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_dependency_supplier_target + ON sysml2.dependency_supplier (project_id, target_identity); + +CREATE TABLE sysml2.element_alias_ids ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + ordinal int NOT NULL, + value text NOT NULL, + PRIMARY KEY (project_id, version_id, ordinal), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.element_owned_relationship ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + ordinal int NOT NULL, + target_identity uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id, ordinal), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_element_owned_relationship_target + ON sysml2.element_owned_relationship (project_id, target_identity); + +CREATE TABLE sysml2.relationship_owned_related_element ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + ordinal int NOT NULL, + target_identity uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id, ordinal), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_relationship_owned_related_element_target + ON sysml2.relationship_owned_related_element (project_id, target_identity); + +CREATE TABLE sysml2.relationship_source ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + ordinal int NOT NULL, + target_identity uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id, ordinal), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_relationship_source_target + ON sysml2.relationship_source (project_id, target_identity); + +CREATE TABLE sysml2.relationship_target ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + ordinal int NOT NULL, + target_identity uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id, ordinal), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_relationship_target_target + ON sysml2.relationship_target (project_id, target_identity); + +------------------------------------------------------------------------------------------------ +-- 7. SUBTYPE TABLES — one per storage-introducing metaclass [GENERATED] +-- +-- 47 tables. A metaclass gets a table iff it DECLARES at least one stored scalar or single +-- reference property of its own. Redeclarations (`new` in C#, redefinition in UML) do NOT get a +-- column — they resolve to the ancestor's column. There are 9 of them: +-- +-- CollectExpression::operator, SelectExpression::operator, FeatureChainExpression::operator, +-- IndexExpression::operator -> operator_expression_version.operator +-- ConnectionDefinition::isSufficient -> type_version.is_sufficient +-- EnumerationDefinition::isVariation -> definition_version.is_variation +-- Expose::isImportAll, Expose::visibility -> import_version.is_import_all / import_version.visibility +-- FramedConcernMembership::kind, +-- RequirementVerificationMembership::kind -> requirement_constraint_membership_version.kind +-- +-- Every table is keyed by (project_id, version_id) and co-partitioned with element_version, so +-- the join is partition-local. NOT NULL is safe on [1..1] properties: a row only exists here if +-- the element's class actually inherits this metaclass. +-- +-- Dependency introduces no scalar properties (client/supplier are link tables), so it has no +-- subtype table. +-- +-- GENERATOR NOTE: core-sql-schema-2.hbs derives these tables mechanically and emits two things +-- this hand-curated file only shows selectively: +-- * a DEFAULT clause for every column whose UML property declares a default value in the XMI +-- (e.g. the Feature booleans DEFAULT false, Membership::visibility DEFAULT 'public', +-- Import::visibility DEFAULT 'private'); +-- * an ix_{table}_{column} reverse-lookup index on EVERY data_identity-referencing column, +-- not just the four hot ones annotated below. The specialization graph indexes remain the +-- load-bearing ones for derived-property impact analysis. +------------------------------------------------------------------------------------------------ + +CREATE TABLE sysml2.relationship_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + is_implied boolean NOT NULL DEFAULT false, + owning_related_element uuid NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_relationship_version_owning_related_element + ON sysml2.relationship_version (project_id, owning_related_element); + +CREATE TABLE sysml2.annotation_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + annotated_element uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_annotation_version_annotated_element + ON sysml2.annotation_version (project_id, annotated_element); + +CREATE TABLE sysml2.comment_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + body text NOT NULL, + locale text NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.conjugation_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + conjugated_type uuid NOT NULL REFERENCES sysml2.data_identity (id), + original_type uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_conjugation_version_conjugated_type + ON sysml2.conjugation_version (project_id, conjugated_type); + +CREATE INDEX ix_conjugation_version_original_type + ON sysml2.conjugation_version (project_id, original_type); + +CREATE TABLE sysml2.differencing_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + differencing_type uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_differencing_version_differencing_type + ON sysml2.differencing_version (project_id, differencing_type); + +CREATE TABLE sysml2.disjoining_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + disjoining_type uuid NOT NULL REFERENCES sysml2.data_identity (id), + type_disjoined uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_disjoining_version_disjoining_type + ON sysml2.disjoining_version (project_id, disjoining_type); + +CREATE INDEX ix_disjoining_version_type_disjoined + ON sysml2.disjoining_version (project_id, type_disjoined); + +CREATE TABLE sysml2.feature_chaining_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + chaining_feature uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_feature_chaining_version_chaining_feature + ON sysml2.feature_chaining_version (project_id, chaining_feature); + +CREATE TABLE sysml2.feature_inverting_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + feature_inverted uuid NOT NULL REFERENCES sysml2.data_identity (id), + inverting_feature uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_feature_inverting_version_feature_inverted + ON sysml2.feature_inverting_version (project_id, feature_inverted); + +CREATE INDEX ix_feature_inverting_version_inverting_feature + ON sysml2.feature_inverting_version (project_id, inverting_feature); + +CREATE TABLE sysml2.import_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + is_import_all boolean NOT NULL DEFAULT false, + is_recursive boolean NOT NULL DEFAULT false, + visibility sysml2.visibility_kind NOT NULL DEFAULT 'private', + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.intersecting_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + intersecting_type uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_intersecting_version_intersecting_type + ON sysml2.intersecting_version (project_id, intersecting_type); + +CREATE TABLE sysml2.membership_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + member_element uuid NOT NULL REFERENCES sysml2.data_identity (id), + member_name text NULL, + member_short_name text NULL, + visibility sysml2.visibility_kind NOT NULL DEFAULT 'public', + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_membership_version_member_element + ON sysml2.membership_version (project_id, member_element); + +CREATE TABLE sysml2.specialization_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + general uuid NOT NULL REFERENCES sysml2.data_identity (id), + specific uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_specialization_version_general + ON sysml2.specialization_version (project_id, general); + +CREATE INDEX ix_specialization_version_specific + ON sysml2.specialization_version (project_id, specific); + +CREATE TABLE sysml2.textual_representation_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + body text NOT NULL, + language text NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.type_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + is_abstract boolean NOT NULL DEFAULT false, + is_sufficient boolean NOT NULL DEFAULT false, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.type_featuring_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + feature_of_type uuid NOT NULL REFERENCES sysml2.data_identity (id), + featuring_type uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_type_featuring_version_feature_of_type + ON sysml2.type_featuring_version (project_id, feature_of_type); + +CREATE INDEX ix_type_featuring_version_featuring_type + ON sysml2.type_featuring_version (project_id, featuring_type); + +CREATE TABLE sysml2.unioning_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + unioning_type uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_unioning_version_unioning_type + ON sysml2.unioning_version (project_id, unioning_type); + +CREATE TABLE sysml2.feature_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + direction sysml2.feature_direction_kind NULL, + is_composite boolean NOT NULL DEFAULT false, + is_constant boolean NOT NULL DEFAULT false, + is_derived boolean NOT NULL DEFAULT false, + is_end boolean NOT NULL DEFAULT false, + is_ordered boolean NOT NULL DEFAULT false, + is_portion boolean NOT NULL DEFAULT false, + is_unique boolean NOT NULL DEFAULT true, + is_variable boolean NOT NULL DEFAULT false, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.feature_typing_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + type uuid NOT NULL REFERENCES sysml2.data_identity (id), + typed_feature uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_feature_typing_version_type + ON sysml2.feature_typing_version (project_id, type); + +CREATE INDEX ix_feature_typing_version_typed_feature + ON sysml2.feature_typing_version (project_id, typed_feature); + +CREATE TABLE sysml2.library_package_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + is_standard boolean NOT NULL DEFAULT false, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.membership_import_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + imported_membership uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_membership_import_version_imported_membership + ON sysml2.membership_import_version (project_id, imported_membership); + +CREATE TABLE sysml2.namespace_import_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + imported_namespace uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_namespace_import_version_imported_namespace + ON sysml2.namespace_import_version (project_id, imported_namespace); + +CREATE TABLE sysml2.port_conjugation_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + original_port_definition uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_port_conjugation_version_original_port_definition + ON sysml2.port_conjugation_version (project_id, original_port_definition); + +CREATE TABLE sysml2.subclassification_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + subclassifier uuid NOT NULL REFERENCES sysml2.data_identity (id), + superclassifier uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_subclassification_version_subclassifier + ON sysml2.subclassification_version (project_id, subclassifier); + +CREATE INDEX ix_subclassification_version_superclassifier + ON sysml2.subclassification_version (project_id, superclassifier); + +CREATE TABLE sysml2.subsetting_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + subsetted_feature uuid NOT NULL REFERENCES sysml2.data_identity (id), + subsetting_feature uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_subsetting_version_subsetted_feature + ON sysml2.subsetting_version (project_id, subsetted_feature); + +CREATE INDEX ix_subsetting_version_subsetting_feature + ON sysml2.subsetting_version (project_id, subsetting_feature); + +CREATE TABLE sysml2.conjugated_port_typing_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + conjugated_port_definition uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_conjugated_port_typing_version_conjugated_port_definition + ON sysml2.conjugated_port_typing_version (project_id, conjugated_port_definition); + +CREATE TABLE sysml2.cross_subsetting_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + crossed_feature uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_cross_subsetting_version_crossed_feature + ON sysml2.cross_subsetting_version (project_id, crossed_feature); + +CREATE TABLE sysml2.definition_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + is_variation boolean NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.feature_value_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + is_default boolean NOT NULL DEFAULT false, + is_initial boolean NOT NULL DEFAULT false, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.redefinition_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + redefined_feature uuid NOT NULL REFERENCES sysml2.data_identity (id), + redefining_feature uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_redefinition_version_redefined_feature + ON sysml2.redefinition_version (project_id, redefined_feature); + +CREATE INDEX ix_redefinition_version_redefining_feature + ON sysml2.redefinition_version (project_id, redefining_feature); + +CREATE TABLE sysml2.reference_subsetting_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + referenced_feature uuid NOT NULL REFERENCES sysml2.data_identity (id), + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE INDEX ix_reference_subsetting_version_referenced_feature + ON sysml2.reference_subsetting_version (project_id, referenced_feature); + +CREATE TABLE sysml2.usage_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + is_variation boolean NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.occurrence_usage_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + is_individual boolean NOT NULL DEFAULT false, + portion_kind sysml2.portion_kind NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.requirement_constraint_membership_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + kind sysml2.requirement_constraint_kind NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.state_subaction_membership_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + kind sysml2.state_subaction_kind NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.transition_feature_membership_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + kind sysml2.transition_feature_kind NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.occurrence_definition_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + is_individual boolean NOT NULL DEFAULT false, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.invariant_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + is_negated boolean NOT NULL DEFAULT false, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.literal_boolean_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + value boolean NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.literal_integer_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + value integer NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.literal_rational_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + value double precision NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.literal_string_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + value text NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.operator_expression_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + operator text NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.state_usage_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + is_parallel boolean NOT NULL DEFAULT false, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.trigger_invocation_expression_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + kind sysml2.trigger_kind NOT NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.state_definition_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + is_parallel boolean NOT NULL DEFAULT false, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.requirement_usage_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + req_id text NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +CREATE TABLE sysml2.requirement_definition_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, + req_id text NULL, + PRIMARY KEY (project_id, version_id), + FOREIGN KEY (project_id, version_id) + REFERENCES sysml2.element_version (project_id, version_id) ON DELETE CASCADE +) PARTITION BY HASH (project_id); + +------------------------------------------------------------------------------------------------ +-- 8. DERIVED ELEMENT STATE — the second append-only stream [HAND-WRITTEN] +-- +-- THE KEY INSIGHT OF THIS SCHEMA. +-- +-- A derived value is a function of (version, SNAPSHOT), not of the version alone. Rename a +-- Namespace and every descendant's qualifiedName changes, even though no descendant's version +-- row changed. So derived state CANNOT hang off element_version — it is keyed by +-- (identity, commit), exactly like a version, and resolves through the SAME fold. +-- +-- A row is written only for elements whose derived values ACTUALLY CHANGED at that commit — +-- the "impact radius" of the change set. A leaf edit writes one row. A Namespace rename writes +-- one row per descendant. Adding a supertype to a widely-specialized Type writes one row per +-- member of its specialization-descendant closure. This is inherent, not a flaw in the design; +-- OMG Clause 2 warns of exactly this: "the values of derived properties of a given Element may +-- be affected by commits that do not directly change that Element." +-- +-- The six hot derived properties are promoted to real columns so the Query service can filter +-- and ORDER BY them (Clause 2, Derived Property Full Conformance: derived properties "can be +-- used in Query structures as PrimitiveConstraint properties"). The remaining ~325 distinct +-- derived property names live in derived_json. +------------------------------------------------------------------------------------------------ + +CREATE TABLE sysml2.derived_version ( + project_id uuid NOT NULL, + derived_id uuid NOT NULL, + identity_id uuid NOT NULL REFERENCES sysml2.data_identity (id), + commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + + -- promoted hot derived properties (all 167 metaclasses declare these via Element) + owner uuid NULL REFERENCES sysml2.data_identity (id), + owning_namespace uuid NULL REFERENCES sysml2.data_identity (id), + qualified_name text NULL, + name text NULL, + short_name text NULL, + is_library_element boolean NOT NULL DEFAULT false, + + -- everything else: ~325 distinct derived property names + derived_json jsonb NOT NULL, + + PRIMARY KEY (project_id, derived_id) +) PARTITION BY HASH (project_id); + +ALTER TABLE sysml2.derived_version ALTER COLUMN derived_json SET COMPRESSION lz4; + +CREATE UNIQUE INDEX ux_derived_version_identity_commit + ON sysml2.derived_version (project_id, identity_id, commit_id); + +CREATE INDEX ix_derived_version_commit ON sysml2.derived_version (project_id, commit_id); +CREATE INDEX ix_derived_version_owner ON sysml2.derived_version (project_id, owner); +CREATE INDEX ix_derived_version_qualified_name ON sysml2.derived_version (project_id, qualified_name); + +-- Ad-hoc PrimitiveConstraint filtering on a derived property that did NOT get promoted to a +-- column falls back to a jsonb containment probe. GIN keeps that from being a sequential scan. +CREATE INDEX ix_derived_version_json ON sysml2.derived_version USING gin (derived_json jsonb_path_ops); + +------------------------------------------------------------------------------------------------ +-- 9. SNAPSHOT RESOLUTION [HAND-WRITTEN] +-- +-- Commit.versionedData is DERIVED: fold `change` over the transitive previousCommit closure +-- (Clause 7.1.2). Doing that fold on every read does not scale, so: +-- +-- * branch_head is a sparse OVERLAY: it stores ONLY the identities that DIVERGE from the +-- branch's base checkpoint (branch.base_commit_id, §3). The head state of a branch is +-- overlay-row-if-present, else checkpoint-row. Updated incrementally on commit with the +-- change-set rows: O(changeset). Branch creation from a checkpointed commit writes ZERO +-- rows; branch deletion deletes only the divergence. At the design scale (100-500 live +-- branches x 1M elements) a fully-materialized per-branch head would be ~500M rows per +-- project — the overlay is what makes hundreds of branches affordable. +-- A deletion on the branch is masked by an overlay row with is_tombstone = true (it also +-- points at the tombstone element_version row). base_commit_id NULL = the overlay IS the +-- complete head state (bootstrap / small-project mode). +-- COMPACTION (service policy): when an overlay exceeds ~10% of the model or ~100k rows, +-- materialize a checkpoint at the branch head, repoint base_commit_id, delete the overlay. +-- +-- * commit_checkpoint materializes the full fold for selected commits. It serves two masters: +-- the base of every branch overlay, and the bound on how far resolve_commit_state() walks. +-- CADENCE (service policy): checkpoint a commit when EITHER ~200 commits have passed since +-- the nearest checkpointed ancestor on that lineage OR the cumulative change-set size since +-- it exceeds ~25% of the model — plus always at branch-fork bases. Checkpoints are O(model) +-- rows each, so cadence must be churn-based, never "every N commits" alone. Retention: drop +-- checkpoints referenced by no branch.base_commit_id (delete the registry row FIRST, then +-- the rows), keeping a sparse historical ladder. +-- +-- * resolve_commit_state() is the general fallback for an arbitrary commit; +-- resolve_element_at_commit() is the single-element variant (O(ancestry), not O(model)). +------------------------------------------------------------------------------------------------ + +CREATE TABLE sysml2.branch_head ( + project_id uuid NOT NULL, + branch_id uuid NOT NULL REFERENCES sysml2.branch (id) ON DELETE CASCADE, + identity_id uuid NOT NULL REFERENCES sysml2.data_identity (id), + version_id uuid NOT NULL, + derived_id uuid NULL, + + -- true = the element is DELETED on this branch relative to the base checkpoint; the row + -- masks the checkpoint row on read. Denormalizes element_version.tombstone so the set-read + -- anti-join never has to visit element_version for masked identities. + is_tombstone boolean NOT NULL DEFAULT false, + + PRIMARY KEY (project_id, branch_id, identity_id) +) PARTITION BY HASH (project_id); + +-- Supports the ON DELETE CASCADE from branch: a cascade filters on branch_id ALONE, and the PK +-- leads with project_id — without this index every branch deletion would sequentially scan all +-- partitions. (Cheap to maintain now that the table only holds divergence.) +CREATE INDEX ix_branch_head_branch ON sysml2.branch_head (branch_id); + +-- One row per CHECKPOINT (not per identity): the resolvers' "is this commit checkpointed?" +-- probe hits this tiny table, never commit_checkpoint itself. Probing commit_checkpoint for +-- existence is a planner trap: all of a checkpoint's ~1M rows share one (project_id, commit_id) +-- value, so n_distinct estimates make the index look useless and the EXISTS degenerates into a +-- repeated sequential scan of the whole partition (measured: 500 probes = 100M rows filtered). +CREATE TABLE sysml2.commit_checkpoint_registry ( + project_id uuid NOT NULL, + commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + created timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (project_id, commit_id) +); + +CREATE TABLE sysml2.commit_checkpoint ( + project_id uuid NOT NULL, + commit_id uuid NOT NULL REFERENCES sysml2.commit (id) ON DELETE CASCADE, + identity_id uuid NOT NULL REFERENCES sysml2.data_identity (id), + version_id uuid NOT NULL, + derived_id uuid NULL, + PRIMARY KEY (project_id, commit_id, identity_id) +) PARTITION BY HASH (project_id); + +-- The general resolver. Walks the commit DAG back from :commit_id (stopping at the nearest +-- checkpoint) and picks, for each identity, the version from the NEWEST ancestor commit. +-- +-- "Newest" is well-defined only because Clause 7.1.2 guarantees monotonically increasing commit +-- timestamps along every parent edge — which trg_commit_parent_monotonic enforces. For a merge +-- commit, the merge itself carries the conflict resolution in its own change set, so it is +-- correctly the newest and wins. +-- +-- SIBLING commits (parallel branches, later merged) are NOT ordered by the monotonicity +-- invariant and may share a timestamp; the `id DESC` tiebreaker makes the fold DETERMINISTIC +-- in that case (an arbitrary-but-stable winner beats a nondeterministic one). A merge that +-- restates its conflicts — as Clause 7.1.2 requires — never reaches the tiebreaker. +CREATE OR REPLACE FUNCTION sysml2.resolve_commit_state(p_project_id uuid, p_commit_id uuid) +RETURNS TABLE (identity_id uuid, version_id uuid, derived_id uuid) +LANGUAGE sql +STABLE +AS $$ + WITH RECURSIVE + checkpoint AS ( + SELECT ccr.commit_id + FROM sysml2.commit_checkpoint_registry ccr + WHERE ccr.project_id = p_project_id AND ccr.commit_id = p_commit_id + ), + ancestry AS ( + SELECT c.id, c.created, (SELECT count(*) > 0 FROM checkpoint) AS at_checkpoint + FROM sysml2.commit c + WHERE c.id = p_commit_id + + UNION + + SELECT parent.id, parent.created, + EXISTS (SELECT 1 FROM sysml2.commit_checkpoint_registry ccr + WHERE ccr.project_id = p_project_id AND ccr.commit_id = parent.id) + FROM ancestry a + JOIN sysml2.commit_parent cp ON cp.commit_id = a.id + JOIN sysml2.commit parent ON parent.id = cp.parent_commit_id + WHERE NOT a.at_checkpoint + ), + folded AS ( + SELECT DISTINCT ON (ev.identity_id) + ev.identity_id, + ev.version_id, + ev.tombstone + FROM ancestry a + JOIN sysml2.element_version ev + ON ev.project_id = p_project_id AND ev.commit_id = a.id + ORDER BY ev.identity_id, a.created DESC, a.id DESC + ), + checkpoint_state AS ( + SELECT cc.identity_id, cc.version_id, cc.derived_id + FROM sysml2.commit_checkpoint cc + JOIN ancestry a ON a.id = cc.commit_id AND a.at_checkpoint + WHERE cc.project_id = p_project_id + ), + checkpointed AS ( + SELECT cs.identity_id, cs.version_id + FROM checkpoint_state cs + WHERE NOT EXISTS (SELECT 1 FROM folded f WHERE f.identity_id = cs.identity_id) + ), + resolved AS ( + SELECT f.identity_id, f.version_id FROM folded f WHERE NOT f.tombstone + UNION ALL + SELECT c.identity_id, c.version_id FROM checkpointed c + ), + -- Derived state folds over the SAME walked ancestry, in one pass (no per-row probe). + -- An identity whose latest derived row predates the walked window falls back to the + -- checkpoint's derived_id — without that fallback, derived state older than the + -- checkpoint would silently resolve to NULL. + derived_folded AS ( + SELECT DISTINCT ON (dv.identity_id) + dv.identity_id, + dv.derived_id + FROM ancestry a + JOIN sysml2.derived_version dv + ON dv.project_id = p_project_id AND dv.commit_id = a.id + ORDER BY dv.identity_id, a.created DESC, a.id DESC + ) + SELECT r.identity_id, + r.version_id, + COALESCE(df.derived_id, cs.derived_id) AS derived_id + FROM resolved r + LEFT JOIN derived_folded df ON df.identity_id = r.identity_id + LEFT JOIN checkpoint_state cs ON cs.identity_id = r.identity_id; +$$; + +-- Single-element variant of the resolver: same ancestry walk, but the fold arms are filtered to +-- one identity, so the cost is O(walked ancestry) index probes — NOT O(model). This is the +-- backing for GET /projects/{p}/commits/{c}/elements/{e}; without it a one-element historical +-- read would fold the entire model. +CREATE OR REPLACE FUNCTION sysml2.resolve_element_at_commit(p_project_id uuid, p_commit_id uuid, p_identity_id uuid) +RETURNS TABLE (identity_id uuid, version_id uuid, derived_id uuid) +LANGUAGE sql +STABLE +AS $$ + WITH RECURSIVE + checkpoint AS ( + SELECT ccr.commit_id + FROM sysml2.commit_checkpoint_registry ccr + WHERE ccr.project_id = p_project_id AND ccr.commit_id = p_commit_id + ), + ancestry AS ( + SELECT c.id, c.created, (SELECT count(*) > 0 FROM checkpoint) AS at_checkpoint + FROM sysml2.commit c + WHERE c.id = p_commit_id + + UNION + + SELECT parent.id, parent.created, + EXISTS (SELECT 1 FROM sysml2.commit_checkpoint_registry ccr + WHERE ccr.project_id = p_project_id AND ccr.commit_id = parent.id) + FROM ancestry a + JOIN sysml2.commit_parent cp ON cp.commit_id = a.id + JOIN sysml2.commit parent ON parent.id = cp.parent_commit_id + WHERE NOT a.at_checkpoint + ), + folded AS ( + SELECT ev.identity_id, ev.version_id, ev.tombstone + FROM ancestry a + JOIN sysml2.element_version ev + ON ev.project_id = p_project_id AND ev.commit_id = a.id AND ev.identity_id = p_identity_id + ORDER BY a.created DESC, a.id DESC + LIMIT 1 + ), + checkpointed AS ( + SELECT cc.identity_id, cc.version_id, cc.derived_id + FROM sysml2.commit_checkpoint cc + JOIN ancestry a ON a.id = cc.commit_id AND a.at_checkpoint + WHERE cc.project_id = p_project_id AND cc.identity_id = p_identity_id + ), + resolved AS ( + SELECT f.identity_id, f.version_id FROM folded f WHERE NOT f.tombstone + UNION ALL + SELECT c.identity_id, c.version_id FROM checkpointed c + WHERE NOT EXISTS (SELECT 1 FROM folded) + ), + derived_folded AS ( + SELECT dv.derived_id + FROM ancestry a + JOIN sysml2.derived_version dv + ON dv.project_id = p_project_id AND dv.commit_id = a.id AND dv.identity_id = p_identity_id + ORDER BY a.created DESC, a.id DESC + LIMIT 1 + ) + SELECT r.identity_id, + r.version_id, + COALESCE((SELECT df.derived_id FROM derived_folded df), + (SELECT c.derived_id FROM checkpointed c)) AS derived_id + FROM resolved r; +$$; + +-- Materializes the full fold of a commit as a checkpoint (idempotent). O(model) by design — +-- run it ASYNCHRONOUSLY per the cadence policy in the §9 banner, never on the commit path. +-- Returns the number of rows written. +CREATE OR REPLACE FUNCTION sysml2.build_commit_checkpoint(p_project_id uuid, p_commit_id uuid) +RETURNS bigint +LANGUAGE sql +VOLATILE +AS $$ + WITH inserted AS ( + INSERT INTO sysml2.commit_checkpoint (project_id, commit_id, identity_id, version_id, derived_id) + SELECT p_project_id, p_commit_id, r.identity_id, r.version_id, r.derived_id + FROM sysml2.resolve_commit_state(p_project_id, p_commit_id) r + ON CONFLICT (project_id, commit_id, identity_id) DO NOTHING + RETURNING 1 + ), + -- same statement, so checkpoint rows + registry row become visible atomically; the EXISTS + -- dependency also skips registering a checkpoint that materialized zero rows + registered AS ( + INSERT INTO sysml2.commit_checkpoint_registry (project_id, commit_id) + SELECT p_project_id, p_commit_id + WHERE EXISTS (SELECT 1 FROM inserted) + ON CONFLICT (project_id, commit_id) DO NOTHING + ) + SELECT count(*) FROM inserted; +$$; + +------------------------------------------------------------------------------------------------ +-- 10. READ PATH [HAND-WRITTEN] +-- +-- GET /projects/{p}/commits/{c}/elements/{e} reduces to ONE jsonb concat over a handful of PK +-- lookups. No joins across 6 subtype tables, no recursion, no derived computation. That is the +-- entire point of splitting stored_json from derived_json. +-- +-- EVERY function here resolves project_id by joining through `branch` FIRST. Filtering a +-- partitioned table on a bare uuid (branch_id / identity_id / version_id) without project_id +-- defeats partition pruning (all 16 leaves visited) AND cannot use the PKs (project_id-leading, +-- and PG16/17 has no btree skip scan) — the hottest query would silently become the worst one. +-- +-- The normalized columns of §5-§7 are the system of record and carry the referential integrity; +-- stored_json / derived_json are the read model built from them at commit time. +------------------------------------------------------------------------------------------------ + +-- Branch-head single-element read over the OVERLAY: the overlay row wins (a tombstoned overlay +-- row masks the base); otherwise fall back to the base checkpoint row. +CREATE OR REPLACE FUNCTION sysml2.get_element_at_branch_head(p_branch_id uuid, p_identity_id uuid) +RETURNS jsonb +LANGUAGE sql +STABLE +AS $$ + SELECT ev.stored_json || COALESCE(dv.derived_json, '{}'::jsonb) + FROM sysml2.branch b + LEFT JOIN sysml2.branch_head bh + ON bh.project_id = b.project_id AND bh.branch_id = b.id AND bh.identity_id = p_identity_id + LEFT JOIN sysml2.commit_checkpoint cc + ON cc.project_id = b.project_id AND cc.commit_id = b.base_commit_id + AND cc.identity_id = p_identity_id AND bh.identity_id IS NULL + JOIN sysml2.element_version ev + ON ev.project_id = b.project_id AND ev.version_id = COALESCE(bh.version_id, cc.version_id) + LEFT JOIN sysml2.derived_version dv + ON dv.project_id = b.project_id AND dv.derived_id = COALESCE(bh.derived_id, cc.derived_id) + WHERE b.id = p_branch_id + AND bh.is_tombstone IS NOT TRUE + AND NOT ev.tombstone; +$$; + +-- Branch-head set read: base checkpoint minus overlaid identities, plus the live overlay. +CREATE OR REPLACE FUNCTION sysml2.get_elements_at_branch_head(p_branch_id uuid) +RETURNS TABLE (identity_id uuid, payload jsonb) +LANGUAGE sql +STABLE +AS $$ + WITH head AS ( + SELECT b.project_id, bh.identity_id, bh.version_id, bh.derived_id + FROM sysml2.branch b + JOIN sysml2.branch_head bh + ON bh.project_id = b.project_id AND bh.branch_id = b.id + WHERE b.id = p_branch_id + AND NOT bh.is_tombstone + + UNION ALL + + SELECT b.project_id, cc.identity_id, cc.version_id, cc.derived_id + FROM sysml2.branch b + JOIN sysml2.commit_checkpoint cc + ON cc.project_id = b.project_id AND cc.commit_id = b.base_commit_id + WHERE b.id = p_branch_id + AND NOT EXISTS (SELECT 1 + FROM sysml2.branch_head masked + WHERE masked.project_id = cc.project_id + AND masked.branch_id = b.id + AND masked.identity_id = cc.identity_id) + ) + SELECT h.identity_id, + ev.stored_json || COALESCE(dv.derived_json, '{}'::jsonb) + FROM head h + JOIN sysml2.element_version ev + ON ev.project_id = h.project_id AND ev.version_id = h.version_id + LEFT JOIN sysml2.derived_version dv + ON dv.project_id = h.project_id AND dv.derived_id = h.derived_id + WHERE NOT ev.tombstone; +$$; + +CREATE OR REPLACE FUNCTION sysml2.get_elements_at_commit(p_project_id uuid, p_commit_id uuid) +RETURNS TABLE (identity_id uuid, payload jsonb) +LANGUAGE sql +STABLE +AS $$ + SELECT r.identity_id, + ev.stored_json || COALESCE(dv.derived_json, '{}'::jsonb) + FROM sysml2.resolve_commit_state(p_project_id, p_commit_id) r + JOIN sysml2.element_version ev + ON ev.project_id = p_project_id AND ev.version_id = r.version_id + LEFT JOIN sysml2.derived_version dv + ON dv.project_id = p_project_id AND dv.derived_id = r.derived_id; +$$; + +CREATE OR REPLACE FUNCTION sysml2.get_element_at_commit(p_project_id uuid, p_commit_id uuid, p_identity_id uuid) +RETURNS jsonb +LANGUAGE sql +STABLE +AS $$ + SELECT ev.stored_json || COALESCE(dv.derived_json, '{}'::jsonb) + FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, p_identity_id) r + JOIN sysml2.element_version ev + ON ev.project_id = p_project_id AND ev.version_id = r.version_id + LEFT JOIN sysml2.derived_version dv + ON dv.project_id = p_project_id AND dv.derived_id = r.derived_id; +$$; + +------------------------------------------------------------------------------------------------ +-- 11. PER-METACLASS FLATTENING VIEWS [GENERATED] +-- +-- One view per concrete metaclass (167), reconstructing the full DTO row shape by LEFT JOINing +-- exactly the subtype tables in that metaclass's supertype closure. These serve the Query service +-- and any consumer that wants columns rather than jsonb. +-- +-- Representative excerpt — the template emits all 167: +-- +-- CREATE VIEW sysml2.vw_part_usage AS +-- SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, +-- ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, +-- ev.owning_relationship, +-- t.is_abstract, t.is_sufficient, +-- f.direction, f.is_composite, f.is_constant, f.is_derived, f.is_end, +-- f.is_ordered, f.is_portion, f.is_unique, f.is_variable, +-- u.is_variation, +-- ou.is_individual, ou.portion_kind +-- FROM sysml2.element_version ev +-- JOIN sysml2.type_version t USING (project_id, version_id) +-- JOIN sysml2.feature_version f USING (project_id, version_id) +-- JOIN sysml2.usage_version u USING (project_id, version_id) +-- JOIN sysml2.occurrence_usage_version ou USING (project_id, version_id) +-- WHERE ev.class_kind = 120 AND NOT ev.tombstone; -- 120 = PartUsage's FROZEN registry id (§2) +-- +-- CREATE VIEW sysml2.vw_flow_usage AS ... -- SIX subtype tables: Connector is both a Feature +-- -- and a Relationship, so relationship_version joins too. + + +CREATE VIEW sysml2.vw_accept_action_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 1 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_action_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation, + occurrence_definition_version.is_individual + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + JOIN sysml2.occurrence_definition_version USING (project_id, version_id) + WHERE ev.class_kind = 2 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_action_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 3 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_actor_membership AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + membership_version.member_element, membership_version.member_name, membership_version.member_short_name, membership_version.visibility + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.membership_version USING (project_id, version_id) + WHERE ev.class_kind = 4 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_allocation_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation, + occurrence_definition_version.is_individual + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + JOIN sysml2.occurrence_definition_version USING (project_id, version_id) + WHERE ev.class_kind = 5 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_allocation_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 6 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_analysis_case_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation, + occurrence_definition_version.is_individual + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + JOIN sysml2.occurrence_definition_version USING (project_id, version_id) + WHERE ev.class_kind = 7 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_analysis_case_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 8 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_annotating_element AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship + FROM sysml2.element_version ev + WHERE ev.class_kind = 9 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_annotation AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + annotation_version.annotated_element + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.annotation_version USING (project_id, version_id) + WHERE ev.class_kind = 10 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_assert_constraint_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind, + invariant_version.is_negated + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + JOIN sysml2.invariant_version USING (project_id, version_id) + WHERE ev.class_kind = 11 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_assignment_action_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 12 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_association AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + type_version.is_abstract, type_version.is_sufficient + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.type_version USING (project_id, version_id) + WHERE ev.class_kind = 13 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_association_structure AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + type_version.is_abstract, type_version.is_sufficient + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.type_version USING (project_id, version_id) + WHERE ev.class_kind = 14 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_attribute_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + WHERE ev.class_kind = 15 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_attribute_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + WHERE ev.class_kind = 16 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_behavior AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + WHERE ev.class_kind = 17 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_binding_connector AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + WHERE ev.class_kind = 18 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_binding_connector_as_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + WHERE ev.class_kind = 19 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_boolean_expression AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + WHERE ev.class_kind = 20 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_calculation_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation, + occurrence_definition_version.is_individual + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + JOIN sysml2.occurrence_definition_version USING (project_id, version_id) + WHERE ev.class_kind = 21 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_calculation_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 22 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_case_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation, + occurrence_definition_version.is_individual + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + JOIN sysml2.occurrence_definition_version USING (project_id, version_id) + WHERE ev.class_kind = 23 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_case_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 24 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_class AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + WHERE ev.class_kind = 25 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_classifier AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + WHERE ev.class_kind = 26 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_collect_expression AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + operator_expression_version.operator + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.operator_expression_version USING (project_id, version_id) + WHERE ev.class_kind = 27 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_comment AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + comment_version.body, comment_version.locale + FROM sysml2.element_version ev + JOIN sysml2.comment_version USING (project_id, version_id) + WHERE ev.class_kind = 28 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_concern_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation, + occurrence_definition_version.is_individual, + requirement_definition_version.req_id + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + JOIN sysml2.occurrence_definition_version USING (project_id, version_id) + JOIN sysml2.requirement_definition_version USING (project_id, version_id) + WHERE ev.class_kind = 29 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_concern_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind, + requirement_usage_version.req_id + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + JOIN sysml2.requirement_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 30 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_conjugated_port_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation, + occurrence_definition_version.is_individual + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + JOIN sysml2.occurrence_definition_version USING (project_id, version_id) + WHERE ev.class_kind = 31 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_conjugated_port_typing AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + specialization_version.general, specialization_version.specific, + feature_typing_version.type, feature_typing_version.typed_feature, + conjugated_port_typing_version.conjugated_port_definition + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.specialization_version USING (project_id, version_id) + JOIN sysml2.feature_typing_version USING (project_id, version_id) + JOIN sysml2.conjugated_port_typing_version USING (project_id, version_id) + WHERE ev.class_kind = 32 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_conjugation AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + conjugation_version.conjugated_type, conjugation_version.original_type + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.conjugation_version USING (project_id, version_id) + WHERE ev.class_kind = 33 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_connection_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation, + occurrence_definition_version.is_individual + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + JOIN sysml2.occurrence_definition_version USING (project_id, version_id) + WHERE ev.class_kind = 34 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_connection_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 35 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_connector AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + WHERE ev.class_kind = 36 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_constraint_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation, + occurrence_definition_version.is_individual + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + JOIN sysml2.occurrence_definition_version USING (project_id, version_id) + WHERE ev.class_kind = 38 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_constraint_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 39 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_constructor_expression AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + WHERE ev.class_kind = 40 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_cross_subsetting AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + specialization_version.general, specialization_version.specific, + subsetting_version.subsetted_feature, subsetting_version.subsetting_feature, + cross_subsetting_version.crossed_feature + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.specialization_version USING (project_id, version_id) + JOIN sysml2.subsetting_version USING (project_id, version_id) + JOIN sysml2.cross_subsetting_version USING (project_id, version_id) + WHERE ev.class_kind = 42 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_data_type AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + WHERE ev.class_kind = 43 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_decision_node AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 44 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + WHERE ev.class_kind = 45 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_dependency AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + WHERE ev.class_kind = 46 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_differencing AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + differencing_version.differencing_type + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.differencing_version USING (project_id, version_id) + WHERE ev.class_kind = 47 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_disjoining AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + disjoining_version.disjoining_type, disjoining_version.type_disjoined + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.disjoining_version USING (project_id, version_id) + WHERE ev.class_kind = 48 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_documentation AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + comment_version.body, comment_version.locale + FROM sysml2.element_version ev + JOIN sysml2.comment_version USING (project_id, version_id) + WHERE ev.class_kind = 49 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_element_filter_membership AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + membership_version.member_element, membership_version.member_name, membership_version.member_short_name, membership_version.visibility + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.membership_version USING (project_id, version_id) + WHERE ev.class_kind = 51 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_end_feature_membership AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + membership_version.member_element, membership_version.member_name, membership_version.member_short_name, membership_version.visibility + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.membership_version USING (project_id, version_id) + WHERE ev.class_kind = 52 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_enumeration_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + WHERE ev.class_kind = 53 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_enumeration_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + WHERE ev.class_kind = 54 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_event_occurrence_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 55 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_exhibit_state_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind, + state_usage_version.is_parallel + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + JOIN sysml2.state_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 56 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_expression AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + WHERE ev.class_kind = 58 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_feature AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + WHERE ev.class_kind = 59 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_feature_chain_expression AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + operator_expression_version.operator + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.operator_expression_version USING (project_id, version_id) + WHERE ev.class_kind = 60 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_feature_chaining AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + feature_chaining_version.chaining_feature + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.feature_chaining_version USING (project_id, version_id) + WHERE ev.class_kind = 61 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_feature_inverting AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + feature_inverting_version.feature_inverted, feature_inverting_version.inverting_feature + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.feature_inverting_version USING (project_id, version_id) + WHERE ev.class_kind = 62 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_feature_membership AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + membership_version.member_element, membership_version.member_name, membership_version.member_short_name, membership_version.visibility + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.membership_version USING (project_id, version_id) + WHERE ev.class_kind = 63 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_feature_reference_expression AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + WHERE ev.class_kind = 64 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_feature_typing AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + specialization_version.general, specialization_version.specific, + feature_typing_version.type, feature_typing_version.typed_feature + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.specialization_version USING (project_id, version_id) + JOIN sysml2.feature_typing_version USING (project_id, version_id) + WHERE ev.class_kind = 65 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_feature_value AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + membership_version.member_element, membership_version.member_name, membership_version.member_short_name, membership_version.visibility, + feature_value_version.is_default, feature_value_version.is_initial + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.membership_version USING (project_id, version_id) + JOIN sysml2.feature_value_version USING (project_id, version_id) + WHERE ev.class_kind = 66 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_flow AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + WHERE ev.class_kind = 67 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_flow_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation, + occurrence_definition_version.is_individual + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + JOIN sysml2.occurrence_definition_version USING (project_id, version_id) + WHERE ev.class_kind = 68 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_flow_end AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + WHERE ev.class_kind = 69 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_flow_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 70 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_for_loop_action_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 71 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_fork_node AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 72 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_framed_concern_membership AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + membership_version.member_element, membership_version.member_name, membership_version.member_short_name, membership_version.visibility, + requirement_constraint_membership_version.kind + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.membership_version USING (project_id, version_id) + JOIN sysml2.requirement_constraint_membership_version USING (project_id, version_id) + WHERE ev.class_kind = 73 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_function AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + WHERE ev.class_kind = 74 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_if_action_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 75 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_include_use_case_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 77 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_index_expression AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + operator_expression_version.operator + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.operator_expression_version USING (project_id, version_id) + WHERE ev.class_kind = 78 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_interaction AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + type_version.is_abstract, type_version.is_sufficient + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.type_version USING (project_id, version_id) + WHERE ev.class_kind = 80 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_interface_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation, + occurrence_definition_version.is_individual + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + JOIN sysml2.occurrence_definition_version USING (project_id, version_id) + WHERE ev.class_kind = 81 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_interface_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 82 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_intersecting AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + intersecting_version.intersecting_type + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.intersecting_version USING (project_id, version_id) + WHERE ev.class_kind = 83 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_invariant AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + invariant_version.is_negated + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.invariant_version USING (project_id, version_id) + WHERE ev.class_kind = 84 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_invocation_expression AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + WHERE ev.class_kind = 85 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_item_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation, + occurrence_definition_version.is_individual + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + JOIN sysml2.occurrence_definition_version USING (project_id, version_id) + WHERE ev.class_kind = 86 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_item_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 87 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_join_node AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 88 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_library_package AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + library_package_version.is_standard + FROM sysml2.element_version ev + JOIN sysml2.library_package_version USING (project_id, version_id) + WHERE ev.class_kind = 89 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_literal_boolean AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + literal_boolean_version.value + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.literal_boolean_version USING (project_id, version_id) + WHERE ev.class_kind = 90 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_literal_expression AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + WHERE ev.class_kind = 91 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_literal_infinity AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + WHERE ev.class_kind = 92 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_literal_integer AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + literal_integer_version.value + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.literal_integer_version USING (project_id, version_id) + WHERE ev.class_kind = 93 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_literal_rational AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + literal_rational_version.value + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.literal_rational_version USING (project_id, version_id) + WHERE ev.class_kind = 94 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_literal_string AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + literal_string_version.value + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.literal_string_version USING (project_id, version_id) + WHERE ev.class_kind = 95 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_membership AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + membership_version.member_element, membership_version.member_name, membership_version.member_short_name, membership_version.visibility + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.membership_version USING (project_id, version_id) + WHERE ev.class_kind = 97 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_membership_expose AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + import_version.is_import_all, import_version.is_recursive, import_version.visibility, + membership_import_version.imported_membership + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.import_version USING (project_id, version_id) + JOIN sysml2.membership_import_version USING (project_id, version_id) + WHERE ev.class_kind = 98 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_membership_import AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + import_version.is_import_all, import_version.is_recursive, import_version.visibility, + membership_import_version.imported_membership + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.import_version USING (project_id, version_id) + JOIN sysml2.membership_import_version USING (project_id, version_id) + WHERE ev.class_kind = 99 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_merge_node AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 100 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_metaclass AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + WHERE ev.class_kind = 101 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_metadata_access_expression AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + WHERE ev.class_kind = 102 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_metadata_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation, + occurrence_definition_version.is_individual + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + JOIN sysml2.occurrence_definition_version USING (project_id, version_id) + WHERE ev.class_kind = 103 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_metadata_feature AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + WHERE ev.class_kind = 104 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_metadata_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 105 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_multiplicity AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + WHERE ev.class_kind = 106 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_multiplicity_range AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + WHERE ev.class_kind = 107 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_namespace AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship + FROM sysml2.element_version ev + WHERE ev.class_kind = 108 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_namespace_expose AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + import_version.is_import_all, import_version.is_recursive, import_version.visibility, + namespace_import_version.imported_namespace + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.import_version USING (project_id, version_id) + JOIN sysml2.namespace_import_version USING (project_id, version_id) + WHERE ev.class_kind = 109 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_namespace_import AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + import_version.is_import_all, import_version.is_recursive, import_version.visibility, + namespace_import_version.imported_namespace + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.import_version USING (project_id, version_id) + JOIN sysml2.namespace_import_version USING (project_id, version_id) + WHERE ev.class_kind = 110 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_null_expression AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + WHERE ev.class_kind = 111 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_objective_membership AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + membership_version.member_element, membership_version.member_name, membership_version.member_short_name, membership_version.visibility + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.membership_version USING (project_id, version_id) + WHERE ev.class_kind = 112 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_occurrence_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation, + occurrence_definition_version.is_individual + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + JOIN sysml2.occurrence_definition_version USING (project_id, version_id) + WHERE ev.class_kind = 113 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_occurrence_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 114 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_operator_expression AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + operator_expression_version.operator + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.operator_expression_version USING (project_id, version_id) + WHERE ev.class_kind = 115 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_owning_membership AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + membership_version.member_element, membership_version.member_name, membership_version.member_short_name, membership_version.visibility + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.membership_version USING (project_id, version_id) + WHERE ev.class_kind = 116 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_package AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship + FROM sysml2.element_version ev + WHERE ev.class_kind = 117 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_parameter_membership AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + membership_version.member_element, membership_version.member_name, membership_version.member_short_name, membership_version.visibility + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.membership_version USING (project_id, version_id) + WHERE ev.class_kind = 118 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_part_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation, + occurrence_definition_version.is_individual + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + JOIN sysml2.occurrence_definition_version USING (project_id, version_id) + WHERE ev.class_kind = 119 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_part_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 120 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_payload_feature AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + WHERE ev.class_kind = 121 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_perform_action_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 122 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_port_conjugation AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + conjugation_version.conjugated_type, conjugation_version.original_type, + port_conjugation_version.original_port_definition + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.conjugation_version USING (project_id, version_id) + JOIN sysml2.port_conjugation_version USING (project_id, version_id) + WHERE ev.class_kind = 123 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_port_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation, + occurrence_definition_version.is_individual + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + JOIN sysml2.occurrence_definition_version USING (project_id, version_id) + WHERE ev.class_kind = 124 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_port_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 125 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_predicate AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + WHERE ev.class_kind = 126 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_redefinition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + specialization_version.general, specialization_version.specific, + subsetting_version.subsetted_feature, subsetting_version.subsetting_feature, + redefinition_version.redefined_feature, redefinition_version.redefining_feature + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.specialization_version USING (project_id, version_id) + JOIN sysml2.subsetting_version USING (project_id, version_id) + JOIN sysml2.redefinition_version USING (project_id, version_id) + WHERE ev.class_kind = 127 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_reference_subsetting AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + specialization_version.general, specialization_version.specific, + subsetting_version.subsetted_feature, subsetting_version.subsetting_feature, + reference_subsetting_version.referenced_feature + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.specialization_version USING (project_id, version_id) + JOIN sysml2.subsetting_version USING (project_id, version_id) + JOIN sysml2.reference_subsetting_version USING (project_id, version_id) + WHERE ev.class_kind = 128 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_reference_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + WHERE ev.class_kind = 129 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_rendering_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation, + occurrence_definition_version.is_individual + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + JOIN sysml2.occurrence_definition_version USING (project_id, version_id) + WHERE ev.class_kind = 131 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_rendering_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 132 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_requirement_constraint_membership AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + membership_version.member_element, membership_version.member_name, membership_version.member_short_name, membership_version.visibility, + requirement_constraint_membership_version.kind + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.membership_version USING (project_id, version_id) + JOIN sysml2.requirement_constraint_membership_version USING (project_id, version_id) + WHERE ev.class_kind = 133 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_requirement_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation, + occurrence_definition_version.is_individual, + requirement_definition_version.req_id + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + JOIN sysml2.occurrence_definition_version USING (project_id, version_id) + JOIN sysml2.requirement_definition_version USING (project_id, version_id) + WHERE ev.class_kind = 134 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_requirement_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind, + requirement_usage_version.req_id + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + JOIN sysml2.requirement_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 135 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_requirement_verification_membership AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + membership_version.member_element, membership_version.member_name, membership_version.member_short_name, membership_version.visibility, + requirement_constraint_membership_version.kind + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.membership_version USING (project_id, version_id) + JOIN sysml2.requirement_constraint_membership_version USING (project_id, version_id) + WHERE ev.class_kind = 136 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_result_expression_membership AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + membership_version.member_element, membership_version.member_name, membership_version.member_short_name, membership_version.visibility + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.membership_version USING (project_id, version_id) + WHERE ev.class_kind = 137 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_return_parameter_membership AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + membership_version.member_element, membership_version.member_name, membership_version.member_short_name, membership_version.visibility + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.membership_version USING (project_id, version_id) + WHERE ev.class_kind = 138 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_satisfy_requirement_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind, + invariant_version.is_negated, + requirement_usage_version.req_id + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + JOIN sysml2.invariant_version USING (project_id, version_id) + JOIN sysml2.requirement_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 139 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_select_expression AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + operator_expression_version.operator + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.operator_expression_version USING (project_id, version_id) + WHERE ev.class_kind = 140 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_send_action_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 141 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_specialization AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + specialization_version.general, specialization_version.specific + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.specialization_version USING (project_id, version_id) + WHERE ev.class_kind = 142 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_stakeholder_membership AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + membership_version.member_element, membership_version.member_name, membership_version.member_short_name, membership_version.visibility + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.membership_version USING (project_id, version_id) + WHERE ev.class_kind = 143 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_state_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation, + occurrence_definition_version.is_individual, + state_definition_version.is_parallel + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + JOIN sysml2.occurrence_definition_version USING (project_id, version_id) + JOIN sysml2.state_definition_version USING (project_id, version_id) + WHERE ev.class_kind = 144 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_state_subaction_membership AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + membership_version.member_element, membership_version.member_name, membership_version.member_short_name, membership_version.visibility, + state_subaction_membership_version.kind + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.membership_version USING (project_id, version_id) + JOIN sysml2.state_subaction_membership_version USING (project_id, version_id) + WHERE ev.class_kind = 145 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_state_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind, + state_usage_version.is_parallel + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + JOIN sysml2.state_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 146 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_step AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + WHERE ev.class_kind = 147 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_structure AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + WHERE ev.class_kind = 148 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_subclassification AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + specialization_version.general, specialization_version.specific, + subclassification_version.subclassifier, subclassification_version.superclassifier + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.specialization_version USING (project_id, version_id) + JOIN sysml2.subclassification_version USING (project_id, version_id) + WHERE ev.class_kind = 149 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_subject_membership AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + membership_version.member_element, membership_version.member_name, membership_version.member_short_name, membership_version.visibility + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.membership_version USING (project_id, version_id) + WHERE ev.class_kind = 150 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_subsetting AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + specialization_version.general, specialization_version.specific, + subsetting_version.subsetted_feature, subsetting_version.subsetting_feature + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.specialization_version USING (project_id, version_id) + JOIN sysml2.subsetting_version USING (project_id, version_id) + WHERE ev.class_kind = 151 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_succession AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + WHERE ev.class_kind = 152 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_succession_as_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + WHERE ev.class_kind = 153 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_succession_flow AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + WHERE ev.class_kind = 154 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_succession_flow_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 155 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_terminate_action_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 156 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_textual_representation AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + textual_representation_version.body, textual_representation_version.language + FROM sysml2.element_version ev + JOIN sysml2.textual_representation_version USING (project_id, version_id) + WHERE ev.class_kind = 157 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_transition_feature_membership AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + membership_version.member_element, membership_version.member_name, membership_version.member_short_name, membership_version.visibility, + transition_feature_membership_version.kind + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.membership_version USING (project_id, version_id) + JOIN sysml2.transition_feature_membership_version USING (project_id, version_id) + WHERE ev.class_kind = 158 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_transition_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 159 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_trigger_invocation_expression AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + trigger_invocation_expression_version.kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.trigger_invocation_expression_version USING (project_id, version_id) + WHERE ev.class_kind = 160 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_type AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + WHERE ev.class_kind = 161 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_type_featuring AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + type_featuring_version.feature_of_type, type_featuring_version.featuring_type + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.type_featuring_version USING (project_id, version_id) + WHERE ev.class_kind = 162 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_unioning AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + unioning_version.unioning_type + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.unioning_version USING (project_id, version_id) + WHERE ev.class_kind = 163 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + WHERE ev.class_kind = 164 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_use_case_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation, + occurrence_definition_version.is_individual + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + JOIN sysml2.occurrence_definition_version USING (project_id, version_id) + WHERE ev.class_kind = 165 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_use_case_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 166 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_variant_membership AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + membership_version.member_element, membership_version.member_name, membership_version.member_short_name, membership_version.visibility + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.membership_version USING (project_id, version_id) + WHERE ev.class_kind = 167 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_verification_case_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation, + occurrence_definition_version.is_individual + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + JOIN sysml2.occurrence_definition_version USING (project_id, version_id) + WHERE ev.class_kind = 168 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_verification_case_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 169 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_view_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation, + occurrence_definition_version.is_individual + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + JOIN sysml2.occurrence_definition_version USING (project_id, version_id) + WHERE ev.class_kind = 170 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_view_rendering_membership AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + relationship_version.is_implied, relationship_version.owning_related_element, + membership_version.member_element, membership_version.member_name, membership_version.member_short_name, membership_version.visibility + FROM sysml2.element_version ev + JOIN sysml2.relationship_version USING (project_id, version_id) + JOIN sysml2.membership_version USING (project_id, version_id) + WHERE ev.class_kind = 171 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_view_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 172 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_viewpoint_definition AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + definition_version.is_variation, + occurrence_definition_version.is_individual, + requirement_definition_version.req_id + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.definition_version USING (project_id, version_id) + JOIN sysml2.occurrence_definition_version USING (project_id, version_id) + JOIN sysml2.requirement_definition_version USING (project_id, version_id) + WHERE ev.class_kind = 173 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_viewpoint_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind, + requirement_usage_version.req_id + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + JOIN sysml2.requirement_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 174 AND NOT ev.tombstone; + +CREATE VIEW sysml2.vw_while_loop_action_usage AS + SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, + ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, ev.owning_relationship, + type_version.is_abstract, type_version.is_sufficient, + feature_version.direction, feature_version.is_composite, feature_version.is_constant, feature_version.is_derived, feature_version.is_end, feature_version.is_ordered, feature_version.is_portion, feature_version.is_unique, feature_version.is_variable, + usage_version.is_variation, + occurrence_usage_version.is_individual, occurrence_usage_version.portion_kind + FROM sysml2.element_version ev + JOIN sysml2.type_version USING (project_id, version_id) + JOIN sysml2.feature_version USING (project_id, version_id) + JOIN sysml2.usage_version USING (project_id, version_id) + JOIN sysml2.occurrence_usage_version USING (project_id, version_id) + WHERE ev.class_kind = 175 AND NOT ev.tombstone; + +------------------------------------------------------------------------------------------------ +-- 12. PARTITIONS +-- +-- Every element-scoped table is PARTITION BY HASH (project_id) with the same modulus, so they are +-- co-located: a join between element_version and a subtype table stays partition-local, and every +-- project-scoped API call prunes to a single partition. +-- +-- 16 is a starting point. The generator emits this loop; tune the modulus to the deployment. +-- +-- !! OPERATIONAL REQUIREMENT — max_locks_per_transaction !! +-- +-- 58 partitioned tables x 16 partitions = 928 leaf partitions, and Postgres CLONES every foreign +-- key onto every leaf: 2,629 FK constraints in total. Any single transaction that touches the +-- whole schema (CREATE SCHEMA, DROP SCHEMA CASCADE, a migration, pg_dump --schema-only) needs a +-- lock per object and will fail on the default max_locks_per_transaction = 64 with: +-- +-- ERROR: out of shared memory +-- HINT: You might need to increase "max_locks_per_transaction". +-- +-- Deploy with at least: +-- max_locks_per_transaction = 4096 +-- +-- This does NOT affect the hot path: a project-scoped query prunes to one partition at plan time +-- and locks a handful of objects. It is a DDL/administration constraint only. Verified against +-- PostgreSQL 17 — the schema fails to install without it. +-- +-- If raising the setting is not an option, halve the modulus (16 -> 8) and/or drop partitioning +-- from the 47 subtype tables, keeping it only on element_version, derived_version, branch_head, +-- commit_checkpoint and the 7 link tables. That trades partition-local subtype joins for a far +-- smaller catalog. +------------------------------------------------------------------------------------------------ + +DO $$ +DECLARE + partitioned_table text; + partition_index int; + partition_count constant int := 16; +BEGIN + FOREACH partitioned_table IN ARRAY ARRAY[ + 'element_version', 'derived_version', 'branch_head', 'commit_checkpoint', + 'dependency_client', + 'dependency_supplier', + 'element_alias_ids', + 'element_owned_relationship', + 'relationship_owned_related_element', + 'relationship_source', + 'relationship_target', + 'relationship_version', + 'annotation_version', + 'comment_version', + 'conjugation_version', + 'differencing_version', + 'disjoining_version', + 'feature_chaining_version', + 'feature_inverting_version', + 'import_version', + 'intersecting_version', + 'membership_version', + 'specialization_version', + 'textual_representation_version', + 'type_version', + 'type_featuring_version', + 'unioning_version', + 'feature_version', + 'feature_typing_version', + 'library_package_version', + 'membership_import_version', + 'namespace_import_version', + 'port_conjugation_version', + 'subclassification_version', + 'subsetting_version', + 'conjugated_port_typing_version', + 'cross_subsetting_version', + 'definition_version', + 'feature_value_version', + 'redefinition_version', + 'reference_subsetting_version', + 'usage_version', + 'occurrence_usage_version', + 'requirement_constraint_membership_version', + 'state_subaction_membership_version', + 'transition_feature_membership_version', + 'occurrence_definition_version', + 'invariant_version', + 'literal_boolean_version', + 'literal_integer_version', + 'literal_rational_version', + 'literal_string_version', + 'operator_expression_version', + 'state_usage_version', + 'trigger_invocation_expression_version', + 'state_definition_version', + 'requirement_usage_version', + 'requirement_definition_version' + ] + LOOP + FOR partition_index IN 0 .. partition_count - 1 LOOP + EXECUTE format( + 'CREATE TABLE sysml2.%I PARTITION OF sysml2.%I FOR VALUES WITH (MODULUS %s, REMAINDER %s)', + partitioned_table || '_p' || partition_index, + partitioned_table, + partition_count, + partition_index); + + -- Storage parameters cannot be set on a partitioned parent — only on leaf partitions. + -- The write profiles differ and so must the tuning: + -- + -- * branch_head is UPSERT-heavy (overlay rows are updated on every commit to the + -- branch and deleted on compaction/branch-delete): fillfactor 90 leaves HOT + -- headroom so the per-commit updates don't bloat the PK, and dead-tuple-driven + -- vacuum applies. + -- + -- * everything else is APPEND-ONLY: dead-tuple thresholds never fire usefully, so + -- drive vacuum off INSERT counts (keeps the visibility map current for index-only + -- scans) and analyze off a threshold sized for bulk commit traffic — absolute-5000 + -- analyze on a 60M-row leaf would sample continuously during imports. + IF partitioned_table = 'branch_head' THEN + + EXECUTE format( + 'ALTER TABLE sysml2.%I SET (' + || 'fillfactor = 90, ' + || 'autovacuum_analyze_scale_factor = 0.0, ' + || 'autovacuum_analyze_threshold = 50000)', + partitioned_table || '_p' || partition_index); + + ELSE + + EXECUTE format( + 'ALTER TABLE sysml2.%I SET (' + || 'autovacuum_vacuum_insert_scale_factor = 0.0, ' + || 'autovacuum_vacuum_insert_threshold = 100000, ' + || 'autovacuum_analyze_scale_factor = 0.0, ' + || 'autovacuum_analyze_threshold = 50000)', + partitioned_table || '_p' || partition_index); + + END IF; + END LOOP; + END LOOP; +END; +$$; + +-- PG18 OPPORTUNISTIC DEFAULTS (version-guarded; a no-op on the PostgreSQL 16/17 floor). +-- +-- On PostgreSQL 18+ the schema self-activates native uuidv7() defaults on every +-- SERVER-MINTED key. Time-ordered ids turn each project's insert pattern into rightmost +-- btree appends instead of random-page scatter (audit finding R8). The application-side +-- Guid.CreateVersion7() remains the PRIMARY id source — the service usually needs the ids +-- before insert (to wire derived_id into branch_head etc.); these defaults are the safety +-- net that keeps ad-hoc/tooling inserts time-ordered too. +-- +-- Deliberately NOT defaulted: data_identity.id (the spec-visible @id is supplied by the +-- API layer or the client — a silent server default would mask missing-id bugs). +DO $$ +BEGIN + + IF current_setting('server_version_num')::int >= 180000 THEN + + ALTER TABLE sysml2.element_version ALTER COLUMN version_id SET DEFAULT uuidv7(); + ALTER TABLE sysml2.derived_version ALTER COLUMN derived_id SET DEFAULT uuidv7(); + ALTER TABLE sysml2.project ALTER COLUMN id SET DEFAULT uuidv7(); + ALTER TABLE sysml2.commit ALTER COLUMN id SET DEFAULT uuidv7(); + ALTER TABLE sysml2.branch ALTER COLUMN id SET DEFAULT uuidv7(); + ALTER TABLE sysml2.tag ALTER COLUMN id SET DEFAULT uuidv7(); + + END IF; +END; +$$; + +------------------------------------------------------------------------------------------------ +-- 13. MODEL VERSION [GENERATED] +------------------------------------------------------------------------------------------------ + +CREATE OR REPLACE FUNCTION sysml2.query_model_version() +RETURNS text +LANGUAGE sql +IMMUTABLE +AS $$ + SELECT 'SysML:_mczcUFn3EfG_XZTXp4TXuA'::text; +$$; + +------------------------------------------------------------------------------------------------ +-- 14. REFERENCE VALIDATION — two tiers [GENERATED] +-- +-- The ON-DEMAND half of referential integrity (guide §7): the FKs prove that every stored +-- reference targets an EXISTING identity; these functions check what FKs cannot — +-- * 'dangling' — a same-project target that is not alive in the snapshot of the given +-- commit (absent or tombstoned); +-- * 'wrong-type' — a target whose metaclass is illegal for the referencing property, +-- checked via the TYPED IDENTITY (data_identity.class_kind, §4) — this +-- check applies to cross-project targets too. +-- Liveness of cross-project targets is deliberately NOT checked here: it depends on the +-- used-project commit (project_usage), which is service-layer resolution. +-- +-- TIER 1 — validate_references_at_commit: the FULL periodic audit over one commit's whole +-- snapshot. It materializes and ANALYZEs the snapshot in a temp table first, so the planner +-- knows the true cardinality and bounds the pass at O(snapshot x log history) — never +-- O(history) — however deep the append-only tables grow. Run asynchronously (checkpoint +-- cadence is a natural rhythm). +-- +-- TIER 2 — validate_references_in_commit: the INCREMENTAL per-commit check, O(change set): +-- the outgoing references of the versions the commit wrote, PLUS the reverse direction its +-- tombstones break — a live, unchanged element left referencing a deleted identity (driven +-- by the reverse-lookup indexes; per-target liveness probed via resolve_element_at_commit). +-- Cheap enough for the synchronous commit-validation path; tier 1 backstops it. +-- +-- Deliberately FUNCTIONS, not constraints: the spec allows transiently dangling references, +-- and liveness is a function of (identity, commit) — unenforceable by FK. One UNION ALL arm +-- per stored reference column, emitted from the UML model with the allowed target +-- metaclasses resolved through ClassKindRegistry. +------------------------------------------------------------------------------------------------ + +CREATE OR REPLACE FUNCTION sysml2.validate_references_at_commit( + p_project_id uuid, + p_commit_id uuid +) +RETURNS TABLE ( + source_table text, + source_column text, + source_identity uuid, + target_identity uuid, + problem text +) +LANGUAGE plpgsql +AS $$ +BEGIN + -- Materialize + ANALYZE the snapshot so the planner knows its TRUE cardinality and + -- can choose per arm between hashing the source (young history) and snapshot-driven + -- PK probes (deep history) — bounding the pass at O(snapshot x log history) instead + -- of O(history). A bare function CTE would be estimated at ~1000 rows. + CREATE TEMP TABLE IF NOT EXISTS validation_snapshot ( + identity_id uuid NOT NULL, + version_id uuid NOT NULL + ) ON COMMIT DROP; + + TRUNCATE validation_snapshot; + + INSERT INTO validation_snapshot (identity_id, version_id) + SELECT r.identity_id, r.version_id + FROM sysml2.resolve_commit_state(p_project_id, p_commit_id) r; + + CREATE INDEX IF NOT EXISTS ix_validation_snapshot_version ON validation_snapshot (version_id); + CREATE INDEX IF NOT EXISTS ix_validation_snapshot_identity ON validation_snapshot (identity_id); + + ANALYZE validation_snapshot; + + RETURN QUERY + SELECT 'annotation_version'::text, 'annotated_element'::text, + snap.identity_id, src.annotated_element, 'dangling'::text + FROM sysml2.annotation_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.annotated_element + LEFT JOIN validation_snapshot live ON live.identity_id = src.annotated_element + WHERE src.project_id = p_project_id + AND src.annotated_element IS NOT NULL + AND ti.project_id = p_project_id + AND live.identity_id IS NULL + UNION ALL + SELECT 'conjugated_port_typing_version'::text, 'conjugated_port_definition'::text, + snap.identity_id, src.conjugated_port_definition, + CASE WHEN ti.class_kind NOT IN (31) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.conjugated_port_typing_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.conjugated_port_definition + LEFT JOIN validation_snapshot live ON live.identity_id = src.conjugated_port_definition + WHERE src.project_id = p_project_id + AND src.conjugated_port_definition IS NOT NULL + AND (ti.class_kind NOT IN (31) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'conjugation_version'::text, 'conjugated_type'::text, + snap.identity_id, src.conjugated_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.conjugation_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.conjugated_type + LEFT JOIN validation_snapshot live ON live.identity_id = src.conjugated_type + WHERE src.project_id = p_project_id + AND src.conjugated_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'conjugation_version'::text, 'original_type'::text, + snap.identity_id, src.original_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.conjugation_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.original_type + LEFT JOIN validation_snapshot live ON live.identity_id = src.original_type + WHERE src.project_id = p_project_id + AND src.original_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'cross_subsetting_version'::text, 'crossed_feature'::text, + snap.identity_id, src.crossed_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.cross_subsetting_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.crossed_feature + LEFT JOIN validation_snapshot live ON live.identity_id = src.crossed_feature + WHERE src.project_id = p_project_id + AND src.crossed_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'dependency_client'::text, 'target_identity'::text, + snap.identity_id, src.target_identity, 'dangling'::text + FROM sysml2.dependency_client src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.target_identity + LEFT JOIN validation_snapshot live ON live.identity_id = src.target_identity + WHERE src.project_id = p_project_id + AND src.target_identity IS NOT NULL + AND ti.project_id = p_project_id + AND live.identity_id IS NULL + UNION ALL + SELECT 'dependency_supplier'::text, 'target_identity'::text, + snap.identity_id, src.target_identity, 'dangling'::text + FROM sysml2.dependency_supplier src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.target_identity + LEFT JOIN validation_snapshot live ON live.identity_id = src.target_identity + WHERE src.project_id = p_project_id + AND src.target_identity IS NOT NULL + AND ti.project_id = p_project_id + AND live.identity_id IS NULL + UNION ALL + SELECT 'differencing_version'::text, 'differencing_type'::text, + snap.identity_id, src.differencing_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.differencing_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.differencing_type + LEFT JOIN validation_snapshot live ON live.identity_id = src.differencing_type + WHERE src.project_id = p_project_id + AND src.differencing_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'disjoining_version'::text, 'disjoining_type'::text, + snap.identity_id, src.disjoining_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.disjoining_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.disjoining_type + LEFT JOIN validation_snapshot live ON live.identity_id = src.disjoining_type + WHERE src.project_id = p_project_id + AND src.disjoining_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'disjoining_version'::text, 'type_disjoined'::text, + snap.identity_id, src.type_disjoined, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.disjoining_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.type_disjoined + LEFT JOIN validation_snapshot live ON live.identity_id = src.type_disjoined + WHERE src.project_id = p_project_id + AND src.type_disjoined IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'element_version'::text, 'owning_relationship'::text, + snap.identity_id, src.owning_relationship, + CASE WHEN ti.class_kind NOT IN (4, 5, 6, 10, 13, 14, 18, 19, 32, 33, 34, 35, 36, 42, 46, 47, 48, 51, 52, 61, 62, 63, 65, 66, 67, 68, 70, 73, 80, 81, 82, 83, 97, 98, 99, 109, 110, 112, 116, 118, 123, 127, 128, 133, 136, 137, 138, 142, 143, 145, 149, 150, 151, 152, 153, 154, 155, 158, 162, 163, 167, 171) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.owning_relationship + LEFT JOIN validation_snapshot live ON live.identity_id = src.owning_relationship + WHERE src.project_id = p_project_id + AND src.owning_relationship IS NOT NULL + AND (ti.class_kind NOT IN (4, 5, 6, 10, 13, 14, 18, 19, 32, 33, 34, 35, 36, 42, 46, 47, 48, 51, 52, 61, 62, 63, 65, 66, 67, 68, 70, 73, 80, 81, 82, 83, 97, 98, 99, 109, 110, 112, 116, 118, 123, 127, 128, 133, 136, 137, 138, 142, 143, 145, 149, 150, 151, 152, 153, 154, 155, 158, 162, 163, 167, 171) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'element_owned_relationship'::text, 'target_identity'::text, + snap.identity_id, src.target_identity, + CASE WHEN ti.class_kind NOT IN (4, 5, 6, 10, 13, 14, 18, 19, 32, 33, 34, 35, 36, 42, 46, 47, 48, 51, 52, 61, 62, 63, 65, 66, 67, 68, 70, 73, 80, 81, 82, 83, 97, 98, 99, 109, 110, 112, 116, 118, 123, 127, 128, 133, 136, 137, 138, 142, 143, 145, 149, 150, 151, 152, 153, 154, 155, 158, 162, 163, 167, 171) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_owned_relationship src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.target_identity + LEFT JOIN validation_snapshot live ON live.identity_id = src.target_identity + WHERE src.project_id = p_project_id + AND src.target_identity IS NOT NULL + AND (ti.class_kind NOT IN (4, 5, 6, 10, 13, 14, 18, 19, 32, 33, 34, 35, 36, 42, 46, 47, 48, 51, 52, 61, 62, 63, 65, 66, 67, 68, 70, 73, 80, 81, 82, 83, 97, 98, 99, 109, 110, 112, 116, 118, 123, 127, 128, 133, 136, 137, 138, 142, 143, 145, 149, 150, 151, 152, 153, 154, 155, 158, 162, 163, 167, 171) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'feature_chaining_version'::text, 'chaining_feature'::text, + snap.identity_id, src.chaining_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.feature_chaining_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.chaining_feature + LEFT JOIN validation_snapshot live ON live.identity_id = src.chaining_feature + WHERE src.project_id = p_project_id + AND src.chaining_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'feature_inverting_version'::text, 'feature_inverted'::text, + snap.identity_id, src.feature_inverted, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.feature_inverting_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.feature_inverted + LEFT JOIN validation_snapshot live ON live.identity_id = src.feature_inverted + WHERE src.project_id = p_project_id + AND src.feature_inverted IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'feature_inverting_version'::text, 'inverting_feature'::text, + snap.identity_id, src.inverting_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.feature_inverting_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.inverting_feature + LEFT JOIN validation_snapshot live ON live.identity_id = src.inverting_feature + WHERE src.project_id = p_project_id + AND src.inverting_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'feature_typing_version'::text, 'type'::text, + snap.identity_id, src.type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.feature_typing_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.type + LEFT JOIN validation_snapshot live ON live.identity_id = src.type + WHERE src.project_id = p_project_id + AND src.type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'feature_typing_version'::text, 'typed_feature'::text, + snap.identity_id, src.typed_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.feature_typing_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.typed_feature + LEFT JOIN validation_snapshot live ON live.identity_id = src.typed_feature + WHERE src.project_id = p_project_id + AND src.typed_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'intersecting_version'::text, 'intersecting_type'::text, + snap.identity_id, src.intersecting_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.intersecting_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.intersecting_type + LEFT JOIN validation_snapshot live ON live.identity_id = src.intersecting_type + WHERE src.project_id = p_project_id + AND src.intersecting_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'membership_version'::text, 'member_element'::text, + snap.identity_id, src.member_element, 'dangling'::text + FROM sysml2.membership_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.member_element + LEFT JOIN validation_snapshot live ON live.identity_id = src.member_element + WHERE src.project_id = p_project_id + AND src.member_element IS NOT NULL + AND ti.project_id = p_project_id + AND live.identity_id IS NULL + UNION ALL + SELECT 'membership_import_version'::text, 'imported_membership'::text, + snap.identity_id, src.imported_membership, + CASE WHEN ti.class_kind NOT IN (4, 51, 52, 63, 66, 73, 97, 112, 116, 118, 133, 136, 137, 138, 143, 145, 150, 158, 167, 171) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.membership_import_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.imported_membership + LEFT JOIN validation_snapshot live ON live.identity_id = src.imported_membership + WHERE src.project_id = p_project_id + AND src.imported_membership IS NOT NULL + AND (ti.class_kind NOT IN (4, 51, 52, 63, 66, 73, 97, 112, 116, 118, 133, 136, 137, 138, 143, 145, 150, 158, 167, 171) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'namespace_import_version'::text, 'imported_namespace'::text, + snap.identity_id, src.imported_namespace, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 108, 111, 113, 114, 115, 117, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.namespace_import_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.imported_namespace + LEFT JOIN validation_snapshot live ON live.identity_id = src.imported_namespace + WHERE src.project_id = p_project_id + AND src.imported_namespace IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 108, 111, 113, 114, 115, 117, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'port_conjugation_version'::text, 'original_port_definition'::text, + snap.identity_id, src.original_port_definition, + CASE WHEN ti.class_kind NOT IN (31, 124) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.port_conjugation_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.original_port_definition + LEFT JOIN validation_snapshot live ON live.identity_id = src.original_port_definition + WHERE src.project_id = p_project_id + AND src.original_port_definition IS NOT NULL + AND (ti.class_kind NOT IN (31, 124) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'redefinition_version'::text, 'redefined_feature'::text, + snap.identity_id, src.redefined_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.redefinition_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.redefined_feature + LEFT JOIN validation_snapshot live ON live.identity_id = src.redefined_feature + WHERE src.project_id = p_project_id + AND src.redefined_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'redefinition_version'::text, 'redefining_feature'::text, + snap.identity_id, src.redefining_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.redefinition_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.redefining_feature + LEFT JOIN validation_snapshot live ON live.identity_id = src.redefining_feature + WHERE src.project_id = p_project_id + AND src.redefining_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'reference_subsetting_version'::text, 'referenced_feature'::text, + snap.identity_id, src.referenced_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.reference_subsetting_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.referenced_feature + LEFT JOIN validation_snapshot live ON live.identity_id = src.referenced_feature + WHERE src.project_id = p_project_id + AND src.referenced_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'relationship_version'::text, 'owning_related_element'::text, + snap.identity_id, src.owning_related_element, 'dangling'::text + FROM sysml2.relationship_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.owning_related_element + LEFT JOIN validation_snapshot live ON live.identity_id = src.owning_related_element + WHERE src.project_id = p_project_id + AND src.owning_related_element IS NOT NULL + AND ti.project_id = p_project_id + AND live.identity_id IS NULL + UNION ALL + SELECT 'relationship_owned_related_element'::text, 'target_identity'::text, + snap.identity_id, src.target_identity, 'dangling'::text + FROM sysml2.relationship_owned_related_element src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.target_identity + LEFT JOIN validation_snapshot live ON live.identity_id = src.target_identity + WHERE src.project_id = p_project_id + AND src.target_identity IS NOT NULL + AND ti.project_id = p_project_id + AND live.identity_id IS NULL + UNION ALL + SELECT 'relationship_source'::text, 'target_identity'::text, + snap.identity_id, src.target_identity, 'dangling'::text + FROM sysml2.relationship_source src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.target_identity + LEFT JOIN validation_snapshot live ON live.identity_id = src.target_identity + WHERE src.project_id = p_project_id + AND src.target_identity IS NOT NULL + AND ti.project_id = p_project_id + AND live.identity_id IS NULL + UNION ALL + SELECT 'relationship_target'::text, 'target_identity'::text, + snap.identity_id, src.target_identity, 'dangling'::text + FROM sysml2.relationship_target src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.target_identity + LEFT JOIN validation_snapshot live ON live.identity_id = src.target_identity + WHERE src.project_id = p_project_id + AND src.target_identity IS NOT NULL + AND ti.project_id = p_project_id + AND live.identity_id IS NULL + UNION ALL + SELECT 'specialization_version'::text, 'general'::text, + snap.identity_id, src.general, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.specialization_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.general + LEFT JOIN validation_snapshot live ON live.identity_id = src.general + WHERE src.project_id = p_project_id + AND src.general IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'specialization_version'::text, 'specific'::text, + snap.identity_id, src.specific, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.specialization_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.specific + LEFT JOIN validation_snapshot live ON live.identity_id = src.specific + WHERE src.project_id = p_project_id + AND src.specific IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'subclassification_version'::text, 'subclassifier'::text, + snap.identity_id, src.subclassifier, + CASE WHEN ti.class_kind NOT IN (2, 5, 7, 13, 14, 15, 17, 21, 23, 25, 26, 29, 31, 34, 38, 43, 45, 53, 68, 74, 80, 81, 86, 101, 103, 113, 119, 124, 126, 131, 134, 144, 148, 165, 168, 170, 173) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.subclassification_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.subclassifier + LEFT JOIN validation_snapshot live ON live.identity_id = src.subclassifier + WHERE src.project_id = p_project_id + AND src.subclassifier IS NOT NULL + AND (ti.class_kind NOT IN (2, 5, 7, 13, 14, 15, 17, 21, 23, 25, 26, 29, 31, 34, 38, 43, 45, 53, 68, 74, 80, 81, 86, 101, 103, 113, 119, 124, 126, 131, 134, 144, 148, 165, 168, 170, 173) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'subclassification_version'::text, 'superclassifier'::text, + snap.identity_id, src.superclassifier, + CASE WHEN ti.class_kind NOT IN (2, 5, 7, 13, 14, 15, 17, 21, 23, 25, 26, 29, 31, 34, 38, 43, 45, 53, 68, 74, 80, 81, 86, 101, 103, 113, 119, 124, 126, 131, 134, 144, 148, 165, 168, 170, 173) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.subclassification_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.superclassifier + LEFT JOIN validation_snapshot live ON live.identity_id = src.superclassifier + WHERE src.project_id = p_project_id + AND src.superclassifier IS NOT NULL + AND (ti.class_kind NOT IN (2, 5, 7, 13, 14, 15, 17, 21, 23, 25, 26, 29, 31, 34, 38, 43, 45, 53, 68, 74, 80, 81, 86, 101, 103, 113, 119, 124, 126, 131, 134, 144, 148, 165, 168, 170, 173) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'subsetting_version'::text, 'subsetted_feature'::text, + snap.identity_id, src.subsetted_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.subsetting_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.subsetted_feature + LEFT JOIN validation_snapshot live ON live.identity_id = src.subsetted_feature + WHERE src.project_id = p_project_id + AND src.subsetted_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'subsetting_version'::text, 'subsetting_feature'::text, + snap.identity_id, src.subsetting_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.subsetting_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.subsetting_feature + LEFT JOIN validation_snapshot live ON live.identity_id = src.subsetting_feature + WHERE src.project_id = p_project_id + AND src.subsetting_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'type_featuring_version'::text, 'feature_of_type'::text, + snap.identity_id, src.feature_of_type, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.type_featuring_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.feature_of_type + LEFT JOIN validation_snapshot live ON live.identity_id = src.feature_of_type + WHERE src.project_id = p_project_id + AND src.feature_of_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'type_featuring_version'::text, 'featuring_type'::text, + snap.identity_id, src.featuring_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.type_featuring_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.featuring_type + LEFT JOIN validation_snapshot live ON live.identity_id = src.featuring_type + WHERE src.project_id = p_project_id + AND src.featuring_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)) + UNION ALL + SELECT 'unioning_version'::text, 'unioning_type'::text, + snap.identity_id, src.unioning_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.unioning_version src + JOIN validation_snapshot snap ON snap.version_id = src.version_id + JOIN sysml2.data_identity ti ON ti.id = src.unioning_type + LEFT JOIN validation_snapshot live ON live.identity_id = src.unioning_type + WHERE src.project_id = p_project_id + AND src.unioning_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id AND live.identity_id IS NULL)); +END; +$$; + +-- The INCREMENTAL tier: validates only commit p_commit_id's change set — outgoing +-- references of its new versions, plus the reverse direction its tombstones break +-- (a live, UNCHANGED element left referencing a deleted identity). O(change set), +-- independent of history and snapshot size; the full pass above remains the +-- periodic audit that backstops it. +CREATE OR REPLACE FUNCTION sysml2.validate_references_in_commit( + p_project_id uuid, + p_commit_id uuid +) +RETURNS TABLE ( + source_table text, + source_column text, + source_identity uuid, + target_identity uuid, + problem text +) +LANGUAGE sql +STABLE +AS $$ + SELECT DISTINCT findings.source_table, findings.source_column, + findings.source_identity, findings.target_identity, findings.problem + FROM ( + SELECT 'annotation_version'::text, 'annotated_element'::text, + changed.identity_id, src.annotated_element, + 'dangling'::text + FROM sysml2.element_version changed + JOIN sysml2.annotation_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.annotated_element + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.annotated_element IS NOT NULL + AND (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.annotated_element))) + UNION ALL + SELECT 'conjugated_port_typing_version'::text, 'conjugated_port_definition'::text, + changed.identity_id, src.conjugated_port_definition, + CASE WHEN ti.class_kind NOT IN (31) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.conjugated_port_typing_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.conjugated_port_definition + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.conjugated_port_definition IS NOT NULL + AND (ti.class_kind NOT IN (31) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.conjugated_port_definition)))) + UNION ALL + SELECT 'conjugation_version'::text, 'conjugated_type'::text, + changed.identity_id, src.conjugated_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.conjugation_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.conjugated_type + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.conjugated_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.conjugated_type)))) + UNION ALL + SELECT 'conjugation_version'::text, 'original_type'::text, + changed.identity_id, src.original_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.conjugation_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.original_type + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.original_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.original_type)))) + UNION ALL + SELECT 'cross_subsetting_version'::text, 'crossed_feature'::text, + changed.identity_id, src.crossed_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.cross_subsetting_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.crossed_feature + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.crossed_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.crossed_feature)))) + UNION ALL + SELECT 'dependency_client'::text, 'target_identity'::text, + changed.identity_id, src.target_identity, + 'dangling'::text + FROM sysml2.element_version changed + JOIN sysml2.dependency_client src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.target_identity + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.target_identity IS NOT NULL + AND (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.target_identity))) + UNION ALL + SELECT 'dependency_supplier'::text, 'target_identity'::text, + changed.identity_id, src.target_identity, + 'dangling'::text + FROM sysml2.element_version changed + JOIN sysml2.dependency_supplier src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.target_identity + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.target_identity IS NOT NULL + AND (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.target_identity))) + UNION ALL + SELECT 'differencing_version'::text, 'differencing_type'::text, + changed.identity_id, src.differencing_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.differencing_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.differencing_type + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.differencing_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.differencing_type)))) + UNION ALL + SELECT 'disjoining_version'::text, 'disjoining_type'::text, + changed.identity_id, src.disjoining_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.disjoining_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.disjoining_type + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.disjoining_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.disjoining_type)))) + UNION ALL + SELECT 'disjoining_version'::text, 'type_disjoined'::text, + changed.identity_id, src.type_disjoined, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.disjoining_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.type_disjoined + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.type_disjoined IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.type_disjoined)))) + UNION ALL + SELECT 'element_version'::text, 'owning_relationship'::text, + changed.identity_id, changed.owning_relationship, + CASE WHEN ti.class_kind NOT IN (4, 5, 6, 10, 13, 14, 18, 19, 32, 33, 34, 35, 36, 42, 46, 47, 48, 51, 52, 61, 62, 63, 65, 66, 67, 68, 70, 73, 80, 81, 82, 83, 97, 98, 99, 109, 110, 112, 116, 118, 123, 127, 128, 133, 136, 137, 138, 142, 143, 145, 149, 150, 151, 152, 153, 154, 155, 158, 162, 163, 167, 171) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.data_identity ti ON ti.id = changed.owning_relationship + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND changed.owning_relationship IS NOT NULL + AND (ti.class_kind NOT IN (4, 5, 6, 10, 13, 14, 18, 19, 32, 33, 34, 35, 36, 42, 46, 47, 48, 51, 52, 61, 62, 63, 65, 66, 67, 68, 70, 73, 80, 81, 82, 83, 97, 98, 99, 109, 110, 112, 116, 118, 123, 127, 128, 133, 136, 137, 138, 142, 143, 145, 149, 150, 151, 152, 153, 154, 155, 158, 162, 163, 167, 171) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, changed.owning_relationship)))) + UNION ALL + SELECT 'element_owned_relationship'::text, 'target_identity'::text, + changed.identity_id, src.target_identity, + CASE WHEN ti.class_kind NOT IN (4, 5, 6, 10, 13, 14, 18, 19, 32, 33, 34, 35, 36, 42, 46, 47, 48, 51, 52, 61, 62, 63, 65, 66, 67, 68, 70, 73, 80, 81, 82, 83, 97, 98, 99, 109, 110, 112, 116, 118, 123, 127, 128, 133, 136, 137, 138, 142, 143, 145, 149, 150, 151, 152, 153, 154, 155, 158, 162, 163, 167, 171) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.element_owned_relationship src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.target_identity + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.target_identity IS NOT NULL + AND (ti.class_kind NOT IN (4, 5, 6, 10, 13, 14, 18, 19, 32, 33, 34, 35, 36, 42, 46, 47, 48, 51, 52, 61, 62, 63, 65, 66, 67, 68, 70, 73, 80, 81, 82, 83, 97, 98, 99, 109, 110, 112, 116, 118, 123, 127, 128, 133, 136, 137, 138, 142, 143, 145, 149, 150, 151, 152, 153, 154, 155, 158, 162, 163, 167, 171) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.target_identity)))) + UNION ALL + SELECT 'feature_chaining_version'::text, 'chaining_feature'::text, + changed.identity_id, src.chaining_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.feature_chaining_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.chaining_feature + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.chaining_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.chaining_feature)))) + UNION ALL + SELECT 'feature_inverting_version'::text, 'feature_inverted'::text, + changed.identity_id, src.feature_inverted, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.feature_inverting_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.feature_inverted + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.feature_inverted IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.feature_inverted)))) + UNION ALL + SELECT 'feature_inverting_version'::text, 'inverting_feature'::text, + changed.identity_id, src.inverting_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.feature_inverting_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.inverting_feature + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.inverting_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.inverting_feature)))) + UNION ALL + SELECT 'feature_typing_version'::text, 'type'::text, + changed.identity_id, src.type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.feature_typing_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.type + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.type)))) + UNION ALL + SELECT 'feature_typing_version'::text, 'typed_feature'::text, + changed.identity_id, src.typed_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.feature_typing_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.typed_feature + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.typed_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.typed_feature)))) + UNION ALL + SELECT 'intersecting_version'::text, 'intersecting_type'::text, + changed.identity_id, src.intersecting_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.intersecting_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.intersecting_type + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.intersecting_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.intersecting_type)))) + UNION ALL + SELECT 'membership_version'::text, 'member_element'::text, + changed.identity_id, src.member_element, + 'dangling'::text + FROM sysml2.element_version changed + JOIN sysml2.membership_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.member_element + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.member_element IS NOT NULL + AND (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.member_element))) + UNION ALL + SELECT 'membership_import_version'::text, 'imported_membership'::text, + changed.identity_id, src.imported_membership, + CASE WHEN ti.class_kind NOT IN (4, 51, 52, 63, 66, 73, 97, 112, 116, 118, 133, 136, 137, 138, 143, 145, 150, 158, 167, 171) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.membership_import_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.imported_membership + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.imported_membership IS NOT NULL + AND (ti.class_kind NOT IN (4, 51, 52, 63, 66, 73, 97, 112, 116, 118, 133, 136, 137, 138, 143, 145, 150, 158, 167, 171) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.imported_membership)))) + UNION ALL + SELECT 'namespace_import_version'::text, 'imported_namespace'::text, + changed.identity_id, src.imported_namespace, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 108, 111, 113, 114, 115, 117, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.namespace_import_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.imported_namespace + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.imported_namespace IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 108, 111, 113, 114, 115, 117, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.imported_namespace)))) + UNION ALL + SELECT 'port_conjugation_version'::text, 'original_port_definition'::text, + changed.identity_id, src.original_port_definition, + CASE WHEN ti.class_kind NOT IN (31, 124) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.port_conjugation_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.original_port_definition + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.original_port_definition IS NOT NULL + AND (ti.class_kind NOT IN (31, 124) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.original_port_definition)))) + UNION ALL + SELECT 'redefinition_version'::text, 'redefined_feature'::text, + changed.identity_id, src.redefined_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.redefinition_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.redefined_feature + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.redefined_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.redefined_feature)))) + UNION ALL + SELECT 'redefinition_version'::text, 'redefining_feature'::text, + changed.identity_id, src.redefining_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.redefinition_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.redefining_feature + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.redefining_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.redefining_feature)))) + UNION ALL + SELECT 'reference_subsetting_version'::text, 'referenced_feature'::text, + changed.identity_id, src.referenced_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.reference_subsetting_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.referenced_feature + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.referenced_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.referenced_feature)))) + UNION ALL + SELECT 'relationship_version'::text, 'owning_related_element'::text, + changed.identity_id, src.owning_related_element, + 'dangling'::text + FROM sysml2.element_version changed + JOIN sysml2.relationship_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.owning_related_element + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.owning_related_element IS NOT NULL + AND (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.owning_related_element))) + UNION ALL + SELECT 'relationship_owned_related_element'::text, 'target_identity'::text, + changed.identity_id, src.target_identity, + 'dangling'::text + FROM sysml2.element_version changed + JOIN sysml2.relationship_owned_related_element src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.target_identity + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.target_identity IS NOT NULL + AND (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.target_identity))) + UNION ALL + SELECT 'relationship_source'::text, 'target_identity'::text, + changed.identity_id, src.target_identity, + 'dangling'::text + FROM sysml2.element_version changed + JOIN sysml2.relationship_source src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.target_identity + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.target_identity IS NOT NULL + AND (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.target_identity))) + UNION ALL + SELECT 'relationship_target'::text, 'target_identity'::text, + changed.identity_id, src.target_identity, + 'dangling'::text + FROM sysml2.element_version changed + JOIN sysml2.relationship_target src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.target_identity + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.target_identity IS NOT NULL + AND (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.target_identity))) + UNION ALL + SELECT 'specialization_version'::text, 'general'::text, + changed.identity_id, src.general, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.specialization_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.general + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.general IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.general)))) + UNION ALL + SELECT 'specialization_version'::text, 'specific'::text, + changed.identity_id, src.specific, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.specialization_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.specific + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.specific IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.specific)))) + UNION ALL + SELECT 'subclassification_version'::text, 'subclassifier'::text, + changed.identity_id, src.subclassifier, + CASE WHEN ti.class_kind NOT IN (2, 5, 7, 13, 14, 15, 17, 21, 23, 25, 26, 29, 31, 34, 38, 43, 45, 53, 68, 74, 80, 81, 86, 101, 103, 113, 119, 124, 126, 131, 134, 144, 148, 165, 168, 170, 173) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.subclassification_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.subclassifier + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.subclassifier IS NOT NULL + AND (ti.class_kind NOT IN (2, 5, 7, 13, 14, 15, 17, 21, 23, 25, 26, 29, 31, 34, 38, 43, 45, 53, 68, 74, 80, 81, 86, 101, 103, 113, 119, 124, 126, 131, 134, 144, 148, 165, 168, 170, 173) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.subclassifier)))) + UNION ALL + SELECT 'subclassification_version'::text, 'superclassifier'::text, + changed.identity_id, src.superclassifier, + CASE WHEN ti.class_kind NOT IN (2, 5, 7, 13, 14, 15, 17, 21, 23, 25, 26, 29, 31, 34, 38, 43, 45, 53, 68, 74, 80, 81, 86, 101, 103, 113, 119, 124, 126, 131, 134, 144, 148, 165, 168, 170, 173) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.subclassification_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.superclassifier + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.superclassifier IS NOT NULL + AND (ti.class_kind NOT IN (2, 5, 7, 13, 14, 15, 17, 21, 23, 25, 26, 29, 31, 34, 38, 43, 45, 53, 68, 74, 80, 81, 86, 101, 103, 113, 119, 124, 126, 131, 134, 144, 148, 165, 168, 170, 173) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.superclassifier)))) + UNION ALL + SELECT 'subsetting_version'::text, 'subsetted_feature'::text, + changed.identity_id, src.subsetted_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.subsetting_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.subsetted_feature + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.subsetted_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.subsetted_feature)))) + UNION ALL + SELECT 'subsetting_version'::text, 'subsetting_feature'::text, + changed.identity_id, src.subsetting_feature, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.subsetting_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.subsetting_feature + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.subsetting_feature IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.subsetting_feature)))) + UNION ALL + SELECT 'type_featuring_version'::text, 'feature_of_type'::text, + changed.identity_id, src.feature_of_type, + CASE WHEN ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.type_featuring_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.feature_of_type + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.feature_of_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 3, 6, 8, 11, 12, 16, 18, 19, 20, 22, 24, 27, 30, 35, 36, 39, 40, 44, 54, 55, 56, 58, 59, 60, 64, 67, 69, 70, 71, 72, 75, 77, 78, 82, 84, 85, 87, 88, 90, 91, 92, 93, 94, 95, 100, 102, 104, 105, 106, 107, 111, 114, 115, 120, 121, 122, 125, 129, 132, 135, 139, 140, 141, 146, 147, 152, 153, 154, 155, 156, 159, 160, 164, 166, 169, 172, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.feature_of_type)))) + UNION ALL + SELECT 'type_featuring_version'::text, 'featuring_type'::text, + changed.identity_id, src.featuring_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.type_featuring_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.featuring_type + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.featuring_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.featuring_type)))) + UNION ALL + SELECT 'unioning_version'::text, 'unioning_type'::text, + changed.identity_id, src.unioning_type, + CASE WHEN ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) THEN 'wrong-type' ELSE 'dangling' END + FROM sysml2.element_version changed + JOIN sysml2.unioning_version src + ON src.project_id = changed.project_id AND src.version_id = changed.version_id + JOIN sysml2.data_identity ti ON ti.id = src.unioning_type + WHERE changed.project_id = p_project_id + AND changed.commit_id = p_commit_id + AND NOT changed.tombstone + AND src.unioning_type IS NOT NULL + AND (ti.class_kind NOT IN (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 34, 35, 36, 38, 39, 40, 43, 44, 45, 53, 54, 55, 56, 58, 59, 60, 64, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 100, 101, 102, 103, 104, 105, 106, 107, 111, 113, 114, 115, 119, 120, 121, 122, 124, 125, 126, 129, 131, 132, 134, 135, 139, 140, 141, 144, 146, 147, 148, 152, 153, 154, 155, 156, 159, 160, 161, 164, 165, 166, 168, 169, 170, 172, 173, 174, 175) + OR (ti.project_id = p_project_id + AND NOT EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, src.unioning_type)))) + UNION ALL + SELECT 'annotation_version'::text, 'annotated_element'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.annotation_version src + ON src.project_id = dead.project_id AND src.annotated_element = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'conjugated_port_typing_version'::text, 'conjugated_port_definition'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.conjugated_port_typing_version src + ON src.project_id = dead.project_id AND src.conjugated_port_definition = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'conjugation_version'::text, 'conjugated_type'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.conjugation_version src + ON src.project_id = dead.project_id AND src.conjugated_type = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'conjugation_version'::text, 'original_type'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.conjugation_version src + ON src.project_id = dead.project_id AND src.original_type = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'cross_subsetting_version'::text, 'crossed_feature'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.cross_subsetting_version src + ON src.project_id = dead.project_id AND src.crossed_feature = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'dependency_client'::text, 'target_identity'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.dependency_client src + ON src.project_id = dead.project_id AND src.target_identity = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'dependency_supplier'::text, 'target_identity'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.dependency_supplier src + ON src.project_id = dead.project_id AND src.target_identity = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'differencing_version'::text, 'differencing_type'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.differencing_version src + ON src.project_id = dead.project_id AND src.differencing_type = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'disjoining_version'::text, 'disjoining_type'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.disjoining_version src + ON src.project_id = dead.project_id AND src.disjoining_type = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'disjoining_version'::text, 'type_disjoined'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.disjoining_version src + ON src.project_id = dead.project_id AND src.type_disjoined = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'element_version'::text, 'owning_relationship'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.element_version holder + ON holder.project_id = dead.project_id AND holder.owning_relationship = dead.identity_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'element_owned_relationship'::text, 'target_identity'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.element_owned_relationship src + ON src.project_id = dead.project_id AND src.target_identity = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'feature_chaining_version'::text, 'chaining_feature'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.feature_chaining_version src + ON src.project_id = dead.project_id AND src.chaining_feature = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'feature_inverting_version'::text, 'feature_inverted'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.feature_inverting_version src + ON src.project_id = dead.project_id AND src.feature_inverted = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'feature_inverting_version'::text, 'inverting_feature'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.feature_inverting_version src + ON src.project_id = dead.project_id AND src.inverting_feature = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'feature_typing_version'::text, 'type'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.feature_typing_version src + ON src.project_id = dead.project_id AND src.type = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'feature_typing_version'::text, 'typed_feature'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.feature_typing_version src + ON src.project_id = dead.project_id AND src.typed_feature = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'intersecting_version'::text, 'intersecting_type'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.intersecting_version src + ON src.project_id = dead.project_id AND src.intersecting_type = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'membership_version'::text, 'member_element'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.membership_version src + ON src.project_id = dead.project_id AND src.member_element = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'membership_import_version'::text, 'imported_membership'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.membership_import_version src + ON src.project_id = dead.project_id AND src.imported_membership = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'namespace_import_version'::text, 'imported_namespace'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.namespace_import_version src + ON src.project_id = dead.project_id AND src.imported_namespace = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'port_conjugation_version'::text, 'original_port_definition'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.port_conjugation_version src + ON src.project_id = dead.project_id AND src.original_port_definition = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'redefinition_version'::text, 'redefined_feature'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.redefinition_version src + ON src.project_id = dead.project_id AND src.redefined_feature = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'redefinition_version'::text, 'redefining_feature'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.redefinition_version src + ON src.project_id = dead.project_id AND src.redefining_feature = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'reference_subsetting_version'::text, 'referenced_feature'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.reference_subsetting_version src + ON src.project_id = dead.project_id AND src.referenced_feature = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'relationship_version'::text, 'owning_related_element'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.relationship_version src + ON src.project_id = dead.project_id AND src.owning_related_element = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'relationship_owned_related_element'::text, 'target_identity'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.relationship_owned_related_element src + ON src.project_id = dead.project_id AND src.target_identity = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'relationship_source'::text, 'target_identity'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.relationship_source src + ON src.project_id = dead.project_id AND src.target_identity = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'relationship_target'::text, 'target_identity'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.relationship_target src + ON src.project_id = dead.project_id AND src.target_identity = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'specialization_version'::text, 'general'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.specialization_version src + ON src.project_id = dead.project_id AND src.general = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'specialization_version'::text, 'specific'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.specialization_version src + ON src.project_id = dead.project_id AND src.specific = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'subclassification_version'::text, 'subclassifier'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.subclassification_version src + ON src.project_id = dead.project_id AND src.subclassifier = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'subclassification_version'::text, 'superclassifier'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.subclassification_version src + ON src.project_id = dead.project_id AND src.superclassifier = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'subsetting_version'::text, 'subsetted_feature'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.subsetting_version src + ON src.project_id = dead.project_id AND src.subsetted_feature = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'subsetting_version'::text, 'subsetting_feature'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.subsetting_version src + ON src.project_id = dead.project_id AND src.subsetting_feature = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'type_featuring_version'::text, 'feature_of_type'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.type_featuring_version src + ON src.project_id = dead.project_id AND src.feature_of_type = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'type_featuring_version'::text, 'featuring_type'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.type_featuring_version src + ON src.project_id = dead.project_id AND src.featuring_type = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + UNION ALL + SELECT 'unioning_version'::text, 'unioning_type'::text, + holder.identity_id, dead.identity_id, 'dangling'::text + FROM sysml2.element_version dead + JOIN sysml2.unioning_version src + ON src.project_id = dead.project_id AND src.unioning_type = dead.identity_id + JOIN sysml2.element_version holder + ON holder.project_id = src.project_id AND holder.version_id = src.version_id + WHERE dead.project_id = p_project_id + AND dead.commit_id = p_commit_id + AND dead.tombstone + AND EXISTS (SELECT 1 FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, holder.identity_id) alive + WHERE alive.version_id = holder.version_id) + ) AS findings (source_table, source_column, source_identity, target_identity, problem); +$$; +------------------------------------------------------------------------------------------------ +--------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +------------------------------------------------------------------------------------------------ diff --git a/SysML2.NET.CodeGenerator/SysML2.NET.CodeGenerator.csproj b/SysML2.NET.CodeGenerator/SysML2.NET.CodeGenerator.csproj index affa35f16..71c6da331 100644 --- a/SysML2.NET.CodeGenerator/SysML2.NET.CodeGenerator.csproj +++ b/SysML2.NET.CodeGenerator/SysML2.NET.CodeGenerator.csproj @@ -1,278 +1,281 @@ - - - net10.0 - 14.0 - 0.22.0 - A Library to generate SysML v2 code. - SysML2.NET - Starion Group S.A. - Copyright © Starion Group S.A. - Apache-2.0 - https://github.com/STARIONGROUP/SysML2.NET.git - Git - Sam Gerene - false - disable - False - false - true - en-US - - - - - - - - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - - - - - - Always - - - Always - - - - - datamodel\KerML_only_xmi.uml - Always - - - - - datamodel\kebnf.g4 - - - datamodel\KerML-textual-bnf.kebnf - Always - - - datamodel\SysML-textual-bnf.kebnf - Always - - + + + net10.0 + 14.0 + 0.22.0 + A Library to generate SysML v2 code. + SysML2.NET + Starion Group S.A. + Copyright © Starion Group S.A. + Apache-2.0 + https://github.com/STARIONGROUP/SysML2.NET.git + Git + Sam Gerene + false + disable + False + false + true + en-US + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + + + + + + Always + + + Always + + + + + datamodel\KerML_only_xmi.uml + Always + + + + + datamodel\kebnf.g4 + + + datamodel\KerML-textual-bnf.kebnf + Always + + + datamodel\SysML-textual-bnf.kebnf + Always + + \ No newline at end of file diff --git a/SysML2.NET.CodeGenerator/Templates/Uml/core-sql-schema-2.hbs b/SysML2.NET.CodeGenerator/Templates/Uml/core-sql-schema-2.hbs new file mode 100644 index 000000000..a8348c602 --- /dev/null +++ b/SysML2.NET.CodeGenerator/Templates/Uml/core-sql-schema-2.hbs @@ -0,0 +1,1038 @@ +------------------------------------------------------------------------------------------------ +--------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +------------------------------------------------------------------------------------------------ +-- SysML2.NET — PostgreSQL persistence schema, generated from the KerML/SysML v2 UML metamodel. +-- +-- Generated by SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/SQLSchemaGenerator.cs +-- from SysML2.NET.CodeGenerator/Templates/Uml/core-sql-schema-2.hbs. +-- Reference design and rationale: SysML2.NET.CodeGenerator/Sql/schema.golden.sql. +-- +-- Target: PostgreSQL 16+. Deploy with max_locks_per_transaction >= 4096 (see the PARTITIONS +-- section for why). +------------------------------------------------------------------------------------------------ + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +CREATE SCHEMA IF NOT EXISTS sysml2; + +SET search_path = sysml2, public; + +------------------------------------------------------------------------------------------------ +-- 1. ENUM TYPES [GENERATED] +-- +-- Labels are the lowercase form of the C# literal, matching the JSON wire format: +-- SysML2.NET.Serializer.Json/Core/AutoGenSerializer/FeatureSerializer.cs:162 writes +-- `Direction.Value.ToString().ToLower()`. Deserialization is case-insensitive. +------------------------------------------------------------------------------------------------ + +{{ #uml_template.SQL2.WriteEnumTypes this }} +------------------------------------------------------------------------------------------------ +-- 2. METAMODEL CATALOGS [GENERATED] +-- +-- model_version registers every metamodel release this database has ever stored data for. +-- The id is an ORDINAL — a higher id is a later release — handed out once by the checked-in +-- registry (SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/ClassKindRegistry.cs) +-- and never renumbered. Every commit is stamped with the release its payloads are written in +-- (commit.model_version_id, §3); that stamp, not the branch or the project, is what a reader +-- consults to know the shape of a historical payload. +-- +-- class_kind interns the metaclass names to a smallint. Every element row carries the +-- smallint, not a VARCHAR(100) — at 1M+ elements that is the difference between a 2-byte and +-- a ~20-byte column on the hottest table in the database. +-- +-- !! THE IDS ARE AN APPEND-ONLY REGISTRY — FROZEN FOREVER ONCE ASSIGNED !! +-- A new metamodel release appends its new metaclasses AFTER the highest existing id (the +-- newcomers alphabetical among themselves); existing ids never change, so persisted +-- element_version.class_kind values and the generated C# ClassKind enum stay valid across +-- upgrades. A metaclass dropped by a release keeps its row, closed with removed_in. The +-- generator FAILS on any drift between the UML model and the registry (unregistered class, +-- stale registration, fingerprint mismatch) instead of silently renumbering, and the seed +-- INSERTs are idempotent (ON CONFLICT DO NOTHING) — safe to re-apply to a populated database. +-- +-- The property->storage routing that earlier designs kept in a property_catalog table is +-- deliberately NOT in the database: the generated service layer carries it as static +-- per-release C# (model-version descriptors). Nothing in this schema reads such a catalog — +-- the views and resolvers are already specialized per metaclass at generation time — and only +-- versioned generated code can describe EVERY registered release at once. +------------------------------------------------------------------------------------------------ + +CREATE TABLE sysml2.model_version ( + id smallint NOT NULL, -- ordinal: higher id == later release + name text NOT NULL, -- human-readable release label, e.g. 'sysml-2.0-beta-4' + source_fingerprint text NOT NULL, -- root-package fingerprint of the generator input + PRIMARY KEY (id), + UNIQUE (name) +); + +CREATE TABLE sysml2.class_kind ( + id smallint NOT NULL, + name text NOT NULL, -- the API @type value, e.g. 'PartUsage' + is_abstract boolean NOT NULL, + introduced_in smallint NOT NULL REFERENCES sysml2.model_version (id), + removed_in smallint NULL REFERENCES sysml2.model_version (id), -- first release WITHOUT the class; NULL = current + PRIMARY KEY (id), + UNIQUE (name) +); + +-- The append-only registry seeds, emitted from ClassKindRegistry (NOT from the UML model — +-- the model only validates against the registry). Idempotent by construction. + +{{ #uml_template.SQL2.WriteMetamodelCatalogRows this }} +------------------------------------------------------------------------------------------------ +-- 3. PIM — PROJECTS, COMMITS, BRANCHES, TAGS [HAND-WRITTEN] +-- +-- Models Clause 7.1.1/7.1.2 of OMG Systems Modeling API and Services v1.0. +-- +-- The C# PIM DTOs (SysML2.NET/PIM/DTO/) match this model: Commit carries `Change` and a +-- multi-valued ordered `PreviousCommit`; Branch/Tag redefine CommitReference.ReferencedCommit; +-- CommitReference.Deleted is nullable. `VersionedData` is deliberately NOT a DTO property — +-- it is derived, unbounded, and resolved here by branch_head / resolve_commit_state (§9). +------------------------------------------------------------------------------------------------ + +CREATE TABLE sysml2.project ( + id uuid NOT NULL, + name text NULL, + description text NULL, + resource_identifier text NULL, + created timestamptz NOT NULL DEFAULT now(), + default_branch_id uuid NULL, -- FK added after branch exists (circular) + + -- Upgrade POLICY, not truth: the highest model_version (§2) new commits may be written + -- in. NULL = no restriction (any registered release). The release a commit actually IS + -- in lives on the commit itself. + target_model_version_id smallint NULL REFERENCES sysml2.model_version (id), + + PRIMARY KEY (id) +); + +CREATE TABLE sysml2.commit ( + id uuid NOT NULL, + project_id uuid NOT NULL REFERENCES sysml2.project (id) ON DELETE CASCADE, + created timestamptz NOT NULL DEFAULT now(), + description text NULL, + + -- The metamodel release (§2) this commit's payloads are written in. Append-only history + -- makes the per-commit stamp the only correct grain: a historical commit stays in the + -- release it was written in forever. A branch upgrades via a CONVERSION COMMIT — a + -- single-parent commit that bumps this stamp and restates every element whose shape + -- changed between the releases (service obligation; force a commit_checkpoint on it so + -- folds rarely cross the release boundary). Readers resolve payload shape from this + -- stamp, never from the branch or the project. + model_version_id smallint NOT NULL REFERENCES sysml2.model_version (id), + + PRIMARY KEY (id) +); + +CREATE INDEX ix_commit_project_created ON sysml2.commit (project_id, created DESC); + +-- Commit.previousCommit is a SET — merges have multiple parents. The DAG lives here. +CREATE TABLE sysml2.commit_parent ( + commit_id uuid NOT NULL REFERENCES sysml2.commit (id) ON DELETE CASCADE, + parent_commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + ordinal smallint NOT NULL, + PRIMARY KEY (commit_id, parent_commit_id) +); + +CREATE INDEX ix_commit_parent_parent ON sysml2.commit_parent (parent_commit_id); + +-- Clause 7.1.2 invariant: "Version histories must monotonically increase in time" — for commit C, +-- C.created is strictly newer than D.created for any D in C.previousCommit. The snapshot resolver +-- in §8 RELIES on this: it picks the version from the newest ancestor commit. Enforce it, because +-- a violation silently produces the wrong snapshot rather than an error. +CREATE OR REPLACE FUNCTION sysml2.assert_commit_monotonic() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +DECLARE + child_created timestamptz; + parent_created timestamptz; +BEGIN + SELECT created INTO child_created FROM sysml2.commit WHERE id = NEW.commit_id; + SELECT created INTO parent_created FROM sysml2.commit WHERE id = NEW.parent_commit_id; + + IF child_created <= parent_created THEN + RAISE EXCEPTION + 'commit % (created %) is not strictly newer than its parent % (created %)', + NEW.commit_id, child_created, NEW.parent_commit_id, parent_created + USING ERRCODE = 'check_violation'; + END IF; + + RETURN NEW; +END; +$$; + +CREATE TRIGGER trg_commit_parent_monotonic + AFTER INSERT ON sysml2.commit_parent + FOR EACH ROW + EXECUTE FUNCTION sysml2.assert_commit_monotonic(); + +-- Model-version invariants on the commit DAG (multi-version support, §2): +-- * a commit is never in an OLDER release than a parent — downgrades are unsupported; +-- * a SINGLE-parent commit MAY bump the release: that is a conversion commit; +-- * a MERGE commit (2+ parents) must have ALL parents in its own release — convert first, +-- then merge. Without this check a merge would silently mix payload shapes. +-- The count-based re-check makes the guard insertion-order independent: the moment a second +-- parent row lands, every already-inserted parent is re-validated against the child's release. +CREATE OR REPLACE FUNCTION sysml2.assert_commit_version_compatible() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +DECLARE + child_version smallint; + parent_version smallint; + parent_count int; + mixed_parents int; +BEGIN + SELECT model_version_id INTO child_version FROM sysml2.commit WHERE id = NEW.commit_id; + SELECT model_version_id INTO parent_version FROM sysml2.commit WHERE id = NEW.parent_commit_id; + + IF child_version < parent_version THEN + RAISE EXCEPTION + 'commit % (model_version %) cannot have parent % (model_version %): downgrades are not supported', + NEW.commit_id, child_version, NEW.parent_commit_id, parent_version + USING ERRCODE = 'check_violation'; + END IF; + + SELECT count(*) INTO parent_count + FROM sysml2.commit_parent + WHERE commit_id = NEW.commit_id; + + IF parent_count >= 2 THEN + + SELECT count(*) INTO mixed_parents + FROM sysml2.commit_parent cp + JOIN sysml2.commit parent ON parent.id = cp.parent_commit_id + WHERE cp.commit_id = NEW.commit_id + AND parent.model_version_id <> child_version; + + IF mixed_parents > 0 THEN + RAISE EXCEPTION + 'merge commit % (model_version %) has a parent in a different model_version: convert every branch to the target release before merging', + NEW.commit_id, child_version + USING ERRCODE = 'check_violation'; + END IF; + + END IF; + + RETURN NEW; +END; +$$; + +CREATE TRIGGER trg_commit_parent_version + AFTER INSERT ON sysml2.commit_parent + FOR EACH ROW + EXECUTE FUNCTION sysml2.assert_commit_version_compatible(); + +-- Branch and Tag are both CommitReference. Branch is mutable + destructible; Tag is immutable + +-- destructible (Clause 7.1.2 mutability table). +CREATE TABLE sysml2.branch ( + id uuid NOT NULL, + project_id uuid NOT NULL REFERENCES sysml2.project (id) ON DELETE CASCADE, + name text NULL, + description text NULL, + head_commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + + -- The base of the branch_head OVERLAY (see §9): a commit for which a commit_checkpoint has + -- been materialized (service-enforced invariant). branch_head then stores ONLY the + -- identities that DIVERGE from that checkpoint, making branch creation and deletion + -- O(divergence) instead of O(model). NULL means the overlay is the complete head state + -- (bootstrap / small-project mode). + base_commit_id uuid NULL REFERENCES sysml2.commit (id), + + created timestamptz NOT NULL DEFAULT now(), + deleted timestamptz NULL, + PRIMARY KEY (id), + UNIQUE (project_id, name) +); + +ALTER TABLE sysml2.project + ADD CONSTRAINT project_default_branch_fk + FOREIGN KEY (default_branch_id) REFERENCES sysml2.branch (id) DEFERRABLE INITIALLY DEFERRED; + +CREATE TABLE sysml2.tag ( + id uuid NOT NULL, + project_id uuid NOT NULL REFERENCES sysml2.project (id) ON DELETE CASCADE, + name text NULL, + description text NULL, + tagged_commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + created timestamptz NOT NULL DEFAULT now(), + deleted timestamptz NULL, + PRIMARY KEY (id), + UNIQUE (project_id, name) +); + +CREATE TABLE sysml2.project_usage ( + id uuid NOT NULL, + project_id uuid NOT NULL REFERENCES sysml2.project (id) ON DELETE CASCADE, + used_project_id uuid NOT NULL REFERENCES sysml2.project (id), + used_project_commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + PRIMARY KEY (id) +); + +------------------------------------------------------------------------------------------------ +-- 4. DATA IDENTITY +-- +-- DataIdentity is the version-independent identity of an element — the stable @id that every +-- SysML2 reference points at. +-- +-- This table is deliberately NOT partitioned and its PK is the bare uuid: ProjectUsage lets an +-- element in project A reference an element in project B, so a (project_id, id) composite key +-- would make every cross-project reference un-FK-able. Project scoping is enforced by the +-- service layer via project_usage, not by the FK. +------------------------------------------------------------------------------------------------ + +-- DELETION IS EXPLICIT, NOT CASCADED. At scale (~1M identities per project), FK cascades are a +-- trap: a cascade executes per-row deletes filtered on the FK column ALONE, which no index in +-- this schema leads with — every cascaded identity would trigger scans of the largest tables. +-- Project deletion is therefore an ordered, batched, per-table procedure (each statement prunes +-- to one partition and uses a PK prefix): +-- +-- DELETE FROM sysml2. WHERE project_id = $1; +-- DELETE FROM sysml2.derived_version WHERE project_id = $1; +-- DELETE FROM sysml2.element_version WHERE project_id = $1; +-- DELETE FROM sysml2.branch_head WHERE project_id = $1; +-- DELETE FROM sysml2.commit_checkpoint WHERE project_id = $1; +-- DELETE FROM sysml2.data_identity WHERE project_id = $1; +-- DELETE FROM sysml2.project WHERE id = $1; -- cascades only PIM rows +-- +-- The remaining NO ACTION FKs guarantee the procedure cannot leave dangling references — they +-- block out-of-order deletion instead of silently scanning. +CREATE TABLE sysml2.data_identity ( + id uuid NOT NULL, + project_id uuid NOT NULL REFERENCES sysml2.project (id), + + -- TYPED IDENTITY. The metaclass of an element is invariant across its versions — an + -- identity is born a PartUsage and stays one — so the type is a property of the IDENTITY + -- and therefore FK-able, unlike everything else about an element. Two consumers: + -- * element_version's composite FK (identity_id, class_kind) makes a version that + -- claims a different metaclass than its identity IMPOSSIBLE; + -- * validate_references_at_commit (§14) type-checks every stored reference against + -- this column — including cross-project targets, because identities are typed + -- regardless of which project they live in. + -- A release conversion that retypes an element (e.g. its metaclass was dropped) must + -- update this column in the same transaction (service obligation, guide §15.16). + class_kind smallint NOT NULL REFERENCES sysml2.class_kind (id), + + PRIMARY KEY (id), + UNIQUE (id, class_kind) -- FK target for element_version's type-consistency check +); + +CREATE INDEX ix_data_identity_project ON sysml2.data_identity (project_id); + +------------------------------------------------------------------------------------------------ +-- 5. STORED ELEMENT STATE — append-only [HAND-WRITTEN] +-- +-- One row per (element, commit-in-which-it-changed). Never UPDATEd, never DELETEd. This is the +-- write hot path: a commit is a pure COPY of new rows. +-- +-- Element's own six stored properties are folded in here rather than into a separate table — +-- every element has them, so a join would be pure overhead. +-- +-- stored_json is a DELIBERATE DENORMALIZATION: the pre-serialized JSON of the element's stored +-- half. The normalized columns and link tables remain the system of record (and carry the FKs); +-- stored_json exists so that a read never has to reassemble an element from six tables. See §9. +------------------------------------------------------------------------------------------------ + +CREATE TABLE sysml2.element_version ( + project_id uuid NOT NULL, + version_id uuid NOT NULL, -- DataVersion.id + identity_id uuid NOT NULL, -- FK via the composite typed-identity check below + commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + class_kind smallint NOT NULL REFERENCES sysml2.class_kind (id), + + -- tombstone == true is DataVersion.payload = null, i.e. a deletion (Clause 7.1.2) + tombstone boolean NOT NULL DEFAULT false, + + -- Element (the only metaclass whose stored properties are folded into the core table) + -- element_id is text, NOT uuid, deliberately: KerML declares Element::elementId : String. + -- Only standard-library elements are normatively required to use name-based (version 5, + -- SHA-1) UUIDs; user-model elementIds carry no format constraint, so uuid would reject + -- spec-valid data. + element_id text NULL, + declared_name text NULL, + declared_short_name text NULL, + is_implied_included boolean NULL, + owning_relationship uuid NULL REFERENCES sysml2.data_identity (id), + + stored_json jsonb NULL, + + PRIMARY KEY (project_id, version_id), + + -- TYPED IDENTITY (§4): one composite FK both anchors the identity AND makes a version + -- that claims a different metaclass than its identity impossible. + FOREIGN KEY (identity_id, class_kind) REFERENCES sysml2.data_identity (id, class_kind), + + CONSTRAINT element_version_tombstone_empty + CHECK (NOT tombstone OR (stored_json IS NULL AND element_id IS NULL)), + CONSTRAINT element_version_payload_present + CHECK (tombstone OR (stored_json IS NOT NULL AND element_id IS NOT NULL AND is_implied_included IS NOT NULL)) +) PARTITION BY HASH (project_id); + +-- Clause 7.1.2: "DataVersion.identity is unique among records listed in Commit.change." +CREATE UNIQUE INDEX ux_element_version_identity_commit + ON sysml2.element_version (project_id, identity_id, commit_id); + +-- lz4 beats the pglz default by a wide margin at this write volume; set BEFORE the partitions +-- are created (§12) so every leaf inherits it. +ALTER TABLE sysml2.element_version ALTER COLUMN stored_json SET COMPRESSION lz4; + +CREATE INDEX ix_element_version_commit ON sysml2.element_version (project_id, commit_id); +CREATE INDEX ix_element_version_class_kind ON sysml2.element_version (project_id, class_kind); +CREATE INDEX ix_element_version_owning_rel ON sysml2.element_version (project_id, owning_relationship) + WHERE owning_relationship IS NOT NULL; + +------------------------------------------------------------------------------------------------ +-- 6. LINK TABLES — ordered multi-valued properties [GENERATED] +-- +-- The entire metamodel has only SIX distinct multi-valued reference properties and ONE +-- multi-valued value property. Every one of them is isOrdered=true, so `ordinal` is part of +-- the key rather than a set. +-- +-- Element::ownedRelationship (composite) × 167 classes +-- Relationship::ownedRelatedElement (composite) × 62 classes +-- Relationship::source × 62 classes +-- Relationship::target × 62 classes +-- Dependency::client × 1 class +-- Dependency::supplier × 1 class +-- Element::aliasIds (string) × 167 classes +-- +-- target_identity FKs to data_identity, NOT to element_version — a reference names an element, +-- not a version of it. +-- +-- GENERATOR NOTE: core-sql-schema-2.hbs derives these mechanically. Every reference-valued link +-- table gets an ix_{table}_target reverse-lookup index (this file shows the same rule). +------------------------------------------------------------------------------------------------ + +{{ #uml_template.SQL2.WriteLinkTables this }} +------------------------------------------------------------------------------------------------ +-- 7. SUBTYPE TABLES — one per storage-introducing metaclass [GENERATED] +-- +-- 47 tables. A metaclass gets a table iff it DECLARES at least one stored scalar or single +-- reference property of its own. Redeclarations (`new` in C#, redefinition in UML) do NOT get a +-- column — they resolve to the ancestor's column. There are 9 of them: +-- +-- CollectExpression::operator, SelectExpression::operator, FeatureChainExpression::operator, +-- IndexExpression::operator -> operator_expression_version.operator +-- ConnectionDefinition::isSufficient -> type_version.is_sufficient +-- EnumerationDefinition::isVariation -> definition_version.is_variation +-- Expose::isImportAll, Expose::visibility -> import_version.is_import_all / import_version.visibility +-- FramedConcernMembership::kind, +-- RequirementVerificationMembership::kind -> requirement_constraint_membership_version.kind +-- +-- Every table is keyed by (project_id, version_id) and co-partitioned with element_version, so +-- the join is partition-local. NOT NULL is safe on [1..1] properties: a row only exists here if +-- the element's class actually inherits this metaclass. +-- +-- Dependency introduces no scalar properties (client/supplier are link tables), so it has no +-- subtype table. +-- +-- GENERATOR NOTE: core-sql-schema-2.hbs derives these tables mechanically and emits two things +-- this hand-curated file only shows selectively: +-- * a DEFAULT clause for every column whose UML property declares a default value in the XMI +-- (e.g. the Feature booleans DEFAULT false, Membership::visibility DEFAULT 'public', +-- Import::visibility DEFAULT 'private'); +-- * an ix_{table}_{column} reverse-lookup index on EVERY data_identity-referencing column, +-- not just the four hot ones annotated below. The specialization graph indexes remain the +-- load-bearing ones for derived-property impact analysis. +------------------------------------------------------------------------------------------------ + +{{ #uml_template.SQL2.WriteSubtypeTables this }} +------------------------------------------------------------------------------------------------ +-- 8. DERIVED ELEMENT STATE — the second append-only stream [HAND-WRITTEN] +-- +-- THE KEY INSIGHT OF THIS SCHEMA. +-- +-- A derived value is a function of (version, SNAPSHOT), not of the version alone. Rename a +-- Namespace and every descendant's qualifiedName changes, even though no descendant's version +-- row changed. So derived state CANNOT hang off element_version — it is keyed by +-- (identity, commit), exactly like a version, and resolves through the SAME fold. +-- +-- A row is written only for elements whose derived values ACTUALLY CHANGED at that commit — +-- the "impact radius" of the change set. A leaf edit writes one row. A Namespace rename writes +-- one row per descendant. Adding a supertype to a widely-specialized Type writes one row per +-- member of its specialization-descendant closure. This is inherent, not a flaw in the design; +-- OMG Clause 2 warns of exactly this: "the values of derived properties of a given Element may +-- be affected by commits that do not directly change that Element." +-- +-- The six hot derived properties are promoted to real columns so the Query service can filter +-- and ORDER BY them (Clause 2, Derived Property Full Conformance: derived properties "can be +-- used in Query structures as PrimitiveConstraint properties"). The remaining ~325 distinct +-- derived property names live in derived_json. +------------------------------------------------------------------------------------------------ + +CREATE TABLE sysml2.derived_version ( + project_id uuid NOT NULL, + derived_id uuid NOT NULL, + identity_id uuid NOT NULL REFERENCES sysml2.data_identity (id), + commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + + -- promoted hot derived properties (all 167 metaclasses declare these via Element) + owner uuid NULL REFERENCES sysml2.data_identity (id), + owning_namespace uuid NULL REFERENCES sysml2.data_identity (id), + qualified_name text NULL, + name text NULL, + short_name text NULL, + is_library_element boolean NOT NULL DEFAULT false, + + -- everything else: ~325 distinct derived property names + derived_json jsonb NOT NULL, + + PRIMARY KEY (project_id, derived_id) +) PARTITION BY HASH (project_id); + +ALTER TABLE sysml2.derived_version ALTER COLUMN derived_json SET COMPRESSION lz4; + +CREATE UNIQUE INDEX ux_derived_version_identity_commit + ON sysml2.derived_version (project_id, identity_id, commit_id); + +CREATE INDEX ix_derived_version_commit ON sysml2.derived_version (project_id, commit_id); +CREATE INDEX ix_derived_version_owner ON sysml2.derived_version (project_id, owner); +CREATE INDEX ix_derived_version_qualified_name ON sysml2.derived_version (project_id, qualified_name); + +-- Ad-hoc PrimitiveConstraint filtering on a derived property that did NOT get promoted to a +-- column falls back to a jsonb containment probe. GIN keeps that from being a sequential scan. +CREATE INDEX ix_derived_version_json ON sysml2.derived_version USING gin (derived_json jsonb_path_ops); + +------------------------------------------------------------------------------------------------ +-- 9. SNAPSHOT RESOLUTION [HAND-WRITTEN] +-- +-- Commit.versionedData is DERIVED: fold `change` over the transitive previousCommit closure +-- (Clause 7.1.2). Doing that fold on every read does not scale, so: +-- +-- * branch_head is a sparse OVERLAY: it stores ONLY the identities that DIVERGE from the +-- branch's base checkpoint (branch.base_commit_id, §3). The head state of a branch is +-- overlay-row-if-present, else checkpoint-row. Updated incrementally on commit with the +-- change-set rows: O(changeset). Branch creation from a checkpointed commit writes ZERO +-- rows; branch deletion deletes only the divergence. At the design scale (100-500 live +-- branches x 1M elements) a fully-materialized per-branch head would be ~500M rows per +-- project — the overlay is what makes hundreds of branches affordable. +-- A deletion on the branch is masked by an overlay row with is_tombstone = true (it also +-- points at the tombstone element_version row). base_commit_id NULL = the overlay IS the +-- complete head state (bootstrap / small-project mode). +-- COMPACTION (service policy): when an overlay exceeds ~10% of the model or ~100k rows, +-- materialize a checkpoint at the branch head, repoint base_commit_id, delete the overlay. +-- +-- * commit_checkpoint materializes the full fold for selected commits. It serves two masters: +-- the base of every branch overlay, and the bound on how far resolve_commit_state() walks. +-- CADENCE (service policy): checkpoint a commit when EITHER ~200 commits have passed since +-- the nearest checkpointed ancestor on that lineage OR the cumulative change-set size since +-- it exceeds ~25% of the model — plus always at branch-fork bases. Checkpoints are O(model) +-- rows each, so cadence must be churn-based, never "every N commits" alone. Retention: drop +-- checkpoints referenced by no branch.base_commit_id (delete the registry row FIRST, then +-- the rows), keeping a sparse historical ladder. +-- +-- * resolve_commit_state() is the general fallback for an arbitrary commit; +-- resolve_element_at_commit() is the single-element variant (O(ancestry), not O(model)). +------------------------------------------------------------------------------------------------ + +CREATE TABLE sysml2.branch_head ( + project_id uuid NOT NULL, + branch_id uuid NOT NULL REFERENCES sysml2.branch (id) ON DELETE CASCADE, + identity_id uuid NOT NULL REFERENCES sysml2.data_identity (id), + version_id uuid NOT NULL, + derived_id uuid NULL, + + -- true = the element is DELETED on this branch relative to the base checkpoint; the row + -- masks the checkpoint row on read. Denormalizes element_version.tombstone so the set-read + -- anti-join never has to visit element_version for masked identities. + is_tombstone boolean NOT NULL DEFAULT false, + + PRIMARY KEY (project_id, branch_id, identity_id) +) PARTITION BY HASH (project_id); + +-- Supports the ON DELETE CASCADE from branch: a cascade filters on branch_id ALONE, and the PK +-- leads with project_id — without this index every branch deletion would sequentially scan all +-- partitions. (Cheap to maintain now that the table only holds divergence.) +CREATE INDEX ix_branch_head_branch ON sysml2.branch_head (branch_id); + +-- One row per CHECKPOINT (not per identity): the resolvers' "is this commit checkpointed?" +-- probe hits this tiny table, never commit_checkpoint itself. Probing commit_checkpoint for +-- existence is a planner trap: all of a checkpoint's ~1M rows share one (project_id, commit_id) +-- value, so n_distinct estimates make the index look useless and the EXISTS degenerates into a +-- repeated sequential scan of the whole partition (measured: 500 probes = 100M rows filtered). +CREATE TABLE sysml2.commit_checkpoint_registry ( + project_id uuid NOT NULL, + commit_id uuid NOT NULL REFERENCES sysml2.commit (id), + created timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (project_id, commit_id) +); + +CREATE TABLE sysml2.commit_checkpoint ( + project_id uuid NOT NULL, + commit_id uuid NOT NULL REFERENCES sysml2.commit (id) ON DELETE CASCADE, + identity_id uuid NOT NULL REFERENCES sysml2.data_identity (id), + version_id uuid NOT NULL, + derived_id uuid NULL, + PRIMARY KEY (project_id, commit_id, identity_id) +) PARTITION BY HASH (project_id); + +-- The general resolver. Walks the commit DAG back from :commit_id (stopping at the nearest +-- checkpoint) and picks, for each identity, the version from the NEWEST ancestor commit. +-- +-- "Newest" is well-defined only because Clause 7.1.2 guarantees monotonically increasing commit +-- timestamps along every parent edge — which trg_commit_parent_monotonic enforces. For a merge +-- commit, the merge itself carries the conflict resolution in its own change set, so it is +-- correctly the newest and wins. +-- +-- SIBLING commits (parallel branches, later merged) are NOT ordered by the monotonicity +-- invariant and may share a timestamp; the `id DESC` tiebreaker makes the fold DETERMINISTIC +-- in that case (an arbitrary-but-stable winner beats a nondeterministic one). A merge that +-- restates its conflicts — as Clause 7.1.2 requires — never reaches the tiebreaker. +CREATE OR REPLACE FUNCTION sysml2.resolve_commit_state(p_project_id uuid, p_commit_id uuid) +RETURNS TABLE (identity_id uuid, version_id uuid, derived_id uuid) +LANGUAGE sql +STABLE +AS $$ + WITH RECURSIVE + checkpoint AS ( + SELECT ccr.commit_id + FROM sysml2.commit_checkpoint_registry ccr + WHERE ccr.project_id = p_project_id AND ccr.commit_id = p_commit_id + ), + ancestry AS ( + SELECT c.id, c.created, (SELECT count(*) > 0 FROM checkpoint) AS at_checkpoint + FROM sysml2.commit c + WHERE c.id = p_commit_id + + UNION + + SELECT parent.id, parent.created, + EXISTS (SELECT 1 FROM sysml2.commit_checkpoint_registry ccr + WHERE ccr.project_id = p_project_id AND ccr.commit_id = parent.id) + FROM ancestry a + JOIN sysml2.commit_parent cp ON cp.commit_id = a.id + JOIN sysml2.commit parent ON parent.id = cp.parent_commit_id + WHERE NOT a.at_checkpoint + ), + folded AS ( + SELECT DISTINCT ON (ev.identity_id) + ev.identity_id, + ev.version_id, + ev.tombstone + FROM ancestry a + JOIN sysml2.element_version ev + ON ev.project_id = p_project_id AND ev.commit_id = a.id + ORDER BY ev.identity_id, a.created DESC, a.id DESC + ), + checkpoint_state AS ( + SELECT cc.identity_id, cc.version_id, cc.derived_id + FROM sysml2.commit_checkpoint cc + JOIN ancestry a ON a.id = cc.commit_id AND a.at_checkpoint + WHERE cc.project_id = p_project_id + ), + checkpointed AS ( + SELECT cs.identity_id, cs.version_id + FROM checkpoint_state cs + WHERE NOT EXISTS (SELECT 1 FROM folded f WHERE f.identity_id = cs.identity_id) + ), + resolved AS ( + SELECT f.identity_id, f.version_id FROM folded f WHERE NOT f.tombstone + UNION ALL + SELECT c.identity_id, c.version_id FROM checkpointed c + ), + -- Derived state folds over the SAME walked ancestry, in one pass (no per-row probe). + -- An identity whose latest derived row predates the walked window falls back to the + -- checkpoint's derived_id — without that fallback, derived state older than the + -- checkpoint would silently resolve to NULL. + derived_folded AS ( + SELECT DISTINCT ON (dv.identity_id) + dv.identity_id, + dv.derived_id + FROM ancestry a + JOIN sysml2.derived_version dv + ON dv.project_id = p_project_id AND dv.commit_id = a.id + ORDER BY dv.identity_id, a.created DESC, a.id DESC + ) + SELECT r.identity_id, + r.version_id, + COALESCE(df.derived_id, cs.derived_id) AS derived_id + FROM resolved r + LEFT JOIN derived_folded df ON df.identity_id = r.identity_id + LEFT JOIN checkpoint_state cs ON cs.identity_id = r.identity_id; +$$; + +-- Single-element variant of the resolver: same ancestry walk, but the fold arms are filtered to +-- one identity, so the cost is O(walked ancestry) index probes — NOT O(model). This is the +-- backing for GET /projects/{p}/commits/{c}/elements/{e}; without it a one-element historical +-- read would fold the entire model. +CREATE OR REPLACE FUNCTION sysml2.resolve_element_at_commit(p_project_id uuid, p_commit_id uuid, p_identity_id uuid) +RETURNS TABLE (identity_id uuid, version_id uuid, derived_id uuid) +LANGUAGE sql +STABLE +AS $$ + WITH RECURSIVE + checkpoint AS ( + SELECT ccr.commit_id + FROM sysml2.commit_checkpoint_registry ccr + WHERE ccr.project_id = p_project_id AND ccr.commit_id = p_commit_id + ), + ancestry AS ( + SELECT c.id, c.created, (SELECT count(*) > 0 FROM checkpoint) AS at_checkpoint + FROM sysml2.commit c + WHERE c.id = p_commit_id + + UNION + + SELECT parent.id, parent.created, + EXISTS (SELECT 1 FROM sysml2.commit_checkpoint_registry ccr + WHERE ccr.project_id = p_project_id AND ccr.commit_id = parent.id) + FROM ancestry a + JOIN sysml2.commit_parent cp ON cp.commit_id = a.id + JOIN sysml2.commit parent ON parent.id = cp.parent_commit_id + WHERE NOT a.at_checkpoint + ), + folded AS ( + SELECT ev.identity_id, ev.version_id, ev.tombstone + FROM ancestry a + JOIN sysml2.element_version ev + ON ev.project_id = p_project_id AND ev.commit_id = a.id AND ev.identity_id = p_identity_id + ORDER BY a.created DESC, a.id DESC + LIMIT 1 + ), + checkpointed AS ( + SELECT cc.identity_id, cc.version_id, cc.derived_id + FROM sysml2.commit_checkpoint cc + JOIN ancestry a ON a.id = cc.commit_id AND a.at_checkpoint + WHERE cc.project_id = p_project_id AND cc.identity_id = p_identity_id + ), + resolved AS ( + SELECT f.identity_id, f.version_id FROM folded f WHERE NOT f.tombstone + UNION ALL + SELECT c.identity_id, c.version_id FROM checkpointed c + WHERE NOT EXISTS (SELECT 1 FROM folded) + ), + derived_folded AS ( + SELECT dv.derived_id + FROM ancestry a + JOIN sysml2.derived_version dv + ON dv.project_id = p_project_id AND dv.commit_id = a.id AND dv.identity_id = p_identity_id + ORDER BY a.created DESC, a.id DESC + LIMIT 1 + ) + SELECT r.identity_id, + r.version_id, + COALESCE((SELECT df.derived_id FROM derived_folded df), + (SELECT c.derived_id FROM checkpointed c)) AS derived_id + FROM resolved r; +$$; + +-- Materializes the full fold of a commit as a checkpoint (idempotent). O(model) by design — +-- run it ASYNCHRONOUSLY per the cadence policy in the §9 banner, never on the commit path. +-- Returns the number of rows written. +CREATE OR REPLACE FUNCTION sysml2.build_commit_checkpoint(p_project_id uuid, p_commit_id uuid) +RETURNS bigint +LANGUAGE sql +VOLATILE +AS $$ + WITH inserted AS ( + INSERT INTO sysml2.commit_checkpoint (project_id, commit_id, identity_id, version_id, derived_id) + SELECT p_project_id, p_commit_id, r.identity_id, r.version_id, r.derived_id + FROM sysml2.resolve_commit_state(p_project_id, p_commit_id) r + ON CONFLICT (project_id, commit_id, identity_id) DO NOTHING + RETURNING 1 + ), + -- same statement, so checkpoint rows + registry row become visible atomically; the EXISTS + -- dependency also skips registering a checkpoint that materialized zero rows + registered AS ( + INSERT INTO sysml2.commit_checkpoint_registry (project_id, commit_id) + SELECT p_project_id, p_commit_id + WHERE EXISTS (SELECT 1 FROM inserted) + ON CONFLICT (project_id, commit_id) DO NOTHING + ) + SELECT count(*) FROM inserted; +$$; + +------------------------------------------------------------------------------------------------ +-- 10. READ PATH [HAND-WRITTEN] +-- +-- GET /projects/{p}/commits/{c}/elements/{e} reduces to ONE jsonb concat over a handful of PK +-- lookups. No joins across 6 subtype tables, no recursion, no derived computation. That is the +-- entire point of splitting stored_json from derived_json. +-- +-- EVERY function here resolves project_id by joining through `branch` FIRST. Filtering a +-- partitioned table on a bare uuid (branch_id / identity_id / version_id) without project_id +-- defeats partition pruning (all 16 leaves visited) AND cannot use the PKs (project_id-leading, +-- and PG16/17 has no btree skip scan) — the hottest query would silently become the worst one. +-- +-- The normalized columns of §5-§7 are the system of record and carry the referential integrity; +-- stored_json / derived_json are the read model built from them at commit time. +------------------------------------------------------------------------------------------------ + +-- Branch-head single-element read over the OVERLAY: the overlay row wins (a tombstoned overlay +-- row masks the base); otherwise fall back to the base checkpoint row. +CREATE OR REPLACE FUNCTION sysml2.get_element_at_branch_head(p_branch_id uuid, p_identity_id uuid) +RETURNS jsonb +LANGUAGE sql +STABLE +AS $$ + SELECT ev.stored_json || COALESCE(dv.derived_json, '{}'::jsonb) + FROM sysml2.branch b + LEFT JOIN sysml2.branch_head bh + ON bh.project_id = b.project_id AND bh.branch_id = b.id AND bh.identity_id = p_identity_id + LEFT JOIN sysml2.commit_checkpoint cc + ON cc.project_id = b.project_id AND cc.commit_id = b.base_commit_id + AND cc.identity_id = p_identity_id AND bh.identity_id IS NULL + JOIN sysml2.element_version ev + ON ev.project_id = b.project_id AND ev.version_id = COALESCE(bh.version_id, cc.version_id) + LEFT JOIN sysml2.derived_version dv + ON dv.project_id = b.project_id AND dv.derived_id = COALESCE(bh.derived_id, cc.derived_id) + WHERE b.id = p_branch_id + AND bh.is_tombstone IS NOT TRUE + AND NOT ev.tombstone; +$$; + +-- Branch-head set read: base checkpoint minus overlaid identities, plus the live overlay. +CREATE OR REPLACE FUNCTION sysml2.get_elements_at_branch_head(p_branch_id uuid) +RETURNS TABLE (identity_id uuid, payload jsonb) +LANGUAGE sql +STABLE +AS $$ + WITH head AS ( + SELECT b.project_id, bh.identity_id, bh.version_id, bh.derived_id + FROM sysml2.branch b + JOIN sysml2.branch_head bh + ON bh.project_id = b.project_id AND bh.branch_id = b.id + WHERE b.id = p_branch_id + AND NOT bh.is_tombstone + + UNION ALL + + SELECT b.project_id, cc.identity_id, cc.version_id, cc.derived_id + FROM sysml2.branch b + JOIN sysml2.commit_checkpoint cc + ON cc.project_id = b.project_id AND cc.commit_id = b.base_commit_id + WHERE b.id = p_branch_id + AND NOT EXISTS (SELECT 1 + FROM sysml2.branch_head masked + WHERE masked.project_id = cc.project_id + AND masked.branch_id = b.id + AND masked.identity_id = cc.identity_id) + ) + SELECT h.identity_id, + ev.stored_json || COALESCE(dv.derived_json, '{}'::jsonb) + FROM head h + JOIN sysml2.element_version ev + ON ev.project_id = h.project_id AND ev.version_id = h.version_id + LEFT JOIN sysml2.derived_version dv + ON dv.project_id = h.project_id AND dv.derived_id = h.derived_id + WHERE NOT ev.tombstone; +$$; + +CREATE OR REPLACE FUNCTION sysml2.get_elements_at_commit(p_project_id uuid, p_commit_id uuid) +RETURNS TABLE (identity_id uuid, payload jsonb) +LANGUAGE sql +STABLE +AS $$ + SELECT r.identity_id, + ev.stored_json || COALESCE(dv.derived_json, '{}'::jsonb) + FROM sysml2.resolve_commit_state(p_project_id, p_commit_id) r + JOIN sysml2.element_version ev + ON ev.project_id = p_project_id AND ev.version_id = r.version_id + LEFT JOIN sysml2.derived_version dv + ON dv.project_id = p_project_id AND dv.derived_id = r.derived_id; +$$; + +CREATE OR REPLACE FUNCTION sysml2.get_element_at_commit(p_project_id uuid, p_commit_id uuid, p_identity_id uuid) +RETURNS jsonb +LANGUAGE sql +STABLE +AS $$ + SELECT ev.stored_json || COALESCE(dv.derived_json, '{}'::jsonb) + FROM sysml2.resolve_element_at_commit(p_project_id, p_commit_id, p_identity_id) r + JOIN sysml2.element_version ev + ON ev.project_id = p_project_id AND ev.version_id = r.version_id + LEFT JOIN sysml2.derived_version dv + ON dv.project_id = p_project_id AND dv.derived_id = r.derived_id; +$$; + +------------------------------------------------------------------------------------------------ +-- 11. PER-METACLASS FLATTENING VIEWS [GENERATED] +-- +-- One view per concrete metaclass (167), reconstructing the full DTO row shape by LEFT JOINing +-- exactly the subtype tables in that metaclass's supertype closure. These serve the Query service +-- and any consumer that wants columns rather than jsonb. +-- +-- Representative excerpt — the template emits all 167: +-- +-- CREATE VIEW sysml2.vw_part_usage AS +-- SELECT ev.project_id, ev.version_id, ev.identity_id, ev.commit_id, +-- ev.element_id, ev.declared_name, ev.declared_short_name, ev.is_implied_included, +-- ev.owning_relationship, +-- t.is_abstract, t.is_sufficient, +-- f.direction, f.is_composite, f.is_constant, f.is_derived, f.is_end, +-- f.is_ordered, f.is_portion, f.is_unique, f.is_variable, +-- u.is_variation, +-- ou.is_individual, ou.portion_kind +-- FROM sysml2.element_version ev +-- JOIN sysml2.type_version t USING (project_id, version_id) +-- JOIN sysml2.feature_version f USING (project_id, version_id) +-- JOIN sysml2.usage_version u USING (project_id, version_id) +-- JOIN sysml2.occurrence_usage_version ou USING (project_id, version_id) +-- WHERE ev.class_kind = 120 AND NOT ev.tombstone; -- 120 = PartUsage's FROZEN registry id (§2) +-- +-- CREATE VIEW sysml2.vw_flow_usage AS ... -- SIX subtype tables: Connector is both a Feature +-- -- and a Relationship, so relationship_version joins too. + + +{{ #uml_template.SQL2.WriteFlatteningViews this }} +------------------------------------------------------------------------------------------------ +-- 12. PARTITIONS +-- +-- Every element-scoped table is PARTITION BY HASH (project_id) with the same modulus, so they are +-- co-located: a join between element_version and a subtype table stays partition-local, and every +-- project-scoped API call prunes to a single partition. +-- +-- 16 is a starting point. The generator emits this loop; tune the modulus to the deployment. +-- +-- !! OPERATIONAL REQUIREMENT — max_locks_per_transaction !! +-- +-- 58 partitioned tables x 16 partitions = 928 leaf partitions, and Postgres CLONES every foreign +-- key onto every leaf: 2,629 FK constraints in total. Any single transaction that touches the +-- whole schema (CREATE SCHEMA, DROP SCHEMA CASCADE, a migration, pg_dump --schema-only) needs a +-- lock per object and will fail on the default max_locks_per_transaction = 64 with: +-- +-- ERROR: out of shared memory +-- HINT: You might need to increase "max_locks_per_transaction". +-- +-- Deploy with at least: +-- max_locks_per_transaction = 4096 +-- +-- This does NOT affect the hot path: a project-scoped query prunes to one partition at plan time +-- and locks a handful of objects. It is a DDL/administration constraint only. Verified against +-- PostgreSQL 17 — the schema fails to install without it. +-- +-- If raising the setting is not an option, halve the modulus (16 -> 8) and/or drop partitioning +-- from the 47 subtype tables, keeping it only on element_version, derived_version, branch_head, +-- commit_checkpoint and the 7 link tables. That trades partition-local subtype joins for a far +-- smaller catalog. +------------------------------------------------------------------------------------------------ + +DO $$ +DECLARE + partitioned_table text; + partition_index int; + partition_count constant int := 16; +BEGIN + FOREACH partitioned_table IN ARRAY ARRAY[ + 'element_version', 'derived_version', 'branch_head', 'commit_checkpoint'{{ #uml_template.SQL2.WritePartitionedTableArray this }} + ] + LOOP + FOR partition_index IN 0 .. partition_count - 1 LOOP + EXECUTE format( + 'CREATE TABLE sysml2.%I PARTITION OF sysml2.%I FOR VALUES WITH (MODULUS %s, REMAINDER %s)', + partitioned_table || '_p' || partition_index, + partitioned_table, + partition_count, + partition_index); + + -- Storage parameters cannot be set on a partitioned parent — only on leaf partitions. + -- The write profiles differ and so must the tuning: + -- + -- * branch_head is UPSERT-heavy (overlay rows are updated on every commit to the + -- branch and deleted on compaction/branch-delete): fillfactor 90 leaves HOT + -- headroom so the per-commit updates don't bloat the PK, and dead-tuple-driven + -- vacuum applies. + -- + -- * everything else is APPEND-ONLY: dead-tuple thresholds never fire usefully, so + -- drive vacuum off INSERT counts (keeps the visibility map current for index-only + -- scans) and analyze off a threshold sized for bulk commit traffic — absolute-5000 + -- analyze on a 60M-row leaf would sample continuously during imports. + IF partitioned_table = 'branch_head' THEN + + EXECUTE format( + 'ALTER TABLE sysml2.%I SET (' + || 'fillfactor = 90, ' + || 'autovacuum_analyze_scale_factor = 0.0, ' + || 'autovacuum_analyze_threshold = 50000)', + partitioned_table || '_p' || partition_index); + + ELSE + + EXECUTE format( + 'ALTER TABLE sysml2.%I SET (' + || 'autovacuum_vacuum_insert_scale_factor = 0.0, ' + || 'autovacuum_vacuum_insert_threshold = 100000, ' + || 'autovacuum_analyze_scale_factor = 0.0, ' + || 'autovacuum_analyze_threshold = 50000)', + partitioned_table || '_p' || partition_index); + + END IF; + END LOOP; + END LOOP; +END; +$$; + +-- PG18 OPPORTUNISTIC DEFAULTS (version-guarded; a no-op on the PostgreSQL 16/17 floor). +-- +-- On PostgreSQL 18+ the schema self-activates native uuidv7() defaults on every +-- SERVER-MINTED key. Time-ordered ids turn each project's insert pattern into rightmost +-- btree appends instead of random-page scatter (audit finding R8). The application-side +-- Guid.CreateVersion7() remains the PRIMARY id source — the service usually needs the ids +-- before insert (to wire derived_id into branch_head etc.); these defaults are the safety +-- net that keeps ad-hoc/tooling inserts time-ordered too. +-- +-- Deliberately NOT defaulted: data_identity.id (the spec-visible @id is supplied by the +-- API layer or the client — a silent server default would mask missing-id bugs). +DO $$ +BEGIN + + IF current_setting('server_version_num')::int >= 180000 THEN + + ALTER TABLE sysml2.element_version ALTER COLUMN version_id SET DEFAULT uuidv7(); + ALTER TABLE sysml2.derived_version ALTER COLUMN derived_id SET DEFAULT uuidv7(); + ALTER TABLE sysml2.project ALTER COLUMN id SET DEFAULT uuidv7(); + ALTER TABLE sysml2.commit ALTER COLUMN id SET DEFAULT uuidv7(); + ALTER TABLE sysml2.branch ALTER COLUMN id SET DEFAULT uuidv7(); + ALTER TABLE sysml2.tag ALTER COLUMN id SET DEFAULT uuidv7(); + + END IF; +END; +$$; + +------------------------------------------------------------------------------------------------ +-- 13. MODEL VERSION [GENERATED] +------------------------------------------------------------------------------------------------ + +CREATE OR REPLACE FUNCTION sysml2.query_model_version() +RETURNS text +LANGUAGE sql +IMMUTABLE +AS $$ + SELECT '{{ #uml_template.SQL2.WriteModelVersion this }}'::text; +$$; + +------------------------------------------------------------------------------------------------ +-- 14. REFERENCE VALIDATION — two tiers [GENERATED] +-- +-- The ON-DEMAND half of referential integrity (guide §7): the FKs prove that every stored +-- reference targets an EXISTING identity; these functions check what FKs cannot — +-- * 'dangling' — a same-project target that is not alive in the snapshot of the given +-- commit (absent or tombstoned); +-- * 'wrong-type' — a target whose metaclass is illegal for the referencing property, +-- checked via the TYPED IDENTITY (data_identity.class_kind, §4) — this +-- check applies to cross-project targets too. +-- Liveness of cross-project targets is deliberately NOT checked here: it depends on the +-- used-project commit (project_usage), which is service-layer resolution. +-- +-- TIER 1 — validate_references_at_commit: the FULL periodic audit over one commit's whole +-- snapshot. It materializes and ANALYZEs the snapshot in a temp table first, so the planner +-- knows the true cardinality and bounds the pass at O(snapshot x log history) — never +-- O(history) — however deep the append-only tables grow. Run asynchronously (checkpoint +-- cadence is a natural rhythm). +-- +-- TIER 2 — validate_references_in_commit: the INCREMENTAL per-commit check, O(change set): +-- the outgoing references of the versions the commit wrote, PLUS the reverse direction its +-- tombstones break — a live, unchanged element left referencing a deleted identity (driven +-- by the reverse-lookup indexes; per-target liveness probed via resolve_element_at_commit). +-- Cheap enough for the synchronous commit-validation path; tier 1 backstops it. +-- +-- Deliberately FUNCTIONS, not constraints: the spec allows transiently dangling references, +-- and liveness is a function of (identity, commit) — unenforceable by FK. One UNION ALL arm +-- per stored reference column, emitted from the UML model with the allowed target +-- metaclasses resolved through ClassKindRegistry. +------------------------------------------------------------------------------------------------ + +{{ #uml_template.SQL2.WriteReferenceValidation this }} +------------------------------------------------------------------------------------------------ +--------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +------------------------------------------------------------------------------------------------ diff --git a/SysML2.NET.CodeGenerator/Templates/Uml/core-sql-schema.hbs b/SysML2.NET.CodeGenerator/Templates/Uml/core-sql-schema.hbs new file mode 100644 index 000000000..233c5c62b --- /dev/null +++ b/SysML2.NET.CodeGenerator/Templates/Uml/core-sql-schema.hbs @@ -0,0 +1,49 @@ +------------------------------------------------------------------------------------------------ +--------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +------------------------------------------------------------------------------------------------ + +-- Initialize database +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE SCHEMA IF NOT EXISTS "UML-CODEGEN"; + +-- Root Thing table +CREATE TABLE "UML-CODEGEN"."Thing" ( + "iid" uuid NOT NULL, + "classKind" VARCHAR(100), + "data" JSONB, + PRIMARY KEY ("iid") +); + +CREATE INDEX "IX_REVISION_THING" ON "UML-CODEGEN"."Thing" ("revisionNumber", "iid"); + +ALTER TABLE "UML-CODEGEN"."Thing" SET (autovacuum_vacuum_scale_factor = 0.0); +ALTER TABLE "UML-CODEGEN"."Thing" SET (autovacuum_vacuum_threshold = 2500); +ALTER TABLE "UML-CODEGEN"."Thing" SET (autovacuum_analyze_scale_factor = 0.0); +ALTER TABLE "UML-CODEGEN"."Thing" SET (autovacuum_analyze_threshold = 2500); + +-- Table definitions +{{#each this as | class |}} + {{#uml_template.SQL.WriteBasicTableDefinitions class}} +{{/each}} + +-- Many to Many link tables including Foreign Key Constrains +{{#each this as | class |}} + {{#uml_template.SQL.WriteManyToManyTableDefinitionsAndConstraints class}} +{{/each}} + +-- Reference Properties that are not Many-to-Many +{{#each this as | class |}} + {{#uml_template.SQL.WriteNormalReferenceConstraints class}} +{{/each}} + +-- ModelVersion +CREATE OR REPLACE FUNCTION "UML-CODEGEN".query_model_version() +RETURNS text AS $$ +BEGIN + RETURN '{{ #uml_template.ModelVersion this}}'; +END; +$$ LANGUAGE plpgsql; + +------------------------------------------------------------------------------------------------ +--------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +------------------------------------------------------------------------------------------------ \ No newline at end of file diff --git a/SysML2.NET.Extensions.Tests/ModelInterchange/ChecksumKindProviderTestFixture.cs b/SysML2.NET.Extensions.Tests/ModelInterchange/ChecksumKindProviderTestFixture.cs index 2e0e457dc..2cffd1592 100644 --- a/SysML2.NET.Extensions.Tests/ModelInterchange/ChecksumKindProviderTestFixture.cs +++ b/SysML2.NET.Extensions.Tests/ModelInterchange/ChecksumKindProviderTestFixture.cs @@ -18,15 +18,14 @@ // // ------------------------------------------------------------------------------------------------ -namespace SysML2.NET.Extensions.Tests.Core.ModelInterchange +namespace SysML2.NET.Extensions.Tests.ModelInterchange { using System; using System.Buffers; using System.Linq; using System.Text; - using NUnit.Framework; - + using SysML2.NET.Extensions.ModelInterchange; using SysML2.NET.ModelInterchange; [TestFixture] diff --git a/SysML2.NET.Extensions/ModelInterchange/ChecksumKindProvider.cs b/SysML2.NET.Extensions/ModelInterchange/ChecksumKindProvider.cs index 376cc8442..a29b7886e 100644 --- a/SysML2.NET.Extensions/ModelInterchange/ChecksumKindProvider.cs +++ b/SysML2.NET.Extensions/ModelInterchange/ChecksumKindProvider.cs @@ -18,10 +18,11 @@ // // ------------------------------------------------------------------------------------------------ -namespace SysML2.NET.ModelInterchange +namespace SysML2.NET.Extensions.ModelInterchange { using System; using System.Buffers; + using NET.ModelInterchange; /// /// Provides high-performance parsing and serialization helpers diff --git a/SysML2.NET.Serializer.Json.Tests/ApiSerializerTestFixture.cs b/SysML2.NET.Serializer.Json.Tests/ApiSerializerTestFixture.cs index 83b2a5e32..78615a5f2 100644 --- a/SysML2.NET.Serializer.Json.Tests/ApiSerializerTestFixture.cs +++ b/SysML2.NET.Serializer.Json.Tests/ApiSerializerTestFixture.cs @@ -23,6 +23,7 @@ namespace SysML2.NET.Serializer.Json.Tests using System; using System.Collections.Generic; using System.IO; + using System.Linq; using System.Text; using System.Text.Json; using System.Threading; @@ -73,7 +74,7 @@ private void CreateTestData() Created = new DateTime(1976, 8, 20), Description = "", OwningProject = Guid.Parse("9b0e1914-3241-461e-b9ee-a3ff5120de4e"), - PreviousCommit = Guid.Empty, + PreviousCommit = [], ResourceIdentifier = "http://www.stariongroup.eu/commit", }; @@ -126,6 +127,46 @@ public void Verify_that_PIM_IData_can_be_serialized(bool includeDerivedPropertie Console.WriteLine(json); } + [Test] + public void Verify_that_merge_commit_with_change_set_can_be_roundtripped() + { + var firstParent = Guid.Parse("11111111-1111-1111-1111-111111111111"); + var secondParent = Guid.Parse("22222222-2222-2222-2222-222222222222"); + List expectedParents = [firstParent, secondParent]; + + this.commit.PreviousCommit = [firstParent, secondParent]; + + this.commit.Change = + [ + new DataVersion + { + Id = Guid.Parse("33333333-3333-3333-3333-333333333333"), + Commit = this.commit.Id, + Identity = new DataIdentity { Id = Guid.Parse("44444444-4444-4444-4444-444444444444") }, + Payload = null + } + ]; + + var stream = new MemoryStream(); + this.serializer.Serialize(this.commit, SerializationModeKind.JSON, false, stream, new JsonWriterOptions()); + + stream.Position = 0; + var deSerializer = new DeSerializer(); + var data = deSerializer.DeSerialize(stream, SerializationModeKind.JSON, SerializationTargetKind.PSM, false).ToList(); + + var roundtrippedCommit = data.OfType().Single(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(roundtrippedCommit.Id, Is.EqualTo(this.commit.Id)); + Assert.That(roundtrippedCommit.PreviousCommit, Is.EqualTo(expectedParents)); + Assert.That(roundtrippedCommit.Change, Has.Count.EqualTo(1)); + Assert.That(roundtrippedCommit.Change[0].Identity.Id, Is.EqualTo(Guid.Parse("44444444-4444-4444-4444-444444444444"))); + Assert.That(roundtrippedCommit.Change[0].Payload, Is.Null, "a null payload represents a deletion and must survive the round-trip"); + Assert.That(roundtrippedCommit.Change[0].Commit, Is.EqualTo(this.commit.Id)); + } + } + [Test] [TestCase(false)] [TestCase(true)] diff --git a/SysML2.NET.Serializer.Json.Tests/DeSerializerTestFixture.cs b/SysML2.NET.Serializer.Json.Tests/DeSerializerTestFixture.cs index 8c3a946b7..24b311284 100644 --- a/SysML2.NET.Serializer.Json.Tests/DeSerializerTestFixture.cs +++ b/SysML2.NET.Serializer.Json.Tests/DeSerializerTestFixture.cs @@ -1,238 +1,238 @@ -// ------------------------------------------------------------------------------------------------- -// -// -// Copyright 2022-2025 Starion Group S.A. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// -// ------------------------------------------------------------------------------------------------ - -namespace SysML2.NET.Serializer.Json.Tests -{ - using System; - using System.IO; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - - using NUnit.Framework; - - using SysML2.NET.Core.Core.Types; - using SysML2.NET.Core.DTO.Core.Features; - using SysML2.NET.PIM.DTO; - using SysML2.NET.Serializer.Json; - - /// - /// Suite of tests for the - /// - [TestFixture] - public class DeSerializerTestFixture - { - private DeSerializer deSerializer; - - [SetUp] - public void SetUp() - { - this.deSerializer = new DeSerializer(); - } - - [Test] - [TestCase(false)] - [TestCase(true)] - public void Verify_that_iData_from_sysmlcore_json_can_be_deserialized(bool shouldDeserializeDerivedProperties) - { - var fileName = Path.Combine(TestContext.CurrentContext.WorkDirectory, "Data", "projects.000e9890-6935-43e6-a5d7-5d7cac601f4c.commits.6d7ad9fd-6520-4ff2-885b-8c5c129e6c27.elements.json"); - using var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read); - - var data = this.deSerializer.DeSerialize(stream, SerializationModeKind.JSON, SerializationTargetKind.CORE, shouldDeserializeDerivedProperties); - - using (Assert.EnterMultipleScope()) - { - Assert.That(data.Count(), Is.EqualTo(100)); - Assert.That(data.OfType().Count(), Is.EqualTo(30)); - } - - var feature = data.OfType().Single(x => x.Id == Guid.Parse("00a6ef10-d3dc-4741-9029-2c9978c2f083")); - - using (Assert.EnterMultipleScope()) - { - Assert.That(feature.AliasIds, Is.Empty); - Assert.That(feature.ElementId, Is.EqualTo("00a6ef10-d3dc-4741-9029-2c9978c2f083")); - Assert.That(feature.IsAbstract, Is.False); - Assert.That(feature.IsComposite, Is.False); - Assert.That(feature.IsSufficient, Is.False); - Assert.That(feature.IsEnd, Is.False); - Assert.That(feature.IsUnique, Is.True); - Assert.That(feature.DeclaredName, Is.Null); - Assert.That(feature.OwnedRelationship, Is.Empty); - Assert.That(feature.OwningRelationship, Is.EqualTo(Guid.Parse("8a780d8b-61a6-472b-8b80-2564aa9f7c36"))); - Assert.That(feature.DeclaredShortName, Is.Null); - Assert.That(feature.Direction, Is.EqualTo(FeatureDirectionKind.Out)); - } - } - - [Test] - [TestCase(false)] - [TestCase(true)] - public async Task Verify_that_iData_from_sysmlcore_json_can_be_deserialized_async(bool shouldDeserializeDerivedProperties) - { - var fileName = Path.Combine(TestContext.CurrentContext.WorkDirectory, "Data", "projects.000e9890-6935-43e6-a5d7-5d7cac601f4c.commits.6d7ad9fd-6520-4ff2-885b-8c5c129e6c27.elements.json"); - - await using var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read); - - var cts = new CancellationTokenSource(); - - var data = await this.deSerializer.DeSerializeAsync(stream, SerializationModeKind.JSON, SerializationTargetKind.CORE, shouldDeserializeDerivedProperties, cts.Token); - - using (Assert.EnterMultipleScope()) - { - Assert.That(data.Count(), Is.EqualTo(100)); - - Assert.That(data.OfType().Count(), Is.EqualTo(30)); - } - - var feature = data.OfType().Single(x => x.Id == Guid.Parse("00a6ef10-d3dc-4741-9029-2c9978c2f083")); - - using (Assert.EnterMultipleScope()) - { - Assert.That(feature.AliasIds, Is.Empty); - Assert.That(feature.ElementId, Is.EqualTo("00a6ef10-d3dc-4741-9029-2c9978c2f083")); - Assert.That(feature.IsAbstract, Is.False); - Assert.That(feature.IsComposite, Is.False); - Assert.That(feature.IsSufficient, Is.False); - Assert.That(feature.IsEnd, Is.False); - Assert.That(feature.IsUnique, Is.True); - Assert.That(feature.DeclaredName, Is.Null); - Assert.That(feature.OwnedRelationship, Is.Empty); - Assert.That(feature.OwningRelationship, Is.EqualTo(Guid.Parse("8a780d8b-61a6-472b-8b80-2564aa9f7c36"))); - Assert.That(feature.DeclaredShortName, Is.Null); - Assert.That(feature.Direction, Is.EqualTo(FeatureDirectionKind.Out)); - } - } - - [Test] - [TestCase(false)] - [TestCase(true)] - public void Verify_that_projects_from_restapi_json_can_be_deserialized(bool shouldDeserializeDerivedProperties) - { - var fileName = Path.Combine(TestContext.CurrentContext.WorkDirectory, "Data", "projects.json"); - using var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read); - - var data = this.deSerializer.DeSerialize(stream, SerializationModeKind.JSON, SerializationTargetKind.PSM, shouldDeserializeDerivedProperties); - - Assert.That(data.Count(), Is.EqualTo(43)); - - var project = data.OfType().Single(x => x.Id == Guid.Parse("000e9890-6935-43e6-a5d7-5d7cac601f4c")); - - using (Assert.EnterMultipleScope()) - { - Assert.That(project.DefaultBranch, Is.EqualTo(Guid.Parse("c294a463-6c9c-47a8-b592-01252c5ab2a7"))); - Assert.That(project.Name, Is.EqualTo("7b-Variant Configurations Mon Mar 13 17:54:29 EDT 2023")); - Assert.That(project.Description, Is.Null); - } - } - - [Test] - [TestCase(false)] - [TestCase(true)] - public void Verify_that_particular_project_from_restapi_json_can_be_deserialized(bool shouldDeserializeDerivedProperties) - { - var fileName = Path.Combine(TestContext.CurrentContext.WorkDirectory, "Data", "projects.000e9890-6935-43e6-a5d7-5d7cac601f4c.json"); - using var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read); - - var data = this.deSerializer.DeSerialize(stream, SerializationModeKind.JSON, SerializationTargetKind.PSM, shouldDeserializeDerivedProperties); - - Assert.That(data.Count(), Is.EqualTo(1)); - - var project = data.OfType().Single(x => x.Id == Guid.Parse("000e9890-6935-43e6-a5d7-5d7cac601f4c")); - - using (Assert.EnterMultipleScope()) - { - Assert.That(project.DefaultBranch, Is.EqualTo(Guid.Parse("c294a463-6c9c-47a8-b592-01252c5ab2a7"))); - Assert.That(project.Name, Is.EqualTo("7b-Variant Configurations Mon Mar 13 17:54:29 EDT 2023")); - Assert.That(project.Description, Is.Null); - } - } - - [Test] - [TestCase(false)] - [TestCase(true)] - public void Verify_that_particular_project_and_commits_from_restapi_json_can_be_deserialized(bool shouldDeserializeDerivedProperties) - { - var fileName = Path.Combine(TestContext.CurrentContext.WorkDirectory, "Data", "projects.000e9890-6935-43e6-a5d7-5d7cac601f4c.commits.json"); - using var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read); - - var data = this.deSerializer.DeSerialize(stream, SerializationModeKind.JSON, SerializationTargetKind.PSM, shouldDeserializeDerivedProperties); - - Assert.That(data.Count(), Is.EqualTo(1)); - - var firstCommit = data.OfType().Single(x => x.Id == Guid.Parse("6d7ad9fd-6520-4ff2-885b-8c5c129e6c27")); - - using (Assert.EnterMultipleScope()) - { - Assert.That(firstCommit.OwningProject, Is.EqualTo(Guid.Parse("000e9890-6935-43e6-a5d7-5d7cac601f4c"))); - Assert.That(firstCommit.PreviousCommit, Is.EqualTo(Guid.Empty)); - Assert.That(firstCommit.Description, Is.Null); - Assert.That(firstCommit.Created, Is.EqualTo(DateTime.Parse("2023-03-13T17:53:59.111354-04:00"))); - } - } - - [Test] - [TestCase(false)] - [TestCase(true)] - public void Verify_that_particular_project_and_particular_commit_from_restapi_json_can_be_deserialized(bool shouldDeserializeDerivedProperties) - { - var fileName = Path.Combine(TestContext.CurrentContext.WorkDirectory, "Data", "projects.000e9890-6935-43e6-a5d7-5d7cac601f4c.commits.6d7ad9fd-6520-4ff2-885b-8c5c129e6c27.json"); - using var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read); - - var data = this.deSerializer.DeSerialize(stream, SerializationModeKind.JSON, SerializationTargetKind.PSM, shouldDeserializeDerivedProperties); - - Assert.That(data.Count(), Is.EqualTo(1)); - - var firstCommit = data.OfType().Single(x => x.Id == Guid.Parse("6d7ad9fd-6520-4ff2-885b-8c5c129e6c27")); - - using (Assert.EnterMultipleScope()) - { - Assert.That(firstCommit.OwningProject, Is.EqualTo(Guid.Parse("000e9890-6935-43e6-a5d7-5d7cac601f4c"))); - Assert.That(firstCommit.PreviousCommit, Is.EqualTo(Guid.Empty)); - Assert.That(firstCommit.Description, Is.Null); - Assert.That(firstCommit.Created, Is.EqualTo(DateTime.Parse("2023-03-13T17:53:59.111354-04:00"))); - } - } - - [Test] - [TestCase(false)] - [TestCase(true)] - public void Verify_that_particular_project_and_branches_from_restapi_json_can_be_deserialized(bool shouldDeserializeDerivedProperties) - { - var fileName = Path.Combine(TestContext.CurrentContext.WorkDirectory, "Data", "projects.000e9890-6935-43e6-a5d7-5d7cac601f4c.branches.json"); - using var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read); - - var data = this.deSerializer.DeSerialize(stream, SerializationModeKind.JSON, SerializationTargetKind.PSM, shouldDeserializeDerivedProperties); - - Assert.That(data.Count(), Is.EqualTo(1)); - - var branch = data.OfType().Single(x => x.Id == Guid.Parse("c294a463-6c9c-47a8-b592-01252c5ab2a7")); - - using (Assert.EnterMultipleScope()) - { - Assert.That(branch.OwningProject, Is.EqualTo(Guid.Parse("000e9890-6935-43e6-a5d7-5d7cac601f4c"))); - Assert.That(branch.Name, Is.EqualTo("main")); - Assert.That(branch.Description, Is.Null); - Assert.That(branch.Head, Is.EqualTo(Guid.Parse("6d7ad9fd-6520-4ff2-885b-8c5c129e6c27"))); - Assert.That(branch.Created, Is.EqualTo(DateTime.Parse("2023-03-13T17:53:50.188295-04:00"))); - } - } - } -} +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2025 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Serializer.Json.Tests +{ + using System; + using System.IO; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + + using NUnit.Framework; + + using SysML2.NET.Core.Core.Types; + using SysML2.NET.Core.DTO.Core.Features; + using SysML2.NET.PIM.DTO; + using SysML2.NET.Serializer.Json; + + /// + /// Suite of tests for the + /// + [TestFixture] + public class DeSerializerTestFixture + { + private DeSerializer deSerializer; + + [SetUp] + public void SetUp() + { + this.deSerializer = new DeSerializer(); + } + + [Test] + [TestCase(false)] + [TestCase(true)] + public void Verify_that_iData_from_sysmlcore_json_can_be_deserialized(bool shouldDeserializeDerivedProperties) + { + var fileName = Path.Combine(TestContext.CurrentContext.WorkDirectory, "Data", "projects.000e9890-6935-43e6-a5d7-5d7cac601f4c.commits.6d7ad9fd-6520-4ff2-885b-8c5c129e6c27.elements.json"); + using var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read); + + var data = this.deSerializer.DeSerialize(stream, SerializationModeKind.JSON, SerializationTargetKind.CORE, shouldDeserializeDerivedProperties); + + using (Assert.EnterMultipleScope()) + { + Assert.That(data.Count(), Is.EqualTo(100)); + Assert.That(data.OfType().Count(), Is.EqualTo(30)); + } + + var feature = data.OfType().Single(x => x.Id == Guid.Parse("00a6ef10-d3dc-4741-9029-2c9978c2f083")); + + using (Assert.EnterMultipleScope()) + { + Assert.That(feature.AliasIds, Is.Empty); + Assert.That(feature.ElementId, Is.EqualTo("00a6ef10-d3dc-4741-9029-2c9978c2f083")); + Assert.That(feature.IsAbstract, Is.False); + Assert.That(feature.IsComposite, Is.False); + Assert.That(feature.IsSufficient, Is.False); + Assert.That(feature.IsEnd, Is.False); + Assert.That(feature.IsUnique, Is.True); + Assert.That(feature.DeclaredName, Is.Null); + Assert.That(feature.OwnedRelationship, Is.Empty); + Assert.That(feature.OwningRelationship, Is.EqualTo(Guid.Parse("8a780d8b-61a6-472b-8b80-2564aa9f7c36"))); + Assert.That(feature.DeclaredShortName, Is.Null); + Assert.That(feature.Direction, Is.EqualTo(FeatureDirectionKind.Out)); + } + } + + [Test] + [TestCase(false)] + [TestCase(true)] + public async Task Verify_that_iData_from_sysmlcore_json_can_be_deserialized_async(bool shouldDeserializeDerivedProperties) + { + var fileName = Path.Combine(TestContext.CurrentContext.WorkDirectory, "Data", "projects.000e9890-6935-43e6-a5d7-5d7cac601f4c.commits.6d7ad9fd-6520-4ff2-885b-8c5c129e6c27.elements.json"); + + await using var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read); + + var cts = new CancellationTokenSource(); + + var data = await this.deSerializer.DeSerializeAsync(stream, SerializationModeKind.JSON, SerializationTargetKind.CORE, shouldDeserializeDerivedProperties, cts.Token); + + using (Assert.EnterMultipleScope()) + { + Assert.That(data.Count(), Is.EqualTo(100)); + + Assert.That(data.OfType().Count(), Is.EqualTo(30)); + } + + var feature = data.OfType().Single(x => x.Id == Guid.Parse("00a6ef10-d3dc-4741-9029-2c9978c2f083")); + + using (Assert.EnterMultipleScope()) + { + Assert.That(feature.AliasIds, Is.Empty); + Assert.That(feature.ElementId, Is.EqualTo("00a6ef10-d3dc-4741-9029-2c9978c2f083")); + Assert.That(feature.IsAbstract, Is.False); + Assert.That(feature.IsComposite, Is.False); + Assert.That(feature.IsSufficient, Is.False); + Assert.That(feature.IsEnd, Is.False); + Assert.That(feature.IsUnique, Is.True); + Assert.That(feature.DeclaredName, Is.Null); + Assert.That(feature.OwnedRelationship, Is.Empty); + Assert.That(feature.OwningRelationship, Is.EqualTo(Guid.Parse("8a780d8b-61a6-472b-8b80-2564aa9f7c36"))); + Assert.That(feature.DeclaredShortName, Is.Null); + Assert.That(feature.Direction, Is.EqualTo(FeatureDirectionKind.Out)); + } + } + + [Test] + [TestCase(false)] + [TestCase(true)] + public void Verify_that_projects_from_restapi_json_can_be_deserialized(bool shouldDeserializeDerivedProperties) + { + var fileName = Path.Combine(TestContext.CurrentContext.WorkDirectory, "Data", "projects.json"); + using var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read); + + var data = this.deSerializer.DeSerialize(stream, SerializationModeKind.JSON, SerializationTargetKind.PSM, shouldDeserializeDerivedProperties); + + Assert.That(data.Count(), Is.EqualTo(43)); + + var project = data.OfType().Single(x => x.Id == Guid.Parse("000e9890-6935-43e6-a5d7-5d7cac601f4c")); + + using (Assert.EnterMultipleScope()) + { + Assert.That(project.DefaultBranch, Is.EqualTo(Guid.Parse("c294a463-6c9c-47a8-b592-01252c5ab2a7"))); + Assert.That(project.Name, Is.EqualTo("7b-Variant Configurations Mon Mar 13 17:54:29 EDT 2023")); + Assert.That(project.Description, Is.Null); + } + } + + [Test] + [TestCase(false)] + [TestCase(true)] + public void Verify_that_particular_project_from_restapi_json_can_be_deserialized(bool shouldDeserializeDerivedProperties) + { + var fileName = Path.Combine(TestContext.CurrentContext.WorkDirectory, "Data", "projects.000e9890-6935-43e6-a5d7-5d7cac601f4c.json"); + using var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read); + + var data = this.deSerializer.DeSerialize(stream, SerializationModeKind.JSON, SerializationTargetKind.PSM, shouldDeserializeDerivedProperties); + + Assert.That(data.Count(), Is.EqualTo(1)); + + var project = data.OfType().Single(x => x.Id == Guid.Parse("000e9890-6935-43e6-a5d7-5d7cac601f4c")); + + using (Assert.EnterMultipleScope()) + { + Assert.That(project.DefaultBranch, Is.EqualTo(Guid.Parse("c294a463-6c9c-47a8-b592-01252c5ab2a7"))); + Assert.That(project.Name, Is.EqualTo("7b-Variant Configurations Mon Mar 13 17:54:29 EDT 2023")); + Assert.That(project.Description, Is.Null); + } + } + + [Test] + [TestCase(false)] + [TestCase(true)] + public void Verify_that_particular_project_and_commits_from_restapi_json_can_be_deserialized(bool shouldDeserializeDerivedProperties) + { + var fileName = Path.Combine(TestContext.CurrentContext.WorkDirectory, "Data", "projects.000e9890-6935-43e6-a5d7-5d7cac601f4c.commits.json"); + using var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read); + + var data = this.deSerializer.DeSerialize(stream, SerializationModeKind.JSON, SerializationTargetKind.PSM, shouldDeserializeDerivedProperties); + + Assert.That(data.Count(), Is.EqualTo(1)); + + var firstCommit = data.OfType().Single(x => x.Id == Guid.Parse("6d7ad9fd-6520-4ff2-885b-8c5c129e6c27")); + + using (Assert.EnterMultipleScope()) + { + Assert.That(firstCommit.OwningProject, Is.EqualTo(Guid.Parse("000e9890-6935-43e6-a5d7-5d7cac601f4c"))); + Assert.That(firstCommit.PreviousCommit, Is.Empty); + Assert.That(firstCommit.Description, Is.Null); + Assert.That(firstCommit.Created, Is.EqualTo(DateTime.Parse("2023-03-13T17:53:59.111354-04:00"))); + } + } + + [Test] + [TestCase(false)] + [TestCase(true)] + public void Verify_that_particular_project_and_particular_commit_from_restapi_json_can_be_deserialized(bool shouldDeserializeDerivedProperties) + { + var fileName = Path.Combine(TestContext.CurrentContext.WorkDirectory, "Data", "projects.000e9890-6935-43e6-a5d7-5d7cac601f4c.commits.6d7ad9fd-6520-4ff2-885b-8c5c129e6c27.json"); + using var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read); + + var data = this.deSerializer.DeSerialize(stream, SerializationModeKind.JSON, SerializationTargetKind.PSM, shouldDeserializeDerivedProperties); + + Assert.That(data.Count(), Is.EqualTo(1)); + + var firstCommit = data.OfType().Single(x => x.Id == Guid.Parse("6d7ad9fd-6520-4ff2-885b-8c5c129e6c27")); + + using (Assert.EnterMultipleScope()) + { + Assert.That(firstCommit.OwningProject, Is.EqualTo(Guid.Parse("000e9890-6935-43e6-a5d7-5d7cac601f4c"))); + Assert.That(firstCommit.PreviousCommit, Is.Empty); + Assert.That(firstCommit.Description, Is.Null); + Assert.That(firstCommit.Created, Is.EqualTo(DateTime.Parse("2023-03-13T17:53:59.111354-04:00"))); + } + } + + [Test] + [TestCase(false)] + [TestCase(true)] + public void Verify_that_particular_project_and_branches_from_restapi_json_can_be_deserialized(bool shouldDeserializeDerivedProperties) + { + var fileName = Path.Combine(TestContext.CurrentContext.WorkDirectory, "Data", "projects.000e9890-6935-43e6-a5d7-5d7cac601f4c.branches.json"); + using var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read); + + var data = this.deSerializer.DeSerialize(stream, SerializationModeKind.JSON, SerializationTargetKind.PSM, shouldDeserializeDerivedProperties); + + Assert.That(data.Count(), Is.EqualTo(1)); + + var branch = data.OfType().Single(x => x.Id == Guid.Parse("c294a463-6c9c-47a8-b592-01252c5ab2a7")); + + using (Assert.EnterMultipleScope()) + { + Assert.That(branch.OwningProject, Is.EqualTo(Guid.Parse("000e9890-6935-43e6-a5d7-5d7cac601f4c"))); + Assert.That(branch.Name, Is.EqualTo("main")); + Assert.That(branch.Description, Is.Null); + Assert.That(branch.Head, Is.EqualTo(Guid.Parse("6d7ad9fd-6520-4ff2-885b-8c5c129e6c27"))); + Assert.That(branch.Created, Is.EqualTo(DateTime.Parse("2023-03-13T17:53:50.188295-04:00"))); + } + } + } +} diff --git a/SysML2.NET.Serializer.Json/ModelInterchange/InterchangeChecksumDeserializer.cs b/SysML2.NET.Serializer.Json/ModelInterchange/InterchangeChecksumDeserializer.cs index f6e29fa48..77e6025ba 100644 --- a/SysML2.NET.Serializer.Json/ModelInterchange/InterchangeChecksumDeserializer.cs +++ b/SysML2.NET.Serializer.Json/ModelInterchange/InterchangeChecksumDeserializer.cs @@ -23,7 +23,7 @@ namespace SysML2.NET.Serializer.Json.ModelInterchange using System; using System.Buffers; using System.Text.Json; - + using Extensions.ModelInterchange; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; diff --git a/SysML2.NET.Serializer.Json/PIM/ApiDeSerializationProvider.cs b/SysML2.NET.Serializer.Json/PIM/ApiDeSerializationProvider.cs index 7cef63a0d..1f4c2a12d 100644 --- a/SysML2.NET.Serializer.Json/PIM/ApiDeSerializationProvider.cs +++ b/SysML2.NET.Serializer.Json/PIM/ApiDeSerializationProvider.cs @@ -45,6 +45,7 @@ private static readonly DictionaryTrue True True - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/SysML2.NET/PIM/DTO/Branch.cs b/SysML2.NET/PIM/DTO/Branch.cs index ca048e762..a3ebe2c50 100644 --- a/SysML2.NET/PIM/DTO/Branch.cs +++ b/SysML2.NET/PIM/DTO/Branch.cs @@ -31,8 +31,13 @@ public class Branch : CommitReference { /// /// Gets or sets the commit to which the branch is currently pointing. It represents the latest state of the - /// on the given branch + /// on the given branch. Redefines — + /// both properties expose the same underlying value. /// - public Guid Head { get; set; } + public Guid Head + { + get => this.ReferencedCommit; + set => this.ReferencedCommit = value; + } } } diff --git a/SysML2.NET/PIM/DTO/Commit.cs b/SysML2.NET/PIM/DTO/Commit.cs index 8eded0d5a..bb8ff9f89 100644 --- a/SysML2.NET/PIM/DTO/Commit.cs +++ b/SysML2.NET/PIM/DTO/Commit.cs @@ -52,8 +52,17 @@ public class Commit : Record public Guid OwningProject { get; set; } /// - /// Gets or sets the immediately preceding s + /// Gets or sets the set of immediately preceding s. A + /// has more than one previous when it merges branches; the order of the + /// entries is significant (first parent first). /// - public Guid PreviousCommit { get; set; } + public List PreviousCommit { get; set; } = []; + + /// + /// Gets or sets the set of records representing data that is created, + /// updated, or deleted in the . A deletion is represented by a + /// whose is null. + /// + public List Change { get; set; } = []; } } diff --git a/SysML2.NET/PIM/DTO/CommitReference.cs b/SysML2.NET/PIM/DTO/CommitReference.cs index 29e62cb5f..91eb66178 100644 --- a/SysML2.NET/PIM/DTO/CommitReference.cs +++ b/SysML2.NET/PIM/DTO/CommitReference.cs @@ -37,9 +37,16 @@ public abstract class CommitReference : Record public DateTime Created { get; set; } /// - /// Gets or sets the timestamp at which the was deleted + /// Gets or sets the timestamp at which the was deleted, or null + /// when the has not been deleted /// - public DateTime Deleted { get; set; } + public DateTime? Deleted { get; set; } + + /// + /// Gets or sets the that the references. + /// and redefine this property. + /// + public Guid ReferencedCommit { get; set; } /// /// Gets or sets the Project that owns the given diff --git a/SysML2.NET/PIM/DTO/DataIdentity.cs b/SysML2.NET/PIM/DTO/DataIdentity.cs index 4183ead12..2c28825ab 100644 --- a/SysML2.NET/PIM/DTO/DataIdentity.cs +++ b/SysML2.NET/PIM/DTO/DataIdentity.cs @@ -30,5 +30,17 @@ namespace SysML2.NET.PIM.DTO /// public class DataIdentity : Record { + /// + /// Gets or sets the derived reference to the in a in + /// which the identified data was created, or null when not computed by the service + /// + public Guid? CreatedAt { get; set; } + + /// + /// Gets or sets the derived reference to the in a in + /// which the identified data was deleted, or null when the data is not deleted or the value is + /// not computed by the service + /// + public Guid? DeletedAt { get; set; } } } diff --git a/SysML2.NET/PIM/DTO/PrimitiveConstraint.cs b/SysML2.NET/PIM/DTO/PrimitiveConstraint.cs index f1de4a81a..aa703f86c 100644 --- a/SysML2.NET/PIM/DTO/PrimitiveConstraint.cs +++ b/SysML2.NET/PIM/DTO/PrimitiveConstraint.cs @@ -26,7 +26,7 @@ namespace SysML2.NET.PIM.DTO /// a subtype of Constraint that represents simple conditions that be modeled using the /// property-operator -value tuple. e.g. mass <= 4 kg, or type instanceOf Generalization /// - public class PrimitiveConstraint + public class PrimitiveConstraint : Constraint { /// /// Gets or sets the name of a property of Element or its subtypes that is being constrained diff --git a/SysML2.NET/PIM/DTO/ProjectUsage.cs b/SysML2.NET/PIM/DTO/ProjectUsage.cs index 03ce74367..f6d0d14fd 100644 --- a/SysML2.NET/PIM/DTO/ProjectUsage.cs +++ b/SysML2.NET/PIM/DTO/ProjectUsage.cs @@ -25,20 +25,16 @@ namespace SysML2.NET.PIM.DTO using SysML2.NET.Common; /// - /// a realization of that represents the use of a in the context of another - /// . + /// a subclass of and a realization of that represents the use + /// of a in the context of another . The constraint + /// usedProject = usedProjectCommit.owningProject applies. /// - public class ProjectUsage : IData + public class ProjectUsage : Record { - /// - /// Gets or sets the unique identifier - /// - public Guid Id { get; set; } - /// /// Gets or sets the reference to the of the being used. /// - public Guid UsedCommit { get; set; } + public Guid UsedProjectCommit { get; set; } /// /// Gets or sets the reference to the of the being used. diff --git a/SysML2.NET/PIM/DTO/Query.cs b/SysML2.NET/PIM/DTO/Query.cs index e178a3d1d..478c9a167 100644 --- a/SysML2.NET/PIM/DTO/Query.cs +++ b/SysML2.NET/PIM/DTO/Query.cs @@ -52,5 +52,11 @@ public class Query : Record /// Query is the owning Project /// public List Scope { get; set; } = []; + + /// + /// Gets or sets the list of properties of Data (or its realizations) used for sorting the query + /// response. The order of the properties in the list governs the sorting order. + /// + public List OrderBy { get; set; } = []; } } diff --git a/SysML2.NET/PIM/DTO/Tag.cs b/SysML2.NET/PIM/DTO/Tag.cs index e0e50414e..32ca2abfd 100644 --- a/SysML2.NET/PIM/DTO/Tag.cs +++ b/SysML2.NET/PIM/DTO/Tag.cs @@ -30,8 +30,13 @@ namespace SysML2.NET.PIM.DTO public class Tag : CommitReference { /// - /// Gets or sets a reference to the tagged + /// Gets or sets a reference to the tagged . Redefines + /// — both properties expose the same underlying value. /// - public Guid TaggedCommit { get; set; } + public Guid TaggedCommit + { + get => this.ReferencedCommit; + set => this.ReferencedCommit = value; + } } } diff --git a/SysML2.NET/PIM/POCO/Branch.cs b/SysML2.NET/PIM/POCO/Branch.cs index 9f549c9ff..519deaba5 100644 --- a/SysML2.NET/PIM/POCO/Branch.cs +++ b/SysML2.NET/PIM/POCO/Branch.cs @@ -29,8 +29,13 @@ public class Branch : CommitReference { /// /// Gets or sets the commit to which the branch is currently pointing. It represents the latest state of the - /// on the given branch + /// on the given branch. Redefines — + /// both properties expose the same underlying value. /// - public Commit Head { get; set; } + public Commit Head + { + get => this.ReferencedCommit; + set => this.ReferencedCommit = value; + } } } \ No newline at end of file diff --git a/SysML2.NET/PIM/POCO/CommitReference.cs b/SysML2.NET/PIM/POCO/CommitReference.cs index 79a176c18..2b581f812 100644 --- a/SysML2.NET/PIM/POCO/CommitReference.cs +++ b/SysML2.NET/PIM/POCO/CommitReference.cs @@ -35,9 +35,16 @@ public abstract class CommitReference : Record public DateTime Created { get; set; } /// - /// Gets or sets the timestamp at which the was deleted + /// Gets or sets the timestamp at which the was deleted, or null + /// when the has not been deleted /// - public DateTime Deleted { get; set; } + public DateTime? Deleted { get; set; } + + /// + /// Gets or sets the that the references. + /// and redefine this property. + /// + public Commit ReferencedCommit { get; set; } /// /// Gets or sets the name of the diff --git a/SysML2.NET/PIM/POCO/DataIdentity.cs b/SysML2.NET/PIM/POCO/DataIdentity.cs index dd3fe0fd9..11afdc51a 100644 --- a/SysML2.NET/PIM/POCO/DataIdentity.cs +++ b/SysML2.NET/PIM/POCO/DataIdentity.cs @@ -32,6 +32,19 @@ public class DataIdentity : Record /// /// Gets or sets the the set of records representing all versions of the given /// - public List Version = new List(); + public List Version { get; set; } = []; + + /// + /// Gets or sets the derived reference to the in a in + /// which the identified data was created, or null when not computed + /// + public Commit CreatedAt { get; set; } + + /// + /// Gets or sets the derived reference to the in a in + /// which the identified data was deleted, or null when the data is not deleted or the value is + /// not computed + /// + public Commit DeletedAt { get; set; } } } diff --git a/SysML2.NET/PIM/POCO/ProjectUsage.cs b/SysML2.NET/PIM/POCO/ProjectUsage.cs index 29e1a375c..1bdf13781 100644 --- a/SysML2.NET/PIM/POCO/ProjectUsage.cs +++ b/SysML2.NET/PIM/POCO/ProjectUsage.cs @@ -25,20 +25,16 @@ namespace SysML2.NET.PIM.POCO using SysML2.NET.Common; /// - /// a realization of that represents the use of a in the context of another - /// . + /// a subclass of and a realization of that represents the use + /// of a in the context of another . The constraint + /// usedProject = usedProjectCommit.owningProject applies. /// - public class ProjectUsage : IData + public class ProjectUsage : Record { - /// - /// Gets or sets the unique identifier - /// - public Guid Id { get; set; } - /// /// Gets or sets the reference to the of the being used. /// - public Commit UsedCommit { get; set; } + public Commit UsedProjectCommit { get; set; } /// /// Gets or sets the reference to the being used. diff --git a/SysML2.NET/PIM/POCO/Tag.cs b/SysML2.NET/PIM/POCO/Tag.cs index 746f35548..7ea86e7dd 100644 --- a/SysML2.NET/PIM/POCO/Tag.cs +++ b/SysML2.NET/PIM/POCO/Tag.cs @@ -30,9 +30,14 @@ namespace SysML2.NET.PIM.POCO public class Tag : CommitReference { /// - /// Gets or sets a reference to the which the Tag is pointing. It represents a - /// -of-interest in the owning . + /// Gets or sets a reference to the which the Tag is pointing. It represents a + /// -of-interest in the owning . Redefines + /// — both properties expose the same underlying value. /// - public Commit TaggedCommit { get; set; } + public Commit TaggedCommit + { + get => this.ReferencedCommit; + set => this.ReferencedCommit = value; + } } } diff --git a/TESTING.md b/TESTING.md index 7c6bf6173..0ed9f7120 100644 --- a/TESTING.md +++ b/TESTING.md @@ -221,7 +221,30 @@ This is the testing-side companion to the broader scope-discipline feedback: a t --- -## 10. Anti-pattern checklist (what NOT to do) +## 10. Integration fixtures (Docker-backed) + +Fixtures that need external infrastructure — currently the SQL-schema fixtures in +`SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/` (`SqlSchemaSmokeTestFixture`, +`SqlSchemaConcurrencyTestFixture`), which run PostgreSQL 18 via `Testcontainers.PostgreSql` — +follow these rules: + +- **Mark the fixture `[Category("Integration")]`.** CI's test invocation filters + `TestCategory!=Integration`, so these run locally only; run them explicitly with + `dotnet test … --filter TestCategory=Integration` or from the IDE. +- **Docker unavailable must SKIP, not fail**: start the container in `[OneTimeSetUp]` and + translate a start failure into `Assert.Ignore(...)` (see `PostgreSqlSchemaTestHost`, which + also resolves Docker Desktop's modern `dockerDesktopLinuxEngine` named pipe on Windows). +- **Test the live artifact, not a checked-in copy**: the SQL fixtures generate the schema + in-process (`SQLSchemaGenerator.GenerateSqlSchemaAsync` returns the DDL) so they also catch + generator/registry drift. +- **Self-calibrating assertions over hardcoded counts**: the smoke fixture asserts the number + of `PASS` notices equals the number of `RAISE NOTICE 'PASS` occurrences in the script + itself — extending a SQL test never requires touching the fixture. +- **No latency assertions** in concurrency tests — report timings via `TestContext.Out`, + assert only deterministic invariants (counts, win/loss tallies, verifier PASSes). +- Expect the **first run to be slow** (docker image pull). + +## 11. Anti-pattern checklist (what NOT to do) - ❌ Splitting one method-under-test into many `…_WhenX_DoesY` tests **when the scenarios share setup and could trivially combine** (combined-form is the default — see §2). Splitting is acceptable when each scenario has a genuinely distinct, complex setup. - ❌ `Assert.Throws` / `Assert.IsTrue` / `Assert.AreEqual` / `Assert.IsNull` (any legacy NUnit API). @@ -234,7 +257,7 @@ This is the testing-side companion to the broader scope-discipline feedback: a t --- -## 11. Reference fixtures +## 12. Reference fixtures When in doubt, model new fixtures on these (they reflect the current canonical styles):