Skip to content

Commit a8203c6

Browse files
authored
Fix: Allow empty files in models folder and refetch lineage on model file change (#746)
1 parent c6dcb3c commit a8203c6

9 files changed

Lines changed: 75 additions & 28 deletions

File tree

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@
9898
],
9999
"web": [
100100
"fastapi==0.95.0",
101-
"hyperscript==0.0.1",
101+
"watchfiles==0.19.0",
102102
"pyarrow==11.0.0",
103103
"uvicorn==0.21.1",
104104
],

sqlmesh/core/loader.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,9 @@ def _load_sql_models(
193193
cache = SqlMeshLoader._Cache(self, context_path)
194194

195195
for path in self._glob_paths(context_path / c.MODELS, config=config, extension=".sql"):
196+
if not os.path.getsize(path):
197+
continue
198+
196199
self._track_file(path)
197200

198201
def _load() -> Model:
@@ -234,6 +237,9 @@ def _load_python_models(self) -> UniqueKeyDict[str, Model]:
234237

235238
for context_path, config in self._context.configs.items():
236239
for path in self._glob_paths(context_path / c.MODELS, config=config, extension=".py"):
240+
if not os.path.getsize(path):
241+
continue
242+
237243
self._track_file(path)
238244
self._import_python_file(path, context_path)
239245
new = registry.keys() - registered

web/client/src/library/components/editor/EditorPreview.tsx

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@ import { ViewColumnsIcon } from '@heroicons/react/24/solid'
1212
import { Button } from '@components/button/Button'
1313
import { EnumVariant } from '~/types/enum'
1414
import { useStoreContext } from '@context/context'
15-
import { apiCancelLineage, useApiModelLineage } from '@api/index'
15+
import { useApiModelLineage } from '@api/index'
1616
import Graph from '@components/graph/Graph'
17-
import { useQueryClient } from '@tanstack/react-query'
1817
import Loading from '@components/loading/Loading'
1918

2019
export const EnumEditorPreviewTabs = {
@@ -40,6 +39,7 @@ export default function EditorPreview({
4039
const previewTable = useStoreEditor(s => s.previewTable)
4140

4241
const [activeTabIndex, setActiveTabIndex] = useState(-1)
42+
const [modelName, setModelName] = useState<string | undefined>()
4343

4444
const tabs = useMemo(() => {
4545
if (tab.file.isLocal)
@@ -58,6 +58,7 @@ export default function EditorPreview({
5858

5959
return [EnumEditorPreviewTabs.Console]
6060
}, [tab])
61+
6162
const [headers, data] = useMemo(
6263
() =>
6364
previewTable == null
@@ -88,6 +89,10 @@ export default function EditorPreview({
8889
setActiveTabIndex(tab.file.isSQLMeshModel ? 3 : -1)
8990
}, [previewTable, previewQuery, previewConsole, tab])
9091

92+
useEffect(() => {
93+
setModelName(models.get(tab.file.path)?.name)
94+
}, [models, tab.file])
95+
9196
const table = useReactTable({
9297
data,
9398
columns,
@@ -106,8 +111,6 @@ export default function EditorPreview({
106111
return tabName === EnumEditorPreviewTabs.Query && isNil(previewQuery)
107112
}
108113

109-
const model = models.get(tab.file.path)
110-
111114
return (
112115
<div
113116
className={clsx(
@@ -262,12 +265,13 @@ export default function EditorPreview({
262265
'w-full h-full ring-white ring-opacity-60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2 p-2',
263266
)}
264267
>
265-
{model == null ? (
268+
{modelName == null ? (
266269
<div>Model Does Not Exist</div>
267270
) : (
268271
<EditorPreviewLineage
269-
key={model.name}
270-
model={model.name}
272+
key={modelName}
273+
model={modelName}
274+
tab={tab}
271275
/>
272276
)}
273277
</Tab.Panel>
@@ -277,44 +281,42 @@ export default function EditorPreview({
277281
)
278282
}
279283

280-
function EditorPreviewLineage({ model }: { model: string }): JSX.Element {
281-
const client = useQueryClient()
282-
283-
const { data: dag, refetch: getModelLineage } = useApiModelLineage(model)
284+
function EditorPreviewLineage({
285+
model,
286+
tab,
287+
}: {
288+
model: string
289+
tab: EditorTab
290+
}): JSX.Element {
291+
const { data: lineage, refetch: getModelLineage } = useApiModelLineage(model)
284292

285293
const previewLineage = useStoreEditor(s => s.previewLineage)
286294
const setPreviewLineage = useStoreEditor(s => s.setPreviewLineage)
287295

288296
const debouncedGetModelLineage = useCallback(
289297
debounceAsync(getModelLineage, 1000, true),
290-
[model],
298+
[model, tab.file.path, tab.file.fingerprint],
291299
)
292300

293301
useEffect(() => {
294302
void debouncedGetModelLineage()
295-
296-
return () => {
297-
debouncedGetModelLineage.cancel()
298-
299-
apiCancelLineage(client)
300-
}
301-
}, [model])
303+
}, [debouncedGetModelLineage])
302304

303305
useEffect(() => {
304-
if (dag == null) {
306+
if (lineage == null) {
305307
setPreviewLineage(undefined)
306308
} else {
307309
setPreviewLineage(
308-
Object.keys(dag).reduce((acc: Record<string, Lineage>, key) => {
310+
Object.keys(lineage).reduce((acc: Record<string, Lineage>, key) => {
309311
acc[key] = {
310-
models: dag[key] ?? [],
312+
models: lineage[key] ?? [],
311313
}
312314

313315
return acc
314316
}, {}),
315317
)
316318
}
317-
}, [dag])
319+
}, [lineage])
318320

319321
return previewLineage == null ? (
320322
<div className="w-full h-full flex items-center justify-center bg-primary-10">

web/client/src/library/components/ide/IDE.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export function IDE(): JSX.Element {
6262

6363
useEffect(() => {
6464
const unsubscribeTasks = subscribe('tasks', updateTasks)
65+
const unsubscribeModels = subscribe('models', setModels)
6566

6667
void debouncedGetEnvironemnts()
6768
void debouncedGetFiles()
@@ -77,6 +78,7 @@ export function IDE(): JSX.Element {
7778
apiCancelGetEnvironments(client)
7879

7980
unsubscribeTasks?.()
81+
unsubscribeModels?.()
8082
}
8183
}, [])
8284

web/client/src/models/file.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ export class ModelFile extends ModelArtifact<InitialFile> {
5252
return this.type === 'model'
5353
}
5454

55+
get fingerprint(): string {
56+
return this._content
57+
}
58+
5559
updateContent(newContent: string = ''): void {
5660
this._content = newContent
5761
this.content = newContent

web/server/api/endpoints/files.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def walk_path(
8787
def get_file(
8888
path: str = Depends(validate_path),
8989
settings: Settings = Depends(get_settings),
90-
path_mapping: t.Dict[str, t.Any] = Depends(get_path_mapping),
90+
path_mapping: t.Dict[Path, models.FileType] = Depends(get_path_mapping),
9191
) -> models.File:
9292
"""Get a file, including its contents."""
9393
try:
@@ -96,7 +96,7 @@ def get_file(
9696
except FileNotFoundError:
9797
raise HTTPException(status_code=HTTP_404_NOT_FOUND)
9898
return models.File(
99-
name=os.path.basename(path), path=path, content=content, type=path_mapping.get(path)
99+
name=os.path.basename(path), path=path, content=content, type=path_mapping.get(Path(path))
100100
)
101101

102102

@@ -107,6 +107,7 @@ async def write_file(
107107
path: str = Depends(validate_path),
108108
settings: Settings = Depends(get_settings),
109109
context: Context = Depends(get_context),
110+
path_mapping: t.Dict[Path, models.FileType] = Depends(get_path_mapping),
110111
) -> models.File:
111112
"""Create, update, or rename a file."""
112113
path_or_new_path = path
@@ -118,7 +119,10 @@ async def write_file(
118119

119120
content = (settings.project_path / path_or_new_path).read_text()
120121
return models.File(
121-
name=os.path.basename(path_or_new_path), path=path_or_new_path, content=content
122+
name=os.path.basename(path_or_new_path),
123+
path=path_or_new_path,
124+
content=content,
125+
type=path_mapping.get(Path(path)),
122126
)
123127

124128

web/server/api/endpoints/plan.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ async def run_plan(
7878
[to_ds(t) for t in make_inclusive(start, end)]
7979
for start, end in interval.merged_intervals
8080
][0],
81-
batches=tasks[interval.snapshot_name],
81+
batches=tasks.get(interval.snapshot_name, 0),
8282
)
8383
for interval in plan.missing_intervals
8484
]

web/server/main.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from web.server.api.endpoints import api_router
99
from web.server.console import ApiConsole
10+
from web.server.watcher import watch_project
1011

1112
app = FastAPI()
1213
api_console = ApiConsole()
@@ -26,11 +27,13 @@ async def dispatch() -> None:
2627

2728
app.state.console_listeners = []
2829
app.state.dispatch_task = asyncio.create_task(dispatch())
30+
app.state.watch_task = asyncio.create_task(watch_project(api_console.queue))
2931

3032

3133
@app.on_event("shutdown")
3234
def shutdown_event() -> None:
3335
app.state.dispatch_task.cancel()
36+
app.state.watch_task.cancel()
3437

3538

3639
@app.get("/health")

web/server/watcher.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import asyncio
2+
import json
3+
4+
from watchfiles import DefaultFilter, awatch
5+
6+
from sqlmesh.core import constants as c
7+
from web.server.api.endpoints.models import get_models
8+
from web.server.settings import get_loaded_context, get_settings
9+
from web.server.sse import Event
10+
11+
12+
async def watch_project(queue: asyncio.Queue) -> None:
13+
settings = get_settings()
14+
context = await get_loaded_context(settings)
15+
16+
async for _ in awatch(
17+
(context.path / c.MODELS).resolve(),
18+
watch_filter=DefaultFilter(
19+
ignore_entity_patterns=context.config.ignore_patterns if context else c.IGNORE_PATTERNS
20+
),
21+
):
22+
context.load()
23+
24+
queue.put_nowait(
25+
Event(event="models", data=json.dumps([model.dict() for model in get_models(context)]))
26+
)

0 commit comments

Comments
 (0)