nishu08 commited on
Commit
8a3099e
·
verified ·
1 Parent(s): 90819c5

Deploy CodeBERT inference Space

Browse files
.gitignore ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ .env
6
+ data/*.parquet
7
+ models/
8
+ .DS_Store
9
+ *.egg-info/
10
+ dist/
11
+ build/
Dockerfile ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Inference Space — loads model from Hugging Face Hub at startup
2
+ FROM python:3.12-slim
3
+
4
+ WORKDIR /app
5
+
6
+ COPY requirements.txt .
7
+ RUN pip install --no-cache-dir --upgrade pip && \
8
+ pip install --no-cache-dir -r requirements.txt
9
+
10
+ COPY . .
11
+
12
+ ENV GRADIO_SERVER_NAME=0.0.0.0
13
+ ENV GRADIO_SERVER_PORT=7860
14
+ ENV SPACE_MODEL_ID=nishu08/sql-codebert-classifier
15
+
16
+ EXPOSE 7860
17
+ CMD ["python", "app.py"]
Dockerfile.inference ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Inference Space — loads model from Hugging Face Hub at startup
2
+ FROM python:3.12-slim
3
+
4
+ WORKDIR /app
5
+
6
+ COPY requirements.txt .
7
+ RUN pip install --no-cache-dir --upgrade pip && \
8
+ pip install --no-cache-dir -r requirements.txt
9
+
10
+ COPY . .
11
+
12
+ ENV GRADIO_SERVER_NAME=0.0.0.0
13
+ ENV GRADIO_SERVER_PORT=7860
14
+ ENV SPACE_MODEL_ID=nishu08/sql-codebert-classifier
15
+
16
+ EXPOSE 7860
17
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -1,10 +1,32 @@
1
  ---
2
- title: Sql Error Classifier
3
- emoji: ⚡
4
- colorFrom: indigo
5
- colorTo: pink
6
  sdk: docker
 
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: SQL Error Classifier
3
+ emoji: 🗄️
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
+ license: mit
10
  ---
11
 
12
+ # SQL Error Classifier — Inference
13
+
14
+ Classify student SQL mistakes using a **CodeBERT cross-encoder** model.
15
+
16
+ Model: [nishu08/sql-codebert-classifier](https://huggingface.co/nishu08/sql-codebert-classifier)
17
+
18
+ ## Inputs
19
+
20
+ - **Question** — what the student was asked
21
+ - **Schema** — available tables/columns
22
+ - **Student SQL** — student's submission
23
+ - **Correct SQL** — reference answer
24
+
25
+ ## Labels
26
+
27
+ `JOIN_ERROR`, `AGGREGATION_ERROR`, `FILTER_ERROR`, `WINDOW_FUNCTION_ERROR`,
28
+ `SUBQUERY_ERROR`, `NULL_HANDLING_ERROR`, `PERFORMANCE_ERROR`, `LOGICAL_ERROR`, `SYNTAX_ERROR`
29
+
30
+ ## Secrets (optional)
31
+
32
+ - `SPACE_MODEL_ID` — override model repo (default: `nishu08/sql-codebert-classifier`)
README_INFERENCE_SPACE.md ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: SQL Error Classifier
3
+ emoji: 🗄️
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ license: mit
10
+ ---
11
+
12
+ # SQL Error Classifier — Inference
13
+
14
+ Classify student SQL mistakes using a **CodeBERT cross-encoder** model.
15
+
16
+ Model: [nishu08/sql-codebert-classifier](https://huggingface.co/nishu08/sql-codebert-classifier)
17
+
18
+ ## Inputs
19
+
20
+ - **Question** — what the student was asked
21
+ - **Schema** — available tables/columns
22
+ - **Student SQL** — student's submission
23
+ - **Correct SQL** — reference answer
24
+
25
+ ## Labels
26
+
27
+ `JOIN_ERROR`, `AGGREGATION_ERROR`, `FILTER_ERROR`, `WINDOW_FUNCTION_ERROR`,
28
+ `SUBQUERY_ERROR`, `NULL_HANDLING_ERROR`, `PERFORMANCE_ERROR`, `LOGICAL_ERROR`, `SYNTAX_ERROR`
29
+
30
+ ## Secrets (optional)
31
+
32
+ - `SPACE_MODEL_ID` — override model repo (default: `nishu08/sql-codebert-classifier`)
README_TRAIN_SPACE.md ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: SQL Error Classifier Training
3
+ emoji: 🧠
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ license: mit
10
+ hardware: t4-small
11
+ ---
12
+
13
+ # SQL Error Classifier — CodeBERT Training Space
14
+
15
+ Train `microsoft/codebert-base` as a **cross-encoder** for multi-label SQL error classification.
16
+
17
+ ## Setup
18
+
19
+ 1. **Hardware:** Settings → Hardware → **GPU t4-small** (recommended)
20
+ 2. **Secrets:** Settings → Secrets → add `HF_TOKEN` (Hugging Face write token) to push models to your account
21
+ 3. **Data:** Include `data/sql_errors_dev.parquet` in this Space repo, or upload parquet at runtime
22
+
23
+ ## Usage
24
+
25
+ 1. Choose bundled dataset or upload your own parquet
26
+ 2. Set epochs, batch size, max samples
27
+ 3. Click **Start Training**
28
+ 4. Optionally enable **Push to Hub** with model id `your-username/sql-codebert-classifier`
29
+
30
+ ## Dataset columns
31
+
32
+ Required (aliases supported):
33
+
34
+ | Column | Aliases |
35
+ |--------|---------|
36
+ | `question` | — |
37
+ | `schema` | — |
38
+ | `student_sql` | `query` |
39
+ | `correct_sql` | `correct_query` |
40
+ | `error_labels` | `label_name` |
41
+
42
+ ## Labels (9-class multi-label)
43
+
44
+ `JOIN_ERROR`, `AGGREGATION_ERROR`, `FILTER_ERROR`, `WINDOW_FUNCTION_ERROR`,
45
+ `SUBQUERY_ERROR`, `NULL_HANDLING_ERROR`, `PERFORMANCE_ERROR`, `LOGICAL_ERROR`, `SYNTAX_ERROR`
app.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CodeBERT inference Gradio app for Hugging Face Spaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ import gradio as gr
8
+
9
+ from src.hf_predict_codebert import CodeBERTSQLErrorClassifier
10
+
11
+ MODEL_ID = os.getenv("SPACE_MODEL_ID", "nishu08/sql-codebert-classifier")
12
+
13
+ try:
14
+ clf = CodeBERTSQLErrorClassifier(MODEL_ID)
15
+ model_status = f"Model loaded: **{MODEL_ID}**"
16
+ except Exception as exc:
17
+ clf = None
18
+ model_status = (
19
+ f"Could not load `{MODEL_ID}`: {exc}\n\n"
20
+ "Push your model first: `python scripts/push_codebert_to_hub.py --repo-id nishu08/sql-codebert-classifier`"
21
+ )
22
+
23
+ EXAMPLES = [
24
+ [
25
+ "What is the average score of students in each department?",
26
+ "students(id, name, score, department_id) | departments(id, name)",
27
+ "SELECT department_id, SUM(score) FROM students GROUP BY department_id",
28
+ "SELECT department_id, AVG(score) FROM students GROUP BY department_id",
29
+ 0.5,
30
+ ],
31
+ [
32
+ "Find students who have not provided an email address.",
33
+ "students(id, name, email, phone)",
34
+ "SELECT name FROM students WHERE email = NULL",
35
+ "SELECT name FROM students WHERE email IS NULL",
36
+ 0.5,
37
+ ],
38
+ [
39
+ "List each student's name along with their department name.",
40
+ "students(id, name, department_id) | departments(id, name)",
41
+ "SELECT students.name, departments.name FROM students JOIN departments",
42
+ "SELECT students.name, departments.name FROM students INNER JOIN departments ON students.department_id = departments.id",
43
+ 0.5,
44
+ ],
45
+ ]
46
+
47
+
48
+ def classify(question, schema, student_sql, correct_sql, threshold):
49
+ if clf is None:
50
+ return "Model not loaded.", ""
51
+ result = clf.predict(
52
+ question=question.strip(),
53
+ schema=schema.strip(),
54
+ student_sql=student_sql.strip(),
55
+ correct_sql=correct_sql.strip(),
56
+ threshold=threshold,
57
+ )
58
+ summary = (
59
+ f"### {result['primary_label']}\n"
60
+ f"Confidence: **{result['primary_confidence']:.1%}**\n\n"
61
+ f"**Active labels:** {', '.join(result['error_labels']) or 'none'}"
62
+ )
63
+ probs = "\n".join(
64
+ f"- **{k}**: {v:.1%}" for k, v in result["probabilities"].items()
65
+ )
66
+ return summary, probs
67
+
68
+
69
+ with gr.Blocks(title="SQL Error Classifier", theme=gr.themes.Soft()) as demo:
70
+ gr.Markdown(
71
+ f"""
72
+ # SQL Error Classifier
73
+ CodeBERT cross-encoder — classifies student SQL mistakes into error categories.
74
+
75
+ {model_status}
76
+ """
77
+ )
78
+ with gr.Row():
79
+ with gr.Column():
80
+ question = gr.Textbox(label="Question", lines=2)
81
+ schema = gr.Textbox(label="Schema", lines=2)
82
+ student_sql = gr.Textbox(label="Student SQL", lines=4)
83
+ correct_sql = gr.Textbox(label="Correct SQL", lines=4)
84
+ threshold = gr.Slider(0.1, 0.9, value=0.5, step=0.05, label="Threshold")
85
+ btn = gr.Button("Classify", variant="primary")
86
+ with gr.Column():
87
+ prediction = gr.Markdown(label="Prediction")
88
+ probabilities = gr.Markdown(label="Probabilities")
89
+
90
+ gr.Examples(examples=EXAMPLES, inputs=[question, schema, student_sql, correct_sql, threshold])
91
+ btn.click(
92
+ classify,
93
+ [question, schema, student_sql, correct_sql, threshold],
94
+ [prediction, probabilities],
95
+ )
96
+
97
+ if __name__ == "__main__":
98
+ demo.launch(server_name="0.0.0.0", server_port=7860)
config/codebert_labels.yaml ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Primary labels for CodeBERT cross-encoder training
2
+ labels:
3
+ - JOIN_ERROR
4
+ - AGGREGATION_ERROR
5
+ - FILTER_ERROR
6
+ - WINDOW_FUNCTION_ERROR
7
+ - SUBQUERY_ERROR
8
+ - NULL_HANDLING_ERROR
9
+ - PERFORMANCE_ERROR
10
+ - LOGICAL_ERROR
11
+ - SYNTAX_ERROR
12
+
13
+ # Map dataset label_name values → one or more CodeBERT labels (multi-label)
14
+ alias_map:
15
+ JOIN_ERROR: [JOIN_ERROR]
16
+ AGGREGATION_ERROR: [AGGREGATION_ERROR]
17
+ HAVING_WHERE_ERROR: [AGGREGATION_ERROR]
18
+ FILTERING_ERROR: [FILTER_ERROR]
19
+ WINDOW_FUNCTION_ERROR: [WINDOW_FUNCTION_ERROR]
20
+ SUBQUERY_ERROR: [SUBQUERY_ERROR]
21
+ NULL_HANDLING_ERROR: [NULL_HANDLING_ERROR]
22
+ PERFORMANCE_ERROR: [PERFORMANCE_ERROR]
23
+ LOGICAL_QUERY_ERROR: [LOGICAL_ERROR]
24
+ SYNTAX_ERROR: [SYNTAX_ERROR]
25
+ DATE_FUNCTION_ERROR: [SYNTAX_ERROR]
26
+ COLUMN_REFERENCE_ERROR: [SYNTAX_ERROR]
27
+ TABLE_REFERENCE_ERROR: [SYNTAX_ERROR]
28
+ DATA_TYPE_ERROR: [SYNTAX_ERROR]
29
+ DUPLICATE_RECORD_ERROR: [FILTER_ERROR]
config/error_categories.yaml ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ categories:
2
+ - id: 0
3
+ name: SYNTAX_ERROR
4
+ description: Missing comma, bracket, quote
5
+ - id: 1
6
+ name: JOIN_ERROR
7
+ description: Missing ON, wrong join condition
8
+ - id: 2
9
+ name: AGGREGATION_ERROR
10
+ description: Missing GROUP BY
11
+ - id: 3
12
+ name: HAVING_WHERE_ERROR
13
+ description: Using aggregate in WHERE
14
+ - id: 4
15
+ name: SUBQUERY_ERROR
16
+ description: Multiple rows returned
17
+ - id: 5
18
+ name: WINDOW_FUNCTION_ERROR
19
+ description: Incorrect OVER/PARTITION BY
20
+ - id: 6
21
+ name: NULL_HANDLING_ERROR
22
+ description: "= NULL instead of IS NULL"
23
+ - id: 7
24
+ name: DATE_FUNCTION_ERROR
25
+ description: Incorrect date format/function
26
+ - id: 8
27
+ name: COLUMN_REFERENCE_ERROR
28
+ description: Column doesn't exist
29
+ - id: 9
30
+ name: TABLE_REFERENCE_ERROR
31
+ description: Table doesn't exist
32
+ - id: 10
33
+ name: DATA_TYPE_ERROR
34
+ description: Comparing integer with string
35
+ - id: 11
36
+ name: DUPLICATE_RECORD_ERROR
37
+ description: Missing DISTINCT
38
+ - id: 12
39
+ name: LOGICAL_QUERY_ERROR
40
+ description: Query runs but answer is wrong
41
+ - id: 13
42
+ name: PERFORMANCE_ERROR
43
+ description: "SELECT *, inefficient joins"
44
+ - id: 14
45
+ name: FILTERING_ERROR
46
+ description: Incorrect WHERE clause
hub/CODEBERT_MODEL_CARD.md ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language: en
3
+ license: mit
4
+ tags:
5
+ - codebert
6
+ - sql
7
+ - education
8
+ - text-classification
9
+ - cross-encoder
10
+ base_model: microsoft/codebert-base
11
+ pipeline_tag: text-classification
12
+ ---
13
+
14
+ # SQL CodeBERT Cross-Encoder
15
+
16
+ Multi-label SQL error classifier using **microsoft/codebert-base** as a cross-encoder.
17
+
18
+ ## Input Format
19
+
20
+ All fields are concatenated into one sequence:
21
+
22
+ ```
23
+ QUESTION:
24
+ {question}
25
+
26
+ SCHEMA:
27
+ {schema}
28
+
29
+ STUDENT_SQL:
30
+ {student_sql}
31
+
32
+ CORRECT_SQL:
33
+ {correct_sql}
34
+ ```
35
+
36
+ ## Labels
37
+
38
+ `JOIN_ERROR`, `AGGREGATION_ERROR`, `FILTER_ERROR`, `WINDOW_FUNCTION_ERROR`,
39
+ `SUBQUERY_ERROR`, `NULL_HANDLING_ERROR`, `PERFORMANCE_ERROR`, `LOGICAL_ERROR`, `SYNTAX_ERROR`
40
+
41
+ ## Training
42
+
43
+ ```bash
44
+ python -m src.hf_train_codebert \
45
+ --data data/sql_errors_1m.parquet \
46
+ --output-dir models/codebert-cross-encoder \
47
+ --epochs 3 \
48
+ --push-to-hub \
49
+ --hub-model-id YOUR_USERNAME/sql-codebert-cross-encoder
50
+ ```
51
+
52
+ ## Inference
53
+
54
+ ```python
55
+ from src.hf_predict_codebert import CodeBERTSQLErrorClassifier
56
+
57
+ clf = CodeBERTSQLErrorClassifier("YOUR_USERNAME/sql-codebert-cross-encoder")
58
+ result = clf.predict(
59
+ question="What is the average score per department?",
60
+ schema="students(id, score, department_id)",
61
+ student_sql="SELECT department_id, SUM(score) FROM students GROUP BY department_id",
62
+ correct_sql="SELECT department_id, AVG(score) FROM students GROUP BY department_id",
63
+ )
64
+ print(result["error_labels"])
65
+ ```
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ numpy>=1.24.0
2
+ pandas>=2.0.0
3
+ scikit-learn>=1.3.0
4
+ pyarrow>=14.0.0
5
+ tqdm>=4.66.0
6
+ pyyaml>=6.0
7
+ torch>=2.0.0
8
+ transformers>=4.36.0
9
+ accelerate>=0.25.0
10
+ datasets>=2.16.0
11
+ huggingface_hub>=0.20.0
12
+ gradio>=4.44.0,<6.0.0
scripts/deploy_inference_space.sh ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Deploy CodeBERT inference Space to Hugging Face.
3
+ # Usage: ./scripts/deploy_inference_space.sh [YOUR_USERNAME/space-name]
4
+ set -euo pipefail
5
+
6
+ SPACE_SUFFIX="${1:-sql-error-classifier}"
7
+ MODEL_REPO="${2:-nishu08/sql-codebert-classifier}"
8
+ TOKEN="${HF_TOKEN:-${HUGGING_FACE_HUB_TOKEN:-}}"
9
+
10
+ if [[ -z "${TOKEN}" ]]; then
11
+ echo "Set HF_TOKEN before deploying."
12
+ exit 1
13
+ fi
14
+
15
+ if [[ "${SPACE_SUFFIX}" == */* ]]; then
16
+ SPACE_ID="${SPACE_SUFFIX}"
17
+ else
18
+ USERNAME=$(python - <<PY
19
+ from huggingface_hub import HfApi
20
+ api = HfApi(token="${TOKEN}")
21
+ print(api.whoami()["name"])
22
+ PY
23
+ )
24
+ SPACE_ID="${USERNAME}/${SPACE_SUFFIX}"
25
+ fi
26
+
27
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
28
+ WORKDIR=$(mktemp -d)
29
+ trap 'rm -rf "${WORKDIR}"' EXIT
30
+
31
+ echo "==> Preparing inference Space files..."
32
+ rsync -a \
33
+ --exclude '.venv' \
34
+ --exclude 'models' \
35
+ --exclude 'data' \
36
+ --exclude '__pycache__' \
37
+ --exclude '.git' \
38
+ "${ROOT}/" "${WORKDIR}/"
39
+
40
+ cp "${ROOT}/README_INFERENCE_SPACE.md" "${WORKDIR}/README.md"
41
+ cp "${ROOT}/Dockerfile.inference" "${WORKDIR}/Dockerfile"
42
+
43
+ # Bake default model id into Dockerfile
44
+ sed -i.bak "s|ENV SPACE_MODEL_ID=.*|ENV SPACE_MODEL_ID=${MODEL_REPO}|" "${WORKDIR}/Dockerfile"
45
+ rm -f "${WORKDIR}/Dockerfile.bak"
46
+
47
+ echo "==> Verifying model repo exists: ${MODEL_REPO}"
48
+ python - <<PY
49
+ from huggingface_hub import HfApi
50
+ api = HfApi(token="${TOKEN}")
51
+ api.model_info("${MODEL_REPO}")
52
+ print("Model found.")
53
+ PY
54
+
55
+ echo "==> Creating Space: ${SPACE_ID}"
56
+ python - <<PY
57
+ from huggingface_hub import HfApi
58
+ api = HfApi(token="${TOKEN}")
59
+ api.create_repo("${SPACE_ID}", repo_type="space", space_sdk="docker", exist_ok=True)
60
+ PY
61
+
62
+ echo "==> Uploading..."
63
+ python - <<PY
64
+ from huggingface_hub import HfApi
65
+ api = HfApi(token="${TOKEN}")
66
+ api.upload_folder(
67
+ folder_path="${WORKDIR}",
68
+ repo_id="${SPACE_ID}",
69
+ repo_type="space",
70
+ commit_message="Deploy CodeBERT inference Space",
71
+ )
72
+ PY
73
+
74
+ echo ""
75
+ echo "Done: https://huggingface.co/spaces/${SPACE_ID}"
76
+ echo "CPU hardware is sufficient for inference."
scripts/deploy_train_space.sh ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Deploy CodeBERT training Space to Hugging Face.
3
+ # Usage: ./scripts/deploy_train_space.sh [YOUR_USERNAME/space-name]
4
+ set -euo pipefail
5
+
6
+ SPACE_SUFFIX="${1:-sql-error-classifier-train}"
7
+ TOKEN="${HF_TOKEN:-${HUGGING_FACE_HUB_TOKEN:-}}"
8
+
9
+ if [[ -z "${TOKEN}" ]]; then
10
+ echo "Set HF_TOKEN before deploying."
11
+ exit 1
12
+ fi
13
+
14
+ # Resolve full space id — must use an account you own
15
+ if [[ "${SPACE_SUFFIX}" == */* ]]; then
16
+ SPACE_ID="${SPACE_SUFFIX}"
17
+ else
18
+ USERNAME=$(python - <<PY
19
+ from huggingface_hub import HfApi
20
+ api = HfApi(token="${TOKEN}")
21
+ print(api.whoami()["name"])
22
+ PY
23
+ )
24
+ SPACE_ID="${USERNAME}/${SPACE_SUFFIX}"
25
+ fi
26
+
27
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
28
+ WORKDIR=$(mktemp -d)
29
+ trap 'rm -rf "${WORKDIR}"' EXIT
30
+
31
+ echo "==> Preparing Space files..."
32
+ rsync -a \
33
+ --exclude '.venv' \
34
+ --exclude 'models' \
35
+ --exclude '__pycache__' \
36
+ --exclude '.git' \
37
+ --exclude '*.joblib' \
38
+ "${ROOT}/" "${WORKDIR}/"
39
+
40
+ cp "${ROOT}/README_TRAIN_SPACE.md" "${WORKDIR}/README.md"
41
+
42
+ echo "==> Creating Space: ${SPACE_ID}"
43
+ python - <<PY
44
+ from huggingface_hub import HfApi
45
+ api = HfApi(token="${TOKEN}")
46
+ api.create_repo("${SPACE_ID}", repo_type="space", space_sdk="docker", exist_ok=True)
47
+ PY
48
+
49
+ echo "==> Uploading..."
50
+ python - <<PY
51
+ from huggingface_hub import HfApi
52
+ api = HfApi(token="${TOKEN}")
53
+ api.upload_folder(
54
+ folder_path="${WORKDIR}",
55
+ repo_id="${SPACE_ID}",
56
+ repo_type="space",
57
+ commit_message="Deploy CodeBERT training Space",
58
+ )
59
+ PY
60
+
61
+ echo "Done: https://huggingface.co/spaces/${SPACE_ID}"
62
+ echo "Set Hardware → GPU t4-small, Secrets → HF_TOKEN"
scripts/push_codebert_to_hub.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Push a locally trained CodeBERT model to Hugging Face Hub."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import os
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
13
+ sys.path.insert(0, str(PROJECT_ROOT))
14
+
15
+ from huggingface_hub import HfApi, create_repo
16
+
17
+ from src.codebert_labels import load_codebert_labels
18
+
19
+ DEFAULT_MODEL_DIR = PROJECT_ROOT / "models" / "codebert-cross-encoder"
20
+ MODEL_CARD = PROJECT_ROOT / "hub" / "CODEBERT_MODEL_CARD.md"
21
+
22
+
23
+ def push(
24
+ model_dir: Path = DEFAULT_MODEL_DIR,
25
+ repo_id: str = "",
26
+ private: bool = False,
27
+ token: str | None = None,
28
+ ) -> str:
29
+ if not model_dir.exists():
30
+ raise FileNotFoundError(f"Model not found at {model_dir}. Train first.")
31
+
32
+ if not repo_id:
33
+ raise ValueError("--repo-id required, e.g. nishu08/sql-codebert-classifier")
34
+
35
+ token = token or os.getenv("HF_TOKEN") or os.getenv("HUGGING_FACE_HUB_TOKEN")
36
+ if not token:
37
+ raise ValueError("Set HF_TOKEN environment variable")
38
+
39
+ # Ensure label config exists for inference
40
+ label_config = model_dir / "label_config.json"
41
+ if not label_config.exists():
42
+ with open(label_config, "w") as f:
43
+ json.dump(
44
+ {
45
+ "labels": load_codebert_labels(),
46
+ "model_name": "microsoft/codebert-base",
47
+ "architecture": "codebert-cross-encoder",
48
+ "threshold": 0.5,
49
+ "max_length": 512,
50
+ },
51
+ f,
52
+ indent=2,
53
+ )
54
+
55
+ api = HfApi(token=token)
56
+ create_repo(repo_id, repo_type="model", private=private, exist_ok=True, token=token)
57
+
58
+ print(f"Uploading {model_dir} → {repo_id} ...")
59
+ api.upload_folder(
60
+ folder_path=str(model_dir),
61
+ repo_id=repo_id,
62
+ repo_type="model",
63
+ token=token,
64
+ commit_message="Upload CodeBERT SQL error classifier",
65
+ )
66
+
67
+ if MODEL_CARD.exists():
68
+ api.upload_file(
69
+ path_or_fileobj=str(MODEL_CARD),
70
+ path_in_repo="README.md",
71
+ repo_id=repo_id,
72
+ repo_type="model",
73
+ token=token,
74
+ )
75
+
76
+ url = f"https://huggingface.co/{repo_id}"
77
+ print(f"Done: {url}")
78
+ return url
79
+
80
+
81
+ def main() -> None:
82
+ parser = argparse.ArgumentParser(description="Push CodeBERT model to HF Hub")
83
+ parser.add_argument("--model-dir", type=Path, default=DEFAULT_MODEL_DIR)
84
+ parser.add_argument("--repo-id", type=str, required=True)
85
+ parser.add_argument("--private", action="store_true")
86
+ parser.add_argument("--token", type=str, default=None)
87
+ args = parser.parse_args()
88
+ push(
89
+ model_dir=args.model_dir,
90
+ repo_id=args.repo_id,
91
+ private=args.private,
92
+ token=args.token,
93
+ )
94
+
95
+
96
+ if __name__ == "__main__":
97
+ main()
src/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """SQL error classification package."""
src/categories.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Dict, List
6
+
7
+ import yaml
8
+
9
+ CONFIG_PATH = Path(__file__).resolve().parent.parent / "config" / "error_categories.yaml"
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class ErrorCategory:
14
+ id: int
15
+ name: str
16
+ description: str
17
+
18
+
19
+ def load_categories(config_path: Path = CONFIG_PATH) -> List[ErrorCategory]:
20
+ with open(config_path) as f:
21
+ data = yaml.safe_load(f)
22
+ return [
23
+ ErrorCategory(id=c["id"], name=c["name"], description=c["description"])
24
+ for c in data["categories"]
25
+ ]
26
+
27
+
28
+ def id_to_name(categories: List[ErrorCategory] | None = None) -> Dict[int, str]:
29
+ cats = categories or load_categories()
30
+ return {c.id: c.name for c in cats}
31
+
32
+
33
+ def name_to_id(categories: List[ErrorCategory] | None = None) -> Dict[str, int]:
34
+ cats = categories or load_categories()
35
+ return {c.name: c.id for c in cats}
src/codebert_dataset.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PyTorch Dataset and preprocessing for CodeBERT Trainer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+ import torch
10
+ from torch.utils.data import Dataset
11
+ from transformers import PreTrainedTokenizerBase
12
+
13
+ from src.codebert_formatting import format_cross_encoder_input
14
+ from src.codebert_labels import label_to_multihot, load_codebert_labels
15
+
16
+
17
+ def normalize_dataframe(df: pd.DataFrame) -> pd.DataFrame:
18
+ """Map project column names to the canonical training schema."""
19
+ col_map = {
20
+ "query": "student_sql",
21
+ "correct_query": "correct_sql",
22
+ "label_name": "error_labels",
23
+ }
24
+ out = df.rename(columns={k: v for k, v in col_map.items() if k in df.columns}).copy()
25
+
26
+ required = ["question", "schema", "student_sql", "correct_sql", "error_labels"]
27
+ missing = [c for c in required if c not in out.columns]
28
+ if missing:
29
+ raise ValueError(
30
+ f"Dataset missing required columns: {missing}. "
31
+ f"Expected {required} (or aliases query/correct_query/label_name)."
32
+ )
33
+ return out
34
+
35
+
36
+ class SQLCodeBERTDataset(Dataset):
37
+ """Tokenized SQL error dataset for Hugging Face Trainer."""
38
+
39
+ def __init__(
40
+ self,
41
+ df: pd.DataFrame,
42
+ tokenizer: PreTrainedTokenizerBase,
43
+ label_list: Optional[List[str]] = None,
44
+ max_length: int = 512,
45
+ ):
46
+ self.df = normalize_dataframe(df).reset_index(drop=True)
47
+ self.tokenizer = tokenizer
48
+ self.label_list = label_list or load_codebert_labels()
49
+ self.max_length = max_length
50
+ self.num_labels = len(self.label_list)
51
+
52
+ def __len__(self) -> int:
53
+ return len(self.df)
54
+
55
+ def __getitem__(self, idx: int) -> Dict[str, Any]:
56
+ row = self.df.iloc[idx]
57
+ text = format_cross_encoder_input(
58
+ question=str(row["question"]),
59
+ schema=str(row["schema"]),
60
+ student_sql=str(row["student_sql"]),
61
+ correct_sql=str(row["correct_sql"]),
62
+ )
63
+ encoded = self.tokenizer(
64
+ text,
65
+ truncation=True,
66
+ max_length=self.max_length,
67
+ padding=False,
68
+ return_tensors=None,
69
+ )
70
+ labels = label_to_multihot(str(row["error_labels"]), self.label_list)
71
+ encoded["labels"] = labels.tolist()
72
+ return encoded
73
+
74
+
75
+ class SQLCodeBERTDataCollator:
76
+ """Pad batches dynamically for Trainer."""
77
+
78
+ def __init__(self, tokenizer: PreTrainedTokenizerBase):
79
+ self.tokenizer = tokenizer
80
+
81
+ def __call__(self, features: List[Dict[str, Any]]) -> Dict[str, torch.Tensor]:
82
+ labels = [f.pop("labels") for f in features]
83
+ batch = self.tokenizer.pad(features, padding=True, return_tensors="pt")
84
+ batch["labels"] = torch.tensor(labels, dtype=torch.float)
85
+ return batch
86
+
87
+
88
+ def prepare_datasets(
89
+ df: pd.DataFrame,
90
+ tokenizer: PreTrainedTokenizerBase,
91
+ test_size: float = 0.1,
92
+ val_size: float = 0.1,
93
+ max_length: int = 512,
94
+ seed: int = 42,
95
+ ) -> tuple[SQLCodeBERTDataset, SQLCodeBERTDataset, SQLCodeBERTDataset]:
96
+ from sklearn.model_selection import train_test_split
97
+
98
+ df = normalize_dataframe(df)
99
+ trainval, test_df = train_test_split(
100
+ df,
101
+ test_size=test_size,
102
+ random_state=seed,
103
+ stratify=df["error_labels"],
104
+ )
105
+ relative_val = val_size / (1 - test_size)
106
+ train_df, val_df = train_test_split(
107
+ trainval,
108
+ test_size=relative_val,
109
+ random_state=seed,
110
+ stratify=trainval["error_labels"],
111
+ )
112
+
113
+ return (
114
+ SQLCodeBERTDataset(train_df, tokenizer, max_length=max_length),
115
+ SQLCodeBERTDataset(val_df, tokenizer, max_length=max_length),
116
+ SQLCodeBERTDataset(test_df, tokenizer, max_length=max_length),
117
+ )
src/codebert_formatting.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Cross-encoder input formatting for CodeBERT."""
2
+
3
+ from __future__ import annotations
4
+
5
+ QUESTION_TAG = "QUESTION:"
6
+ SCHEMA_TAG = "SCHEMA:"
7
+ STUDENT_TAG = "STUDENT_SQL:"
8
+ CORRECT_TAG = "CORRECT_SQL:"
9
+
10
+
11
+ def format_cross_encoder_input(
12
+ question: str,
13
+ schema: str,
14
+ student_sql: str,
15
+ correct_sql: str,
16
+ ) -> str:
17
+ """
18
+ Concatenate all fields into a single CodeBERT input sequence.
19
+
20
+ The model attends jointly across question intent, schema, student SQL,
21
+ and the reference solution — cross-encoder style in one forward pass.
22
+ """
23
+ return (
24
+ f"{QUESTION_TAG}\n{question.strip()}\n\n"
25
+ f"{SCHEMA_TAG}\n{schema.strip()}\n\n"
26
+ f"{STUDENT_TAG}\n{student_sql.strip()}\n\n"
27
+ f"{CORRECT_TAG}\n{correct_sql.strip()}"
28
+ )
src/codebert_labels.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Label utilities for CodeBERT multi-label classification."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Dict, List, Sequence, Union
7
+
8
+ import numpy as np
9
+ import yaml
10
+
11
+ CONFIG_PATH = (
12
+ Path(__file__).resolve().parent.parent / "config" / "codebert_labels.yaml"
13
+ )
14
+
15
+
16
+ def load_codebert_labels(config_path: Path = CONFIG_PATH) -> List[str]:
17
+ with open(config_path) as f:
18
+ data = yaml.safe_load(f)
19
+ return list(data["labels"])
20
+
21
+
22
+ def load_alias_map(config_path: Path = CONFIG_PATH) -> Dict[str, List[str]]:
23
+ with open(config_path) as f:
24
+ data = yaml.safe_load(f)
25
+ return {k: list(v) for k, v in data["alias_map"].items()}
26
+
27
+
28
+ def label_to_multihot(
29
+ error_labels: Union[str, Sequence[str]],
30
+ label_list: List[str] | None = None,
31
+ alias_map: Dict[str, List[str]] | None = None,
32
+ ) -> np.ndarray:
33
+ """
34
+ Convert error label(s) to multi-hot vector.
35
+
36
+ Accepts:
37
+ - comma-separated string: "JOIN_ERROR,AGGREGATION_ERROR"
38
+ - list of label strings
39
+ - single dataset label_name (resolved via alias_map)
40
+ """
41
+ labels = label_list or load_codebert_labels()
42
+ aliases = alias_map or load_alias_map()
43
+ index = {name: i for i, name in enumerate(labels)}
44
+ vec = np.zeros(len(labels), dtype=np.float32)
45
+
46
+ if isinstance(error_labels, str):
47
+ raw = [s.strip() for s in error_labels.split(",") if s.strip()]
48
+ if len(raw) == 1 and raw[0] in aliases:
49
+ raw = aliases[raw[0]]
50
+ elif len(raw) == 1 and raw[0] in index:
51
+ raw = [raw[0]]
52
+ elif len(raw) == 1 and raw[0] not in index:
53
+ mapped = aliases.get(raw[0], [])
54
+ raw = mapped
55
+ else:
56
+ raw = list(error_labels)
57
+ expanded: List[str] = []
58
+ for item in raw:
59
+ if item in aliases:
60
+ expanded.extend(aliases[item])
61
+ elif item in index:
62
+ expanded.append(item)
63
+ raw = expanded
64
+
65
+ for name in raw:
66
+ if name not in index:
67
+ raise ValueError(f"Unknown label '{name}'. Expected one of {labels}")
68
+ vec[index[name]] = 1.0
69
+
70
+ if vec.sum() == 0:
71
+ raise ValueError(f"No valid labels found in {error_labels}")
72
+ return vec
73
+
74
+
75
+ def multihot_to_label_names(
76
+ vec: np.ndarray,
77
+ label_list: List[str] | None = None,
78
+ threshold: float = 0.5,
79
+ ) -> List[str]:
80
+ labels = label_list or load_codebert_labels()
81
+ indices = np.where(vec >= threshold)[0]
82
+ return [labels[i] for i in indices]
src/device_utils.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pick the best available torch device."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import torch
6
+
7
+
8
+ def get_device() -> str:
9
+ if torch.cuda.is_available():
10
+ return "cuda"
11
+ if torch.backends.mps.is_available():
12
+ return "mps"
13
+ return "cpu"
src/exercises.py ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generate playground exercises: schema + question + correct SQL."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
6
+ from dataclasses import dataclass
7
+ from typing import Callable, Dict, List
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class Exercise:
12
+ schema: str
13
+ question: str
14
+ correct_query: str
15
+ tables: tuple[str, ...]
16
+ columns: tuple[str, ...]
17
+
18
+
19
+ def _fmt_schema(tables: Dict[str, List[str]]) -> str:
20
+ parts = [f"{name}({', '.join(cols)})" for name, cols in tables.items()]
21
+ return " | ".join(parts)
22
+
23
+
24
+ ExerciseBuilder = Callable[[random.Random], Exercise]
25
+
26
+ EXERCISE_BUILDERS: List[ExerciseBuilder] = []
27
+
28
+
29
+ def _register(builder: ExerciseBuilder) -> ExerciseBuilder:
30
+ EXERCISE_BUILDERS.append(builder)
31
+ return builder
32
+
33
+
34
+ @_register
35
+ def exercise_avg_by_department(rng: random.Random) -> Exercise:
36
+ tables = {
37
+ "students": ["id", "name", "email", "score", "department_id"],
38
+ "departments": ["id", "name", "city"],
39
+ }
40
+ return Exercise(
41
+ schema=_fmt_schema(tables),
42
+ question="What is the average score of students in each department?",
43
+ correct_query=(
44
+ "SELECT department_id, AVG(score) "
45
+ "FROM students GROUP BY department_id"
46
+ ),
47
+ tables=tuple(tables),
48
+ columns=("department_id", "score"),
49
+ )
50
+
51
+
52
+ @_register
53
+ def exercise_student_department_names(rng: random.Random) -> Exercise:
54
+ tables = {
55
+ "students": ["id", "name", "department_id"],
56
+ "departments": ["id", "name"],
57
+ }
58
+ return Exercise(
59
+ schema=_fmt_schema(tables),
60
+ question="List each student's name along with their department name.",
61
+ correct_query=(
62
+ "SELECT students.name, departments.name "
63
+ "FROM students "
64
+ "INNER JOIN departments ON students.department_id = departments.id"
65
+ ),
66
+ tables=tuple(tables),
67
+ columns=("name", "department_id"),
68
+ )
69
+
70
+
71
+ @_register
72
+ def exercise_high_scoring_students(rng: random.Random) -> Exercise:
73
+ threshold = rng.randint(70, 90)
74
+ tables = {"students": ["id", "name", "age", "score", "status"]}
75
+ return Exercise(
76
+ schema=_fmt_schema(tables),
77
+ question=(
78
+ f"Find names of students older than 18 with a score above {threshold}."
79
+ ),
80
+ correct_query=(
81
+ f"SELECT name FROM students "
82
+ f"WHERE age > 18 AND score > {threshold}"
83
+ ),
84
+ tables=tuple(tables),
85
+ columns=("name", "age", "score", "status"),
86
+ )
87
+
88
+
89
+ @_register
90
+ def exercise_unique_cities(rng: random.Random) -> Exercise:
91
+ tables = {"students": ["id", "name", "city", "country"]}
92
+ return Exercise(
93
+ schema=_fmt_schema(tables),
94
+ question="List the unique cities where students live.",
95
+ correct_query="SELECT DISTINCT city FROM students",
96
+ tables=tuple(tables),
97
+ columns=("city",),
98
+ )
99
+
100
+
101
+ @_register
102
+ def exercise_top_scorer(rng: random.Random) -> Exercise:
103
+ tables = {"students": ["id", "name", "score"], "grades": ["id", "score"]}
104
+ return Exercise(
105
+ schema=_fmt_schema(tables),
106
+ question="Find students whose score equals the highest score in the class.",
107
+ correct_query=(
108
+ "SELECT name FROM students "
109
+ "WHERE score = (SELECT MAX(score) FROM grades)"
110
+ ),
111
+ tables=tuple(tables),
112
+ columns=("name", "score"),
113
+ )
114
+
115
+
116
+ @_register
117
+ def exercise_departments_over_budget(rng: random.Random) -> Exercise:
118
+ budget = rng.randint(3, 8)
119
+ tables = {"employees": ["id", "name", "department_id", "salary"]}
120
+ return Exercise(
121
+ schema=_fmt_schema(tables),
122
+ question=f"Which departments have more than {budget} employees?",
123
+ correct_query=(
124
+ f"SELECT department_id, COUNT(*) AS cnt "
125
+ f"FROM employees GROUP BY department_id "
126
+ f"HAVING COUNT(*) > {budget}"
127
+ ),
128
+ tables=tuple(tables),
129
+ columns=("department_id", "salary"),
130
+ )
131
+
132
+
133
+ @_register
134
+ def exercise_recent_orders(rng: random.Random) -> Exercise:
135
+ year = rng.randint(2020, 2024)
136
+ tables = {"orders": ["id", "customer_id", "amount", "order_date", "status"]}
137
+ return Exercise(
138
+ schema=_fmt_schema(tables),
139
+ question=f"Show orders placed on or after January 1, {year}.",
140
+ correct_query=(
141
+ f"SELECT id, amount FROM orders "
142
+ f"WHERE order_date >= DATE '{year}-01-01'"
143
+ ),
144
+ tables=tuple(tables),
145
+ columns=("order_date", "amount", "status"),
146
+ )
147
+
148
+
149
+ @_register
150
+ def exercise_missing_email(rng: random.Random) -> Exercise:
151
+ tables = {"students": ["id", "name", "email", "phone"]}
152
+ return Exercise(
153
+ schema=_fmt_schema(tables),
154
+ question="Find students who have not provided an email address.",
155
+ correct_query="SELECT name FROM students WHERE email IS NULL",
156
+ tables=tuple(tables),
157
+ columns=("email", "name"),
158
+ )
159
+
160
+
161
+ @_register
162
+ def exercise_rank_by_score(rng: random.Random) -> Exercise:
163
+ tables = {"students": ["id", "name", "score", "department_id"]}
164
+ return Exercise(
165
+ schema=_fmt_schema(tables),
166
+ question="Rank students by score within each department.",
167
+ correct_query=(
168
+ "SELECT name, score, "
169
+ "RANK() OVER (PARTITION BY department_id ORDER BY score DESC) AS rnk "
170
+ "FROM students"
171
+ ),
172
+ tables=tuple(tables),
173
+ columns=("name", "score", "department_id"),
174
+ )
175
+
176
+
177
+ @_register
178
+ def exercise_course_enrollment_count(rng: random.Random) -> Exercise:
179
+ tables = {
180
+ "courses": ["id", "title"],
181
+ "enrollments": ["id", "course_id", "student_id"],
182
+ }
183
+ return Exercise(
184
+ schema=_fmt_schema(tables),
185
+ question="How many students are enrolled in each course?",
186
+ correct_query=(
187
+ "SELECT courses.title, COUNT(enrollments.student_id) AS enrolled "
188
+ "FROM courses "
189
+ "INNER JOIN enrollments ON courses.id = enrollments.course_id "
190
+ "GROUP BY courses.title"
191
+ ),
192
+ tables=tuple(tables),
193
+ columns=("title", "student_id", "course_id"),
194
+ )
195
+
196
+
197
+ @_register
198
+ def exercise_active_employees(rng: random.Random) -> Exercise:
199
+ tables = {"employees": ["id", "name", "salary", "status", "hire_date"]}
200
+ return Exercise(
201
+ schema=_fmt_schema(tables),
202
+ question="What is the total salary paid to active employees?",
203
+ correct_query=(
204
+ "SELECT SUM(salary) FROM employees WHERE status = 'active'"
205
+ ),
206
+ tables=tuple(tables),
207
+ columns=("salary", "status"),
208
+ )
209
+
210
+
211
+ @_register
212
+ def exercise_product_price_filter(rng: random.Random) -> Exercise:
213
+ lo, hi = rng.randint(10, 50), rng.randint(100, 500)
214
+ tables = {"products": ["id", "name", "price", "category"]}
215
+ return Exercise(
216
+ schema=_fmt_schema(tables),
217
+ question=f"List products priced between {lo} and {hi}.",
218
+ correct_query=(
219
+ f"SELECT name, price FROM products "
220
+ f"WHERE price BETWEEN {lo} AND {hi}"
221
+ ),
222
+ tables=tuple(tables),
223
+ columns=("name", "price", "category"),
224
+ )
225
+
226
+
227
+ def generate_exercise(rng: random.Random) -> Exercise:
228
+ return rng.choice(EXERCISE_BUILDERS)(rng)
src/generate_dataset.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generate labeled SQL error dataset at scale."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import random
7
+ from concurrent.futures import ProcessPoolExecutor, as_completed
8
+ from pathlib import Path
9
+ from typing import Dict, List, Tuple
10
+
11
+ import pandas as pd
12
+ from tqdm import tqdm
13
+
14
+ from src.categories import load_categories
15
+ from src.exercises import generate_exercise
16
+ from src.sql_templates import ERROR_INJECTORS
17
+
18
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
19
+ DEFAULT_OUTPUT = PROJECT_ROOT / "data" / "sql_errors_1m.parquet"
20
+
21
+
22
+ def generate_dataset(
23
+ total_samples: int = 1_000_000,
24
+ output_path: Path = DEFAULT_OUTPUT,
25
+ batch_size: int = 10_000,
26
+ workers: int = 8,
27
+ seed: int = 42,
28
+ ) -> Path:
29
+ categories = load_categories()
30
+ label_ids = [c.id for c in categories]
31
+ samples_per_class = total_samples // len(label_ids)
32
+ remainder = total_samples % len(label_ids)
33
+
34
+ # Balanced label schedule: each class gets equal share (+1 for first `remainder` classes)
35
+ schedule: List[int] = []
36
+ for cat in categories:
37
+ count = samples_per_class + (1 if cat.id < remainder else 0)
38
+ schedule.extend([cat.id] * count)
39
+ random.Random(seed).shuffle(schedule)
40
+
41
+ output_path.parent.mkdir(parents=True, exist_ok=True)
42
+ chunks: List[pd.DataFrame] = []
43
+ num_batches = (total_samples + batch_size - 1) // batch_size
44
+
45
+ with ProcessPoolExecutor(max_workers=workers) as executor:
46
+ futures = []
47
+ offset = 0
48
+ for batch_idx in range(num_batches):
49
+ current_batch = min(batch_size, total_samples - offset)
50
+ batch_labels = schedule[offset : offset + current_batch]
51
+ futures.append(
52
+ executor.submit(
53
+ _generate_batch_with_labels,
54
+ batch_labels,
55
+ seed + batch_idx,
56
+ )
57
+ )
58
+ offset += current_batch
59
+
60
+ for future in tqdm(as_completed(futures), total=len(futures), desc="Generating"):
61
+ rows = future.result()
62
+ chunks.append(pd.DataFrame(rows))
63
+
64
+ df = pd.concat(chunks, ignore_index=True)
65
+ df = df.sample(frac=1, random_state=seed).reset_index(drop=True)
66
+ df.to_parquet(output_path, index=False)
67
+
68
+ print(f"Saved {len(df):,} samples to {output_path}")
69
+ print("\nClass distribution:")
70
+ print(df["label_name"].value_counts().sort_index().to_string())
71
+ return output_path
72
+
73
+
74
+ def _generate_batch_with_labels(label_ids: List[int], seed: int) -> List[Dict]:
75
+ rng = random.Random(seed)
76
+ categories = load_categories()
77
+ rows = []
78
+ for label_id in label_ids:
79
+ exercise = generate_exercise(rng)
80
+ injector = ERROR_INJECTORS[label_id]
81
+ query, error_message = injector(rng, exercise)
82
+ rows.append(
83
+ {
84
+ "schema": exercise.schema,
85
+ "question": exercise.question,
86
+ "correct_query": exercise.correct_query,
87
+ "query": query.strip(),
88
+ "error_message": error_message,
89
+ "label_id": label_id,
90
+ "label_name": categories[label_id].name,
91
+ }
92
+ )
93
+ return rows
94
+
95
+
96
+ def main() -> None:
97
+ parser = argparse.ArgumentParser(description="Generate labeled SQL error dataset")
98
+ parser.add_argument("--samples", type=int, default=1_000_000, help="Total samples")
99
+ parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
100
+ parser.add_argument("--batch-size", type=int, default=10_000)
101
+ parser.add_argument("--workers", type=int, default=8)
102
+ parser.add_argument("--seed", type=int, default=42)
103
+ args = parser.parse_args()
104
+
105
+ generate_dataset(
106
+ total_samples=args.samples,
107
+ output_path=args.output,
108
+ batch_size=args.batch_size,
109
+ workers=args.workers,
110
+ seed=args.seed,
111
+ )
112
+
113
+
114
+ if __name__ == "__main__":
115
+ main()
src/hf_eval_codebert.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Evaluate a trained CodeBERT cross-encoder."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ from pathlib import Path
8
+
9
+ import pandas as pd
10
+ from transformers import AutoModelForSequenceClassification, AutoTokenizer, Trainer
11
+
12
+ from src.codebert_dataset import SQLCodeBERTDataCollator, SQLCodeBERTDataset, normalize_dataframe
13
+ from src.hf_metrics import build_compute_metrics, compute_multilabel_metrics
14
+
15
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
16
+ DEFAULT_DATA = PROJECT_ROOT / "data" / "sql_errors_dev.parquet"
17
+ DEFAULT_MODEL = PROJECT_ROOT / "models" / "codebert-cross-encoder"
18
+
19
+
20
+ def evaluate(
21
+ model_dir: Path = DEFAULT_MODEL,
22
+ data_path: Path = DEFAULT_DATA,
23
+ sample_size: int = 10_000,
24
+ threshold: float = 0.5,
25
+ seed: int = 42,
26
+ ) -> dict:
27
+ df = normalize_dataframe(pd.read_parquet(data_path))
28
+ if len(df) > sample_size:
29
+ df = df.sample(n=sample_size, random_state=seed)
30
+
31
+ tokenizer = AutoTokenizer.from_pretrained(model_dir)
32
+ model = AutoModelForSequenceClassification.from_pretrained(model_dir)
33
+ dataset = SQLCodeBERTDataset(df, tokenizer)
34
+
35
+ trainer_kwargs = dict(
36
+ model=model,
37
+ data_collator=SQLCodeBERTDataCollator(tokenizer),
38
+ compute_metrics=build_compute_metrics(threshold=threshold),
39
+ )
40
+ try:
41
+ trainer = Trainer(processing_class=tokenizer, **trainer_kwargs)
42
+ except TypeError:
43
+ trainer = Trainer(tokenizer=tokenizer, **trainer_kwargs)
44
+
45
+ output = trainer.predict(dataset)
46
+ metrics = compute_multilabel_metrics(
47
+ output.predictions, output.label_ids, threshold=threshold
48
+ )
49
+ print(json.dumps(metrics, indent=2))
50
+ return metrics
51
+
52
+
53
+ def main() -> None:
54
+ parser = argparse.ArgumentParser(description="Evaluate CodeBERT SQL classifier")
55
+ parser.add_argument("--model-dir", type=Path, default=DEFAULT_MODEL)
56
+ parser.add_argument("--data", type=Path, default=DEFAULT_DATA)
57
+ parser.add_argument("--sample-size", type=int, default=10_000)
58
+ parser.add_argument("--threshold", type=float, default=0.5)
59
+ args = parser.parse_args()
60
+ evaluate(
61
+ model_dir=args.model_dir,
62
+ data_path=args.data,
63
+ sample_size=args.sample_size,
64
+ threshold=args.threshold,
65
+ )
66
+
67
+
68
+ if __name__ == "__main__":
69
+ main()
src/hf_metrics.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Evaluation metrics for multi-label SQL error classification."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Dict
6
+
7
+ import numpy as np
8
+ from sklearn.metrics import (
9
+ accuracy_score,
10
+ f1_score,
11
+ hamming_loss,
12
+ precision_score,
13
+ recall_score,
14
+ )
15
+
16
+
17
+ def sigmoid(x: np.ndarray) -> np.ndarray:
18
+ return 1.0 / (1.0 + np.exp(-x))
19
+
20
+
21
+ def compute_multilabel_metrics(
22
+ logits: np.ndarray,
23
+ labels: np.ndarray,
24
+ threshold: float = 0.5,
25
+ ) -> Dict[str, float]:
26
+ probs = sigmoid(logits)
27
+ preds = (probs >= threshold).astype(int)
28
+ labels = labels.astype(int)
29
+
30
+ return {
31
+ "accuracy": float(accuracy_score(labels, preds)),
32
+ "f1_macro": float(f1_score(labels, preds, average="macro", zero_division=0)),
33
+ "f1_micro": float(f1_score(labels, preds, average="micro", zero_division=0)),
34
+ "precision_macro": float(
35
+ precision_score(labels, preds, average="macro", zero_division=0)
36
+ ),
37
+ "recall_macro": float(
38
+ recall_score(labels, preds, average="macro", zero_division=0)
39
+ ),
40
+ "hamming_loss": float(hamming_loss(labels, preds)),
41
+ "subset_accuracy": float((preds == labels).all(axis=1).mean()),
42
+ }
43
+
44
+
45
+ def build_compute_metrics(threshold: float = 0.5):
46
+ """Factory for Hugging Face Trainer compute_metrics callback."""
47
+
48
+ def compute_metrics(eval_pred) -> Dict[str, float]:
49
+ logits, labels = eval_pred
50
+ return compute_multilabel_metrics(logits, labels, threshold=threshold)
51
+
52
+ return compute_metrics
src/hf_predict_codebert.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Inference for CodeBERT SQL error cross-encoder."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ from pathlib import Path
8
+ from typing import List, Optional, Union
9
+
10
+ import numpy as np
11
+ import torch
12
+ from transformers import AutoModelForSequenceClassification, AutoTokenizer
13
+
14
+ from src.codebert_formatting import format_cross_encoder_input
15
+ from src.device_utils import get_device
16
+ from src.codebert_labels import load_codebert_labels, multihot_to_label_names
17
+ from src.hf_metrics import sigmoid
18
+
19
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
20
+ DEFAULT_MODEL_DIR = PROJECT_ROOT / "models" / "codebert-cross-encoder"
21
+
22
+
23
+ def _is_hub_id(model_dir: Union[str, Path]) -> bool:
24
+ text = str(model_dir)
25
+ local = Path(text)
26
+ return "/" in text and not local.exists()
27
+
28
+
29
+ class CodeBERTSQLErrorClassifier:
30
+ """CodeBERT cross-encoder inference wrapper."""
31
+
32
+ def __init__(
33
+ self,
34
+ model_dir: Union[str, Path] = DEFAULT_MODEL_DIR,
35
+ threshold: float = 0.5,
36
+ device: Optional[str] = None,
37
+ ):
38
+ self.hub_id = str(model_dir) if _is_hub_id(model_dir) else None
39
+ self.model_dir = Path(model_dir) if not self.hub_id else None
40
+ self.threshold = threshold
41
+ # MPS inference can be flaky for some ops; CPU is reliable on Mac
42
+ self.device = device or (
43
+ "cuda" if torch.cuda.is_available() else "cpu"
44
+ )
45
+ model_ref = self.hub_id or str(self.model_dir)
46
+
47
+ if self.hub_id:
48
+ self.label_list = load_codebert_labels()
49
+ self.max_length = 512
50
+ else:
51
+ config_path = self.model_dir / "label_config.json"
52
+ if config_path.exists():
53
+ with open(config_path) as f:
54
+ cfg = json.load(f)
55
+ self.label_list = cfg.get("labels", load_codebert_labels())
56
+ self.threshold = cfg.get("threshold", threshold)
57
+ self.max_length = cfg.get("max_length", 512)
58
+ else:
59
+ self.label_list = load_codebert_labels()
60
+ self.max_length = 512
61
+
62
+ self.tokenizer = AutoTokenizer.from_pretrained(model_ref)
63
+ self.model = AutoModelForSequenceClassification.from_pretrained(
64
+ model_ref
65
+ ).to(self.device)
66
+ self.model.eval()
67
+
68
+ def predict(
69
+ self,
70
+ question: str,
71
+ schema: str,
72
+ student_sql: str,
73
+ correct_sql: str,
74
+ threshold: Optional[float] = None,
75
+ top_k: int = 5,
76
+ ) -> dict:
77
+ text = format_cross_encoder_input(
78
+ question=question,
79
+ schema=schema,
80
+ student_sql=student_sql,
81
+ correct_sql=correct_sql,
82
+ )
83
+ encoded = self.tokenizer(
84
+ text,
85
+ truncation=True,
86
+ max_length=self.max_length,
87
+ padding=True,
88
+ return_tensors="pt",
89
+ ).to(self.device)
90
+
91
+ with torch.no_grad():
92
+ logits = self.model(**encoded).logits.cpu().numpy()[0]
93
+
94
+ probs = sigmoid(logits)
95
+ thr = threshold if threshold is not None else self.threshold
96
+ predicted = multihot_to_label_names(probs, self.label_list, threshold=thr)
97
+
98
+ ranked = sorted(
99
+ zip(self.label_list, probs.tolist()),
100
+ key=lambda x: x[1],
101
+ reverse=True,
102
+ )[:top_k]
103
+
104
+ return {
105
+ "error_labels": predicted,
106
+ "probabilities": {name: float(p) for name, p in ranked},
107
+ "top_k": [
108
+ {"label": name, "probability": float(p)} for name, p in ranked
109
+ ],
110
+ "primary_label": ranked[0][0],
111
+ "primary_confidence": float(ranked[0][1]),
112
+ }
113
+
114
+ def predict_batch(
115
+ self,
116
+ examples: List[dict],
117
+ batch_size: int = 16,
118
+ ) -> List[dict]:
119
+ results = []
120
+ for i in range(0, len(examples), batch_size):
121
+ chunk = examples[i : i + batch_size]
122
+ texts = [
123
+ format_cross_encoder_input(
124
+ question=x["question"],
125
+ schema=x["schema"],
126
+ student_sql=x["student_sql"],
127
+ correct_sql=x["correct_sql"],
128
+ )
129
+ for x in chunk
130
+ ]
131
+ encoded = self.tokenizer(
132
+ texts,
133
+ truncation=True,
134
+ max_length=self.max_length,
135
+ padding=True,
136
+ return_tensors="pt",
137
+ ).to(self.device)
138
+
139
+ with torch.no_grad():
140
+ logits = self.model(**encoded).logits.cpu().numpy()
141
+
142
+ for j, row in enumerate(logits):
143
+ probs = sigmoid(row)
144
+ results.append(
145
+ {
146
+ "error_labels": multihot_to_label_names(
147
+ probs, self.label_list, self.threshold
148
+ ),
149
+ "primary_label": self.label_list[int(np.argmax(probs))],
150
+ "primary_confidence": float(np.max(probs)),
151
+ }
152
+ )
153
+ return results
154
+
155
+
156
+ def main() -> None:
157
+ parser = argparse.ArgumentParser(description="CodeBERT SQL error inference")
158
+ parser.add_argument("--model-dir", type=Path, default=DEFAULT_MODEL_DIR)
159
+ parser.add_argument("--question", type=str, required=True)
160
+ parser.add_argument("--schema", type=str, required=True)
161
+ parser.add_argument("--student-sql", type=str, required=True)
162
+ parser.add_argument("--correct-sql", type=str, required=True)
163
+ parser.add_argument("--threshold", type=float, default=0.5)
164
+ args = parser.parse_args()
165
+
166
+ clf = CodeBERTSQLErrorClassifier(args.model_dir, threshold=args.threshold)
167
+ result = clf.predict(
168
+ question=args.question,
169
+ schema=args.schema,
170
+ student_sql=args.student_sql,
171
+ correct_sql=args.correct_sql,
172
+ )
173
+ print(json.dumps(result, indent=2))
174
+
175
+
176
+ if __name__ == "__main__":
177
+ main()
src/hf_train_codebert.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Train CodeBERT cross-encoder for SQL error classification with HF Trainer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ from pathlib import Path
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+ import torch
12
+ from transformers import (
13
+ AutoModelForSequenceClassification,
14
+ AutoTokenizer,
15
+ EarlyStoppingCallback,
16
+ Trainer,
17
+ TrainingArguments,
18
+ )
19
+
20
+ from src.device_utils import get_device
21
+ from src.codebert_dataset import (
22
+ SQLCodeBERTDataCollator,
23
+ prepare_datasets,
24
+ )
25
+ from src.codebert_labels import load_codebert_labels
26
+ from src.hf_metrics import build_compute_metrics, compute_multilabel_metrics
27
+
28
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
29
+ DEFAULT_DATA = PROJECT_ROOT / "data" / "sql_errors_1m.parquet"
30
+ DEFAULT_OUTPUT = PROJECT_ROOT / "models" / "codebert-cross-encoder"
31
+ DEFAULT_MODEL = "microsoft/codebert-base"
32
+
33
+
34
+ def train(
35
+ data_path: Path | None = DEFAULT_DATA,
36
+ dataframe: pd.DataFrame | None = None,
37
+ output_dir: Path = DEFAULT_OUTPUT,
38
+ model_name: str = DEFAULT_MODEL,
39
+ epochs: float = 3.0,
40
+ batch_size: int = 16,
41
+ eval_batch_size: int = 32,
42
+ learning_rate: float = 2e-5,
43
+ weight_decay: float = 0.01,
44
+ warmup_ratio: float = 0.06,
45
+ max_length: int = 512,
46
+ max_samples: int | None = None,
47
+ test_size: float = 0.1,
48
+ val_size: float = 0.1,
49
+ threshold: float = 0.5,
50
+ seed: int = 42,
51
+ push_to_hub: bool = False,
52
+ hub_model_id: str | None = None,
53
+ fp16: bool = False,
54
+ save_strategy: str = "no",
55
+ hub_token: str | None = None,
56
+ ) -> dict:
57
+ if dataframe is not None:
58
+ df = dataframe.copy()
59
+ print(f"Loaded dataframe with {len(df):,} rows")
60
+ elif data_path is not None:
61
+ print(f"Loading dataset from {data_path}...")
62
+ df = pd.read_parquet(data_path)
63
+ else:
64
+ raise ValueError("Either data_path or dataframe must be provided")
65
+ if max_samples and len(df) > max_samples:
66
+ df = df.sample(n=max_samples, random_state=seed)
67
+
68
+ label_list = load_codebert_labels()
69
+ num_labels = len(label_list)
70
+ print(f"Labels ({num_labels}): {label_list}")
71
+ print(f"Samples: {len(df):,}")
72
+ device = get_device()
73
+ use_fp16 = fp16 and device == "cuda"
74
+ print(f"Device: {device} | fp16: {use_fp16}")
75
+
76
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
77
+ model = AutoModelForSequenceClassification.from_pretrained(
78
+ model_name,
79
+ num_labels=num_labels,
80
+ problem_type="multi_label_classification",
81
+ id2label={i: name for i, name in enumerate(label_list)},
82
+ label2id={name: i for i, name in enumerate(label_list)},
83
+ )
84
+
85
+ train_ds, val_ds, test_ds = prepare_datasets(
86
+ df,
87
+ tokenizer,
88
+ test_size=test_size,
89
+ val_size=val_size,
90
+ max_length=max_length,
91
+ seed=seed,
92
+ )
93
+ print(f"Train: {len(train_ds):,} | Val: {len(val_ds):,} | Test: {len(test_ds):,}")
94
+
95
+ output_dir.mkdir(parents=True, exist_ok=True)
96
+ label_info = {
97
+ "labels": label_list,
98
+ "model_name": model_name,
99
+ "architecture": "codebert-cross-encoder",
100
+ "input_format": "QUESTION + SCHEMA + STUDENT_SQL + CORRECT_SQL",
101
+ "max_length": max_length,
102
+ "threshold": threshold,
103
+ }
104
+ with open(output_dir / "label_config.json", "w") as f:
105
+ json.dump(label_info, f, indent=2)
106
+
107
+ training_args = TrainingArguments(
108
+ output_dir=str(output_dir),
109
+ num_train_epochs=epochs,
110
+ per_device_train_batch_size=batch_size,
111
+ per_device_eval_batch_size=eval_batch_size,
112
+ learning_rate=learning_rate,
113
+ weight_decay=weight_decay,
114
+ warmup_ratio=warmup_ratio,
115
+ eval_strategy="epoch",
116
+ save_strategy=save_strategy,
117
+ logging_strategy="steps",
118
+ logging_steps=50,
119
+ load_best_model_at_end=save_strategy == "epoch",
120
+ metric_for_best_model="f1_macro",
121
+ greater_is_better=True,
122
+ save_total_limit=1,
123
+ seed=seed,
124
+ report_to="none",
125
+ fp16=use_fp16,
126
+ use_mps_device=(device == "mps"),
127
+ push_to_hub=push_to_hub,
128
+ hub_model_id=hub_model_id,
129
+ hub_token=hub_token,
130
+ )
131
+
132
+ callbacks = []
133
+ if save_strategy == "epoch":
134
+ callbacks.append(EarlyStoppingCallback(early_stopping_patience=2))
135
+
136
+ trainer_kwargs = dict(
137
+ model=model,
138
+ args=training_args,
139
+ train_dataset=train_ds,
140
+ eval_dataset=val_ds,
141
+ data_collator=SQLCodeBERTDataCollator(tokenizer),
142
+ compute_metrics=build_compute_metrics(threshold=threshold),
143
+ callbacks=callbacks,
144
+ )
145
+ try:
146
+ trainer = Trainer(processing_class=tokenizer, **trainer_kwargs)
147
+ except TypeError:
148
+ trainer = Trainer(tokenizer=tokenizer, **trainer_kwargs)
149
+
150
+ print("Starting CodeBERT cross-encoder training...")
151
+ train_result = trainer.train()
152
+
153
+ print("Evaluating on validation set...")
154
+ val_metrics = trainer.evaluate()
155
+
156
+ print("Evaluating on held-out test set...")
157
+ test_output = trainer.predict(test_ds)
158
+ test_metrics = compute_multilabel_metrics(
159
+ test_output.predictions,
160
+ test_output.label_ids,
161
+ threshold=threshold,
162
+ )
163
+
164
+ trainer.save_model(str(output_dir))
165
+ tokenizer.save_pretrained(str(output_dir))
166
+
167
+ metrics = {
168
+ "train_samples": len(train_ds),
169
+ "val_samples": len(val_ds),
170
+ "test_samples": len(test_ds),
171
+ "train_runtime": train_result.metrics.get("train_runtime"),
172
+ "validation": val_metrics,
173
+ "test": test_metrics,
174
+ }
175
+ with open(output_dir / "metrics.json", "w") as f:
176
+ json.dump(metrics, f, indent=2, default=float)
177
+
178
+ print(f"\nValidation F1 (macro): {val_metrics.get('eval_f1_macro', 0):.4f}")
179
+ print(f"Test F1 (macro): {test_metrics['f1_macro']:.4f}")
180
+ print(f"Test subset accuracy: {test_metrics['subset_accuracy']:.4f}")
181
+ print(f"Model saved to {output_dir}")
182
+ return metrics
183
+
184
+
185
+ def main() -> None:
186
+ parser = argparse.ArgumentParser(
187
+ description="Train CodeBERT cross-encoder with Hugging Face Trainer"
188
+ )
189
+ parser.add_argument("--data", type=Path, default=DEFAULT_DATA)
190
+ parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT)
191
+ parser.add_argument("--model-name", type=str, default=DEFAULT_MODEL)
192
+ parser.add_argument("--epochs", type=float, default=3.0)
193
+ parser.add_argument("--batch-size", type=int, default=16)
194
+ parser.add_argument("--eval-batch-size", type=int, default=32)
195
+ parser.add_argument("--learning-rate", type=float, default=2e-5)
196
+ parser.add_argument("--max-length", type=int, default=512)
197
+ parser.add_argument("--max-samples", type=int, default=None)
198
+ parser.add_argument("--threshold", type=float, default=0.5)
199
+ parser.add_argument("--seed", type=int, default=42)
200
+ parser.add_argument("--push-to-hub", action="store_true")
201
+ parser.add_argument("--hub-model-id", type=str, default=None)
202
+ parser.add_argument("--fp16", action="store_true")
203
+ parser.add_argument(
204
+ "--save-strategy",
205
+ choices=["no", "epoch"],
206
+ default="no",
207
+ help="Use 'no' to save only final model (saves disk space)",
208
+ )
209
+ args = parser.parse_args()
210
+
211
+ train(
212
+ data_path=args.data,
213
+ output_dir=args.output_dir,
214
+ model_name=args.model_name,
215
+ epochs=args.epochs,
216
+ batch_size=args.batch_size,
217
+ eval_batch_size=args.eval_batch_size,
218
+ learning_rate=args.learning_rate,
219
+ max_length=args.max_length,
220
+ max_samples=args.max_samples,
221
+ threshold=args.threshold,
222
+ seed=args.seed,
223
+ push_to_hub=args.push_to_hub,
224
+ hub_model_id=args.hub_model_id,
225
+ fp16=args.fp16,
226
+ save_strategy=args.save_strategy,
227
+ )
228
+
229
+
230
+ if __name__ == "__main__":
231
+ main()
src/sql_templates.py ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Error injectors that transform exercise context into labeled mistakes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
6
+ from typing import Callable, Dict, List, Tuple
7
+
8
+ from src.exercises import Exercise
9
+
10
+ FAKE_COLUMNS = ["fullname", "studentname", "coursename", "dept_name", "totals"]
11
+ FAKE_TABLES = ["student", "course", "enrolment", "employe", "orderz"]
12
+
13
+
14
+ def _pick(rng: random.Random, items: List[str], k: int = 1) -> str | List[str]:
15
+ if k == 1:
16
+ return rng.choice(items)
17
+ return rng.sample(items, k)
18
+
19
+
20
+ def _first_table(exercise: Exercise) -> str:
21
+ return exercise.tables[0]
22
+
23
+
24
+ def _second_table(exercise: Exercise) -> str:
25
+ return exercise.tables[1] if len(exercise.tables) > 1 else exercise.tables[0]
26
+
27
+
28
+ # --- Error injectors: (exercise) -> (erroneous_sql, error_message) ---
29
+
30
+ def inject_syntax_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
31
+ sql = exercise.correct_query
32
+ mutations = [
33
+ lambda s: s.replace("SELECT", "SELEC", 1),
34
+ lambda s: s.replace("FROM", "FRO", 1),
35
+ lambda s: s[:-1],
36
+ lambda s: s.replace(")", "", 1),
37
+ lambda s: s + " WHERE",
38
+ lambda s: s.replace(",", "", 1),
39
+ lambda s: s.replace("'", '"', 1) if "'" in s else s + " 'unclosed",
40
+ ]
41
+ return rng.choice(mutations)(sql), "syntax error at or near unexpected token"
42
+
43
+
44
+ def inject_join_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
45
+ t1, t2 = _first_table(exercise), _second_table(exercise)
46
+ col = _pick(rng, list(exercise.columns))
47
+ variants = [
48
+ f"SELECT {col} FROM {t1} JOIN {t2}",
49
+ f"SELECT {col} FROM {t1} INNER JOIN {t2} ON {t1}.id = {t2}.id",
50
+ (
51
+ f"SELECT {t1}.{col} FROM {t1} "
52
+ f"LEFT JOIN {t2} ON {t1}.{col} = {t2}.{col}"
53
+ ),
54
+ f"SELECT * FROM {t1}, {t2} WHERE {t1}.wrong_id = {t2}.wrong_id",
55
+ ]
56
+ return rng.choice(variants), "missing ON clause or invalid join condition"
57
+
58
+
59
+ def inject_aggregation_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
60
+ t = _first_table(exercise)
61
+ cols = list(exercise.columns)
62
+ group_col = cols[0]
63
+ agg_col = cols[-1]
64
+ bad = f"SELECT {group_col}, AVG({agg_col}) FROM {t}"
65
+ return bad, "column must appear in GROUP BY clause or be used in aggregate function"
66
+
67
+
68
+ def inject_having_where_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
69
+ t = _first_table(exercise)
70
+ cols = list(exercise.columns)
71
+ group_col, agg_col = cols[0], cols[-1]
72
+ bad = (
73
+ f"SELECT {group_col}, COUNT({agg_col}) FROM {t} "
74
+ f"WHERE COUNT({agg_col}) > {rng.randint(1, 5)}"
75
+ )
76
+ return bad, "aggregate functions are not allowed in WHERE"
77
+
78
+
79
+ def inject_subquery_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
80
+ t1, t2 = _first_table(exercise), _second_table(exercise)
81
+ col = _pick(rng, list(exercise.columns))
82
+ bad = f"SELECT {col} FROM {t1} WHERE {col} = (SELECT {col} FROM {t2})"
83
+ return bad, "subquery returned more than one row"
84
+
85
+
86
+ def inject_window_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
87
+ t = _first_table(exercise)
88
+ col = _pick(rng, list(exercise.columns))
89
+ variants = [
90
+ f"SELECT {col}, ROW_NUMBER() OVER () FROM {t}",
91
+ f"SELECT {col}, SUM({col}) OVER (ORDER BY {col}) FROM {t} GROUP BY {col}",
92
+ f"SELECT {col}, RANK() OVER (PARTITION {col}) FROM {t}",
93
+ ]
94
+ return rng.choice(variants), "window function requires PARTITION BY or ORDER BY"
95
+
96
+
97
+ def inject_null_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
98
+ t = _first_table(exercise)
99
+ col = _pick(rng, list(exercise.columns))
100
+ bad = f"SELECT * FROM {t} WHERE {col} = NULL"
101
+ return bad, "use IS NULL or IS NOT NULL to test for null values"
102
+
103
+
104
+ def inject_date_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
105
+ t = _first_table(exercise)
106
+ variants = [
107
+ f"SELECT * FROM {t} WHERE order_date = '31/02/2023'",
108
+ f"SELECT * FROM {t} WHERE order_date = DATE '2023-13-40'",
109
+ f"SELECT * FROM {t} WHERE STR_TO_DATE('bad-date', '%Y-%m-%d')",
110
+ f"SELECT * FROM {t} WHERE hire_date > 'yesterday'",
111
+ ]
112
+ return rng.choice(variants), "invalid date format or unknown date function"
113
+
114
+
115
+ def inject_column_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
116
+ t = _first_table(exercise)
117
+ col = _pick(rng, FAKE_COLUMNS)
118
+ bad = f"SELECT {col} FROM {t}"
119
+ return bad, f"column '{col}' does not exist"
120
+
121
+
122
+ def inject_table_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
123
+ tbl = _pick(rng, FAKE_TABLES)
124
+ col = _pick(rng, list(exercise.columns))
125
+ bad = f"SELECT {col} FROM {tbl}"
126
+ return bad, f"relation '{tbl}' does not exist"
127
+
128
+
129
+ def inject_datatype_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
130
+ t = _first_table(exercise)
131
+ col = _pick(rng, list(exercise.columns))
132
+ bad = f"SELECT {col} FROM {t} WHERE {col} = '{rng.choice(['abc', 'ten', 'N/A'])}'"
133
+ return bad, "operator does not exist: integer = character varying"
134
+
135
+
136
+ def inject_duplicate_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
137
+ """Drop DISTINCT when the question asks for unique values."""
138
+ sql = exercise.correct_query
139
+ if "DISTINCT" in sql.upper():
140
+ bad = sql.upper().replace("DISTINCT ", "").replace("distinct ", "")
141
+ # restore original casing loosely
142
+ bad = sql.replace("DISTINCT ", "").replace("distinct ", "")
143
+ else:
144
+ col = _pick(rng, list(exercise.columns))
145
+ bad = f"SELECT {col} FROM {_first_table(exercise)}"
146
+ return bad, "query returns duplicate rows; DISTINCT may be required"
147
+
148
+
149
+ def inject_logical_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
150
+ """
151
+ Produce a query that runs against the schema but answers the question incorrectly.
152
+ Variants are tied to the exercise question and correct answer.
153
+ """
154
+ sql = exercise.correct_query
155
+ q = exercise.question.lower()
156
+ variants: List[str] = []
157
+
158
+ if "average" in q or "avg" in sql.lower():
159
+ variants.append(sql.replace("AVG(", "SUM(", 1))
160
+ variants.append(sql.replace("AVG(", "MAX(", 1))
161
+ if " and " in q and " AND " in sql:
162
+ variants.append(sql.replace(" AND ", " OR ", 1))
163
+ if "join" in sql.lower():
164
+ t1, t2 = _first_table(exercise), _second_table(exercise)
165
+ variants.append(
166
+ f"SELECT {t1}.name, {t2}.name FROM {t1} "
167
+ f"JOIN {t2} ON {t1}.id = {t2}.id"
168
+ )
169
+ variants.append(sql.replace("INNER JOIN", "LEFT JOIN", 1))
170
+ if "between" in q and "BETWEEN" in sql.upper():
171
+ upper = sql.upper()
172
+ between_part = upper.split("BETWEEN", 1)[1]
173
+ bounds = between_part.split("AND", 1)
174
+ if len(bounds) == 2:
175
+ lo = bounds[0].strip().split()[-1]
176
+ hi = bounds[1].strip().split()[0]
177
+ variants.append(
178
+ sql.split("WHERE", 1)[0]
179
+ + f" WHERE price BETWEEN {hi} AND {lo}"
180
+ )
181
+ if "rank" in q or "over" in sql.lower():
182
+ col = _pick(rng, list(exercise.columns))
183
+ variants.append(
184
+ f"SELECT name, {col} FROM {_first_table(exercise)} ORDER BY {col} DESC"
185
+ )
186
+ if "total" in q and "WHERE" in sql.upper():
187
+ variants.append(sql.replace("active", "inactive"))
188
+ if "highest" in q or "max" in sql.lower():
189
+ col = _pick(rng, list(exercise.columns))
190
+ variants.append(
191
+ f"SELECT name FROM {_first_table(exercise)} "
192
+ f"WHERE {col} >= (SELECT AVG({col}) FROM {_second_table(exercise)})"
193
+ )
194
+ if "enrolled" in q and "INNER JOIN" in sql.upper():
195
+ variants.append(sql.replace("INNER JOIN", "LEFT JOIN", 1))
196
+ if "not provided" in q or "is null" in sql.lower():
197
+ variants.append(sql.replace("IS NULL", "= ''"))
198
+
199
+ if not variants:
200
+ col = _pick(rng, list(exercise.columns))
201
+ t = _first_table(exercise)
202
+ variants = [
203
+ f"SELECT {col} FROM {t} ORDER BY {col} DESC LIMIT 10",
204
+ f"SELECT COUNT(*) FROM {t}",
205
+ ]
206
+
207
+ bad = rng.choice(variants)
208
+ return bad, "query executes but produces incorrect result set"
209
+
210
+
211
+ def inject_performance_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
212
+ t1, t2 = _first_table(exercise), _second_table(exercise)
213
+ variants = [
214
+ f"SELECT * FROM {t1}",
215
+ f"SELECT * FROM {t1} JOIN {t2} ON {t1}.id = {t2}.id",
216
+ (
217
+ f"SELECT * FROM {t1} "
218
+ f"WHERE {_pick(rng, list(exercise.columns))} "
219
+ f"LIKE '%{rng.choice(['a', 'e', 'i'])}%'"
220
+ ),
221
+ f"SELECT * FROM {t1} CROSS JOIN {t2}",
222
+ ]
223
+ return rng.choice(variants), "inefficient query: SELECT * or cartesian join detected"
224
+
225
+
226
+ def inject_filtering_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
227
+ sql = exercise.correct_query
228
+ col = _pick(rng, list(exercise.columns))
229
+ t = _first_table(exercise)
230
+ threshold = rng.randint(50, 90)
231
+ variants = [
232
+ sql.replace(">", "<", 1) if ">" in sql else sql.replace("=", "!=", 1),
233
+ f"SELECT {col} FROM {t} WHERE {col} > {threshold} AND {col} < {threshold - 20}",
234
+ f"SELECT {col} FROM {t} WHERE NOT {col} > {threshold}",
235
+ sql.replace(" AND ", " OR ", 1) if " AND " in sql else (
236
+ f"SELECT {col} FROM {t} WHERE {col} BETWEEN {threshold} AND {threshold - 10}"
237
+ ),
238
+ ]
239
+ return rng.choice(variants), "WHERE clause filters incorrect rows"
240
+
241
+
242
+ ERROR_INJECTORS: Dict[int, Callable[[random.Random, Exercise], Tuple[str, str]]] = {
243
+ 0: inject_syntax_error,
244
+ 1: inject_join_error,
245
+ 2: inject_aggregation_error,
246
+ 3: inject_having_where_error,
247
+ 4: inject_subquery_error,
248
+ 5: inject_window_error,
249
+ 6: inject_null_error,
250
+ 7: inject_date_error,
251
+ 8: inject_column_error,
252
+ 9: inject_table_error,
253
+ 10: inject_datatype_error,
254
+ 11: inject_duplicate_error,
255
+ 12: inject_logical_error,
256
+ 13: inject_performance_error,
257
+ 14: inject_filtering_error,
258
+ }
train_space_app.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hugging Face Space — CodeBERT SQL Error Classifier Training UI.
3
+
4
+ Deploy as a Gradio Space with app_file: train_space_app.py
5
+ Set hardware to GPU (t4-small recommended).
6
+ Add HF_TOKEN secret to push trained models to your Hub account.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import shutil
14
+ import tempfile
15
+ from pathlib import Path
16
+
17
+ import gradio as gr
18
+ import pandas as pd
19
+
20
+ from src.hf_train_codebert import train
21
+
22
+ PROJECT_ROOT = Path(__file__).parent
23
+ DEFAULT_DATA = PROJECT_ROOT / "data" / "sql_errors_dev.parquet"
24
+ OUTPUT_DIR = PROJECT_ROOT / "models" / "codebert-cross-encoder"
25
+ BUNDLED_DATASETS = {
26
+ "Dev (15K samples)": str(PROJECT_ROOT / "data" / "sql_errors_dev.parquet"),
27
+ "Full (1M samples)": str(PROJECT_ROOT / "data" / "sql_errors_1m.parquet"),
28
+ }
29
+
30
+
31
+ def _format_metrics(metrics: dict) -> str:
32
+ val = metrics.get("validation", {})
33
+ test = metrics.get("test", {})
34
+ lines = [
35
+ "## Training complete",
36
+ "",
37
+ f"- Train samples: **{metrics.get('train_samples', 0):,}**",
38
+ f"- Val samples: **{metrics.get('val_samples', 0):,}**",
39
+ f"- Test samples: **{metrics.get('test_samples', 0):,}**",
40
+ "",
41
+ "### Validation",
42
+ f"- F1 macro: **{val.get('eval_f1_macro', 0):.4f}**",
43
+ f"- F1 micro: **{val.get('eval_f1_micro', 0):.4f}**",
44
+ "",
45
+ "### Test",
46
+ f"- F1 macro: **{test.get('f1_macro', 0):.4f}**",
47
+ f"- F1 micro: **{test.get('f1_micro', 0):.4f}**",
48
+ f"- Subset accuracy: **{test.get('subset_accuracy', 0):.4f}**",
49
+ "",
50
+ f"Model saved to `{OUTPUT_DIR}`",
51
+ ]
52
+ if metrics.get("hub_url"):
53
+ lines.append(f"\n**Hub model:** {metrics['hub_url']}")
54
+ return "\n".join(lines)
55
+
56
+
57
+ def run_training(
58
+ dataset_choice: str,
59
+ uploaded_file,
60
+ max_samples: int,
61
+ epochs: float,
62
+ batch_size: int,
63
+ learning_rate: float,
64
+ max_length: int,
65
+ fp16: bool,
66
+ push_to_hub: bool,
67
+ hub_model_id: str,
68
+ progress=gr.Progress(),
69
+ ):
70
+ progress(0, desc="Preparing dataset...")
71
+
72
+ if uploaded_file is not None:
73
+ data_path = Path(uploaded_file.name)
74
+ else:
75
+ data_path = Path(BUNDLED_DATASETS.get(dataset_choice, DEFAULT_DATA))
76
+ if not data_path.exists():
77
+ return (
78
+ f"Dataset not found: `{data_path}`. "
79
+ "Upload a parquet file or include data/ in the Space repo.",
80
+ None,
81
+ None,
82
+ )
83
+
84
+ hub_token = os.getenv("HF_TOKEN") or os.getenv("HUGGING_FACE_HUB_TOKEN")
85
+ if push_to_hub and not hub_token:
86
+ return (
87
+ "Add `HF_TOKEN` to Space secrets to push models to the Hub.",
88
+ None,
89
+ None,
90
+ )
91
+ if push_to_hub and not hub_model_id.strip():
92
+ return "Enter a Hub model id (e.g. `your-username/sql-codebert-classifier`).", None, None
93
+
94
+ if OUTPUT_DIR.exists():
95
+ shutil.rmtree(OUTPUT_DIR, ignore_errors=True)
96
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
97
+
98
+ samples = int(max_samples) if max_samples and max_samples > 0 else None
99
+ progress(0.1, desc="Starting CodeBERT training...")
100
+
101
+ try:
102
+ metrics = train(
103
+ data_path=data_path,
104
+ output_dir=OUTPUT_DIR,
105
+ epochs=epochs,
106
+ batch_size=batch_size,
107
+ learning_rate=learning_rate,
108
+ max_length=max_length,
109
+ max_samples=samples,
110
+ fp16=fp16,
111
+ save_strategy="no",
112
+ push_to_hub=push_to_hub,
113
+ hub_model_id=hub_model_id.strip() or None,
114
+ hub_token=hub_token,
115
+ )
116
+ except Exception as exc:
117
+ return f"Training failed:\n\n```\n{exc}\n```", None, None
118
+
119
+ progress(1.0, desc="Done")
120
+ if push_to_hub and hub_model_id.strip():
121
+ metrics["hub_url"] = f"https://huggingface.co/{hub_model_id.strip()}"
122
+
123
+ metrics_path = OUTPUT_DIR / "metrics.json"
124
+ summary = _format_metrics(metrics)
125
+ return summary, str(metrics_path) if metrics_path.exists() else None, str(OUTPUT_DIR)
126
+
127
+
128
+ def load_preview(dataset_choice: str, uploaded_file) -> str:
129
+ try:
130
+ if uploaded_file is not None:
131
+ df = pd.read_parquet(uploaded_file.name)
132
+ else:
133
+ path = BUNDLED_DATASETS.get(dataset_choice, DEFAULT_DATA)
134
+ if not Path(path).exists():
135
+ return f"Dataset not found: {path}"
136
+ df = pd.read_parquet(path)
137
+ cols = list(df.columns)
138
+ sample = df.head(2).to_dict(orient="records")
139
+ return f"**Rows:** {len(df):,}\n\n**Columns:** `{cols}`\n\n**Sample:**\n```json\n{json.dumps(sample, indent=2)[:2000]}\n```"
140
+ except Exception as exc:
141
+ return f"Could not load preview: {exc}"
142
+
143
+
144
+ with gr.Blocks(title="SQL Error Classifier — Train") as demo:
145
+ gr.Markdown(
146
+ """
147
+ # SQL Error Classifier — CodeBERT Training
148
+ Train **microsoft/codebert-base** as a cross-encoder on this Space.
149
+
150
+ **Input format:** `QUESTION` + `SCHEMA` + `STUDENT_SQL` + `CORRECT_SQL` (single sequence)
151
+
152
+ **GPU recommended** — upgrade Space hardware to `t4-small` or better.
153
+ """
154
+ )
155
+
156
+ with gr.Row():
157
+ with gr.Column(scale=1):
158
+ dataset_choice = gr.Dropdown(
159
+ choices=list(BUNDLED_DATASETS.keys()),
160
+ value="Dev (15K samples)",
161
+ label="Bundled dataset",
162
+ )
163
+ uploaded = gr.File(
164
+ label="Or upload parquet",
165
+ file_types=[".parquet"],
166
+ )
167
+ preview_btn = gr.Button("Preview dataset")
168
+ preview_out = gr.Markdown()
169
+
170
+ max_samples = gr.Number(
171
+ label="Max samples (0 = all)",
172
+ value=5000,
173
+ precision=0,
174
+ )
175
+ epochs = gr.Slider(1, 10, value=2, step=1, label="Epochs")
176
+ batch_size = gr.Slider(4, 64, value=8, step=4, label="Batch size")
177
+ learning_rate = gr.Number(label="Learning rate", value=2e-5)
178
+ max_length = gr.Slider(128, 512, value=512, step=64, label="Max length")
179
+ fp16 = gr.Checkbox(label="FP16 (GPU only)", value=True)
180
+
181
+ push_to_hub = gr.Checkbox(label="Push to Hugging Face Hub", value=False)
182
+ hub_model_id = gr.Textbox(
183
+ label="Hub model id",
184
+ placeholder="your-username/sql-codebert-classifier",
185
+ )
186
+
187
+ train_btn = gr.Button("Start Training", variant="primary")
188
+
189
+ with gr.Column(scale=1):
190
+ result = gr.Markdown(label="Results")
191
+ metrics_file = gr.File(label="metrics.json")
192
+ model_dir = gr.Textbox(label="Model output path", interactive=False)
193
+
194
+ preview_btn.click(load_preview, [dataset_choice, uploaded], preview_out)
195
+ train_btn.click(
196
+ run_training,
197
+ [
198
+ dataset_choice,
199
+ uploaded,
200
+ max_samples,
201
+ epochs,
202
+ batch_size,
203
+ learning_rate,
204
+ max_length,
205
+ fp16,
206
+ push_to_hub,
207
+ hub_model_id,
208
+ ],
209
+ [result, metrics_file, model_dir],
210
+ )
211
+
212
+ gr.Markdown(
213
+ """
214
+ ### Space setup
215
+ 1. Create a Gradio Space and push this repo
216
+ 2. Set **Hardware → GPU (t4-small)**
217
+ 3. Add secret `HF_TOKEN` (write token) to push models
218
+ 4. Include `data/sql_errors_dev.parquet` in the repo (or upload at runtime)
219
+
220
+ ### After training
221
+ Use the saved model with:
222
+ ```python
223
+ from src.hf_predict_codebert import CodeBERTSQLErrorClassifier
224
+ clf = CodeBERTSQLErrorClassifier("models/codebert-cross-encoder")
225
+ ```
226
+ """
227
+ )
228
+
229
+ if __name__ == "__main__":
230
+ demo.launch(server_name="0.0.0.0", server_port=7860)