Skip to content

Commit

Permalink
Merge pull request #97 from MartinXPN/feature/sql
Browse files Browse the repository at this point in the history
Feature/sql
  • Loading branch information
MartinXPN authored Dec 2, 2023
2 parents 5f61bcb + 6fae41a commit e9b08d8
Show file tree
Hide file tree
Showing 8 changed files with 339 additions and 6 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/pre-commit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: 3.12
- uses: pre-commit/[email protected]
11 changes: 11 additions & 0 deletions bouncer/coderunners.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ def from_language(language: str) -> 'CodeRunner':
return JsRunner()
if language in JavaRunner.supported_standards:
return JavaRunner()
if language in SQLiteRunner.supported_standards:
return SQLiteRunner()
raise ValueError(f'{language} does not have a compiler yet')

def invoke(self, aws_lambda_client, request: SubmissionRequest) -> SubmissionResult:
Expand Down Expand Up @@ -100,3 +102,12 @@ class JavaRunner(CodeRunner):
@property
def name(self) -> str:
return 'CodeRunnerJava'


@dataclass
class SQLiteRunner(CodeRunner):
supported_standards = {'sql', 'sqlite'}

@property
def name(self) -> str:
return 'CodeRunnerSQLite'
23 changes: 21 additions & 2 deletions coderunners/compilers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pathlib import Path
from typing import ClassVar

from coderunners.executors import Executor, ProcessExecutor
from coderunners.executors import Executor, ProcessExecutor, SQLiteExecutor
from coderunners.process import Process
from models import RunResult, Status

Expand All @@ -24,7 +24,7 @@ def find_main_file_path(cls, submission_paths: list[Path], main_file_name: str)

@staticmethod
def from_language(language: str) -> 'Compiler':
language = language.lower()
language = language.lower().strip()
if language in TxtCompiler.supported_standards:
return TxtCompiler()
if language in CppCompiler.supported_standards:
Expand All @@ -39,6 +39,8 @@ def from_language(language: str) -> 'Compiler':
return JsCompiler(language_standard=language)
if language in JavaCompiler.supported_standards:
return JavaCompiler()
if language in SQLiteCompiler.supported_standards:
return SQLiteCompiler()
raise ValueError(f'{language} does not have a compiler yet')


Expand Down Expand Up @@ -186,3 +188,20 @@ def compile(self, submission_paths: list[Path]):
compile_res = Process(f'cd {self.build_dir} && jar cvf Main.jar *', timeout=10, memory_limit_mb=512).run()
print('Compile res:', compile_res)
return ProcessExecutor(command=command), compile_res


@dataclass
class SQLiteCompiler(Compiler):
supported_standards = {'sql', 'sqlite'}
db_name: str = 'main.db'

def compile(self, submission_paths: list[Path]):
if len(submission_paths) != 1:
return ProcessExecutor(command='echo "Only one file is allowed"'), RunResult(
status=Status.CE, memory=0, time=0, return_code=0, outputs=None,
errors='Only one file is allowed for SQL submissions',
)

script = submission_paths[0].read_text()
executor = SQLiteExecutor(script=script, db_name=self.db_name)
return executor, RunResult(status=Status.OK, memory=0, time=0, return_code=0, outputs=None, errors=None)
105 changes: 104 additions & 1 deletion coderunners/executors.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import sqlite3
from abc import ABC, abstractmethod
from collections.abc import Iterable
from dataclasses import dataclass
from io import StringIO
from pathlib import Path

from coderunners.process import Process
from models import RunResult, TestCase
from models import RunResult, Status, TestCase


class Executor(ABC):
Expand Down Expand Up @@ -58,3 +60,104 @@ def cleanup(self, test: TestCase) -> None:
if (self.ROOT / filename).exists():
print('Removing file at:', self.ROOT / filename)
(self.ROOT / filename).unlink()


@dataclass
class SQLiteExecutor(Executor):
script: str
db_name: str = 'main.db'

def __post_init__(self):
self.db = sqlite3.connect(self.db_name)

def __del__(self):
self.db.close()

def run(self, test: TestCase, **kwargs) -> RunResult:
"""
self.script is the SQL script that needs to be run on the database
test.input is the initialization SQL script
test.input_files are all the tables that need to be populated
test.target is the expected output of the SQL script (can be empty)
test.target_files are all the tables that need to be populated by the SQL script
"""
import pandas as pd
cursor = self.db.cursor()

try:
cursor.executescript(test.input)
self.db.commit()
except sqlite3.Error as e:
cursor.close()
return RunResult(
status=Status.RUNTIME_ERROR, memory=0, time=0, return_code=0, outputs=None,
errors=str(e),
)

for filename, content in (test.input_files or {}).items():
# Load the content of the file into a dataframe and then load it into the db (filename)
try:
print('Creating table:', filename)
csv_data = StringIO(content)
df = pd.read_csv(csv_data)
print(df.head())
df.to_sql(filename, self.db, if_exists='replace', index=False)
print('--- Done ---')
except (sqlite3.Error, pd.errors.ParserError, pd.errors.DatabaseError, ValueError) as e:
cursor.close()
return RunResult(
status=Status.RUNTIME_ERROR, memory=0, time=0, return_code=0, outputs=None,
errors=str(e),
)

self.db.commit()

# Execute the self.script as a single command and get the output
try:
print('Executing script:', self.script)
if self.script.strip().upper().startswith('SELECT'):
res = pd.read_sql_query(self.script, self.db).to_csv(index=False)
else:
cursor.executescript(self.script)
self.db.commit()
res = ''
print('Result:', res)
except (sqlite3.Error, pd.errors.ParserError, pd.errors.DatabaseError, ValueError) as e:
cursor.close()
return RunResult(
status=Status.RUNTIME_ERROR, memory=0, time=0, return_code=0, outputs=None,
errors=str(e),
)

# Read output files into the result
try:
r = RunResult(
status=Status.OK, memory=0, time=0, return_code=0, outputs=res,
output_files={
filename: pd.read_sql_query(f'SELECT * FROM {filename}', self.db).to_csv(index=False)
for filename in (test.target_files or {}).keys()
})
cursor.close()
return r
except (sqlite3.Error, pd.errors.ParserError, pd.errors.DatabaseError, ValueError) as e:
cursor.close()
return RunResult(
status=Status.RUNTIME_ERROR, memory=0, time=0, return_code=0, outputs=None,
errors=str(e),
)

def cleanup(self, test: TestCase) -> None:
""" Drops all the tables in the database """
cursor = self.db.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
tables = cursor.fetchall()

print(f'Dropping {len(tables)} tables')
for table_name in tables:
print('Dropping table:', table_name[0], end='...')
cursor.execute(f'DROP TABLE {table_name[0]}')
print('Done')

print('Saving changes')
self.db.commit()
cursor.close()
18 changes: 18 additions & 0 deletions coderunners/lang/sqlite.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM public.ecr.aws/lambda/python:3.12

# Initial setup
RUN pip install --upgrade pip
RUN pip install awslambdaric -t "${LAMBDA_TASK_ROOT}"

# Install dependencies
COPY coderunners/requirements.txt ./
RUN pip install -r requirements.txt -t "${LAMBDA_TASK_ROOT}"
RUN python -m pip install --upgrade pandas

# Setup source files
COPY coderunners/*.py ${LAMBDA_TASK_ROOT}/coderunners/
COPY models.py ${LAMBDA_TASK_ROOT}/

# Run the lambda function handler
ENTRYPOINT [ "python", "-m", "awslambdaric" ]
CMD [ "coderunners.app.run_code_lambda" ]
9 changes: 6 additions & 3 deletions coderunners/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def check(self) -> SubmissionResult:
self.test_cases[0],
time_limit=self.time_limit, memory_limit_mb=self.memory_limit, output_limit_mb=self.output_limit
)
executor.cleanup(self.test_cases[0])
print('Done')

# Process all tests
Expand All @@ -107,7 +108,7 @@ def check(self) -> SubmissionResult:
input_files=test.input_files, output_files=r.output_files, target_files=test.target_files,
input_assets=test.input_assets, output_assets=r.output_assets, target_assets=test.target_assets,
) if r.status == Status.OK else (r.status, 0, r.message)
print(f'Test {i} res: {r.status} => {r.score}')
print(f'Test {i} res: {r.status} => score {r.score}')

# Clean up
executor.cleanup(test)
Expand Down Expand Up @@ -135,8 +136,10 @@ def check(self) -> SubmissionResult:
test_results += [
RunResult(status=Status.SKIPPED, memory=0, time=0, return_code=0)
] * (len(self.test_cases) - i - 1)
print('Expected:', test.target, test.target_files)
print('Actual:', test_results[-1].outputs, test_results[-1].output_files)
print('Expected:', test.target)
print('Actual:', test_results[-1].outputs)
print('Expected files:', test.target_files)
print('Actual files:', test_results[-1].output_files)
break
print('test_results:', test_results)
assert len(test_results) == len(self.test_cases)
Expand Down
24 changes: 24 additions & 0 deletions template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,7 @@ Resources:
DockerTag: js-v1
DockerContext: ./
Dockerfile: coderunners/lang/js.Dockerfile

CodeRunnerJava:
Type: AWS::Serverless::Function
DependsOn: CodeRunnerMountTarget
Expand All @@ -534,6 +535,26 @@ Resources:
DockerContext: ./
Dockerfile: coderunners/lang/java.Dockerfile

CodeRunnerSQLite:
Type: AWS::Serverless::Function
DependsOn: CodeRunnerMountTarget
Properties:
FunctionName: CodeRunnerSQLite
PackageType: Image
Role: !GetAtt ContestantRole.Arn
VpcConfig:
SecurityGroupIds:
- !GetAtt JudgeVPC.DefaultSecurityGroup
SubnetIds:
- !Ref CodeRunnerPrivateSubnet
FileSystemConfigs:
- Arn: !GetAtt AccessPointResource.Arn
LocalMountPath: '/mnt/efs'
Metadata:
DockerTag: sqlite-v1
DockerContext: ./
Dockerfile: coderunners/lang/sqlite.Dockerfile

Outputs:
# ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
# Find out more about other implicit resources you can reference within SAM
Expand Down Expand Up @@ -569,6 +590,9 @@ Outputs:
CodeRunnerJava:
Description: 'AWS Lambda for executing a Java code and getting the outputs'
Value: !GetAtt CodeRunnerJava.Arn
CodeRunnerSQLite:
Description: 'AWS Lambda for executing a SQL/SQLite code and getting the results with tables'
Value: !GetAtt CodeRunnerSQLite.Arn

SyncS3WithEFSName:
Description: 'AWS Lambda for syncing S3 bucket for test cases with EFS'
Expand Down
Loading

0 comments on commit e9b08d8

Please sign in to comment.