This commit is contained in:
Joe Germuska 2019-10-10 08:54:02 -05:00
parent 4db91abc16
commit da49fb70f2
13 changed files with 28657 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

131
css/main.css Normal file
View File

@ -0,0 +1,131 @@
/* datatable */
.datatable {
border: solid 1px grey;
border-collapse: collapse;
font-size: 12px;
}
.datatable thead, td {
padding: 5px;
border: solid 1px grey;
}
.datatable thead {
background: lightgrey;
font-weight: bold;
}
/* sql quiz component */
sql-quiz .sqlQuizHomeDiv {
margin: 1em 0em;
background: #f9f9f9;
padding: 6px;
border-radius: 6px;
font-weight: 200;
}
sql-quiz .sqlQuizTitle {
font-weight: 700;
}
sql-quiz .sqlQuizDescription {
}
sql-quiz .sqlQuizInputArea {
line-height: 130%;
min-width: 100%;
}
sql-quiz .sqlQuizInputArea .sqlOption {
min-width: 100%;
margin: 0.5em 0em;
}
sql-quiz input[type=checkbox] {
transform: scale(1.5);
float: left;
}
sql-quiz .sqlOption .optionText {
display: table;
}
sql-quiz .sqlOption .hintSpan {
/*when revealed, display: table-row;*/
display: none;
color: blue;
font-weight: 200;
}
sql-quiz .sqlQuizInputArea input {
margin-right: 1em;
background: none;
border-radius: 4px;
font-size: 0.8em;
font-weight: 200;
}
sql-quiz .sqlQuizOutputArea .returnOkay {
margin-top: 0.5em;
}
/* sql exercise component */
sql-exercise .sqlExHomeDiv{
margin: 1em 0em;
font-family: Roboto, sans-serif;
background: #f9f9f9;
padding: 6px;
border-radius: 6px;
}
sql-exercise .sqlExQuestion {
font-weight: 700;
}
sql-exercise .sqlExComment {
font-weight: 200;
}
sql-exercise form {
display: block;
}
sql-exercise .sqlExInputArea {
line-height: 130%;
min-width: 100%;
}
sql-exercise .sqlExOutputArea {
max-width: 52rem;
overflow: auto;
background: #f9f9f9;
}
sql-exercise .sqlExOutputArea .returnOkay {
margin-top: 0.5em;
}
sql-exercise .sqlExOutputArea .returnError {
margin-top: 0.5em;
color: red;
}
sql-exercise .CodeMirror {
margin: 0.5em 0em;
}
sql-exercise .sqlExInputArea input {
margin-right: 1em;
background: none;
border-radius: 4px;
font-size: 0.8em;
font-weight: 200;
}
#experienced-schema {
display: none;
}
#experienced-schema.show {
display: block;
}

14
css/neat.css Normal file
View File

@ -0,0 +1,14 @@
.cm-s-neat span.cm-comment { color: #a86; }
.cm-s-neat span.cm-keyword { line-height: 1em; font-weight: bold; color: blue; }
.cm-s-neat span.cm-string { color: #a22; }
.cm-s-neat span.cm-builtin { line-height: 1em; font-weight: bold; color: #077; }
.cm-s-neat span.cm-special { line-height: 1em; font-weight: bold; color: #0aa; }
.cm-s-neat span.cm-variable { color: black; }
.cm-s-neat span.cm-number, .cm-s-neat span.cm-atom { color: #3a3; }
.cm-s-neat span.cm-meta { color: #555; }
.cm-s-neat span.cm-link { color: #3a3; }
.cm-s-neat .CodeMirror-activeline-background { background: #e8f2ff; }
.cm-s-neat .CodeMirror-matchingbracket { outline:1px solid grey; color:black !important; }
.cm-s-neat .CodeMirror-sizer { background: #eaf4f7; }

292
index.html Normal file
View File

@ -0,0 +1,292 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" charset="utf-8">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSS -->
<link rel="stylesheet" href="https://cloud.webtype.com/css/d4767ecb-457a-4677-8761-72f890add836.css"/>
<link rel="stylesheet" href="https://cdn.knightlab.com/libs/orangeline/0.1.1/css/orangeline.min.css"/>
<!-- /CSS -->
<!-- FAVICONS -->
<link rel="apple-touch-icon" sizes="180x180" href="https://cdn.knightlab.com/libs/orangeline/0.1.1/assets/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" href="https://cdn.knightlab.com/libs/orangeline/0.1.1/assets/favicons/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="https://cdn.knightlab.com/libs/orangeline/0.1.1/assets/favicons/favicon-16x16.png" sizes="16x16">
<link rel="manifest" href="https://cdn.knightlab.com/libs/orangeline/0.1.1/assets/favicons/manifest.json">
<link rel="mask-icon" href="https://cdn.knightlab.com/libs/orangeline/0.1.1/assets/favicons/safari-pinned-tab.svg" color="#5bbad5">
<meta name="theme-color" content="#ffffff">
<!-- /FAVICONS -->
<title>The SQL Murder Mystery</title>
<!-- Meta -->
<meta name="keywords" content="SQL, databases, game, fun">
<meta name="description" content="Use SQL queries to solve the murder mystery. Suitable for beginners or experienced SQL sleuths.">
<!-- /Meta -->
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:title" content="SQL Murder Mystery" />
<meta property="og:description" content="Use SQL queries to solve the murder mystery. Suitable for beginners or experienced SQL sleuths." />
<meta property="og:image" content="174092-clue-illustration.png" />
<meta property="og:url" content="PERMALINK" />
<meta property="og:site_name" content="Knight Lab's SQL Murder Mystery" />
<!-- /Open Graph -->
<!-- Twitter Card -->
<meta name="twitter:title" content="Knight Lab's SQL Murder Mystery">
<meta name="twitter:description" content="Use SQL queries to solve the murder mystery. Suitable for beginners or experienced SQL sleuths.">
<meta name="twitter:image" content="174092-clue-illustration.png">
<meta name="twitter:site" content="@knightlab">
<!-- /Twitter Card -->
<link rel="stylesheet" href='https://codemirror.net/lib/codemirror.css'>
<link rel="stylesheet" href="css/main.css"/>
<link rel="stylesheet" href="css/neat.css"/>
<script src="https://unpkg.com/@webcomponents/custom-elements@1.2.0/custom-elements.min.js"></script>
<script src="scripts/codemirror.js"></script>
<script src="scripts/codemirrorsql.js"></script>
<script src="scripts/autorefresh.js"></script>
<script src="scripts/main.js"></script>
<script>
window.onload = () => loadData('sql-murder-mystery.db');
</script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-27829802-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-27829802-1');
</script>
<title>The SQL Murder Mystery</title>
</head>
<body>
<nav id="navbar-product-top" class="navbar navbar-dark">
<ul>
<li class="logo">
<a href="http://knightlab.com">
<img src="https://cdn.knightlab.com/libs/orangeline/0.1.1/assets/knightlab-dark.png" />
</a>
</li>
</ul>
<div class="nav-mobile-menu">
<button class="button-plain">
<span class="icon-menu"></span>
</button>
</div>
<ul class="nav-right navbar-nav">
<li><a class="button button-dark button-small-tablet button-active" href="https://knightlab.northwestern.edu/projects/">Projects</a></li>
<li><a class="button button-dark button-small-tablet " href="https://studio.knightlab.com/">Studio</a></li>
<li><a class="button button-dark button-small-tablet " href="https://localnewsinitiative.northwestern.edu/">Local News</a></li>
<li><a class="button button-dark button-small-tablet " href="https://knightlab.northwestern.edu/posts/">Posts</a></li>
<li><a class="button button-dark button-small-tablet " href="https://knightlab.northwestern.edu/community/">Community</a></li>
</ul>
</nav>
<header class="header-product">
<h1 class="product-logo product-logo-large">
SQL Murder Mystery
</h1>
<h2 class="product-tagline">Can you find out whodunnit?</h2>
</header>
<div class="container">
<div id="intro" class="grid">
<div class="grid-item">
<img src="174092-clue-illustration.png" class="img-rounded" alt="A decorative illustration of a detective looking at an evidence board.">
<p>
There's been a Murder in SQL City! The SQL Murder Mystery is designed to be both a self-directed lesson to learn SQL concepts and commands and a fun game for experienced SQL users to solve an intriguing crime.
</p>
</div>
</div>
<div class="grid">
<div class="grid-item">
<h2>New to SQL?</h2>
<p>
This exercise is meant more as a way to practice SQL skills than a full tutorial. If you've never used SQL
at all, <a href="walkthrough.html">try the walkthrough</a>.
If you really want to learn a lot about SQL, you may prefer a complete tutorial like <a href="https://selectstarsql.com/">Select Star SQL.</a>
</p>
<p>If you're comfortable with SQL, you can <a href="#experienced">dive in below</a>!</p>
</div>
</div>
<div class="grid">
<div class="grid-item">
<h2 id="experienced">Experienced SQL sleuths start here</h2>
<p>
A crime has taken place and the detective needs your help. The detective gave you the crime scene report, but you somehow lost it. You vaguely remember that the crime was a <strong>murder</strong> that occurred sometime on <strong>Jan.15, 2018</strong> and that it took place in <strong>SQL City</strong>. Start by retrieving the corresponding crime scene report from the police departments database.
</p>
<h3>Exploring the Database Structure</h3>
<p>
Experienced SQL users can often use database queries to infer the structure of a database. But each database system has different ways of managing this information. The SQL Murder Mystery is built using SQLite.
Use this SQL command to find the tables in the Murder Mystery database.
</p>
<sql-exercise
data-question="Run this query to find the names of the tables in this database."
data-comment="This command is specific to SQLite. For other databases, you'll have to learn their specific syntax."
data-default-text="SELECT name
FROM sqlite_master
where type = 'table'"></sql-exercise>
</div>
</div>
<div class="grid">
<div class="grid-item">
<p>
Besides knowing the table names, you need to know how each table is structured. To do this using SQL
</p>
<sql-exercise
data-question="Run this query to find the structure of the `crime_scene_report` table"
data-comment="Change the value of 'name' to see the structure of the other tables you learned about with the previous query."
data-default-text="SELECT sql
FROM sqlite_master
where name = 'crime_scene_report'"></sql-exercise>
</div>
</div>
<div class="grid">
<div class="grid-item">
<h3>The rest is up to you!</h3>
<p>
If you're really comfortable with SQL, you can probably get it from here.
</p>
<p>
But <a id="show-schema">click here</a> to show the schema diagram.
<div>
<img id="experienced-schema" src="schema.png" width='1002' height='491'>
</p>
<p>
And you can always go to the <a href="walkthrough.html">walkthrough</a>.
</p>
<sql-exercise
data-question="Use your knowledge of the database schema and SQL commands to find out who committed the murder."
data-comment="When you think you know the answer, go to the next section."
data-default-text="
"></sql-exercise><!-- newlines in data-default-text force input to be larger -->
</div>
</div>
<div class="grid">
<div class="grid-item">
<h3>Check your solution</h3>
<sql-exercise
data-question="Use your knowledge of the database schema and SQL commands to find out who committed the murder."
data-comment="When you think you know the answer, go to the next section."
data-default-text="INSERT INTO solution VALUES (1, 'Insert the name of the person you found here');
SELECT value FROM solution;"></sql-exercise>
</div>
</div>
<div id="credits" class="grid">
<div class="grid-item">
<h3>Credits</h3>
<p>The SQL Murder Mystery was created by <a href="https://twitter.com/joonparkmusic">Joon Park</a> and <a href="https://twitter.com/Cathy_MeiyingHe">Cathy He</a> while they were Knight Lab fellows. See the <a href="https://github.com/NUKnightLab/sql-mysteries">GitHub repository</a> for more information.</p>
<p>This mystery was inspired by <a href="https://github.com/veltman/clmystery">a crime in the neighboring Terminal City.</a></p>
<p>Web-based SQL is made possible by <a href="https://github.com/kripken/sql.js/">SQL.js</a></p>
<p>SQL query custom web components created and released to the public domain by Zi Chong Kao, creator of <a href="https://selectstarsql.com/">Select Star SQL.</a></p>
<p>Detective illustration courtesy of <a href="https://www.vecteezy.com/">Vectors by Vecteezy</a></p>
</div>
</div>
</div>
</div>
<footer class="footer-knightlab--dark">
<div class="container">
<div class='grid grid-center'>
<div class='column-2 column-4-phone column-4-tablet'>
<a href="http://knightlab.northwestern.edu" target="_blank">
<img src="https://cdn.knightlab.com/libs/orangeline/0.1.1/assets/logo-knightlab-stacked-dark-small.png" style="margin-left:auto;">
</a>
<ul class="list--social text-align-center">
<li><a class="link--no-style" href="http://www.twitter.com/knightlab" target="_blank" title="Knight Lab on Twitter"><span class="icon-twitter"></span></a></li>
<li><a class="link--no-style" href="https://www.facebook.com/knightlab" target="_blank" title="Knight Lab on Facebook"><span class="icon-facebook"></span></a></li>
<li><a class="link--no-style" href="https://github.com/NUKnightLab/" target="_blank" title="Knight Lab on GitHub"><span class="icon-github"></span></a></li>
</ul>
</div>
<div class='column-4 column-5-tablet column-6-phone footer-description'>
<p>
The <a title="Northwestern University" href="http://www.northwestern.edu/" target="_blank">Northwestern University</a> Knight Lab is a team of technologists and journalists working at advancing news media innovation through exploration and experimentation.
</p>
<div class="grid-size-2 grid-size-2-phone">
<div class="grid-item">
<a title="Medill School of Journalism, Media, Integrated Marketing Communications" href="http://www.medill.northwestern.edu/" target="_blank">
<img src="https://cdn.knightlab.com/libs/orangeline/0.1.1/assets/logo-medill-dark.png">
</a>
</div>
<div class="grid-item">
<a title="McCormick School of Engineering" href="http://www.mccormick.northwestern.edu/" target="_blank">
<img src="https://cdn.knightlab.com/libs/orangeline/0.1.1/assets/logo-mccormick-dark.png">
</a>
</div>
</div>
</div>
</div>
<div class="grid grid-center">
<div class="grid-item">
<address itemscope="" itemtype="http://data-vocabulary.org/Organization">
<span style="display:none;" itemprop="name" title="Knight Lab | Advancing news media innovation through exploration and experimentation." class="address-name">Knight Lab</span>
<span itemprop="tel" class="tel">(847) 467-4971</span>
<span itemprop="address" itemscope="" itemtype="http://data-vocabulary.org/Address" class="address">
<span itemprop="street-address" class="street-address">1845 Sheridan Road</span>
<span class="room-num">Fisk #109 &amp; #111</span>
<div class="address-group">
<span itemprop="locality">Evanston,</span> <span itemprop="region">IL</span> <span itemprop="postal-code">60208 </span>
<span style="display:none;" itemprop="geo" itemscope="" itemtype="http://www.data-vocabulary.org/Geo/" class="geo">
Latitude: <span itemprop="latitude">42.056893</span><br>Longitude:
<span itemprop="longitude">-87.676735</span>
</span>
<a style="display:none;" href="http://knightlab.northwestern.edu" itemprop="url" class="url">Northwesten University Knight Lab | Advancing media innovation through exploration and experimentation.</a>
</div>
</span>
</address>
<span class="copyright">© Copyright 2019 Northwestern University</span>
</div>
</div>
</div>
</footer>
<script src="https://cdn.knightlab.com/libs/orangeline/0.1.1/js/orangeline.js"></script>
<script type="text/javascript">
document.querySelector('#show-schema').addEventListener('click', function() {
document.querySelector('#experienced-schema').classList.add('show');
})
</script>
</body>
</html>

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
jupyterlab==1.1.4
ipython-sql==0.3.9

BIN
schema.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

47
scripts/autorefresh.js Normal file
View File

@ -0,0 +1,47 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"))
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod)
else // Plain browser env
mod(CodeMirror)
})(function(CodeMirror) {
"use strict"
CodeMirror.defineOption("autoRefresh", false, function(cm, val) {
if (cm.state.autoRefresh) {
stopListening(cm, cm.state.autoRefresh)
cm.state.autoRefresh = null
}
if (val && cm.display.wrapper.offsetHeight == 0)
startListening(cm, cm.state.autoRefresh = {delay: val.delay || 250})
})
function startListening(cm, state) {
function check() {
if (cm.display.wrapper.offsetHeight) {
stopListening(cm, state)
if (cm.display.lastWrapHeight != cm.display.wrapper.clientHeight)
cm.refresh()
} else {
state.timeout = setTimeout(check, state.delay)
}
}
state.timeout = setTimeout(check, state.delay)
state.hurry = function() {
clearTimeout(state.timeout)
state.timeout = setTimeout(check, 50)
}
CodeMirror.on(window, "mouseup", state.hurry)
CodeMirror.on(window, "keyup", state.hurry)
}
function stopListening(_cm, state) {
clearTimeout(state.timeout)
CodeMirror.off(window, "mouseup", state.hurry)
CodeMirror.off(window, "keyup", state.hurry)
}
});

9696
scripts/codemirror.js Normal file

File diff suppressed because it is too large Load Diff

500
scripts/codemirrorsql.js Normal file

File diff suppressed because one or more lines are too long

17105
scripts/lodash.js Normal file

File diff suppressed because it is too large Load Diff

368
scripts/main.js Normal file
View File

@ -0,0 +1,368 @@
// Set up DB
function loadData(dbFile) {
if (!dbFile) { return; }
window.worker = new Worker("scripts/worker.sql.js");
var xhr = new XMLHttpRequest();
xhr.open('GET', dbFile, true);
xhr.responseType = 'arraybuffer';
xhr.onload = () => {
var uInt8Array = new Uint8Array(xhr.response);
worker.onmessage = event => {
if (event.data.ready) {
query('SELECT 1', (e) => {
console.log('DB initialization successful');
document.querySelectorAll("input.sql-exercise-submit").forEach(
(button) => {button.disabled = false;});
});
} else {
console.log('DB initialization failed');
}
}
worker.postMessage({
id:1,
action:'open',
buffer: uInt8Array,
});
}
xhr.send();
}
function query(sql, cb, err_cb) {
if (err_cb) {
worker.onerror = e => err_cb(e);
} else {
worker.onerror = e => { throw new Error(e.message); }
}
worker.onmessage = event => {
cb(event.data.results);
}
worker.postMessage({
id: 2,
action: 'exec',
sql: sql
});
}
function datatable (data) {
var tbl = document.createElement("table");
tbl.className = 'datatable'
var header_labels = data[0].columns;
for (var idx in header_labels) {
var col = document.createElement('col');
col.className = header_labels[idx];
tbl.appendChild(col);
}
// create header row
var thead = tbl.createTHead();
var row = thead.insertRow(0);
for (var idx in header_labels) {
var cell = row.insertCell(idx);
cell.innerHTML = header_labels[idx];
}
// fill table body
var tbody = document.createElement("tbody");
for (var row_idx in data[0]['values']) {
var body_row = tbody.insertRow();
for (var header_idx in header_labels) {
var body_cell = body_row.insertCell();
body_cell.appendChild(document.createTextNode(data[0]['values'][row_idx][header_idx]));
}
}
tbl.appendChild(tbody);
return tbl;
}
//////////////////////////
// SQL Quiz Component
//////////////////////////
function setdiff(a, b) { // https://stackoverflow.com/a/36504668
var seta = new Set(a);
var setb = new Set(b);
var res = new Set([...seta].filter(x => !setb.has(x)));
return res
}
class sqlQuizOption extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
var value = this.getAttribute('data-value') || ''
var statement = this.getAttribute('data-statement') || '';
var dataCorrect = this.getAttribute('data-correct') || false;
var hint = this.getAttribute('data-hint') || '';
var quizoption = `
<div class='sqlOption'>
<label>
<input type=checkbox name="input"
data-correct=${dataCorrect}
value=${value} />
<div class="optionText">
${statement}
<div class="hintSpan">${hint}</div>
</div>
</label>
</div>
`
this.parentNode.querySelector('.sqlQuizOptions').insertAdjacentHTML("beforeend", quizoption);
}
}
customElements.define('sql-quiz-option', sqlQuizOption);
class sqlQuiz extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
var title = this.getAttribute('data-title') || '';
var description = this.getAttribute('data-description') || '';
var homeDiv = document.createElement('div');
homeDiv.className = 'sqlQuizHomeDiv';
if (title) {
var caption = `<div class="sqlQuizTitle">${title}</div>`;
homeDiv.insertAdjacentHTML("beforeend", caption);
}
if (description) {
var commentbox = `
<div class="sqlQuizDescription">
${description}
</div>
`
homeDiv.insertAdjacentHTML("beforeend", commentbox);
}
var form = document.createElement('form');
// Input Area
var inputArea = document.createElement('div');
inputArea.className = 'sqlQuizInputArea';
var options = document.createElement('div');
options.className = 'sqlQuizOptions';
inputArea.appendChild(options);
var submitButton = document.createElement('input');
submitButton.type = 'submit';
submitButton.value = 'Check Answers';
inputArea.appendChild(submitButton);
var hintButton = document.createElement('input');
hintButton.name = "hint";
hintButton.type = "button";
hintButton.value = "Show Explanations";
hintButton.onclick = (e) => {
document.querySelectorAll('.hintSpan').forEach(i => i.style.display = 'table-row');
};
inputArea.appendChild(hintButton);
form.appendChild(inputArea);
// Output Area
var outputArea = document.createElement('div');
outputArea.className = 'sqlQuizOutputArea';
var outputBox = document.createElement('output');
outputBox.name = 'output';
outputArea.appendChild(outputBox);
// Link everything together
form.appendChild(outputArea);
form['onsubmit'] = (e) => {
e && e.preventDefault();
var value = Array.prototype.filter.call(form.input, i => i.checked).map(i => i.value);
var correct = Array.prototype.filter.call(form.input, i => i.dataset.correct === "true").map(i => i.value);
var mistakes = setdiff(correct, value).size + setdiff(value, correct).size;
var res = mistakes >= 2 ? mistakes + " mistakes" :
mistakes == 1 ? mistakes + " mistake" : "All correct!"
form.output.innerHTML = `<div class='returnOkay'>${res}</div>`;
};
homeDiv.append(form);
this.append(homeDiv);
}
}
customElements.define('sql-quiz', sqlQuiz);
//////////////////////////
// SQL Exercise Component
//////////////////////////
class sqlExercise extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
var question = this.getAttribute('data-question') || '';
var comment = this.getAttribute('data-comment') || '';
var defaultText = this.getAttribute('data-default-text') || '';
var solution = this.getAttribute('data-solution') || '';
var orderSensitive = this.getAttribute('data-orderSensitive') || false;
var homeDiv = document.createElement('div');
homeDiv.className = 'sqlExHomeDiv';
if (question) {
var caption = `<div class="sqlExQuestion">${question}</div>`;
homeDiv.insertAdjacentHTML("beforeend", caption);
}
if (comment) {
var commentbox = `<div class = 'sqlExComment'>${comment}</div>`;
homeDiv.insertAdjacentHTML("beforeend", commentbox);
}
var form = document.createElement('form');
// Input Area
var inputArea = document.createElement('div');
inputArea.className = 'sqlExInputArea';
var textArea = document.createElement('textarea');
textArea.textContent = defaultText;
textArea.name = 'input';
inputArea.appendChild(textArea);
var editor = CodeMirror.fromTextArea(textArea, {
mode: 'text/x-sql',
indentWithTabs: true,
smartIndent: true,
lineNumbers: true,
textWrapping: false,
autoRefresh: true,
theme: 'neat',
viewportMargin: Infinity
});
editor.setSize('100%', 'auto');
editor.refresh();
var runButton = `<input class="sql-exercise-submit" type="submit" value="Run &#x21e9;" disabled>`;
inputArea.insertAdjacentHTML("beforeend", runButton);
form['onsubmit'] = (e) => {
e && e.preventDefault();
var result_div = document.createElement('div');
var handleSubmit = (submission_data) => {
result_div.className = 'returnOkay';
if (solution) {
var verdict_div = document.createElement('div');
result_div.appendChild(verdict_div);
query(solution, (solution_data) => {
var submission_u = submission_data[0].values;
var solution_u = solution_data[0].values;
if (!orderSensitive) {
submission_u.sort();
solution_u.sort();
}
var verdict = arraysEqual(submission_u, solution_u) ? "Correct" : "Incorrect";
// http://adripofjavascript.com/blog/drips/object-equality-in-javascript.html
verdict_div.innerText = verdict;
});
}
if (submission_data.length > 0) {
result_div.appendChild(datatable(submission_data));
} else {
result_div.insertAdjacentHTML("beforeend", `No data returned`);
}
}
var handleError = (e) => {
result_div.className = 'returnError';
result_div.innerText = e.message;
}
query(editor.getValue(), handleSubmit, handleError);
outputBox.innerHTML = '';
outputBox.appendChild(result_div);
};
form['onkeydown'] = (e) => {
if (e.keyCode == 13 && e.shiftKey) {
e.preventDefault();
form.onsubmit();
};
};
if (solution) {
var solutionButton = document.createElement('input');
solutionButton.name = 'solution';
solutionButton.type = 'button';
solutionButton.value = 'Show Solution';
solutionButton.onclick = (e) => {
var existingCode = editor.getValue();
editor.setValue(existingCode + "\n/* " + solution);
};
inputArea.appendChild(solutionButton);
};
var resetButton = document.createElement('input');
resetButton.type = 'button';
resetButton.value = 'Reset';
resetButton.onclick = (e) => {
editor.setValue(defaultText);
outputBox.textContent = '';
};
inputArea.appendChild(resetButton);
form.appendChild(inputArea);
// Output Area
var outputArea = document.createElement('div');
outputArea.className = 'sqlExOutputArea';
var outputBox = document.createElement('output');
outputBox.name = 'output';
outputArea.appendChild(outputBox);
form.appendChild(outputArea);
homeDiv.appendChild(form);
this.appendChild(homeDiv);
}
}
customElements.define('sql-exercise', sqlExercise);
//////////////////////////
// Utility functions
//////////////////////////
function arraysEqual(a,b) {
/*
https://stackoverflow.com/questions/3115982/how-to-check-if-two-arrays-are-equal-with-javascript
Array-aware equality checker:
Returns whether arguments a and b are == to each other;
however if they are equal-lengthed arrays, returns whether their
elements are pairwise == to each other recursively under this
definition.
*/
if (a instanceof Array && b instanceof Array) {
if (a.length != b.length) { // assert same length
return false;
}
for (var i=0; i<a.length; i++) { // assert each element equal
if (!arraysEqual(a[i],b[i]))
return false;
}
return true;
} else {
return a == b; // if not both arrays, should be the same
}
}

99
scripts/worker.sql.js Normal file

File diff suppressed because one or more lines are too long

403
walkthrough.html Normal file
View File

@ -0,0 +1,403 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" charset="utf-8">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSS -->
<link rel="stylesheet" href="https://cloud.webtype.com/css/d4767ecb-457a-4677-8761-72f890add836.css"/>
<link rel="stylesheet" href="https://cdn.knightlab.com/libs/orangeline/0.1.1/css/orangeline.min.css"/>
<!-- /CSS -->
<!-- FAVICONS -->
<link rel="apple-touch-icon" sizes="180x180" href="https://cdn.knightlab.com/libs/orangeline/0.1.1/assets/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" href="https://cdn.knightlab.com/libs/orangeline/0.1.1/assets/favicons/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="https://cdn.knightlab.com/libs/orangeline/0.1.1/assets/favicons/favicon-16x16.png" sizes="16x16">
<link rel="manifest" href="https://cdn.knightlab.com/libs/orangeline/0.1.1/assets/favicons/manifest.json">
<link rel="mask-icon" href="https://cdn.knightlab.com/libs/orangeline/0.1.1/assets/favicons/safari-pinned-tab.svg" color="#5bbad5">
<meta name="theme-color" content="#ffffff">
<!-- /FAVICONS -->
<title>The SQL Murder Mystery: Detailed Walkthrough</title>
<!-- Meta -->
<meta name="keywords" content="SQL, databases, game, fun">
<meta name="description" content="Use SQL queries to solve the murder mystery. Suitable for beginners or experienced SQL sleuths.">
<!-- /Meta -->
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:title" content="SQL Murder Mystery" />
<meta property="og:description" content="Use SQL queries to solve the murder mystery. Suitable for beginners or experienced SQL sleuths." />
<meta property="og:image" content="174092-clue-illustration.png" />
<meta property="og:url" content="PERMALINK" />
<meta property="og:site_name" content="Knight Lab's SQL Murder Mystery" />
<!-- /Open Graph -->
<!-- Twitter Card -->
<meta name="twitter:title" content="Knight Lab's SQL Murder Mystery">
<meta name="twitter:description" content="Use SQL queries to solve the murder mystery. Suitable for beginners or experienced SQL sleuths.">
<meta name="twitter:image" content="174092-clue-illustration.png">
<meta name="twitter:site" content="@knightlab">
<!-- /Twitter Card -->
<link rel="stylesheet" href='https://codemirror.net/lib/codemirror.css'>
<link rel="stylesheet" href="css/main.css"/>
<link rel="stylesheet" href="css/neat.css"/>
<script src="https://unpkg.com/@webcomponents/custom-elements@1.2.0/custom-elements.min.js"></script>
<script src="scripts/codemirror.js"></script>
<script src="scripts/codemirrorsql.js"></script>
<script src="scripts/autorefresh.js"></script>
<script src="scripts/main.js"></script>
<script>
window.onload = () => loadData('sql-murder-mystery.db');
</script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-27829802-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-27829802-1');
</script>
<title>The SQL Murder Mystery</title>
</head>
<body>
<nav id="navbar-product-top" class="navbar navbar-dark">
<ul>
<li class="logo">
<a href="http://knightlab.com">
<img src="https://cdn.knightlab.com/libs/orangeline/0.1.1/assets/knightlab-dark.png" />
</a>
</li>
</ul>
<div class="nav-mobile-menu">
<button class="button-plain">
<span class="icon-menu"></span>
</button>
</div>
<ul class="nav-right navbar-nav">
<li><a class="button button-dark button-small-tablet button-active" href="https://knightlab.northwestern.edu/projects/">Projects</a></li>
<li><a class="button button-dark button-small-tablet " href="https://studio.knightlab.com/">Studio</a></li>
<li><a class="button button-dark button-small-tablet " href="https://localnewsinitiative.northwestern.edu/">Local News</a></li>
<li><a class="button button-dark button-small-tablet " href="https://knightlab.northwestern.edu/posts/">Posts</a></li>
<li><a class="button button-dark button-small-tablet " href="https://knightlab.northwestern.edu/community/">Community</a></li>
</ul>
</nav>
<header class="header-product">
<h1 class="product-logo product-logo-large">
SQL Murder Mystery
</h1>
<h2 class="product-tagline">Can you find out whodunnit?</h2>
</header>
<div class="container">
<div id="intro" class="grid">
<div class="grid-item">
<img src="174092-clue-illustration.png" class="img-rounded" alt="A decorative illustration of a detective looking at an evidence board.">
<p>
There's been a Murder in SQL City! The SQL Murder Mystery is designed to be both a self-directed lesson to learn SQL concepts and commands and a fun game for experienced SQL users to solve an intriguing crime.
</p>
</div>
</div>
<div class="grid">
<div class="grid-item">
<h2>Walkthrough for SQL Beginners</h2>
<p>
If you're comfortable with SQL, you can <a href="index.html#experienced">skip these explanations</a> and put your skills to the test! Below we introduce some basic SQL concepts, and just enough detail to solve the murder. If you'd like a more complete introduction to SQL, try <a href="https://selectstarsql.com/">Select Star SQL.</a>
</p>
</div>
</div>
<div class="grid">
<div class="grid-item">
<p>
A crime has taken place and the detective needs your help. The detective gave you the crime scene report, but you somehow lost it. You vaguely remember that the crime was a <strong>murder</strong> that occurred sometime on <strong>Jan.15, 2018</strong> and that it took place in <strong>SQL City</strong>. Start by retrieving the corresponding crime scene report from the police departments database.
</p>
<p>
All the clues to this mystery are buried in a huge database, and you need to use SQL to navigate through this vast network of information. Your first step to solving the mystery is to retrieve the corresponding crime scene report from the police departments database. Below we'll explain from a high level the commands you need to know; whenever you are ready, you can start adapting the examples to create your own SQL commands in search of clues -- you can run any SQL in any of the code boxes, no matter what was in the box when you started.
</p>
</div>
</div>
<div class="grid">
<div class="grid-item">
<h2>Some Definitions</h2>
<h3>What is SQL?</h3>
<p>
SQL, which stands for Structured Query Language, is a way to interact with relational databases and tables in a way that allows us humans to glean specific, meaningful information.
</p>
<h3>Wait, what is a relational database?</h3>
<p>There's no single definition for the word <em>database</em>. In general, databases are systems for managing information.
Databases can have varying amounts of structure imposed on the data. When the data is more structured, it can help people and computers work with the data more efficiently.
</p>
<p>Relational databases are probably the best known kind of database. At their heart, relational databases are made up of <em>tables</em>, which are a lot like spreadsheets. Each column in the table has a name and a data type (text, number, etc.), and each row in the table is a specific instance of whatever the table is "about." The "relational" part comes with specific rules about how to connect data between different tables.
</p>
<h3>What is an ERD?</h3>
<p>
ERD, which stands for Entity Relationship Diagram, is a visual representation of the relationships among all relevant tables within a database. You can find the ERD for our SQL Murder Mystery database below. The diagram shows that each table has a name (top of the box, in bold), a list of column names (on the left) and their corresponding data types (on the right, in all caps).
There are also some gold key icons, blue arrow icons and gray arrows on the ERD. A gold key indicates that the column is the primary key of the corresponding table, and a blue arrow indicates that the column is the foreign key of the corresponding table.
<dl>
<dt>
Primary Key:
</dt>
<dd>
a unique identifier for each row in a table.
</dd>
<dt>
Foreign Key:
</dt>
<dd>
used to reference data in one table to those in another table.
</dd>
</dl>
If two tables are related, the matching columns, i.e. the common identifiers of the two tables, are connected by a gray arrow in the diagram.
</p>
<p>Here is the ERD for our database:
</p>
<img src="schema.png" width='1002' height='491'>
</div>
</div>
<div class="grid">
<div class="grid-item">
<h2>
What is a query?
</h2>
<p>If you were to look at the data in this database, you would see that the tables are huge! There are so many data points; it simply isnt possible to go through the tables row by row to find the information we need. What are we supposed to do?</p>
<p>
This is where queries come in. Queries are statements we construct to get data from the database. Queries read like natural English (for the most part).
Let's try a few queries against our database. For each of the boxes below, click the "run" to "execute" the query in the box. You can edit the queries right here on the page to explore further. (Note that SQL commands are not case-sensitive, but it's conventional to capitalize them for readability. You can also use new lines and white space as you like to format the command for readability. Most database systems require you to end a query with a semicolon (';') although the system for running them in this web page is more forgiving.)
</p>
<sql-exercise
data-question="Just how many people are in this database?"
data-comment="Don't worry about exactly what this means, but know that you can change 'person' to any other table from the ERD to learn how many rows that table has. Try it!"
data-default-text="SELECT count(*)
FROM person;"></sql-exercise>
<sql-exercise
data-question="What do we know about those people?"
data-comment="If you want all the data for each row in a table, use '*' after 'SELECT'. As you just learned, there are thousands of people, so rather than see all of them, we'll limit the results to just the first 10. Run this, then change the table name or limit number and see what happens."
data-default-text="SELECT * FROM person LIMIT 10;"></sql-exercise>
<sql-exercise
data-question="What are possible values for a column?"
data-comment='When working with data, always see if you can find documentation that explains the database structure (like the ERD) and valid values. But sometimes thats not available. Here we show how the DISTINCT keyword can give you a quick look at which values are in the database. After you run it, delete the word DISTINCT and run it again. See the difference? (After you try that, you may want to click the "reset" button and run one more time before you continue.)'
data-default-text="SELECT DISTINCT type FROM crime_scene_report;"></sql-exercise>
</div>
</div>
<div class="grid">
<div class="grid-item">
<h3>What elements does a SQL query have?</h3>
<p>A SQL query can contain:
<ul>
<li>SQL keywords (like the SELECT and FROM above)</li>
<li>Column names (like the name column above)</li>
<li>Table names (like the person table above)</li>
<li>Wildcard characters (such as <code>%</code>)</li>
<li>Functions</li>
<li>Specific filtering criteria</li>
<li>Etc</li>
</ul>
</p>
<h4 id="sql-keywords">SQL Keywords and Wildcards</h4>
<p>
SQL keywords are used to specify actions in your queries. SQL keywords are not case sensitive, but we suggest using all caps for SQL keywords so that you can easily set them apart from the rest of the query. Some frequently used keywords are:
</p>
<h5>SELECT</h5>
<p>SELECT allows us to grab data for specific columns from the database:</p>
<ul>
<li>
* (asterisk): it is used after SELECT to grab all columns from the table;
</li>
<li>
column_name(s): to select specific columns, put the names of the columns after SELECT and use commas to separate them.
</li>
</ul>
<h5>FROM</h5>
<p>FROM allows us to specify which table(s) we care about; to select multiple tables, list the table names and use commas to separate them. (But until you learn the JOIN keyword, you may be surprised at what happens. That will come later.)</p>
<h5>WHERE</h5>
<p>The WHERE clause in a query is used to filter results by specific criteria.</p>
<p>Let's try some of these things.</p>
<sql-exercise
data-question="Here's a simple query to get everything about a specific person. (Don't worry&mdash;all of the SSNs are made up.) "
data-comment="Note that you need to use single straight quotes (') around literal text so the database can tell it apart from table and column names. After you run it, try again with Yessenia Fossen, Ted Denfip or Davina Gangwer."
data-default-text="SELECT * FROM person WHERE name = 'Kinsey Erickson'"></sql-exercise>
<p>The AND keyword is used to string together multiple filtering criteria so that the filtered results meet each and every one of the criterion.</p>
<sql-exercise
data-question="This query only returns reports about a specific type of crime in a specific city."
data-comment=" Notice that when querying for text values, you must match the data as it is in the database. Try changing 'Chicago' to 'chicago' and running the statement. Then, see if you can edit this SQL statement to find the first clue based on the prompt above."
data-default-text="SELECT * FROM crime_scene_report
WHERE type = 'theft'
AND city = 'Chicago';"
data-solution="SELECT * FROM crime_scene_report
WHERE type = 'murder'
AND city = 'SQL City';"></sql-exercise>
<p>If you haven't found the right crime scene report yet, click "show solution" above, and replace the contents of the box with just the revealed command. If you figured out the query that shows the single crime scene report instead of a few for the same city and type, then congratulations and disregard the word "incorrect". You'll know if you got it!
</p>
</div>
</div>
<div id="credits" class="grid">
<div class="grid-item">
<h2>Digging deeper</h2>
<h3>SQL Aggregate Functions</h3>
<p>
Sometimes the questions you want to ask arent as simple as finding the row of data that fits a set of criteria. You may want to ask more complex questions such as “Who is the oldest person?” or “Who is the shortest person?” <em>Aggregate functions</em> can help you answer these questions. In fact, you learned an aggregate function above, <code>COUNT</code>.
</p>
<p>
How old is the oldest person with a drivers license? With a small amount of data, you might be able to just eyeball it, but there thousands of records in the <code>drivers_license</code> table. (Try <code>COUNT</code> if you want to know just how many!) You can't just look over that list to find the answer.
</p>
<p>Here are a few useful aggregate functions SQL provides:
<dl>
<dt>
MAX
</dt> <dd>
finds the maximum value
</dd>
<dt>
MIN
</dt> <dd>
finds the minimum value
</dd>
<dt>
SUM
</dt> <dd>
calculates the sum of the specified column values
</dd>
<dt>
AVG
</dt> <dd>
calculates the average of the specified column values
</dd>
<dt>
COUNT
</dt><dd>
counts the number of specified column values
</dd>
</dl>
</p>
<sql-exercise
data-question="Run this query, then try some of the other aggregate functions."
data-comment=""
data-default-text="SELECT max(age) FROM drivers_license;"
></sql-exercise>
<p>There's another way to find minimum and maximum values, while also seeing more of the data. You can control the sort order of results you get. It's really quite intuitive: just use <code>ORDER BY</code> followed by a column name. It can be challenging when there's a lot of data! (When people get serious about working with SQL, they use better tools than this web-based system!) By default, ORDER BY goes in "ascending" (smallest to largest, or A to Z) order, but you can be specific with <code>ASC</code> for ascending, or you can reverse it with <code>DESC</code>.
</p>
<sql-exercise
data-question="Run this query to see how to control sort order."
data-comment="After you've run it, change ASC to DESC, or take that part out completely. Try sorting on other columns."
data-default-text="SELECT * FROM drivers_license ORDER BY age ASC LIMIT 10"
></sql-exercise>
</div>
</div>
<div id="credits" class="grid">
<div class="grid-item">
<h3>Credits</h3>
<p>The SQL Murder Mystery was created by <a href="https://twitter.com/joonparkmusic">Joon Park</a> and <a href="https://twitter.com/Cathy_MeiyingHe">Cathy He</a> while they were Knight Lab fellows. See the <a href="https://github.com/NUKnightLab/sql-mysteries">GitHub repository</a> for more information.</p>
<p>This mystery was inspired by <a href="https://github.com/veltman/clmystery">a crime in the neighboring Terminal City.</a></p>
<p>Web-based SQL is made possible by <a href="https://github.com/kripken/sql.js/">SQL.js</a></p>
<p>SQL query custom web components created and released to the public domain by Zi Chong Kao, creator of <a href="https://selectstarsql.com/">Select Star SQL.</a></p>
<p>Detective illustration courtesy of <a href="https://www.vecteezy.com/">Vectors by Vecteezy</a></p>
</div>
</div>
</div>
<footer class="footer-knightlab--dark">
<div class="container">
<div class='grid grid-center'>
<div class='column-2 column-4-phone column-4-tablet'>
<a href="http://knightlab.northwestern.edu" target="_blank">
<img src="https://cdn.knightlab.com/libs/orangeline/0.1.1/assets/logo-knightlab-stacked-dark-small.png" style="margin-left:auto;">
</a>
<ul class="list--social text-align-center">
<li><a class="link--no-style" href="http://www.twitter.com/knightlab" target="_blank" title="Knight Lab on Twitter"><span class="icon-twitter"></span></a></li>
<li><a class="link--no-style" href="https://www.facebook.com/knightlab" target="_blank" title="Knight Lab on Facebook"><span class="icon-facebook"></span></a></li>
<li><a class="link--no-style" href="https://github.com/NUKnightLab/" target="_blank" title="Knight Lab on GitHub"><span class="icon-github"></span></a></li>
</ul>
</div>
<div class='column-4 column-5-tablet column-6-phone footer-description'>
<p>
The <a title="Northwestern University" href="http://www.northwestern.edu/" target="_blank">Northwestern University</a> Knight Lab is a team of technologists and journalists working at advancing news media innovation through exploration and experimentation.
</p>
<div class="grid-size-2 grid-size-2-phone">
<div class="grid-item">
<a title="Medill School of Journalism, Media, Integrated Marketing Communications" href="http://www.medill.northwestern.edu/" target="_blank">
<img src="https://cdn.knightlab.com/libs/orangeline/0.1.1/assets/logo-medill-dark.png">
</a>
</div>
<div class="grid-item">
<a title="McCormick School of Engineering" href="http://www.mccormick.northwestern.edu/" target="_blank">
<img src="https://cdn.knightlab.com/libs/orangeline/0.1.1/assets/logo-mccormick-dark.png">
</a>
</div>
</div>
</div>
</div>
<div class="grid grid-center">
<div class="grid-item">
<address itemscope="" itemtype="http://data-vocabulary.org/Organization">
<span style="display:none;" itemprop="name" title="Knight Lab | Advancing news media innovation through exploration and experimentation." class="address-name">Knight Lab</span>
<span itemprop="tel" class="tel">(847) 467-4971</span>
<span itemprop="address" itemscope="" itemtype="http://data-vocabulary.org/Address" class="address">
<span itemprop="street-address" class="street-address">1845 Sheridan Road</span>
<span class="room-num">Fisk #109 &amp; #111</span>
<div class="address-group">
<span itemprop="locality">Evanston,</span> <span itemprop="region">IL</span> <span itemprop="postal-code">60208 </span>
<span style="display:none;" itemprop="geo" itemscope="" itemtype="http://www.data-vocabulary.org/Geo/" class="geo">
Latitude: <span itemprop="latitude">42.056893</span><br>Longitude:
<span itemprop="longitude">-87.676735</span>
</span>
<a style="display:none;" href="http://knightlab.northwestern.edu" itemprop="url" class="url">Northwesten University Knight Lab | Advancing media innovation through exploration and experimentation.</a>
</div>
</span>
</address>
<span class="copyright">© Copyright 2019 Northwestern University</span>
</div>
</div>
</div>
</footer>
<script src="https://cdn.knightlab.com/libs/orangeline/0.1.1/js/orangeline.js"></script>
<script type="text/javascript">
document.querySelector('#show-schema').addEventListener('click', function() {
document.querySelector('#experienced-schema').classList.add('show');
})
</script>
</body>
</html>