diff --git a/README.md b/README.md
index 4ce3a2a..9b2a4ce 100644
--- a/README.md
+++ b/README.md
@@ -146,11 +146,28 @@ The central types in this area are:
## Databases
-The Java implementation is SQL-oriented today. The project currently includes
-dependencies and test coverage around:
+The Java implementation is SQL-oriented today. The project includes JDBC drivers for:
- PostgreSQL
- H2
+- Microsoft SQL Server (`mssql-jdbc`), used by Farnam Control 4.0 and similar apps.
+
+Path-style URLs use `SQLTerminal`, for example:
+
+```text
+sqlserver://user:password@host:1433/databaseName
+```
+
+Optional semicolon properties after the database segment are forwarded into the JDBC URL (for example `encrypt=true`).
+
+### Integration tests against SQL Server
+
+Tests that need a live database read **`FC4_SQL_URL` first**, then **`DB_URL`** (historic convention). Example:
+
+```bash
+export FC4_SQL_URL='sqlserver://user:pass@host:1433/mydb'
+./gradlew test --tests com.reliancy.dbo.sql.SQLTerminalSqlServerSmokeTest
+```
The broader API is designed so application code can stay closer to entities and
actions than to vendor-specific SQL strings.
diff --git a/build.gradle b/build.gradle
index 219eb07..66630c0 100644
--- a/build.gradle
+++ b/build.gradle
@@ -29,10 +29,12 @@ dependencies {
// Database drivers
implementation 'com.h2database:h2:2.3.232'
implementation 'org.postgresql:postgresql:42.7.4'
+ implementation 'com.microsoft.sqlserver:mssql-jdbc:12.8.1.jre11'
implementation 'com.zaxxer:HikariCP:5.1.0'
// Testing
testImplementation "junit:junit:4.13.2"
+ testRuntimeOnly 'org.slf4j:slf4j-simple:2.0.16'
}
repositories {
diff --git a/src/main/java/com/reliancy/dbo/meta/ModelManager.java b/src/main/java/com/reliancy/dbo/meta/ModelManager.java
new file mode 100644
index 0000000..ff2a9e6
--- /dev/null
+++ b/src/main/java/com/reliancy/dbo/meta/ModelManager.java
@@ -0,0 +1,326 @@
+package com.reliancy.dbo.meta;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+import com.reliancy.dbo.Action;
+import com.reliancy.dbo.DBO;
+import com.reliancy.dbo.Entity;
+import com.reliancy.dbo.Field;
+import com.reliancy.dbo.sql.SQLTerminal;
+import com.reliancy.rec.JSON;
+import com.reliancy.rec.JSONEncoder;
+import com.reliancy.rec.Rec;
+import com.reliancy.rec.Slot;
+
+/**
+ * Reusable management helper for bstore-backed application CLIs.
+ *
+ *
This class centralizes the repetitive schema and change-log orchestration
+ * commonly implemented in application-specific CLIs:
+ *
+ * - entity recall from model classes
+ * - schema ensuring
+ * - metadata-driven migrate/snapshot flows
+ * - change capture to JSON revision files
+ * - change replay from JSON revision files
+ * - listing persisted {@link ChangeEvent} rows
+ *
+ *
+ * Application-specific concerns such as seeding and environment lookup stay
+ * outside this helper.
+ */
+public class ModelManager {
+ private final List entities;
+ private final Supplier terminalSupplier;
+ private final Path migrationDir;
+
+ @SafeVarargs
+ public ModelManager(String dbUrl, Path migrationDir, Class extends DBO>... modelClasses) {
+ this(() -> new SQLTerminal(dbUrl), migrationDir, recallEntities(modelClasses));
+ }
+
+ public ModelManager(Supplier terminalSupplier, Path migrationDir, Iterable entities) {
+ this.terminalSupplier = Objects.requireNonNull(terminalSupplier, "terminalSupplier");
+ this.migrationDir = migrationDir;
+ this.entities = new ArrayList<>();
+ if (entities != null) {
+ for (Entity entity : entities) {
+ if (entity != null) {
+ this.entities.add(entity);
+ }
+ }
+ }
+ }
+
+ @SafeVarargs
+ private static List recallEntities(Class extends DBO>... modelClasses) {
+ List entities = new ArrayList<>();
+ if (modelClasses != null) {
+ for (Class extends DBO> modelClass : modelClasses) {
+ if (modelClass != null) {
+ entities.add(Entity.recall(modelClass));
+ }
+ }
+ }
+ return entities;
+ }
+
+ public List getEntities() {
+ return new ArrayList<>(entities);
+ }
+
+ public Path defaultRevisionPath(String migrationId) {
+ if (migrationDir == null) {
+ throw new IllegalStateException("migrationDir is not configured");
+ }
+ return migrationDir.resolve(migrationId + ".json");
+ }
+
+ public void ensureSchema() throws IOException {
+ try (SQLTerminal terminal = terminalSupplier.get()) {
+ MetaTerminal meta = terminal.meta(null);
+ for (Entity entity : entities) {
+ meta.assertEntity("model-manager", "ensure-schema", entity);
+ }
+ }
+ }
+
+ public void migrate(String originatorId, String migrationId) throws IOException {
+ try (SQLTerminal terminal = terminalSupplier.get()) {
+ terminal.meta(null).migrate(originatorId, migrationId, entities.toArray(Entity[]::new));
+ }
+ }
+
+ public List snapshot() throws IOException {
+ try (SQLTerminal terminal = terminalSupplier.get()) {
+ MetaTerminal meta = terminal.meta(null);
+ List ret = new ArrayList<>();
+ for (Entity entity : entities) {
+ ret.add(meta.discover_entity(entity));
+ }
+ return ret;
+ }
+ }
+
+ public List captureChanges(String originatorId, String migrationId) throws IOException {
+ try (SQLTerminal terminal = terminalSupplier.get()) {
+ MetaTerminal meta = terminal.meta(null);
+ List ret = new ArrayList<>();
+ for (Entity entity : entities) {
+ EntityDefinition desired = meta.discover_entity(entity);
+ EntityDefinition actual = meta.discover_entity(entity.getName());
+ for (ChangeEvent change : meta.discover_changes(originatorId, migrationId, actual, desired)) {
+ ret.add(change);
+ }
+ }
+ return ret;
+ }
+ }
+
+ public List captureChanges(String originatorId, String migrationId, Path outputPath) throws IOException {
+ List changes = captureChanges(originatorId, migrationId);
+ writeChanges(changes, outputPath);
+ return changes;
+ }
+
+ public void writeChanges(Iterable changes, Path outputPath) throws IOException {
+ List