BigQuery language server
bqls-mini.mp4
- Go 1.25 or later
Download the latest binary from GitHub Releases and place it in your PATH.
go installgcloud auth login
gcloud auth application-default loginYou can use bqls.nvim to integrate BigQuery with Neovim.
You can specify your BigQuery Project ID and location in the configuration. If not specified, the plugin will use gcloud config get project for the Project ID and US as the default location.
require("lspconfig").bqls.setup({
settings = {
project_id = "YOUR_PROJECT_ID",
location = "YOUR_LOCATION",
},
})You can change project_id with workspace/didChangeConfiguration.
vim.lsp.buf_notify(0, "workspace/didChangeConfiguration", { settings = { project_id = "ANOTHER_PROJECT_ID", location = "ANOTHER_LOCATION" } })Settings omitted from the notification keep their current values, so you can send only what changed. A lint setting, when sent, replaces the whole rule set; lint = vim.empty_dict() or lint = vim.NIL restores the defaults (a plain {} is encoded as a JSON array and rejected).
bqls also supports fetching virtual document previews (table/job info) asynchronously so the
client isn't blocked while BigQuery is queried; see bqls/publishVirtualTextDocument
in the API reference. This is opt-in via supports_async_virtual_text_document in
initializationOptions, so bqls.nvim keeps working unmodified until it adopts the new flow.
The official VSCode extension lives in editors/vscode in this repository.
You can also use the community extension bqls-vscode.
bqls reports lint warnings and information (diagnostic source bqls-lint, code = rule name) for queries that analyze without errors.
| Rule | Default | Description |
|---|---|---|
null-comparison |
on | x = CAST(NULL AS T) / != always yields NULL; quick fix rewrites it to IS [NOT] NULL. |
in-null |
on | x IN (NULL) / x NOT IN (NULL) whose list holds only NULLs (including CAST(NULL AS T) and SAFE_CAST(NULL AS T)) always yields NULL. No quick fix, since IS [NOT] NULL is not equivalent. |
unused-cte |
on | A WITH entry that is never referenced (a recursive CTE's self reference does not count). Reported as information; quick fix removes the entry. |
partition-filter |
on | A time- or range-partitioned table that is read without any filter on its partition column (_PARTITIONTIME / _PARTITIONDATE for ingestion-time partitioning), so the query may scan every partition. A filter anywhere in the statement on the column or a value derived from it (through CTEs, subqueries, and UNION) counts. Statements bqls cannot trace (e.g. recursive CTEs, table functions), view definitions, DML targets, wildcard tables, unreferenced CTEs, and reads that consume no column (e.g. SELECT COUNT(*), unless the table requires a partition filter) are skipped. |
Bare x = NULL / x != NULL is rejected by BigQuery itself ("Operands of = cannot be literal NULL"); bqls offers an IS [NOT] NULL quick fix on that error.
Rules can be toggled with the lint setting (also via workspace/didChangeConfiguration):
settings = {
project_id = "YOUR_PROJECT_ID",
lint = { rules = { ["null-comparison"] = false } },
}To share lint settings between your editor and CI, put a .bqls.yaml in your repository. Both the language server and bqls lint look for it from the SQL file's directory up to the repository root (the directory containing .git); the nearest one wins.
lint:
rules:
unused-cte: false
some-rule: # a rule can also take an object
enabled: true
some_option: 10 # rule-specific optionsEach rule is either true / false or an object with an optional enabled and the rule's options; the editor's lint setting takes the same form. Unknown rules and options are errors (bqls lint exits with 2; the language server logs the error and ignores the file). The language server rereads the file when it changes.
Precedence: --enable / --disable flags or the editor's lint setting > .bqls.yaml > rule defaults. Rule options are merged key by key, so a setting only needs the options it changes.
To silence a single occurrence, add a bqls:ignore comment. A comment on its own line applies to the next line; a trailing comment applies to its own line. Omitting rule names ignores every rule.
-- bqls:ignore null-comparison
SELECT * FROM t WHERE x = CAST(NULL AS INT64)
SELECT * FROM t WHERE y = CAST(NULL AS INT64) -- bqls:ignorebqls lint runs the same analysis and lint rules as the language server, without an editor. Table schemas are fetched with Application Default Credentials; no query is run.
$ bqls lint [flags] <file|dir|->...
$ bqls lint --project my-project queries/
queries/report.sql:3:6: info: CTE `tmp` is defined but never used (unused-cte)
queries/report.sql:8:7: error: Unrecognized name: yDirectories are searched recursively for *.sql files (skipping directories whose name starts with ., such as .git), and - reads from stdin. Flags must come before files. Run bqls lint --help for the full list of rules. Each file uses the nearest .bqls.yaml (stdin uses the one found from the working directory).
| Flag | Description |
|---|---|
--project |
BigQuery project ID. Defaults to gcloud config get project. |
--location |
BigQuery location. Defaults to US. |
--enable <rule> / --disable <rule> |
Turn a rule on or off. Repeatable; the last one wins. Takes precedence over the config file. |
--config <path> |
Use this config file for every input instead of looking up .bqls.yaml. |
--fix |
Apply the quick fixes (the first one of each finding) and write the files back, then report the findings that are left. With -, the fixed SQL is printed to stdout and the findings go to stderr. A fix that overlaps another fix in the same file is skipped (noted on stderr) and its finding is reported; running --fix again applies it. |
--format text|json|github |
text (default) prints path:line:col: severity: message (rule). json prints an array of {file, line, column, endLine, endColumn, severity, rule, message}. github prints workflow commands that show up as annotations on pull requests. |
Lines and columns are 1-based and the end position is exclusive. Columns are currently off on lines containing non-ASCII text (#375). Analysis errors are reported with severity error and no rule. With --format github, file paths are made relative to $GITHUB_WORKSPACE so annotations attach to the diff.
Exit codes: 0 nothing found, 1 any lint finding or analysis error (with --fix, any that is left after fixing), 2 usage or runtime error (bad flags, invalid config file, unreadable or unwritable file, authentication/permission/network failure while fetching table metadata). An unreadable file does not stop the others from being linted; their findings are still printed and the exit code is 2.
GitHub Actions example. The service account only needs to read table metadata (e.g. roles/bigquery.metadataViewer). Pass --project explicitly, since the fallback to gcloud config get project only works where gcloud is installed and configured. Instead of go install, you can download a binary from GitHub Releases (the lint subcommand needs a release that includes it).
jobs:
lint:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: stable
- uses: google-github-actions/auth@v3
with:
workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ secrets.SERVICE_ACCOUNT }}
- run: go install github.com/kitagry/bqls@latest
- run: bqls lint --project my-project --format github .In order to save for spreadsheet, you should enable Google Drive API.
- Enable Google Drive API and Google Sheets API
gcloud auth application-default login --scopes=https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/drive
- textDocument/formatting
- textDocument/hover
- textDocument/completion
- textDocument/definition
- textDocument/codeAction
- workspace/executeCommand
- workspace/didChangeConfiguration
- bqls/virtualTextDocument