Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V2 Stored Procedure Support #697

Merged
merged 53 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
7927a87
add args type parameter to StoredProcedure
devhawk Dec 10, 2024
fc9302e
Merge branch 'main' into devhawk/storedProc-v2
devhawk Dec 10, 2024
6c1a58c
Merge branch 'main' into devhawk/storedProc-v2
devhawk Dec 10, 2024
29fc08b
tests passing
devhawk Dec 10, 2024
dfacda8
more logic move from WF ctx -> Dbos Exec
devhawk Dec 11, 2024
7d1f188
Merge branch 'main' into devhawk/storedProc-v2
devhawk Dec 11, 2024
3f058d0
add procedure to DBOSExecutorContext
devhawk Dec 11, 2024
26473cf
update dbos.invoke proc path to use callProcedureFunction
devhawk Dec 11, 2024
0461e3d
Merge remote-tracking branch 'origin/main' into devhawk/storedProc-v2
devhawk Dec 11, 2024
0482fbc
runWithStoredProcContext
devhawk Dec 11, 2024
fca2dfc
append random string to proc tests to enable rerun
devhawk Dec 12, 2024
0d9a6bb
Merge branch 'main' into devhawk/storedProc-v2
devhawk Dec 12, 2024
adf0e70
update paths in proc test
devhawk Dec 12, 2024
07846a3
dummy dbos.storedProc
devhawk Dec 12, 2024
dd82ce3
dbos.storedproc
devhawk Dec 12, 2024
c618af8
update proc test
devhawk Dec 12, 2024
6bda3b5
use date.now insteasd of random string
devhawk Dec 13, 2024
f62cca2
use v1 stored proc in proc test txAndProcGreetingWorkflow_v2
devhawk Dec 13, 2024
b3177d5
Merge remote-tracking branch 'origin/main' into devhawk/storedProc-v2
chuck-dbos Dec 13, 2024
838bed7
Merge remote-tracking branch 'origin/main' into devhawk/storedProc-v2
chuck-dbos Dec 14, 2024
c6be28d
Maybe
chuck-dbos Dec 14, 2024
232a681
WHAT?
chuck-dbos Dec 14, 2024
7600700
Name back
chuck-dbos Dec 14, 2024
a4f5c64
And... back.
chuck-dbos Dec 14, 2024
75e594b
Merge branch 'main' into devhawk/storedProc-v2
devhawk Jan 8, 2025
a24dcc6
fix merge bug
devhawk Jan 8, 2025
c3f2c23
fix proc test dependency
devhawk Jan 8, 2025
3f9f958
v2 stored proc exec local
devhawk Jan 8, 2025
da75f5f
Merge remote-tracking branch 'origin/main' into devhawk/storedProc-v2
devhawk Jan 9, 2025
c313afb
more tests
devhawk Jan 9, 2025
99511f3
get version info from DBOS decorators
devhawk Jan 9, 2025
f7414bd
include version info in method info returned from compile
devhawk Jan 9, 2025
a4c1f91
move api version to StoredProcConfig interface
devhawk Jan 10, 2025
da4c1f7
Merge branch 'main' into devhawk/storedProc-v2
devhawk Jan 10, 2025
b24a887
update proc test
devhawk Jan 10, 2025
e1b65d7
fix compile bug
devhawk Jan 10, 2025
455daef
initial v2 proc generation
devhawk Jan 10, 2025
19ae156
update run_v2 to use function.apply to have a custom this object w/ c…
devhawk Jan 12, 2025
9aff67f
add proc test debug config
devhawk Jan 13, 2025
9286ebc
Merge remote-tracking branch 'origin/HEAD' into devhawk/storedProc-v2
devhawk Jan 13, 2025
5c4acdd
increase scheduled recover tests timeouts to 20sec
devhawk Jan 13, 2025
8a544de
Revert "increase scheduled recover tests timeouts to 20sec"
devhawk Jan 13, 2025
ca945c3
lint fixes
devhawk Jan 13, 2025
1c346fc
use global state for context info
devhawk Jan 14, 2025
fe48f35
Merge remote-tracking branch 'origin/main' into devhawk/storedProc-v2
devhawk Jan 14, 2025
ea1ec33
fix test
devhawk Jan 14, 2025
f9fdc2b
Merge remote-tracking branch 'origin/main' into devhawk/storedProc-v2
devhawk Jan 15, 2025
70efafe
update stored proc invoke wrapper to match tx and step
devhawk Jan 15, 2025
2db2d7b
cleanup
devhawk Jan 15, 2025
63e2699
move v2 proc tests to separate describe block
devhawk Jan 15, 2025
d53561e
linter
devhawk Jan 15, 2025
c7a05c3
PR Feedback
devhawk Jan 16, 2025
a92c31d
Merge remote-tracking branch 'origin/main' into devhawk/storedProc-v2
devhawk Jan 16, 2025
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
7 changes: 7 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,12 @@
"type": "node-terminal",
"cwd": "${workspaceFolder}/examples/hello",
},
{
"command": "npx jest --testTimeout 1000000",
"name": "Launch Proc Test",
"request": "launch",
"type": "node-terminal",
"cwd": "${workspaceFolder}/tests/proc-test",
},
]
}
161 changes: 97 additions & 64 deletions packages/dbos-compiler/compiler.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import tsm from 'ts-morph';

type CompileMethodInfo = readonly [tsm.MethodDeclaration, StoredProcedureConfig];
export type CompileResult = {
project: tsm.Project;
methods: (readonly [tsm.MethodDeclaration, StoredProcedureConfig])[];
methods: CompileMethodInfo[];
};

export type IsolationLevel = "READ UNCOMMITTED" | "READ COMMITTED" | "REPEATABLE READ" | "SERIALIZABLE";
export interface StoredProcedureConfig {
isolationLevel?: IsolationLevel;
readOnly?: boolean;
executeLocally?: boolean
executeLocally?: boolean;
version: DbosDecoratorVersion;
}

function hasError(diags: readonly tsm.ts.Diagnostic[]) {
Expand Down Expand Up @@ -44,7 +46,7 @@ export function compile(configFileOrProject: string | tsm.Project, suppressWarni

const methods = project.getSourceFiles()
.flatMap(getProcMethods)
.map(m => [m, getStoredProcConfig(m)] as const);
.map(([m, v]) => [m, getStoredProcConfig(m, v)] as const);

diags.push(...checkStoredProcNames(methods.map(([m]) => m)));
diags.push(...checkStoredProcConfig(methods, false));
Expand Down Expand Up @@ -121,7 +123,7 @@ export function checkStoredProcNames(methods: readonly tsm.MethodDeclaration[]):
return diags;
}

export function checkStoredProcConfig(methods: readonly (readonly [tsm.MethodDeclaration, StoredProcedureConfig])[], error: boolean = false): readonly tsm.ts.Diagnostic[] {
export function checkStoredProcConfig(methods: readonly CompileMethodInfo[], error: boolean = false): readonly tsm.ts.Diagnostic[] {
const category = error ? tsm.ts.DiagnosticCategory.Error : tsm.ts.DiagnosticCategory.Warning;
const diags = new Array<tsm.ts.Diagnostic>();
for (const [method, config] of methods) {
Expand All @@ -136,8 +138,10 @@ export function checkStoredProcConfig(methods: readonly (readonly [tsm.MethodDec

function getStoredProcDecorator(method: tsm.MethodDeclaration) {
for (const decorator of method.getDecorators()) {
const kind = getDbosDecoratorKind(decorator);
if (kind === "storedProcedure") { return decorator; }
const info = getDbosDecoratorInfo(decorator);
if (info?.kind === "storedProcedure") {
return decorator;
}
}
}
}
Expand All @@ -147,8 +151,9 @@ export function removeDbosMethods(file: tsm.SourceFile) {
if (tsm.Node.isClassDeclaration(node)) {
traversal.skip();
for (const method of node.getStaticMethods()) {
const kind = getDbosMethodKind(method);
switch (kind) {
const info = getDbosMethodInfo(method);
if (!info) { continue; }
switch (info.kind) {
case 'workflow':
case 'step':
case 'initializer':
Expand All @@ -158,12 +163,11 @@ export function removeDbosMethods(file: tsm.SourceFile) {
break;
}
case 'storedProcedure':
case undefined:
break;
default: {
const _: never = kind;
const _never: never = info.kind;
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unexpected DBOS method kind: ${kind}`);
throw new Error(`Unexpected DBOS method kind: ${info.kind}`);
}
}
}
Expand All @@ -172,14 +176,14 @@ export function removeDbosMethods(file: tsm.SourceFile) {
}

export function getProcMethods(file: tsm.SourceFile) {
const methods = new Array<tsm.MethodDeclaration>();
const methods = new Array<[tsm.MethodDeclaration, DbosDecoratorVersion]>();
file.forEachDescendant((node, traversal) => {
if (tsm.Node.isClassDeclaration(node)) {
traversal.skip();
for (const method of node.getStaticMethods()) {
const kind = getDbosMethodKind(method);
if (kind === 'storedProcedure') {
methods.push(method);
const info = getDbosMethodInfo(method);
if (info?.kind === 'storedProcedure') {
methods.push([method, info.version]);
}
}
}
Expand All @@ -190,7 +194,7 @@ export function getProcMethods(file: tsm.SourceFile) {
function getProcMethodDeclarations(file: tsm.SourceFile) {
// initialize set of declarations with all tx methods and their class declaration parents
const declSet = new Set<tsm.Node>();
for (const method of getProcMethods(file)) {
for (const [method, _version] of getProcMethods(file)) {
declSet.add(method);
const parent = method.getParentIfKind(tsm.SyntaxKind.ClassDeclaration);
if (parent) { declSet.add(parent); }
Expand Down Expand Up @@ -310,7 +314,8 @@ function deAsync(project: tsm.Project) {
sourceFile.forEachChild(node => {
if (tsm.Node.isClassDeclaration(node)) {
for (const method of node.getStaticMethods()) {
if (getDbosMethodKind(method) === 'storedProcedure') {
const info = getDbosMethodInfo(method)
if (info?.kind === 'storedProcedure') {
method.setIsAsync(false);
method.getBody()?.transform(traversal => {
const node = traversal.visitChildren();
Expand All @@ -328,6 +333,12 @@ function deAsync(project: tsm.Project) {
function isValid<T>(value: T | null | undefined): value is T { return !!value; }

type DbosDecoratorKind = "handler" | "storedProcedure" | "transaction" | "workflow" | "step" | "initializer";
type DbosDecoratorVersion = 1 | 2;

interface DbosDecoratorInfo {
kind: DbosDecoratorKind;
version: DbosDecoratorVersion;
}

export function getImportSpecifier(node: tsm.Identifier | undefined): tsm.ImportSpecifier | undefined {
const symbol = node?.getSymbol();
Expand All @@ -343,12 +354,12 @@ export function getImportSpecifier(node: tsm.Identifier | undefined): tsm.Import
return undefined;
}

function isDbosImport(node: tsm.ImportSpecifier ): boolean {
function isDbosImport(node: tsm.ImportSpecifier): boolean {
const modSpec = node.getImportDeclaration().getModuleSpecifier();
return modSpec.getLiteralText() === "@dbos-inc/dbos-sdk";
}

function getDbosDecoratorKind(node: tsm.Decorator): DbosDecoratorKind | undefined {
function getDbosDecoratorInfo(node: tsm.Decorator): DbosDecoratorInfo | undefined {
if (!node.isDecoratorFactory()) { return undefined; }

const expr = node.getCallExpressionOrThrow().getExpression();
Expand All @@ -357,24 +368,8 @@ function getDbosDecoratorKind(node: tsm.Decorator): DbosDecoratorKind | undefine
if (tsm.Node.isIdentifier(expr)) {
const impSpec = getImportSpecifier(expr);
if (impSpec && isDbosImport(impSpec)) {
const { name } = impSpec.getStructure();
switch (name) {
case "GetApi":
case "PostApi":
case "PutApi":
case "PatchApi":
case "DeleteApi":
return "handler";
case "StoredProcedure": return "storedProcedure";
case "Transaction": return "transaction";
case "Workflow": return "workflow";
case "Communicator":
case "Step":
return "step";
case "DBOSInitializer":
case "DBOSDeploy":
return "initializer";
}
const kind = getImportSpecifierStructureKind(impSpec.getStructure());
if (kind) { return { kind, version: 1 }; }
}
}

Expand All @@ -383,55 +378,90 @@ function getDbosDecoratorKind(node: tsm.Decorator): DbosDecoratorKind | undefine
const impSpec = getImportSpecifier(expr.getExpressionIfKind(tsm.SyntaxKind.Identifier));
if (impSpec && isDbosImport(impSpec)) {
const { name } = impSpec.getStructure();
if (name === "DBOS") {
switch (expr.getName()) {
case "getApi":
case "postApi":
case "putApi":
case "patchApi":
case "deleteApi":
return "handler";
case "workflow": return "workflow";
case "transaction": return "transaction";
case "step": return "step";
case "storedProcedure": return "storedProcedure";
}
if (name === "DBOS") {
const kind = getPropertyAccessExpressionKind(expr);
if (kind) { return { kind, version: 2 }; }
}
}
}

return undefined;

function getImportSpecifierStructureKind(
{ name }: tsm.ImportSpecifierStructure
): DbosDecoratorKind | undefined {
switch (name) {
case "GetApi":
case "PostApi":
case "PutApi":
case "PatchApi":
case "DeleteApi":
return "handler";
case "StoredProcedure": return "storedProcedure";
case "Transaction": return "transaction";
case "Workflow": return "workflow";
case "Communicator":
case "Step":
return "step";
case "DBOSInitializer":
case "DBOSDeploy":
return "initializer";
default: return undefined;
}
}

function getPropertyAccessExpressionKind(
node: tsm.PropertyAccessExpression
): DbosDecoratorKind | undefined {
switch (node.getName()) {
case "getApi":
case "postApi":
case "putApi":
case "patchApi":
case "deleteApi":
return "handler";
case "workflow": return "workflow";
case "transaction": return "transaction";
case "step": return "step";
case "storedProcedure": return "storedProcedure";
default: return undefined;
}
}
}

// helper function to determine the kind of DBOS method
export function getDbosMethodKind(node: tsm.MethodDeclaration): DbosDecoratorKind | undefined {
export function getDbosMethodInfo(node: tsm.MethodDeclaration): DbosDecoratorInfo | undefined {
// Note, other DBOS method decorators (Scheduled, KafkaConsume, RequiredRole) modify runtime behavior
// of DBOS methods, but are not their own unique kind.
// Get/PostApi decorators are atypical in that they can be used on @Step/@Transaction/@Workflow
// methods as well as on their own.
let isHandler = false;
let handlerVersion: DbosDecoratorVersion | undefined = undefined;
for (const decorator of node.getDecorators()) {
const kind = getDbosDecoratorKind(decorator);
switch (kind) {
const info = getDbosDecoratorInfo(decorator);
if (!info) { continue; }
switch (info.kind) {
case "storedProcedure":
case "transaction":
case "workflow":
case "step":
case "initializer":
return kind;
return info;
case "handler":
isHandler = true;
break;
case undefined:
devhawk marked this conversation as resolved.
Show resolved Hide resolved
if (handlerVersion !== undefined) {
throw new Error("Multiple handler decorators");
}
handlerVersion = info.version;
break;
default: {
const _never: never = kind;
const _never: never = info.kind;
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unexpected DBOS method kind: ${kind}`);
throw new Error(`Unexpected DBOS method kind: ${info.kind}`);
}
}
}
return isHandler ? "handler" : undefined;
return handlerVersion
? { kind: "handler", version: handlerVersion }
: undefined;
}

export type DecoratorArgument = boolean | string | number | DecoratorArgument[] | Record<string, unknown>;
Expand Down Expand Up @@ -469,15 +499,18 @@ export function parseDecoratorArgument(node: tsm.Node): DecoratorArgument {
}
}

export function getStoredProcConfig(node: tsm.MethodDeclaration): StoredProcedureConfig {
export function getStoredProcConfig(node: tsm.MethodDeclaration, version: DbosDecoratorVersion): StoredProcedureConfig {
const decorators = node.getDecorators();
const procDecorator = decorators.find(d => getDbosDecoratorKind(d) === "storedProcedure");
const procDecorator = decorators.find(d => {
const info = getDbosDecoratorInfo(d);
return info?.kind === "storedProcedure";
});
if (!procDecorator) { throw new Error("Missing StoredProcedure decorator"); }

const arg0 = procDecorator.getCallExpression()?.getArguments()[0] ?? undefined;
const configArg = arg0 ? parseDecoratorArgument(arg0) as Partial<StoredProcedureConfig> : undefined;
const readOnly = configArg?.readOnly;
const executeLocally = configArg?.executeLocally;
const isolationLevel = configArg?.isolationLevel;
return { isolationLevel, readOnly, executeLocally };
return { isolationLevel, readOnly, executeLocally, version };
}
10 changes: 6 additions & 4 deletions packages/dbos-compiler/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,10 @@ async function generateDbosDrop(appVersion: string | undefined) {
}

function getMethodContext(method: tsm.MethodDeclaration, config: StoredProcedureConfig, appVersion: string | undefined) {
const readOnly = config?.readOnly ?? false;
const executeLocally = config?.executeLocally ?? false;
const isolationLevel = config?.isolationLevel ?? "SERIALIZABLE";
const readOnly = config.readOnly ?? false;
const executeLocally = config.executeLocally ?? false;
const isolationLevel = config.isolationLevel ?? "SERIALIZABLE";
const apiVersion = config.version;

const methodName = method.getName();
const className = method.getParentIfKindOrThrow(tsm.SyntaxKind.ClassDeclaration).getName();
Expand All @@ -82,7 +83,8 @@ function getMethodContext(method: tsm.MethodDeclaration, config: StoredProcedure
methodName,
className,
moduleName,
appVersion
appVersion,
apiVersion,
};
}

Expand Down
Loading
Loading