From de62e109bb5fb31df85af75c6020e29a602e5f63 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Mon, 2 Nov 2026 15:39:31 +0000 Subject: [PATCH 1/5] Detect notebooks with the workspace's first-line rule The CLI classifies a local source file as a notebook only to predict the remote name the workspace will give it (sync snapshot, translate_paths, deployment state, workspace import-dir). It required the first line to equal the language-specific header exactly, while the import-file endpoint only requires the first line of the leading 200 bytes to contain "Databricks notebook source" (NotebookUtils.checkSourceFileNotebook). A BOM-prefixed SQL notebook therefore landed as notebook "bom" while the sync snapshot tracked "bom.sql"; removing it locally deleted a path that did not exist and left the notebook behind. detectSource now applies the server's rule. Co-authored-by: Isaac --- .../bundle/sync/replace-node-type/output.txt | 10 ++++++++ .../bundle/sync/replace-node-type/script | 5 ++++ libs/notebook/detect.go | 25 ++++++++++--------- libs/notebook/detect_test.go | 5 ++++ 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/acceptance/bundle/sync/replace-node-type/output.txt b/acceptance/bundle/sync/replace-node-type/output.txt index 9e482f6a28e..a048e55c572 100644 --- a/acceptance/bundle/sync/replace-node-type/output.txt +++ b/acceptance/bundle/sync/replace-node-type/output.txt @@ -20,3 +20,13 @@ Uploaded plain.sql >>> list NOTEBOOK bom FILE plain.sql + +=== Remove both local files and sync again + +>>> [CLI] bundle sync --output text +Action: DELETE: bom, plain.sql +Deleted bom +Deleted plain.sql +Initial Sync Complete + +>>> list diff --git a/acceptance/bundle/sync/replace-node-type/script b/acceptance/bundle/sync/replace-node-type/script index 32dd2c3cc53..eca6be849c3 100644 --- a/acceptance/bundle/sync/replace-node-type/script +++ b/acceptance/bundle/sync/replace-node-type/script @@ -23,3 +23,8 @@ trace list title "Sync replaces both\n" trace $CLI bundle sync --output text 2>&1 | sort trace list + +title "Remove both local files and sync again\n" +rm bom.sql plain.sql +trace $CLI bundle sync --output text 2>&1 | sort +trace list diff --git a/libs/notebook/detect.go b/libs/notebook/detect.go index d9be548a5c6..786da2c6b3c 100644 --- a/libs/notebook/detect.go +++ b/libs/notebook/detect.go @@ -22,8 +22,13 @@ type FileInfoWithWorkspaceObjectInfo interface { WorkspaceObjectInfo() workspace.ObjectInfo } -// Maximum length in bytes of the notebook header. -const headerLength = 32 +// Marker the workspace looks for on the first line of a source file to import it as a +// notebook (NOTEBOOK_IDENTIFIER in universe's NotebookUtils). +const notebookSourceMarker = "Databricks notebook source" + +// Number of leading bytes the workspace inspects for the marker +// (NOTEBOOK_IDENTIFIER_BUFFER_LENGTH in universe's NotebookUtils). +const headerLength = 200 // file wraps an fs.File and implements a few helper methods such that // they don't need to be inlined in the [DetectWithFS] function below. @@ -126,29 +131,25 @@ func DetectBytes(name string, content []byte) (notebook bool, language workspace } // detectSource reports whether a source file with the given name and first line is a notebook. +// It applies the same rule as the workspace's import-file endpoint (checkSourceFileNotebook in +// universe's NotebookUtils), so the remote name the CLI predicts matches what the server creates: +// the extension picks the language and the first line only has to contain the marker. A leading +// byte order mark or a different comment prefix therefore still counts. func detectSource(name, fileHeader string) (notebook bool, language workspace.Language) { - header := "" - - // Determine which header to expect based on filename extension. - ext := strings.ToLower(filepath.Ext(name)) - switch ext { + switch strings.ToLower(filepath.Ext(name)) { case ExtensionPython: - header = `# Databricks notebook source` language = workspace.LanguagePython case ExtensionR: - header = `# Databricks notebook source` language = workspace.LanguageR case ExtensionScala: - header = "// Databricks notebook source" language = workspace.LanguageScala case ExtensionSql: - header = "-- Databricks notebook source" language = workspace.LanguageSql default: return false, "" } - if fileHeader != header { + if !strings.Contains(fileHeader, notebookSourceMarker) { return false, "" } diff --git a/libs/notebook/detect_test.go b/libs/notebook/detect_test.go index c774612c9bf..0c0b7203ca5 100644 --- a/libs/notebook/detect_test.go +++ b/libs/notebook/detect_test.go @@ -1,6 +1,7 @@ package notebook import ( + "bytes" "io/fs" "os" "path/filepath" @@ -140,6 +141,10 @@ func TestDetectBytes(t *testing.T) { }{ {"nb.py", []byte("# Databricks notebook source\nprint(1)\n"), true, workspace.LanguagePython}, {"nb.sql", []byte("-- Databricks notebook source\nSELECT 1\n"), true, workspace.LanguageSql}, + {"marker_mid_line.sql", []byte("-- MAGIC Databricks notebook source\nSELECT 1\n"), true, workspace.LanguageSql}, + {"other_comment.py", []byte("// Databricks notebook source\nprint(1)\n"), true, workspace.LanguagePython}, + {"second_line.py", []byte("print(1)\n# Databricks notebook source\n"), false, ""}, + {"past_buffer.py", append(bytes.Repeat([]byte("#"), headerLength), []byte(" Databricks notebook source\n")...), false, ""}, {"nb.ipynb", ipynb, true, workspace.LanguagePython}, {"plain.py", []byte("print(1)\n"), false, ""}, {"space.geniespace.json", []byte("{}"), false, ""}, From b4834797d0d7d75962c24b08d8b5f398f5afc9e8 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Mon, 2 Nov 2026 15:40:28 +0000 Subject: [PATCH 2/5] Add changelog fragment Co-authored-by: Isaac --- .nextchanges/bundles/notebook-detect-first-line.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 .nextchanges/bundles/notebook-detect-first-line.md diff --git a/.nextchanges/bundles/notebook-detect-first-line.md b/.nextchanges/bundles/notebook-detect-first-line.md new file mode 100644 index 00000000000..7faa2256689 --- /dev/null +++ b/.nextchanges/bundles/notebook-detect-first-line.md @@ -0,0 +1 @@ +* Detect notebooks with the same rule as the workspace: a `.py`, `.sql`, `.scala` or `.r` file is a notebook when its first line contains `Databricks notebook source`, for example after a byte order mark. Previously such a file was tracked as a plain file, so removing it locally left the remote notebook behind. ([#7290](https://github.com/databricks/cli/pull/7290)) From 7fcdf29124bb9f6f4353383c37721e76a2d1a8c0 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Tue, 3 Nov 2026 11:57:37 +0000 Subject: [PATCH 3/5] Clarify headerLength comment Co-authored-by: Isaac --- libs/notebook/detect.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/notebook/detect.go b/libs/notebook/detect.go index 786da2c6b3c..e5d56ae990d 100644 --- a/libs/notebook/detect.go +++ b/libs/notebook/detect.go @@ -26,8 +26,8 @@ type FileInfoWithWorkspaceObjectInfo interface { // notebook (NOTEBOOK_IDENTIFIER in universe's NotebookUtils). const notebookSourceMarker = "Databricks notebook source" -// Number of leading bytes the workspace inspects for the marker -// (NOTEBOOK_IDENTIFIER_BUFFER_LENGTH in universe's NotebookUtils). +// Number of leading bytes the workspace reads before checking only the first line among them +// for the marker (NOTEBOOK_IDENTIFIER_BUFFER_LENGTH in universe's NotebookUtils). const headerLength = 200 // file wraps an fs.File and implements a few helper methods such that From f434242bf3fb5f9179d6ef325c71e0a060f959aa Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Tue, 3 Nov 2026 12:03:33 +0000 Subject: [PATCH 4/5] Shorten detect.go comments Co-authored-by: Isaac --- libs/notebook/detect.go | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/libs/notebook/detect.go b/libs/notebook/detect.go index e5d56ae990d..d6952bb6c3d 100644 --- a/libs/notebook/detect.go +++ b/libs/notebook/detect.go @@ -22,12 +22,10 @@ type FileInfoWithWorkspaceObjectInfo interface { WorkspaceObjectInfo() workspace.ObjectInfo } -// Marker the workspace looks for on the first line of a source file to import it as a -// notebook (NOTEBOOK_IDENTIFIER in universe's NotebookUtils). +// Marker the workspace looks for to import a source file as a notebook. const notebookSourceMarker = "Databricks notebook source" -// Number of leading bytes the workspace reads before checking only the first line among them -// for the marker (NOTEBOOK_IDENTIFIER_BUFFER_LENGTH in universe's NotebookUtils). +// The workspace reads the first line out of this many leading bytes. const headerLength = 200 // file wraps an fs.File and implements a few helper methods such that @@ -130,11 +128,8 @@ func DetectBytes(name string, content []byte) (notebook bool, language workspace return notebook, language, nil } -// detectSource reports whether a source file with the given name and first line is a notebook. -// It applies the same rule as the workspace's import-file endpoint (checkSourceFileNotebook in -// universe's NotebookUtils), so the remote name the CLI predicts matches what the server creates: -// the extension picks the language and the first line only has to contain the marker. A leading -// byte order mark or a different comment prefix therefore still counts. +// detectSource reports whether a source file with the given name and first line is a notebook, +// using the import-file rule: the extension picks the language and the first line contains the marker. func detectSource(name, fileHeader string) (notebook bool, language workspace.Language) { switch strings.ToLower(filepath.Ext(name)) { case ExtensionPython: From 7f0b96e4c6bb61ceb939827de14e35cad382220b Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Tue, 3 Nov 2026 12:05:09 +0000 Subject: [PATCH 5/5] Restore original detectSource doc comment Co-authored-by: Isaac --- libs/notebook/detect.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libs/notebook/detect.go b/libs/notebook/detect.go index d6952bb6c3d..0f8c872b297 100644 --- a/libs/notebook/detect.go +++ b/libs/notebook/detect.go @@ -128,8 +128,7 @@ func DetectBytes(name string, content []byte) (notebook bool, language workspace return notebook, language, nil } -// detectSource reports whether a source file with the given name and first line is a notebook, -// using the import-file rule: the extension picks the language and the first line contains the marker. +// detectSource reports whether a source file with the given name and first line is a notebook. func detectSource(name, fileHeader string) (notebook bool, language workspace.Language) { switch strings.ToLower(filepath.Ext(name)) { case ExtensionPython: