Skip to content

Commit 45646e1

Browse files
committed
fix: resolve project context for untitled query files (#1827)
Untitled files created via 'New query' or manually silently failed to execute or compile because the selected project was never persisted to workspace state, and executeSQL/compileQuery could not resolve untitled URIs to a dbt project. Changes: - createSqlFile now stores the selected project in workspace state - Extract resolveProjectUri() helper with safe Uri reconstruction from deserialized workspace state; apply to both executeSQL and compileQuery - Add ensureProjectForUntitledUri() fallback in RunModel that validates stored project still exists (clears stale state) and falls back to getOrPickProjectFromWorkspace for untitled files not created via createSqlFile - Extract toProjectQuickPickItem() to eliminate duplicated shape across 3 call sites - Invalidate dbtPowerUser.projectSelected when a project is unregistered - Use clean 'untitled' modelName instead of raw fsPath for untitled URIs - Wire QueryManifestService into RunModel via DI Fixes #1827
1 parent b0a58e4 commit 45646e1

5 files changed

Lines changed: 145 additions & 25 deletions

File tree

src/commands/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ import { DBTProject } from "../dbt_client/dbtProject";
3333
import { DBTProjectContainer } from "../dbt_client/dbtProjectContainer";
3434
import { PythonEnvironment } from "../dbt_client/pythonEnvironment";
3535
import { NotebookQuickPick } from "../quickpick/notebookQuickPick";
36-
import { ProjectQuickPickItem } from "../quickpick/projectQuickPick";
36+
import {
37+
ProjectQuickPickItem,
38+
toProjectQuickPickItem,
39+
} from "../quickpick/projectQuickPick";
3740
import { DiagnosticsOutputChannel } from "../services/diagnosticsOutputChannel";
3841
import { QueryManifestService } from "../services/queryManifestService";
3942
import { RunHistoryService } from "../services/runHistoryService";
@@ -697,6 +700,13 @@ export class VSCodeCommands implements Disposable {
697700
return;
698701
}
699702

703+
// Persist project selection so untitled files can resolve
704+
// project context during query execution and compilation
705+
this.dbtProjectContainer.setToWorkspaceState(
706+
"dbtPowerUser.projectSelected",
707+
toProjectQuickPickItem(project),
708+
);
709+
700710
// Open a new untitled sql file by default
701711
let docOpenPromise = workspace.openTextDocument({
702712
language: "jinja-sql",

src/commands/runModel.ts

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@ import { RunModelType } from "@altimateai/dbt-integration";
33
import { Uri, window } from "vscode";
44
import { GenerateModelFromSourceParams } from "../code_lens_provider/sourceModelCreationCodeLensProvider";
55
import { DBTProjectContainer } from "../dbt_client/dbtProjectContainer";
6+
import { toProjectQuickPickItem } from "../quickpick/projectQuickPick";
7+
import { QueryManifestService } from "../services/queryManifestService";
68
import { NodeTreeItem } from "../treeview_provider/modelTreeviewProvider";
79
import { extendErrorWithSupportLinks } from "../utils";
810

911
export class RunModel {
10-
constructor(private dbtProjectContainer: DBTProjectContainer) {}
12+
constructor(
13+
private dbtProjectContainer: DBTProjectContainer,
14+
private queryManifestService: QueryManifestService,
15+
) {}
1116

1217
runModelOnActiveWindow(type?: RunModelType) {
1318
if (!window.activeTextEditor) {
@@ -41,17 +46,61 @@ export class RunModel {
4146
this.compileDBTModel(fullPath);
4247
}
4348

44-
compileQueryOnActiveWindow() {
49+
async compileQueryOnActiveWindow() {
4550
if (!window.activeTextEditor) {
4651
return;
4752
}
4853
const fullPath = window.activeTextEditor.document.uri;
54+
if (fullPath.scheme === "untitled") {
55+
const resolved = await this.ensureProjectForUntitledUri();
56+
if (!resolved) {
57+
return;
58+
}
59+
}
4960
const query = window.activeTextEditor.document.getText();
5061
if (query !== undefined) {
5162
this.compileDBTQuery(fullPath, query);
5263
}
5364
}
5465

66+
/**
67+
* Ensures a dbt project is stored in workspace state for untitled files.
68+
* If no project is stored yet — or the stored project is stale (removed
69+
* between sessions) — falls back to getOrPickProjectFromWorkspace() which
70+
* auto-selects the single project or prompts the user to pick one.
71+
* Returns true if a project is available, false if the user cancelled or
72+
* no projects exist.
73+
*/
74+
private async ensureProjectForUntitledUri(): Promise<boolean> {
75+
const selectedProject = this.dbtProjectContainer.getFromWorkspaceState(
76+
"dbtPowerUser.projectSelected",
77+
);
78+
if (selectedProject?.uri) {
79+
// Validate the stored project still exists in the workspace
80+
const raw = selectedProject.uri;
81+
const storedUri = Uri.file(raw.fsPath || raw.path);
82+
if (this.dbtProjectContainer.findDBTProject(storedUri)) {
83+
return true;
84+
}
85+
// Stale — clear it and fall through to re-pick
86+
this.dbtProjectContainer.setToWorkspaceState(
87+
"dbtPowerUser.projectSelected",
88+
undefined,
89+
);
90+
}
91+
const project =
92+
await this.queryManifestService.getOrPickProjectFromWorkspace();
93+
if (!project) {
94+
window.showErrorMessage("Unable to find dbt project for this query.");
95+
return false;
96+
}
97+
this.dbtProjectContainer.setToWorkspaceState(
98+
"dbtPowerUser.projectSelected",
99+
toProjectQuickPickItem(project),
100+
);
101+
return true;
102+
}
103+
55104
private getQuery() {
56105
if (!window.activeTextEditor) {
57106
return;
@@ -62,16 +111,26 @@ export class RunModel {
62111
);
63112
}
64113

65-
executeQueryOnActiveWindow() {
114+
async executeQueryOnActiveWindow() {
66115
const query = this.getQuery();
67116
if (query === undefined) {
68117
return;
69118
}
70119
const modelPath = window.activeTextEditor?.document.uri;
71-
if (modelPath) {
72-
const modelName = path.basename(modelPath.fsPath, ".sql");
73-
this.executeSQL(window.activeTextEditor!.document.uri, query, modelName);
120+
if (!modelPath) {
121+
return;
122+
}
123+
if (modelPath.scheme === "untitled") {
124+
const resolved = await this.ensureProjectForUntitledUri();
125+
if (!resolved) {
126+
return;
127+
}
74128
}
129+
const modelName =
130+
modelPath.scheme === "untitled"
131+
? "untitled"
132+
: path.basename(modelPath.fsPath, ".sql");
133+
this.executeSQL(modelPath, query, modelName);
75134
}
76135

77136
runModelOnNodeTreeItem(type: RunModelType) {

src/dbt_client/dbtProjectContainer.ts

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export class DBTProjectContainer implements Disposable {
102102
this.projects.set(event.root, event.name);
103103
} else {
104104
this.projects.delete(event.root);
105+
this.clearStaleProjectSelection(event.root);
105106
}
106107
});
107108
}
@@ -240,15 +241,10 @@ export class DBTProjectContainer implements Disposable {
240241
}
241242

242243
executeSQL(uri: Uri, query: string, modelName: string): void {
243-
if (uri.scheme === "untitled") {
244-
const selectedProject = this.getFromWorkspaceState(
245-
"dbtPowerUser.projectSelected",
246-
);
247-
if (selectedProject) {
248-
uri = selectedProject.uri;
249-
}
250-
}
251-
this.findDBTProject(uri)?.executeSQLOnQueryPanel(query, modelName);
244+
this.findDBTProject(this.resolveProjectUri(uri))?.executeSQLOnQueryPanel(
245+
query,
246+
modelName,
247+
);
252248
}
253249

254250
runModel(modelPath: Uri, type?: RunModelType) {
@@ -286,7 +282,9 @@ export class DBTProjectContainer implements Disposable {
286282
}
287283

288284
compileQuery(modelPath: Uri, query: string) {
289-
return this.findDBTProject(modelPath)?.compileQuery(query);
285+
return this.findDBTProject(this.resolveProjectUri(modelPath))?.compileQuery(
286+
query,
287+
);
290288
}
291289

292290
showRunSQL(modelPath: Uri) {
@@ -479,6 +477,49 @@ export class DBTProjectContainer implements Disposable {
479477
return this.dbtWorkspaceFolders.find((folder) => folder.contains(uri));
480478
}
481479

480+
/**
481+
* Resolves an untitled document URI to the selected project's URI.
482+
* When users create ad-hoc query files (via "New query" or manually),
483+
* the document has an `untitled:` scheme that can't be matched to a
484+
* project directory. This method looks up the stored project selection
485+
* from workspace state and returns a real file URI that findDBTProject
486+
* can resolve.
487+
*
488+
* Workspace state stores Uri as a plain JSON object (not a Uri instance),
489+
* so we reconstruct it via Uri.file() to ensure .fsPath works correctly.
490+
*/
491+
private resolveProjectUri(uri: Uri): Uri {
492+
if (uri.scheme === "untitled") {
493+
const selectedProject = this.getFromWorkspaceState(
494+
"dbtPowerUser.projectSelected",
495+
);
496+
if (selectedProject?.uri) {
497+
const raw = selectedProject.uri;
498+
return Uri.file(raw.fsPath || raw.path);
499+
}
500+
}
501+
return uri;
502+
}
503+
504+
/**
505+
* Clear the stored project selection if it points to a project that was
506+
* just unregistered. Prevents stale state from causing silent failures
507+
* when the user later opens an untitled query file.
508+
*/
509+
private clearStaleProjectSelection(removedRoot: Uri): void {
510+
const selectedProject = this.getFromWorkspaceState(
511+
"dbtPowerUser.projectSelected",
512+
);
513+
if (!selectedProject?.uri) {
514+
return;
515+
}
516+
const raw = selectedProject.uri;
517+
const selectedPath = raw.fsPath || raw.path;
518+
if (selectedPath === removedRoot.fsPath) {
519+
this.setToWorkspaceState("dbtPowerUser.projectSelected", undefined);
520+
}
521+
}
522+
482523
async checkIfAltimateDatapilotInstalled() {
483524
const datapilotVersion =
484525
await this.altimateDatapilot.checkIfAltimateDatapilotInstalled();

src/inversify.config.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1415,7 +1415,10 @@ container
14151415
container
14161416
.bind(RunModel)
14171417
.toDynamicValue((context) => {
1418-
return new RunModel(context.container.get(DBTProjectContainer));
1418+
return new RunModel(
1419+
context.container.get(DBTProjectContainer),
1420+
context.container.get(QueryManifestService),
1421+
);
14191422
})
14201423
.inSingletonScope();
14211424

src/quickpick/projectQuickPick.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,24 @@ export interface ProjectQuickPickItem extends QuickPickItem {
77
uri: Uri;
88
}
99

10+
/** Build a ProjectQuickPickItem from a DBTProject for workspace state storage. */
11+
export function toProjectQuickPickItem(
12+
project: DBTProject,
13+
): ProjectQuickPickItem {
14+
return {
15+
label: project.getProjectName(),
16+
description: project.projectRoot.fsPath,
17+
uri: project.projectRoot,
18+
};
19+
}
20+
1021
export class ProjectQuickPick {
1122
async projectPicker(
1223
projects: DBTProject[],
1324
): Promise<ProjectQuickPickItem | undefined> {
14-
const options: ProjectQuickPickItem[] = projects.map((item) => {
15-
return {
16-
label: item.getProjectName(),
17-
description: item.projectRoot.fsPath,
18-
uri: item.projectRoot,
19-
};
20-
});
25+
const options: ProjectQuickPickItem[] = projects.map(
26+
toProjectQuickPickItem,
27+
);
2128

2229
const pick: ProjectQuickPickItem | undefined = await window.showQuickPick(
2330
options,

0 commit comments

Comments
 (0)