Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion cli/vm/BUILD
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package(default_visibility = ["//visibility:public"])

load("//testing:index.bzl", "ts_test_suite")
load("//tools:ts_library.bzl", "ts_library")

ts_library(
Expand All @@ -10,14 +11,14 @@ ts_library(
],
deps = [
"//common/protos",
"//common/vm:vm_runner",
"//core",
"//protos:ts",
"@npm//@types/glob",
"@npm//@types/node",
"@npm//@types/semver",
"@npm//glob",
"@npm//semver",
"@npm//vm2",
],
)

Expand Down Expand Up @@ -67,3 +68,25 @@ nodejs_binary(
"--bazel_patch_module_resolver",
],
)

ts_test_suite(
name = "tests",
srcs = [
"compile_test.ts",
],
data = [
"//core:node_modules",
],
deps = [
":vm",
"//common/protos",
"//protos:ts",
"//testing",
"@npm//@types/chai",
"@npm//@types/fs-extra",
"@npm//@types/node",
"@npm//chai",
"@npm//fs-extra",
],
)

74 changes: 35 additions & 39 deletions cli/vm/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import * as fs from "fs";
import * as glob from "glob";
import * as path from "path";
import * as semver from "semver";
import { CompilerFunction, NodeVM } from "vm2";

import { encode64 } from "df/common/protos";
import { CompilerFunction, VmRunner } from "df/common/vm/vm_runner";
import { dataform } from "df/protos/ts";

export function compile(compileConfig: dataform.ICompileConfig) {
Expand All @@ -31,14 +31,9 @@ export function compile(compileConfig: dataform.ICompileConfig) {
// through Node's resolver inside the vm covers every install layout
// (package.json, workflow_settings.yaml, JiT) and matches what the user's
// code will see. require() caches the bundle so the second call is free.
const indexGeneratorVm = new NodeVM({
wrapper: "none",
require: {
context: "sandbox",
root: compileConfig.projectDir,
external: true,
builtin: ["path"],
},
const indexGeneratorVm = new VmRunner({
projectDir: compileConfig.projectDir,
builtinModules: ["path"],
});
const compiler: CompilerFunction = indexGeneratorVm.run(
'return require("@dataform/core").compiler',
Expand Down Expand Up @@ -71,43 +66,37 @@ export function compile(compileConfig: dataform.ICompileConfig) {
}
const needsCallerFileShim = semver.lt(dataformCoreVersion, "3.0.57");

// vm2 strips file paths from V8 CallSite objects inside the sandbox, so
// getCallerFile() in @dataform/core needs a fallback. Track the currently
// While VmRunner preserves V8 CallSite file paths natively, older @dataform/core
// versions check global.__dataform_current_file as a fallback. Track the currently
// executing file via a host-side stack exposed through sandbox helpers, and
// expose it as a getter on `global.__dataform_current_file`.
const fileStack: string[] = [];
const sandbox: Record<string, any> = {};
if (needsCallerFileShim) {
sandbox.__df_enter = (p: string) => {
fileStack.push(p);
};
sandbox.__df_exit = () => {
fileStack.pop();
};
sandbox.__df_current = () => (fileStack.length > 0 ? fileStack[fileStack.length - 1] : null);
}

// Then use vm2's native compiler integration to apply the compiler to files.
const userCodeVm = new NodeVM({
wrapper: "none",
sandbox: {
__df_enter: (p: string) => {
fileStack.push(p);
},
__df_exit: () => {
fileStack.pop();
},
__df_current: () => (fileStack.length > 0 ? fileStack[fileStack.length - 1] : null),
},
require: {
builtin: ["path"],
context: "sandbox",
external: true,
root: compileConfig.projectDir,
resolve: (moduleName, parentDirName) =>
path.join(
parentDirName,
path.relative(parentDirName, compileConfig.projectDir),
moduleName,
),
},
sourceExtensions: ["js", "sql", "sqlx", "yaml", "yml"],
// Then use VmRunner to apply the compiler to files.
const userCodeVm = new VmRunner({
projectDir: compileConfig.projectDir,
sandbox,
builtinModules: ["path"],
sourceExtensions: ["js", "sql", "sqlx", "yaml", "yml", "ipynb", "md"],
compiler: (code, filePath) => {
let source = code;
if (needsCallerFileShim && filePath === coreBundlePath) {
source = patchOldCoreCallerFile(source);
}
const compiledCode = compiler(source, filePath);
if (!needsCallerFileShim) {
return compiledCode;
}
return `
__df_enter(${JSON.stringify(filePath)});
try {
Expand All @@ -126,13 +115,20 @@ export function compile(compileConfig: dataform.ICompileConfig) {

return userCodeVm.run(
`
Object.defineProperty(global, '__dataform_current_file', {
${
needsCallerFileShim
? `Object.defineProperty(global, '__dataform_current_file', {
configurable: true,
get: function() { return __df_current(); }
});
});`
: ""
}
${
hasWorkflowSettingsYaml
? 'global.workflowSettingsYaml = require("./workflow_settings.yaml");'
? `global.workflowSettingsYaml = (function() {
try { return require("./workflow_settings.yaml"); }
catch(e) { console.error("YAML require failed run_core:", e); }
})();`
: ""
}
${hasDataformJson ? 'global.dataformJson = require("./dataform.json");' : ""}
Expand Down
152 changes: 152 additions & 0 deletions cli/vm/compile_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { expect } from "chai";
import * as fs from "fs-extra";
import * as path from "path";

import { compile } from "df/cli/vm/compile";
import { handleJitRequest } from "df/cli/vm/jit_worker";
import { decode64 } from "df/common/protos";
import { dataform } from "df/protos/ts";
import { suite, test } from "df/testing";
import { TmpDirFixture } from "df/testing/fixtures";

suite("cli/vm", ({ afterEach }) => {
const tmpDirFixture = new TmpDirFixture(afterEach);

// Allow require("@dataform/core") to resolve to the prebuilt core bundle in the test environment
// tslint:disable-next-line: no-require-imports
const Module = require("module");
const origResolve = Module._resolveFilename;
Module._resolveFilename = function (request: string, parent: any, isMain: boolean, options: any) {
if (request === "@dataform/core") {
return path.join(process.cwd(), "core", "node_modules", "@dataform", "core", "bundle.js");
}
return origResolve.apply(this, arguments);
};

test("compile() runs end-to-end against prebuilt @dataform/core bundle", () => {
const projectDir = tmpDirFixture.createNewTmpDir();

// Copy built @dataform/core from Bazel runfiles into the project's node_modules.
fs.copySync(
path.join(process.cwd(), "core", "node_modules"),
path.join(projectDir, "node_modules"),
);

fs.writeFileSync(
path.join(projectDir, "workflow_settings.yaml"),
`
defaultProject: test-project
defaultDataset: test-dataset
defaultLocation: US
`,
);

fs.mkdirSync(path.join(projectDir, "definitions"));
fs.writeFileSync(
path.join(projectDir, "definitions", "example.sqlx"),
`
config {
type: "table",
name: "example"
}
SELECT 1 AS col
`,
);
fs.writeFileSync(
path.join(projectDir, "definitions", "actions.yaml"),
`
actions:
- notebook:
filename: test_notebook.ipynb
`,
);
fs.writeFileSync(
path.join(projectDir, "definitions", "test_notebook.ipynb"),
JSON.stringify({ cells: [] }),
);

const encodedResponse = compile({ projectDir });
const response = decode64(dataform.CoreExecutionResponse, encodedResponse);

expect(response.compile).to.be.an("object");
expect(response.compile.compiledGraph).to.be.an("object");
const tables = response.compile.compiledGraph.tables;
expect(tables).to.have.lengthOf(1);
expect(tables[0].target.name).to.equal("example");
expect(tables[0].target.schema).to.equal("test-dataset");
expect(tables[0].target.database).to.equal("test-project");

const notebooks = response.compile.compiledGraph.notebooks;
expect(notebooks).to.have.lengthOf(1);
expect(notebooks[0].target.name).to.equal("test_notebook");
});

test("handleJitRequest compiles request with local core (hasProjectLocalCore = true)", async () => {
const projectDir = tmpDirFixture.createNewTmpDir();
fs.copySync(
path.join(process.cwd(), "core", "node_modules"),
path.join(projectDir, "node_modules"),
);

const request = dataform.JitCompilationRequest.create({
jitCode: `async (ctx) => "SELECT 1"`,
target: {
database: "db",
schema: "schema",
name: "test_op",
},
compilationTargetType:
dataform.JitCompilationTargetType.JIT_COMPILATION_TARGET_TYPE_OPERATION,
});

const messages: any[] = [];
const origSend = process.send;
(process as any).send = (msg: any) => messages.push(msg);

try {
await handleJitRequest({ request, projectDir });
} finally {
(process as any).send = origSend;
}

expect(messages).to.have.lengthOf(1);
expect(messages[0].type).to.equal("jit_response");
expect(messages[0].response.operation.queries).to.deep.equal(["SELECT 1"]);
});

test("handleJitRequest compiles request with fallback core (hasProjectLocalCore = false)", async () => {
const projectDir = tmpDirFixture.createNewTmpDir();
// Provide only package.json for @dataform/core without bundle.js so hasProjectLocalCore is false
const coreDir = path.join(projectDir, "node_modules", "@dataform", "core");
fs.mkdirSync(coreDir, { recursive: true });
fs.writeFileSync(
path.join(coreDir, "package.json"),
JSON.stringify({ name: "@dataform/core", version: "3.0.0" }),
);

const request = dataform.JitCompilationRequest.create({
jitCode: `async (ctx) => "SELECT 42"`,
target: {
database: "db",
schema: "schema",
name: "test_op2",
},
compilationTargetType:
dataform.JitCompilationTargetType.JIT_COMPILATION_TARGET_TYPE_OPERATION,
});

const messages: any[] = [];
const origSend = process.send;
(process as any).send = (msg: any) => messages.push(msg);

try {
await handleJitRequest({ request, projectDir });
} finally {
(process as any).send = origSend;
}

expect(messages).to.have.lengthOf(1);
expect(messages[0].type).to.equal("jit_response");
expect(messages[0].response.operation.queries).to.deep.equal(["SELECT 42"]);
});
});
23 changes: 10 additions & 13 deletions cli/vm/jit_worker.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as fs from "fs";
import * as path from "path";
import { NodeVM } from "vm2";

import { VmRunner } from "df/common/vm/vm_runner";
import { dataform } from "df/protos/ts";

const pendingRpcCallbacks = new Map<
Expand Down Expand Up @@ -85,18 +85,15 @@ export async function handleJitRequest(message: { request: any; projectDir: stri

const vmFileName = path.resolve(projectDir, "index.js");

const vm = new NodeVM({
require: {
builtin: [],
context: "sandbox",
external: { modules: ["@dataform/*"], transitive: false },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this intended that you don't pass these external modules in new version?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restored. Added allowedModules option to VmRunner and passed ["@dataform/*"] here.

root: projectDir,
mock: hasProjectLocalCore
? {}
: {
"@dataform/core": require("@dataform/core"),
},
},
const vm = new VmRunner({
projectDir,
builtinModules: [],
allowedModules: ["@dataform/*"],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vm2 had external: { modules: ["@dataform/"], transitive: false }. The new allowedModules: ["@dataform/"] drops the transitive-blocking semantics and means that if @dataform/core does ANY dynamic require("some-runtime-dep") (not a static bundled import), it'll be rejected. Confirm the core bundle has no dynamic requires at runtime, or widen the allowlist. Add a test covering the hasProjectLocalCore = true path (currently only a unit test of the VmRunner primitive exists - no test wires jit_worker end-to-end).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Core bundle dependencies: Confirmed that @dataform/core is fully self-contained. Rollup/webpack inlines all external dependencies (protobufjs, js-yaml, tarjan-graph, moo) statically into bundle.js, with zero dynamic runtime require(...) calls to external packages.
  2. JiT worker integration tests: Added end-to-end integration tests in cli/vm/compile_test.ts testing handleJitRequest() with both:
    • hasProjectLocalCore = true (resolving against the installed @dataform/core/bundle.js).
    • hasProjectLocalCore = false (falling back to ambient/mocked @dataform/core).

mockModules: hasProjectLocalCore
? {}
: {
"@dataform/core": require("@dataform/core"),
},
sourceExtensions: ["js", "json", "yaml", "yml"],
});
Comment thread
apilaskowski marked this conversation as resolved.

Expand Down
Loading
Loading