From e539b73fe0e84f06edc9a4211e3b44e1f0d72fa7 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Wed, 16 Sep 2026 08:33:27 +0200 Subject: [PATCH 1/2] Add dialect labeler --- .github/PULL_REQUEST_TEMPLATE.md | 2 + .github/workflows/labeler.yml | 50 +++++++ .github/workflows/labeler/label_dialects.js | 136 ++++++++++++++++++++ dev/release/rat_exclude_files.txt | 3 +- 4 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/labeler.yml create mode 100644 .github/workflows/labeler/label_dialects.js diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..9847867434 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,2 @@ + +Dialects: diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000000..f8bf5a73a7 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,50 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: labeler + +# pull_request_target runs on the base branch, so the script below is never taken from the PR head. +on: + pull_request_target: + types: [opened, edited] + issues: + types: [opened, edited] + issue_comment: + types: [created] + +jobs: + label-dialects: + name: Label dialects + # Bot edits and comments arrive in bursts and never carry a dialect. + if: >- + github.event.sender.type != 'Bot' && + (github.event_name != 'issue_comment' || contains(github.event.comment.body, 'dialect')) + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: .github/workflows/labeler + persist-credentials: false + - uses: actions/github-script@v9 + with: + script: | + const label = require('./.github/workflows/labeler/label_dialects.js'); + await label({ github, context }); diff --git a/.github/workflows/labeler/label_dialects.js b/.github/workflows/labeler/label_dialects.js new file mode 100644 index 0000000000..a1660ac100 --- /dev/null +++ b/.github/workflows/labeler/label_dialects.js @@ -0,0 +1,136 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +const DIALECTS = [ + { label: "BigQuery", stems: ["bigquery"], pattern: /\bbig\s?query\b/i }, + { label: "ClickHouse", stems: ["clickhouse"], pattern: /\bclick\s?house\b/i }, + { label: "Databricks", stems: ["databricks"], pattern: /\bdatabricks\b/i }, + { label: "DuckDB", stems: ["duckdb"], pattern: /\bduck\s?db\b/i }, + { label: "Hive", stems: ["hive"], pattern: /\bhive\b/i }, + { label: "MySQL", stems: ["mysql"], pattern: /\b(mysql|mariadb)\b/i }, + { label: "Oracle", stems: ["oracle"], pattern: /\boracle\b/i }, + { label: "PostgreSQL", stems: ["postgresql", "postgres"], pattern: /\bpostgres/i }, + { label: "Redshift", stems: ["redshift"], pattern: /\bredshift\b/i }, + { label: "Snowflake", stems: ["snowflake"], pattern: /\bsnowflake\b/i }, + { label: "Spark", stems: ["spark"], pattern: /\bspark\b/i }, + { label: "SQL Server", stems: ["mssql"], pattern: /\b(mssql|ms sql|sql server|t-?sql)\b/i }, + { label: "SQLite", stems: ["sqlite"], pattern: /\bsqlite\b/i }, + { label: "Teradata", stems: ["teradata"], pattern: /\bteradata\b/i }, +]; + +const DECLARATION = /^[ \t]*dialects?[ \t]*:[ \t]*(.*?)[ \t]*$/im; +const MARKER = ""; +const TRUSTED_ASSOCIATIONS = ["OWNER", "MEMBER", "COLLABORATOR"]; + +function matchDialects(text) { + return DIALECTS.filter((d) => d.pattern.test(text)).map((d) => d.label); +} + +// `undefined` when no `Dialects:` line exists, otherwise the labels it names (possibly none). +function declaredDialects(text) { + const value = DECLARATION.exec(text ?? "")?.[1]; + return value ? matchDialects(value) : undefined; +} + +function stripMarkup(text) { + return text.replace(//g, "").replace(/```[\s\S]*?```/g, ""); +} + +function resolve({ title, body }) { + body ??= ""; + const labels = new Set(matchDialects(title)); + const declared = declaredDialects(body); + for (const label of declared ?? []) labels.add(label); + if (labels.size === 0 && declared === undefined) { + for (const label of matchDialects(stripMarkup(body))) labels.add(label); + } + return { labels: [...labels], answered: labels.size > 0 || declared !== undefined }; +} + +function touchedDialects(paths) { + const stems = new Set(); + for (const path of paths) { + const stem = /^src\/dialect\/(\w+)\.rs$|^tests\/sqlparser_(\w+)\.rs$/.exec(path); + if (stem) stems.add(stem[1] ?? stem[2]); + } + return DIALECTS.filter((d) => d.stems.some((s) => stems.has(s))).map((d) => d.label); +} + +function isCodeChange(paths) { + return paths.some((path) => path.startsWith("src/") || path.startsWith("tests/")); +} + +function askComment(touched) { + const lines = [ + MARKER, + "No dialect label was applied because the title does not name a SQL dialect.", + "If this change targets one or more dialects, edit the title to `[, ]: `, add a `Dialects: [, ]` line to the description, or reply with one. Reply `Dialects: none` if the change is dialect-agnostic.", + ]; + if (touched.length) lines.push(`Changed files touch ${touched.join(", ")}.`); + return lines.join("\n\n"); +} + +function trustedComment(comment, issue) { + if (comment.user.type === "Bot") return false; + return comment.user.id === issue.user.id || TRUSTED_ASSOCIATIONS.includes(comment.author_association); +} + +async function run({ github, context }) { + const { owner, repo } = context.repo; + const { payload } = context; + const issue = payload.pull_request ?? payload.issue; + const issue_number = issue.number; + let labels = []; + let comment; + + if (context.eventName === "issue_comment") { + if (!trustedComment(payload.comment, issue)) return; + labels = declaredDialects(payload.comment.body) ?? []; + } else { + const resolved = resolve(issue); + labels = resolved.labels; + if (!resolved.answered && payload.pull_request && payload.action === "opened") { + const files = await github.paginate(github.rest.pulls.listFiles, { + owner, + repo, + pull_number: issue_number, + per_page: 100, + }); + const paths = files.map((f) => f.filename); + if (isCodeChange(paths)) comment = askComment(touchedDialects(paths)); + } + } + + const existing = new Set(issue.labels.map((l) => l.name)); + const missing = labels.filter((l) => !existing.has(l)); + if (missing.length) { + await github.rest.issues.addLabels({ owner, repo, issue_number, labels: missing }); + } + + if (comment) { + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number, + per_page: 100, + }); + if (comments.some((c) => c.body?.includes(MARKER))) return; + await github.rest.issues.createComment({ owner, repo, issue_number, body: comment }); + } +} + +module.exports = run; diff --git a/dev/release/rat_exclude_files.txt b/dev/release/rat_exclude_files.txt index b8b7b81d08..da678cedad 100644 --- a/dev/release/rat_exclude_files.txt +++ b/dev/release/rat_exclude_files.txt @@ -6,4 +6,5 @@ dev/release/rat_exclude_files.txt sqlparser_bench/img/flamegraph.svg **Cargo.lock filtered_rat.txt -AGENTS.md \ No newline at end of file +AGENTS.md +.github/PULL_REQUEST_TEMPLATE.md \ No newline at end of file From f623a88719428be7f00517daf7f306b719546c40 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Wed, 16 Sep 2026 14:07:08 +0200 Subject: [PATCH 2/2] Accept regex suggestions --- .github/workflows/labeler/label_dialects.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/labeler/label_dialects.js b/.github/workflows/labeler/label_dialects.js index a1660ac100..ef752ef2dd 100644 --- a/.github/workflows/labeler/label_dialects.js +++ b/.github/workflows/labeler/label_dialects.js @@ -21,13 +21,13 @@ const DIALECTS = [ { label: "Databricks", stems: ["databricks"], pattern: /\bdatabricks\b/i }, { label: "DuckDB", stems: ["duckdb"], pattern: /\bduck\s?db\b/i }, { label: "Hive", stems: ["hive"], pattern: /\bhive\b/i }, - { label: "MySQL", stems: ["mysql"], pattern: /\b(mysql|mariadb)\b/i }, + { label: "MySQL", stems: ["mysql"], pattern: /\b(mysql|maria\s?db)\b/i }, { label: "Oracle", stems: ["oracle"], pattern: /\boracle\b/i }, { label: "PostgreSQL", stems: ["postgresql", "postgres"], pattern: /\bpostgres/i }, { label: "Redshift", stems: ["redshift"], pattern: /\bredshift\b/i }, { label: "Snowflake", stems: ["snowflake"], pattern: /\bsnowflake\b/i }, { label: "Spark", stems: ["spark"], pattern: /\bspark\b/i }, - { label: "SQL Server", stems: ["mssql"], pattern: /\b(mssql|ms sql|sql server|t-?sql)\b/i }, + { label: "SQL Server", stems: ["mssql"], pattern: /\b(ms\s?sql|sql server|t-?sql)\b/i }, { label: "SQLite", stems: ["sqlite"], pattern: /\bsqlite\b/i }, { label: "Teradata", stems: ["teradata"], pattern: /\bteradata\b/i }, ];