refactor(dynamic): enhance migration harnesses with Prisma, Sequelize-CLI, Laravel, Rails, Flask support; implement fallback logic and extend SQL framework integration

This commit is contained in:
elipeter 2026-05-27 15:06:51 -05:00
parent ed8decb510
commit fd39304eed
7 changed files with 431 additions and 14 deletions

View file

@ -133,15 +133,25 @@ const NODE_KNEX: &[VersionedPackage] = &[VersionedPackage {
name: "knex",
version: "^3.1.0",
}];
const NODE_PRISMA: &[VersionedPackage] = &[VersionedPackage {
name: "@prisma/client",
version: "^5.14.0",
}];
const NODE_PRISMA: &[VersionedPackage] = &[
VersionedPackage {
name: "@prisma/client",
version: "^5.14.0",
},
VersionedPackage {
name: "prisma",
version: "^5.14.0",
},
];
const NODE_SEQUELIZE: &[VersionedPackage] = &[
VersionedPackage {
name: "sequelize",
version: "^6.37.3",
},
VersionedPackage {
name: "sequelize-cli",
version: "^6.6.2",
},
VersionedPackage {
name: "sqlite3",
version: "^5.1.7",

View file

@ -1582,6 +1582,11 @@ fn emit_migration(spec: &HarnessSpec, version: Option<&str>, is_typescript: bool
let (preamble, entry_subpath) = nyx_js_preamble(spec, is_typescript);
let handler = &spec.entry_name;
let version_repr = version.unwrap_or("<no-version>");
let framework = spec
.framework
.as_ref()
.map(|binding| binding.adapter.as_str())
.unwrap_or("");
let body = format!(
r#"{preamble}
// Phase 21 (Track M.3) — migration.
@ -1591,6 +1596,7 @@ if (_h == null) {{
process.stderr.write('NYX_HANDLER_NOT_FOUND: ' + {handler:?} + '\n');
process.exit(78);
}}
const _nyxFramework = {framework:?};
function _nyxLooksLikeSql(sql) {{
const upper = String(sql).toUpperCase();
return ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'CREATE', 'ALTER', 'DROP'].some((k) => upper.includes(k));
@ -1618,6 +1624,124 @@ function _nyxMigrationSqlRecord(sql, driver) {{
const sqliteDriver = _nyxTryExecuteSqlite(sql);
__nyx_stub_sql_record(String(sql), {{ driver: driver, source: 'migration', sqlite_driver: sqliteDriver }});
}}
function _nyxCliBin(names) {{
const fs = require('fs');
const path = require('path');
const suffix = process.platform === 'win32' ? '.cmd' : '';
for (const name of names) {{
const candidate = path.join(__dirname, 'node_modules', '.bin', name + suffix);
if (fs.existsSync(candidate)) return candidate;
}}
return null;
}}
function _nyxWriteSequelizeCliConfig(configPath) {{
const open = String.fromCharCode(123);
const close = String.fromCharCode(125);
const lines = [
"const fs = require('fs');",
"function record(sql) " + open,
" const p = process.env.NYX_SQL_LOG;",
" if (!p || !sql) return;",
" let out = '# driver: sequelize-cli\\n# source: migration-cli\\n' + String(sql);",
" if (!out.endsWith('\\n')) out += '\\n';",
" try " + open + " fs.appendFileSync(p, out); " + close + " catch (_) " + open + close,
close,
"module.exports = " + open + " nyx: " + open + " dialect: 'sqlite', storage: process.env.NYX_SQL_ENDPOINT || 'nyx.sqlite', logging: record " + close + " " + close + ";",
];
require('fs').writeFileSync(configPath, lines.join('\n'));
}}
function _nyxTrySequelizeCli() {{
if (_nyxFramework !== 'migration-sequelize') return false;
try {{
const fs = require('fs');
const path = require('path');
const cp = require('child_process');
const bin = _nyxCliBin(['sequelize', 'sequelize-cli']);
if (!bin) return false;
const migrationsDir = path.join(__dirname, 'migrations');
fs.mkdirSync(migrationsDir, {{ recursive: true }});
fs.writeFileSync(
path.join(migrationsDir, '00000000000000-nyx-migration.js'),
"module.exports = require('../" + {entry_subpath:?} + "');\n"
);
const configPath = path.join(__dirname, 'nyx-sequelize-config.cjs');
_nyxWriteSequelizeCliConfig(configPath);
const result = cp.spawnSync(bin, [
'db:migrate',
'--migrations-path', migrationsDir,
'--config', configPath,
'--env', 'nyx',
], {{
cwd: __dirname,
env: process.env,
encoding: 'utf8',
}});
if (result.stdout) process.stdout.write(result.stdout);
if (result.stderr) process.stderr.write(result.stderr);
return result.status === 0;
}} catch (e) {{
process.stderr.write('NYX_SEQUELIZE_CLI_FALLBACK: ' + (e && e.message ? e.message : String(e)) + '\n');
return false;
}}
}}
function _nyxRecordPrismaMigrationFiles(schemaPath) {{
try {{
const fs = require('fs');
const path = require('path');
const root = path.dirname(schemaPath);
const migrations = path.join(root, 'migrations');
if (!fs.existsSync(migrations)) return;
for (const dirent of fs.readdirSync(migrations, {{ withFileTypes: true }})) {{
if (!dirent.isDirectory()) continue;
const sqlPath = path.join(migrations, dirent.name, 'migration.sql');
if (!fs.existsSync(sqlPath)) continue;
const sql = fs.readFileSync(sqlPath, 'utf8');
_nyxMigrationSqlRecord(sql, 'prisma-cli');
}}
}} catch (e) {{
process.stderr.write('NYX_PRISMA_CLI_SQLLOG_FALLBACK: ' + (e && e.message ? e.message : String(e)) + '\n');
}}
}}
function _nyxTryPrismaCli() {{
if (_nyxFramework !== 'migration-prisma') return false;
try {{
const fs = require('fs');
const path = require('path');
const cp = require('child_process');
const bin = _nyxCliBin(['prisma']);
if (!bin) return false;
const candidates = [
path.join(__dirname, 'prisma', 'schema.prisma'),
path.join(__dirname, 'schema.prisma'),
];
const schemaPath = candidates.find((p) => fs.existsSync(p));
if (!schemaPath) return false;
if (process.env.NYX_SQL_ENDPOINT && !process.env.DATABASE_URL) {{
process.env.DATABASE_URL = 'file:' + process.env.NYX_SQL_ENDPOINT;
}}
let result = cp.spawnSync(bin, ['migrate', 'deploy', '--schema', schemaPath], {{
cwd: __dirname,
env: process.env,
encoding: 'utf8',
}});
if (result.status !== 0) {{
process.stderr.write('NYX_PRISMA_CLI_DEPLOY_FALLBACK: ' + (result.stderr || result.stdout || '') + '\n');
result = cp.spawnSync(bin, ['db', 'push', '--skip-generate', '--schema', schemaPath], {{
cwd: __dirname,
env: process.env,
encoding: 'utf8',
}});
}}
if (result.stdout) process.stdout.write(result.stdout);
if (result.stderr) process.stderr.write(result.stderr);
if (result.status !== 0) return false;
_nyxRecordPrismaMigrationFiles(schemaPath);
return true;
}} catch (e) {{
process.stderr.write('NYX_PRISMA_CLI_FALLBACK: ' + (e && e.message ? e.message : String(e)) + '\n');
return false;
}}
}}
function _nyxTryRealSequelize() {{
try {{
const SequelizeLib = require('sequelize');
@ -1732,6 +1856,8 @@ async function _nyxTryRealPrismaClient() {{
}}
(async () => {{
try {{
if (_nyxTryPrismaCli()) return;
if (_nyxTrySequelizeCli()) return;
_realPrisma = await _nyxTryRealPrismaClient();
if (_realPrisma && _realPrisma.prisma) {{
_prisma = _realPrisma.prisma;
@ -1769,6 +1895,8 @@ async function _nyxTryRealPrismaClient() {{
preamble = preamble,
handler = handler,
version = version_repr,
framework = framework,
entry_subpath = entry_subpath,
);
HarnessSource {
source: body,
@ -3105,7 +3233,7 @@ fn message_handler_dependency_files(spec: &HarnessSpec) -> Vec<(String, String)>
}
fn framework_dependency_files(spec: &HarnessSpec) -> Vec<(String, String)> {
if spec.expected_cap != crate::labels::Cap::CODE_EXEC {
if !should_stage_framework_dependency_files(spec) {
return Vec::new();
}
let Some(adapter) = spec.framework.as_ref().map(|b| b.adapter.as_str()) else {
@ -3134,6 +3262,14 @@ fn framework_dependency_files(spec: &HarnessSpec) -> Vec<(String, String)> {
]
}
fn should_stage_framework_dependency_files(spec: &HarnessSpec) -> bool {
spec.expected_cap == crate::labels::Cap::CODE_EXEC
|| matches!(
&spec.entry_kind,
crate::evidence::EntryKind::Migration { .. }
)
}
fn js_message_handler_deps(source: &str) -> Vec<(&'static str, &'static str)> {
let mut deps = Vec::new();
for raw_line in source.lines() {

View file

@ -3135,7 +3135,28 @@ function __nyx_make_middleware_request(string $payload) {{
$req = __nyx_make_middleware_request($payload);
$next = function ($r) {{ return $r; }};
function __nyx_try_laravel_pipeline(string $handler, $req, callable $next): bool {{
if (!class_exists('Illuminate\\Pipeline\\Pipeline')) return false;
try {{
$container = class_exists('Illuminate\\Container\\Container')
? new Illuminate\Container\Container()
: null;
$pipeline = $container === null
? new Illuminate\Pipeline\Pipeline()
: new Illuminate\Pipeline\Pipeline($container);
$result = $pipeline->send($req)->through([$handler])->then($next);
if ($result !== null) echo (string)$result . "\n";
return true;
}} catch (Throwable $e) {{
fwrite(STDERR, 'NYX_LARAVEL_PIPELINE_FALLBACK: ' . get_class($e) . ': ' . $e->getMessage() . "\n");
return false;
}}
}}
if (class_exists({handler:?})) {{
if (__nyx_try_laravel_pipeline({handler:?}, $req, $next)) {{
exit(0);
}}
$inst = new {handler}();
if (method_exists($inst, 'handle')) {{
try {{
@ -3217,7 +3238,88 @@ function __nyx_try_execute_migration_sqlite($value): string {{
}}
}}
function __nyx_snake_migration_name(string $class): string {{
$base = preg_replace('/[^A-Za-z0-9]+/', '_', $class);
$snake = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $base));
return trim($snake, '_') ?: 'nyx_migration';
}}
function __nyx_boot_laravel_database(): bool {{
if (!class_exists('Illuminate\\Database\\Capsule\\Manager')) return false;
try {{
$endpoint = getenv('NYX_SQL_ENDPOINT');
if ($endpoint === false || $endpoint === '') $endpoint = ':memory:';
$capsule = new Illuminate\Database\Capsule\Manager();
$capsule->addConnection([
'driver' => 'sqlite',
'database' => $endpoint,
'prefix' => '',
]);
$capsule->setAsGlobal();
$capsule->bootEloquent();
$db = $capsule->getDatabaseManager();
$GLOBALS['__nyx_laravel_db'] = $db;
if (class_exists('Illuminate\\Container\\Container')) {{
$container = new Illuminate\Container\Container();
$container->instance('db', $db);
$container->instance('db.connection', $db->connection());
if (class_exists('Illuminate\\Support\\Facades\\Facade')) {{
Illuminate\Support\Facades\Facade::setFacadeApplication($container);
}}
}}
try {{
$db->connection()->listen(function ($query) {{
$sql = is_object($query) && isset($query->sql) ? $query->sql : (string)$query;
__nyx_record_migration_result($sql, 'laravel-listener');
}});
}} catch (Throwable $e) {{}}
return true;
}} catch (Throwable $e) {{
fwrite(STDERR, 'NYX_LARAVEL_DB_BOOTSTRAP_FALLBACK: ' . get_class($e) . ': ' . $e->getMessage() . "\n");
return false;
}}
}}
function __nyx_try_laravel_migrator(string $class): bool {{
if (!class_exists($class)) return false;
if (!class_exists('Illuminate\\Database\\Migrations\\Migrator')) return false;
if (!class_exists('Illuminate\\Database\\Migrations\\DatabaseMigrationRepository')) return false;
if (!class_exists('Illuminate\\Filesystem\\Filesystem')) return false;
if (!__nyx_boot_laravel_database()) return false;
try {{
$db = $GLOBALS['__nyx_laravel_db'] ?? null;
if (!$db) return false;
}} catch (Throwable $e) {{
try {{
$db = Illuminate\Support\Facades\DB::getFacadeRoot();
}} catch (Throwable $inner) {{
fwrite(STDERR, 'NYX_LARAVEL_MIGRATOR_DB_FALLBACK: ' . get_class($inner) . ': ' . $inner->getMessage() . "\n");
return false;
}}
}}
try {{
$repo = new Illuminate\Database\Migrations\DatabaseMigrationRepository($db, 'migrations');
if (!$repo->repositoryExists()) {{
$repo->createRepository();
}}
$files = new Illuminate\Filesystem\Filesystem();
$migrator = new Illuminate\Database\Migrations\Migrator($repo, $db, $files);
$dir = __DIR__ . '/nyx_laravel_migrations';
if (!is_dir($dir)) mkdir($dir, 0777, true);
$file = $dir . '/2026_01_01_000000_' . __nyx_snake_migration_name($class) . '.php';
file_put_contents($file, "<?php\nrequire_once __DIR__ . '/../entry.php';\n");
$migrator->run([$dir], ['pretend' => false]);
return true;
}} catch (Throwable $e) {{
fwrite(STDERR, 'NYX_LARAVEL_MIGRATOR_FALLBACK: ' . get_class($e) . ': ' . $e->getMessage() . "\n");
return false;
}}
}}
if (class_exists({handler:?})) {{
if (__nyx_try_laravel_migrator({handler:?})) {{
exit(0);
}}
$inst = new {handler}();
if (method_exists($inst, 'up')) {{
try {{
@ -3258,7 +3360,7 @@ if (class_exists({handler:?})) {{
}
fn framework_dependency_files(spec: &HarnessSpec) -> Vec<(String, String)> {
if spec.expected_cap != crate::labels::Cap::CODE_EXEC {
if !should_stage_framework_dependency_files(spec) {
return Vec::new();
}
let Some(adapter) = spec.framework.as_ref().map(|b| b.adapter.as_str()) else {
@ -3281,6 +3383,14 @@ fn framework_dependency_files(spec: &HarnessSpec) -> Vec<(String, String)> {
vec![("composer.json".to_owned(), body)]
}
fn should_stage_framework_dependency_files(spec: &HarnessSpec) -> bool {
spec.expected_cap == crate::labels::Cap::CODE_EXEC
|| matches!(
&spec.entry_kind,
crate::evidence::EntryKind::Migration { .. }
)
}
fn build_call_expr(spec: &HarnessSpec, shape: PhpShape, func: &str) -> String {
match shape {
PhpShape::TopLevelScript => "null".to_owned(),

View file

@ -4026,7 +4026,7 @@ fn message_handler_dependency_files(spec: &HarnessSpec) -> Vec<(String, String)>
}
fn framework_dependency_files(spec: &HarnessSpec) -> Vec<(String, String)> {
if spec.expected_cap != crate::labels::Cap::CODE_EXEC {
if !should_stage_framework_dependency_files(spec) {
return Vec::new();
}
let Some(adapter) = spec.framework.as_ref().map(|b| b.adapter.as_str()) else {
@ -4049,6 +4049,14 @@ fn framework_dependency_files(spec: &HarnessSpec) -> Vec<(String, String)> {
vec![("requirements.txt".to_owned(), body)]
}
fn should_stage_framework_dependency_files(spec: &HarnessSpec) -> bool {
spec.expected_cap == crate::labels::Cap::CODE_EXEC
|| matches!(
&spec.entry_kind,
crate::evidence::EntryKind::Migration { .. }
)
}
fn python_message_handler_deps(source: &str) -> Vec<&'static str> {
let mut deps = Vec::new();
for raw_line in source.lines() {

View file

@ -1054,6 +1054,51 @@ def __nyx_patch_active_record_sql_recording
end
end
def __nyx_try_rails_migration_context(cls, version)
return false unless defined?(Rails)
return false unless defined?(ActiveRecord::MigrationContext)
return false unless Rails.respond_to?(:application) && Rails.application
begin
paths = []
begin
app_paths = Rails.application.paths if Rails.application.respond_to?(:paths)
if app_paths && app_paths['db/migrate']
paths = Array(app_paths['db/migrate'].respond_to?(:existent) ? app_paths['db/migrate'].existent : app_paths['db/migrate'])
end
rescue StandardError
paths = []
end
paths = [File.dirname(File.expand_path(__FILE__))] if paths.empty?
__nyx_patch_active_record_sql_recording
context = begin
if defined?(ActiveRecord::SchemaMigration)
ActiveRecord::MigrationContext.new(paths, ActiveRecord::SchemaMigration)
else
ActiveRecord::MigrationContext.new(paths)
end
end
target = nil
if version && version != '<no-version>'
begin
target = Integer(version)
rescue ArgumentError
target = nil
end
end
if target && target > 0 && context.respond_to?(:run)
context.run(:up, target)
elsif context.respond_to?(:up)
context.up
else
return false
end
true
rescue StandardError => e
STDERR.puts("NYX_RAILS_MIGRATION_CONTEXT_FALLBACK: #{{e.class.name}}: #{{e.message}}")
false
end
end
# ActiveRecord migrations expose `up` / `down` / `change` on a subclass.
if Object.const_defined?({handler:?})
cls = Object.const_get({handler:?})
@ -1061,6 +1106,9 @@ if Object.const_defined?({handler:?})
if defined?(ActiveRecord::Migration) && cls.is_a?(Class) && cls < ActiveRecord::Migration
begin
__nyx_patch_active_record_sql_recording
if __nyx_try_rails_migration_context(cls, {ver:?})
exit 0
end
cls.migrate(:up)
exit 0
rescue StandardError => e
@ -1125,7 +1173,7 @@ end
}
fn framework_dependency_files(spec: &HarnessSpec) -> Vec<(String, String)> {
if spec.expected_cap != crate::labels::Cap::CODE_EXEC {
if !should_stage_framework_dependency_files(spec) {
return Vec::new();
}
let Some(adapter) = spec.framework.as_ref().map(|b| b.adapter.as_str()) else {
@ -1147,6 +1195,14 @@ fn framework_dependency_files(spec: &HarnessSpec) -> Vec<(String, String)> {
vec![("Gemfile".to_owned(), body)]
}
fn should_stage_framework_dependency_files(spec: &HarnessSpec) -> bool {
spec.expected_cap == crate::labels::Cap::CODE_EXEC
|| matches!(
&spec.entry_kind,
crate::evidence::EntryKind::Migration { .. }
)
}
/// Phase 03 — Track J.1 deserialize harness for Ruby.
///
/// Wraps a call to `Marshal.load(input)` with a const-lookup

View file

@ -698,14 +698,14 @@ fn kafka_parse_produce_request(version: i16, body: &[u8]) -> Vec<(String, i32, S
return Vec::new();
};
let mut out = Vec::new();
for _ in 0..topic_len.max(0).min(256) {
for _ in 0..topic_len.clamp(0, 256) {
let Some(topic) = reader.string() else {
break;
};
let Some(partition_len) = reader.array_len() else {
break;
};
for _ in 0..partition_len.max(0).min(256) {
for _ in 0..partition_len.clamp(0, 256) {
let Some(partition) = reader.i32() else {
break;
};
@ -783,7 +783,7 @@ fn kafka_parse_fetch_request(version: i16, body: &[u8]) -> BTreeMap<String, Vec<
let Some(topic_len) = reader.array_len() else {
return out;
};
for _ in 0..topic_len.max(0).min(256) {
for _ in 0..topic_len.clamp(0, 256) {
let Some(topic) = reader.string() else {
break;
};
@ -791,7 +791,7 @@ fn kafka_parse_fetch_request(version: i16, body: &[u8]) -> BTreeMap<String, Vec<
break;
};
let mut partitions = Vec::new();
for _ in 0..partition_len.max(0).min(256) {
for _ in 0..partition_len.clamp(0, 256) {
let Some(partition) = reader.i32() else {
break;
};
@ -840,7 +840,7 @@ fn kafka_parse_list_offsets_request(body: &[u8]) -> BTreeMap<String, Vec<(i32, i
let Some(topic_len) = reader.array_len() else {
return out;
};
for _ in 0..topic_len.max(0).min(256) {
for _ in 0..topic_len.clamp(0, 256) {
let Some(topic) = reader.string() else {
break;
};
@ -848,7 +848,7 @@ fn kafka_parse_list_offsets_request(body: &[u8]) -> BTreeMap<String, Vec<(i32, i
break;
};
let mut partitions = Vec::new();
for _ in 0..partition_len.max(0).min(256) {
for _ in 0..partition_len.clamp(0, 256) {
let Some(partition) = reader.i32() else {
break;
};