From cf333a0072d11217d7b2a3b3230306999d9c448d Mon Sep 17 00:00:00 2001 From: Sergii Khomych Date: Tue, 24 Mar 2026 13:50:05 -0400 Subject: [PATCH 1/8] Implement sql queries runner --- .github/scripts/requirements.txt | 3 + .github/workflows/sql_assignment_runner.yml | 106 ++++++++++++++++++++ tests/conftest.py | 26 +++++ tests/test_assignment.py | 83 +++++++++++++++ 4 files changed, 218 insertions(+) create mode 100644 .github/scripts/requirements.txt create mode 100644 .github/workflows/sql_assignment_runner.yml create mode 100644 tests/conftest.py create mode 100644 tests/test_assignment.py diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt new file mode 100644 index 000000000..201c92d7a --- /dev/null +++ b/.github/scripts/requirements.txt @@ -0,0 +1,3 @@ +pytest>=7.4 +pytest-timeout>=2.3 +pytest-json-report>=1.5 \ No newline at end of file diff --git a/.github/workflows/sql_assignment_runner.yml b/.github/workflows/sql_assignment_runner.yml new file mode 100644 index 000000000..fed0f25fd --- /dev/null +++ b/.github/workflows/sql_assignment_runner.yml @@ -0,0 +1,106 @@ +name: SQL Unit Tests + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + +jobs: + run-assignment-queries: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + pip install -r ./.github/scripts/requirements.txt + + - name: Get changed files + id: changes + run: | + git fetch + git diff --name-only origin/main > changed_files.txt + + echo "Changed files:" + cat changed_files.txt + + if grep -qE '(^|/)assignment2\.sql$' changed_files.txt; then + echo "assignment_changed=$(grep -qE '(^|/)assignment2\.sql$')" >> "$GITHUB_OUTPUT" + fi + if grep -qE '(^|/)assignment1\.sql$' changed_files.txt; then + echo "assignment_changed=$(grep -qE '(^|/)assignment1\.sql$')" >> "$GITHUB_OUTPUT" + fi + + - name: Run tests + id: pytest + run: | + pytest tests/test_assignment.py --file_path="${{steps.changes.outputs.assignment_changed}}" --tb=short --disable-warnings \ + --junitxml=pytest-report.xml || true + + - name: Post test results to PR + uses: actions/github-script@v7 + with: + script: | + function jsonToMarkdownTable(data) { + if (!Array.isArray(data) || data.length === 0) { + return 'No data'; + } + + const headers = Object.keys(data[0]); + + // Header row + const headerRow = `| ${headers.join(' | ')} |`; + + // Separator row + const separatorRow = `| ${headers.map(() => '---').join(' | ')} |`; + + // Data rows + const rows = data.map(row => + `| ${headers.map(h => formatValue(row[h])).join(' | ')} |` + ); + + return [headerRow, separatorRow, ...rows].join('\n'); + } + + function formatValue(value) { + if (value === null || value === undefined) return ''; + if (typeof value === 'object') return `\`${JSON.stringify(value)}\``; + return String(value); + } + + const fs = require('fs') + const file_read_result = fs.readFileSync('test-results.json', 'utf8') + const results = JSON.parse(file_read_result) + let body = `### 🧪 SQL Queries Run Results\n\n` + body += `
Click to expand/collapse assignment queries execution results` + for (const result of results) { + body += `✅ Query ${result.number}: \n\n *${result.query}*, \n\n **Results**: \n` + const table = jsonToMarkdownTable(result.result) + body += `${table} \n` + body += `\n` + body += `-------------------------------------------------------- \n` + } + body += `
` + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body + }) + diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..c892acf02 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,26 @@ +import sqlite3 +import pytest +from pathlib import Path + +@pytest.fixture +def sqlite_db(): + source_db = Path("05_src/sql/farmersmarket.db") + # Create in-memory DB + conn = sqlite3.connect(":memory:") + + # Load schema.db into memory + disk_db = sqlite3.connect(source_db) + conn.row_factory = sqlite3.Row + disk_db.backup(conn) + disk_db.close() + + yield conn + conn.close() + +def pytest_addoption(parser): + parser.addoption("--file_path", action="store", default="02_activities/assignments/DC_Cohort/assignment1.sql") + +def pytest_generate_tests(metafunc): + option_value = metafunc.config.option.file_path + if 'file_path' in metafunc.fixturenames and option_value is not None: + metafunc.parametrize("file_path", [option_value]) \ No newline at end of file diff --git a/tests/test_assignment.py b/tests/test_assignment.py new file mode 100644 index 000000000..3d75686ca --- /dev/null +++ b/tests/test_assignment.py @@ -0,0 +1,83 @@ +import json +from pathlib import Path +import sqlite3 +import re + +def load_queries(sql_file): + lines = Path(sql_file).read_text().splitlines() + + queries = [] + current_query = None + buffer = [] + + for line in lines: + stripped = line.strip() + + # Start marker: -- QUERY + if stripped.lower().startswith("--query"): + if current_query is not None: + raise AssertionError("Nested QUERY blocks are not allowed") + + parts = stripped.split() + if len(parts) < 2: + raise AssertionError(f"Invalid QUERY marker: {line}") + + try: + current_query = int(parts[1]) + except ValueError: + raise AssertionError(f"Invalid QUERY number: {line}") + + buffer = [] + continue + + # End marker: -- END QUERY or -- END STATEMENT + if stripped.lower().startswith("--end"): + if current_query is None: + continue # ignore stray END + + query = "\n".join(buffer).strip().rstrip(";") + + queries.append({ + "number": current_query, + "query": query + }) + + current_query = None + buffer = [] + continue + + # Collect lines inside a query block + if current_query is not None: + buffer.append(line) + + if not queries: + raise AssertionError( + "No queries found. Use '-- QUERY ' and '-- END QUERY' markers." + ) + + return queries + +def run_query(conn, query): + cursor = conn.cursor() + cursor.execute(query) + rows = cursor.fetchall() + return [dict(row) for row in rows] + +def test_assignment(sqlite_db, file_path): + run_assignment(sqlite_db, file_path) + +def run_assignment(sqlite_db, file_path): + json_file = open("test-results.json", "w") + queries = load_queries(file_path) + test_result = [] + for parsed_query in queries: + try: + rows = run_query(sqlite_db, parsed_query['query']) + test_result.append( { "number": parsed_query['number'], "query": parsed_query['query'], "result": rows[0:3] }) + except Exception as e: + print(f"An unexpected error occurred: {e}") + json.dump(test_result, json_file, indent=2) + json_file.close() + assert True, "test execution query {} result {}".format(queries, test_result) + + From 3eda95a5b12d4a62f1b1320daa4ae6364f986b32 Mon Sep 17 00:00:00 2001 From: Sergii Khomych Date: Tue, 24 Mar 2026 14:13:44 -0400 Subject: [PATCH 2/8] Implement sql queries runner --- .github/scripts/requirements.txt | 2 +- .github/workflows/sql_assignment_runner.yml | 14 +++++++++----- tests/conftest.py | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index 201c92d7a..e336b28f4 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -1,3 +1,3 @@ pytest>=7.4 pytest-timeout>=2.3 -pytest-json-report>=1.5 \ No newline at end of file +pytest-json-report>=1.5 diff --git a/.github/workflows/sql_assignment_runner.yml b/.github/workflows/sql_assignment_runner.yml index fed0f25fd..0dbac0b01 100644 --- a/.github/workflows/sql_assignment_runner.yml +++ b/.github/workflows/sql_assignment_runner.yml @@ -3,7 +3,8 @@ name: SQL Unit Tests on: pull_request: types: [opened, synchronize, reopened] - + branches: + - assignment-** permissions: contents: read pull-requests: write @@ -38,13 +39,15 @@ jobs: echo "Changed files:" cat changed_files.txt - - if grep -qE '(^|/)assignment2\.sql$' changed_files.txt; then - echo "assignment_changed=$(grep -qE '(^|/)assignment2\.sql$')" >> "$GITHUB_OUTPUT" - fi + // The goal here to search for a file modified during PR that is match to a pattern + // and give priority in next order 2, 1 as it quite often happens when students submit + // PR without following assignments order and git flow. It will to run latest / greatest if grep -qE '(^|/)assignment1\.sql$' changed_files.txt; then echo "assignment_changed=$(grep -qE '(^|/)assignment1\.sql$')" >> "$GITHUB_OUTPUT" fi + if grep -qE '(^|/)assignment2\.sql$' changed_files.txt; then + echo "assignment_changed=$(grep -qE '(^|/)assignment2\.sql$')" >> "$GITHUB_OUTPUT" + fi - name: Run tests id: pytest @@ -86,6 +89,7 @@ jobs: const fs = require('fs') const file_read_result = fs.readFileSync('test-results.json', 'utf8') const results = JSON.parse(file_read_result) + // Format PR comment let body = `### 🧪 SQL Queries Run Results\n\n` body += `
Click to expand/collapse assignment queries execution results` for (const result of results) { diff --git a/tests/conftest.py b/tests/conftest.py index c892acf02..5415915f8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,4 +23,4 @@ def pytest_addoption(parser): def pytest_generate_tests(metafunc): option_value = metafunc.config.option.file_path if 'file_path' in metafunc.fixturenames and option_value is not None: - metafunc.parametrize("file_path", [option_value]) \ No newline at end of file + metafunc.parametrize("file_path", [option_value]) From f842ff44e3099b37543c566d8f59699593bfa879 Mon Sep 17 00:00:00 2001 From: Sergii Khomych Date: Tue, 24 Mar 2026 14:30:10 -0400 Subject: [PATCH 3/8] Change branch filtering --- .github/workflows/sql_assignment_runner.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sql_assignment_runner.yml b/.github/workflows/sql_assignment_runner.yml index 0dbac0b01..c629fa41a 100644 --- a/.github/workflows/sql_assignment_runner.yml +++ b/.github/workflows/sql_assignment_runner.yml @@ -3,14 +3,14 @@ name: SQL Unit Tests on: pull_request: types: [opened, synchronize, reopened] - branches: - - assignment-** + permissions: contents: read pull-requests: write jobs: run-assignment-queries: + if: startsWith(github.head_ref, 'assignment-') runs-on: ubuntu-latest permissions: contents: read From aa79e2675f77b9374402e6ffa2479f1a2a970b56 Mon Sep 17 00:00:00 2001 From: Sergii Khomych Date: Tue, 24 Mar 2026 14:41:20 -0400 Subject: [PATCH 4/8] Fix path extraction --- .github/workflows/sql_assignment_runner.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sql_assignment_runner.yml b/.github/workflows/sql_assignment_runner.yml index c629fa41a..ff075515b 100644 --- a/.github/workflows/sql_assignment_runner.yml +++ b/.github/workflows/sql_assignment_runner.yml @@ -43,10 +43,10 @@ jobs: // and give priority in next order 2, 1 as it quite often happens when students submit // PR without following assignments order and git flow. It will to run latest / greatest if grep -qE '(^|/)assignment1\.sql$' changed_files.txt; then - echo "assignment_changed=$(grep -qE '(^|/)assignment1\.sql$')" >> "$GITHUB_OUTPUT" + echo "assignment_changed=$(grep -E '(^|/)assignment1\.sql$' changed_files.txt)" >> "$GITHUB_OUTPUT" fi if grep -qE '(^|/)assignment2\.sql$' changed_files.txt; then - echo "assignment_changed=$(grep -qE '(^|/)assignment2\.sql$')" >> "$GITHUB_OUTPUT" + echo "assignment_changed=$(grep -E '(^|/)assignment2\.sql$' changed_files.txt)" >> "$GITHUB_OUTPUT" fi - name: Run tests From 2843051b236f52400a6051ec23a4190827013180 Mon Sep 17 00:00:00 2001 From: Sergii Khomych Date: Tue, 24 Mar 2026 15:06:37 -0400 Subject: [PATCH 5/8] Comments fix --- .github/workflows/sql_assignment_runner.yml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/sql_assignment_runner.yml b/.github/workflows/sql_assignment_runner.yml index ff075515b..d16d02c7c 100644 --- a/.github/workflows/sql_assignment_runner.yml +++ b/.github/workflows/sql_assignment_runner.yml @@ -39,14 +39,20 @@ jobs: echo "Changed files:" cat changed_files.txt - // The goal here to search for a file modified during PR that is match to a pattern - // and give priority in next order 2, 1 as it quite often happens when students submit - // PR without following assignments order and git flow. It will to run latest / greatest - if grep -qE '(^|/)assignment1\.sql$' changed_files.txt; then - echo "assignment_changed=$(grep -E '(^|/)assignment1\.sql$' changed_files.txt)" >> "$GITHUB_OUTPUT" - fi + # The goal here to search for a file modified during PR that is match to a pattern + # and give priority in next order 2, 1 as it quite often happens when students submit + # PR without following assignments order and git flow. It will to run latest / greatest + assignment_changed="" + + # Priority: assignment2 > assignment1 if grep -qE '(^|/)assignment2\.sql$' changed_files.txt; then - echo "assignment_changed=$(grep -E '(^|/)assignment2\.sql$' changed_files.txt)" >> "$GITHUB_OUTPUT" + assignment_changed=$(grep -E '(^|/)assignment2\.sql$' changed_files.txt | head -n1) + elif grep -qE '(^|/)assignment1\.sql$' changed_files.txt; then + assignment_changed=$(grep -E '(^|/)assignment1\.sql$' changed_files.txt | head -n1) + fi + + if [ -n "$assignment_changed" ]; then + echo "assignment_changed=$assignment_changed" >> "$GITHUB_OUTPUT" fi - name: Run tests From 4e74f54698028efc2b8b19469db433ad79789078 Mon Sep 17 00:00:00 2001 From: Sergii Khomych Date: Tue, 24 Mar 2026 17:02:01 -0400 Subject: [PATCH 6/8] Minor modification --- .github/workflows/sql_assignment_runner.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sql_assignment_runner.yml b/.github/workflows/sql_assignment_runner.yml index d16d02c7c..66dfd338a 100644 --- a/.github/workflows/sql_assignment_runner.yml +++ b/.github/workflows/sql_assignment_runner.yml @@ -96,7 +96,7 @@ jobs: const file_read_result = fs.readFileSync('test-results.json', 'utf8') const results = JSON.parse(file_read_result) // Format PR comment - let body = `### 🧪 SQL Queries Run Results\n\n` + let body = `### 🧪 SQL Queries Run Results (up to 3 rows)\n\n` body += `
Click to expand/collapse assignment queries execution results` for (const result of results) { body += `✅ Query ${result.number}: \n\n *${result.query}*, \n\n **Results**: \n` From 0349f734f57dfcc286389a4dffa3262a13340598 Mon Sep 17 00:00:00 2001 From: Sergii Khomych Date: Fri, 27 Mar 2026 22:53:58 -0400 Subject: [PATCH 7/8] PR addressed --- .github/workflows/sql_assignment_runner.yml | 12 ++++++----- tests/test_assignment.py | 24 ++++++++++----------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/.github/workflows/sql_assignment_runner.yml b/.github/workflows/sql_assignment_runner.yml index 66dfd338a..a621298f9 100644 --- a/.github/workflows/sql_assignment_runner.yml +++ b/.github/workflows/sql_assignment_runner.yml @@ -99,11 +99,13 @@ jobs: let body = `### 🧪 SQL Queries Run Results (up to 3 rows)\n\n` body += `
Click to expand/collapse assignment queries execution results` for (const result of results) { - body += `✅ Query ${result.number}: \n\n *${result.query}*, \n\n **Results**: \n` - const table = jsonToMarkdownTable(result.result) - body += `${table} \n` - body += `\n` - body += `-------------------------------------------------------- \n` + if (result.result.trim().length > 0) { + body += `✅ Query ${result.number}: \n\n *${result.query}*, \n\n **Results**: \n` + const table = jsonToMarkdownTable(result.result) + body += `${table} \n` + body += `\n` + body += `-------------------------------------------------------- \n` + } } body += `
` diff --git a/tests/test_assignment.py b/tests/test_assignment.py index 3d75686ca..db0dee2ef 100644 --- a/tests/test_assignment.py +++ b/tests/test_assignment.py @@ -67,17 +67,17 @@ def test_assignment(sqlite_db, file_path): run_assignment(sqlite_db, file_path) def run_assignment(sqlite_db, file_path): - json_file = open("test-results.json", "w") - queries = load_queries(file_path) - test_result = [] - for parsed_query in queries: - try: - rows = run_query(sqlite_db, parsed_query['query']) - test_result.append( { "number": parsed_query['number'], "query": parsed_query['query'], "result": rows[0:3] }) - except Exception as e: - print(f"An unexpected error occurred: {e}") - json.dump(test_result, json_file, indent=2) - json_file.close() - assert True, "test execution query {} result {}".format(queries, test_result) + with open("test-results.json", "w") as json_file: + queries = load_queries(file_path) + test_result = [] + for parsed_query in queries: + try: + rows = run_query(sqlite_db, parsed_query['query']) + test_result.append( { "number": parsed_query['number'], "query": parsed_query['query'], "result": rows[0:3] }) + except Exception as e: + print(f"An unexpected error occurred: {e}") + json.dump(test_result, json_file, indent=2) + # The purpose of it to have report in the future in case DSI will want to go with unit testing style. + # assert True, "test execution query {} result {}".format(queries, test_result) From 4ee18540ec6f4786eb4c2d850bf2aefe15b13021 Mon Sep 17 00:00:00 2001 From: Sergii Khomych Date: Mon, 30 Mar 2026 14:52:21 -0400 Subject: [PATCH 8/8] PR addressed 1 --- .github/workflows/sql_assignment_runner.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sql_assignment_runner.yml b/.github/workflows/sql_assignment_runner.yml index a621298f9..fb5392176 100644 --- a/.github/workflows/sql_assignment_runner.yml +++ b/.github/workflows/sql_assignment_runner.yml @@ -99,7 +99,7 @@ jobs: let body = `### 🧪 SQL Queries Run Results (up to 3 rows)\n\n` body += `
Click to expand/collapse assignment queries execution results` for (const result of results) { - if (result.result.trim().length > 0) { + if (result.result && result.result.trim().length > 0) { body += `✅ Query ${result.number}: \n\n *${result.query}*, \n\n **Results**: \n` const table = jsonToMarkdownTable(result.result) body += `${table} \n`