Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/sql #97

Merged
merged 5 commits into from
Dec 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/action@v3.0.0
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