Skip to content

Commit

Permalink
Add binary operators (for now, cross join)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelmior committed Aug 21, 2019
1 parent 7f63271 commit 3cd89df
Show file tree
Hide file tree
Showing 10 changed files with 347 additions and 50 deletions.
17 changes: 15 additions & 2 deletions src/Home.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Props = {
expr: {[string]: any},
data: DataState,
sources: {[string]: Data},
types: {[string]: Array<string>},

changeExpr: typeof changeExpr,
exprFromSql: typeof exprFromSql,
Expand Down Expand Up @@ -49,6 +50,7 @@ class Home extends Component<Props> {
<SqlEditor
defaultText="SELECT * FROM Doctor"
exprFromSql={this.props.exprFromSql}
types={this.props.types}
/>

{/* Relational algebra expression display */}
Expand All @@ -71,9 +73,20 @@ class Home extends Component<Props> {
}

const mapStateToProps = state => {
// Get just the column names from the source data
const types = Object.fromEntries(
Object.entries(state.data.sourceData).map(([name, data]) => {
return [
name,
data != null && typeof data === 'object' ? data.columns : [],
];
})
);

return {
expr: state.relexp.expr,
data: state.data,
types: types,
sources: state.data.sourceData,
};
};
Expand All @@ -83,8 +96,8 @@ const mapDispatchToProps = dispatch => {
changeExpr: data => {
dispatch(changeExpr(data));
},
exprFromSql: data => {
dispatch(exprFromSql(data));
exprFromSql: (sql, types) => {
dispatch(exprFromSql(sql, types));
},
};
};
Expand Down
22 changes: 15 additions & 7 deletions src/RelExpr.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @flow
import React, {Component} from 'react';
import RelOp, {Projection, Rename, Selection} from './RelOp';
import {UnaryRelOp, Projection, Rename, Selection, BinaryRelOp, Join} from './RelOp';
import Relation from './Relation';
import {changeExpr} from './modules/data';

Expand Down Expand Up @@ -53,7 +53,7 @@ class RelExpr extends Component<Props> {
switch (Object.keys(expr)[0]) {
case 'projection':
return (
<RelOp
<UnaryRelOp
operator={
<Projection project={expr.projection.arguments.project} />
}
Expand All @@ -62,12 +62,12 @@ class RelExpr extends Component<Props> {
expr={expr.projection.children[0]}
changeExpr={this.props.changeExpr}
/>
</RelOp>
</UnaryRelOp>
);

case 'selection':
return (
<RelOp
<UnaryRelOp
operator={
<Selection
select={this.conditionToString(expr.selection.arguments.select)}
Expand All @@ -78,22 +78,30 @@ class RelExpr extends Component<Props> {
expr={expr.selection.children[0]}
changeExpr={this.props.changeExpr}
/>
</RelOp>
</UnaryRelOp>
);

case 'rename':
return (
<RelOp operator={<Rename rename={expr.rename.arguments.rename} />}>
<UnaryRelOp
operator={<Rename rename={expr.rename.arguments.rename} />}
>
<RelExpr
expr={expr.rename.children[0]}
changeExpr={this.props.changeExpr}
/>
</RelOp>
</UnaryRelOp>
);

case 'relation':
return <Relation name={expr.relation} />;

case 'join':
return <BinaryRelOp
operator={<Join/>}
left={<RelExpr expr={expr.join.left}/>}
right={<RelExpr expr={expr.join.right}/>}/>;

default:
throw new Error('Invalid expression ' + JSON.stringify(expr) + '.');
}
Expand Down
45 changes: 42 additions & 3 deletions src/RelOp.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@ import './RelOp.css';

type Props = {
operator: Element<any>,
children: Node,
};

type State = {
isHovered: boolean,
};

/** Base class for all relational algebra operators */
class RelOp extends Component<Props, State> {
class RelOp<T> extends Component<T, State> {
constructor() {
super();
this.state = {isHovered: false};
Expand All @@ -29,7 +28,13 @@ class RelOp extends Component<Props, State> {
return {...state, isHovered: hovering};
});
}
}

type UnaryProps = Props & {
children: Node,
};

class UnaryRelOp extends RelOp<UnaryProps> {
render() {
const hoverClass = 'RelOp ' + (this.state.isHovered ? 'hovering' : '');
return (
Expand Down Expand Up @@ -85,4 +90,38 @@ class Selection extends Component<{select: Array<string>}> {
}
}

export {RelOp as default, Projection, Rename, Selection};
type BinaryProps = Props & {
left: Node,
right: Node,
};

class BinaryRelOp extends RelOp<BinaryProps> {
render() {
const hoverClass = 'RelOp ' + (this.state.isHovered ? 'hovering' : '');
return (
<span
className={hoverClass}
onMouseOver={this.handleHover}
onMouseOut={this.handleHover}
>
{this.props.left}{this.props.operator}{this.props.right}
</span>
);
}
}

class Join extends Component<{}> {
render() {
return <span>&times;</span>;
}
}

export {
RelOp as default,
UnaryRelOp,
Projection,
Rename,
Selection,
BinaryRelOp,
Join,
};
3 changes: 2 additions & 1 deletion src/SqlEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const parser = require('js-sql-parser');
type Props = {
defaultText: string,
exprFromSql: typeof exprFromSql,
types: {[string]: Array<string>},
};

type State = {
Expand Down Expand Up @@ -43,7 +44,7 @@ class SqlEditor extends Component<Props, State> {
const sql = parser.parse(text);
if (sql.nodeType === 'Main' && sql.value.type === 'Select') {
// Parse SELECT queries
this.props.exprFromSql(sql.value);
this.props.exprFromSql(sql.value, this.props.types);
if (!skipState) {
this.setState({error: null});
}
Expand Down
9 changes: 8 additions & 1 deletion src/Table.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@ class Table extends Component<Props> {
let columns = [
{
Header: this.props.tableName,
columns: this.props.columns.map(c => ({Header: c, accessor: c})),

// Define the column with a default accessor to ignore the
// default behaviour of asking nested properties via dots
columns: this.props.columns.map(c => ({
id: c,
Header: c,
accessor: d => d[c]
})),
},
];

Expand Down
10 changes: 1 addition & 9 deletions src/Table.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,9 @@ it('can render a table', () => {
const table = wrapper.find(ReactTable).first();
expect(table).toHaveProp({
data: data,
columns: [
{
Header: 'foo',
columns: [
{Header: 'bar', accessor: 'bar'},
{Header: 'baz', accessor: 'baz'},
],
},
],
sortable: true,
});
expect(table).toHaveProp('columns');
});

/** @test {Table} */
Expand Down
105 changes: 100 additions & 5 deletions src/modules/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,38 @@ const initialState = {
},
};

function resolveColumn(path: string, row: {[string]: any}): string {
let [table, column] = path.split('.');
if (!column) {
column = table;
table = undefined;
}

if (table) {
if (row.hasOwnProperty(path)) {
// Use the dotted path
return path;
} else if (row.hasOwnProperty(column)) {
// Use the column name without the table qualifier
return column;
}
}

const columns = [];
for (const rowCol in row) {
if (rowCol === column || rowCol.endsWith('.' + column)) {
columns.push(rowCol);
}
}

// Ensure we find the correct column
if (columns.length === 1) {
return columns[0];
}

throw new Error('Invalid column ' + path);
}

/**
* @param expr - a relational algebra expression to evaluate
* @param sourceData - source data from relations
Expand All @@ -57,7 +89,10 @@ function applyExpr(expr, sourceData) {
let projData = applyExpr(expr.projection.children[0], sourceData);

// Get the columns which should be deleted
const deleted = projData.columns.filter(
const columns = projData.columns.map(col =>
resolveColumn(col, projData.data[0])
);
const deleted = columns.filter(
column => expr.projection.arguments.project.indexOf(column) === -1
);

Expand All @@ -83,7 +118,7 @@ function applyExpr(expr, sourceData) {
// Loop over all expressions to be evaluauted
for (var i = 0; keep && i < select.length; i++) {
// Get the column to compare and the comparison operator
const col = Object.keys(select[i])[0];
const col = resolveColumn(Object.keys(select[i])[0], item);
const op = Object.keys(select[i][col])[0];

// Update the flag indicating whether we should keep this tuple
Expand Down Expand Up @@ -123,13 +158,19 @@ function applyExpr(expr, sourceData) {

// Loop over all pairs of things to rename
Object.entries(expr.rename.arguments.rename).forEach(([from, to]) => {
// Ensure target name is a string
if (typeof to !== 'string') {
throw new Error('Invalid target for rename');
}

// Add a new column with the new name
renData.columns[renData.columns.indexOf(from)] = to;
const fromColumn = resolveColumn(from, renData.data[0]);
renData.columns[renData.columns.indexOf(fromColumn)] = to;

// Copy all column data and delete the original column
for (let j = 0; j < renData.data.length; j++) {
renData.data[j][to] = renData.data[j][from];
delete renData.data[j][from];
renData.data[j][to] = renData.data[j][fromColumn];
delete renData.data[j][fromColumn];
}
});
return renData;
Expand All @@ -138,6 +179,60 @@ function applyExpr(expr, sourceData) {
// Make a copy of the data from a source table and return it
return JSON.parse(JSON.stringify(sourceData[expr.relation]));

case 'join':
// Process each side of the join
const left = applyExpr(expr.join.left, sourceData);
const right = applyExpr(expr.join.right, sourceData);

// Combine columns adding relation name where needed
const combinedColumns: Array<string> = [];
for (const leftColumn of left.columns) {
if (right.columns.includes(leftColumn)) {
combinedColumns.push(left.name + '.' + leftColumn);
} else {
combinedColumns.push(leftColumn);
}
}
for (const rightColumn of right.columns) {
if (left.columns.includes(rightColumn)) {
combinedColumns.push(right.name + '.' + rightColumn);
} else {
combinedColumns.push(rightColumn);
}
}

const output = {
name: left.name + ' × ' + right.name,
columns: combinedColumns,
data: [],
};

// Perform the cross product
for (const leftRow of left.data) {
for (const rightRow of right.data) {
// Combine data from the two objects including the relation name
const combinedData = {};
for (const leftKey in leftRow) {
combinedData[left.name + '.' + leftKey] = leftRow[leftKey];
}
for (const rightKey in rightRow) {
combinedData[right.name + '.' + rightKey] = rightRow[rightKey];
}

// Resolve the output data according to the combined data
// This may remove relation names where they are not needed
const outputData = {};
for (const column of combinedColumns) {
outputData[column] =
combinedData[resolveColumn(column, combinedData)];
}

output.data.push(outputData);
}
}

return output;

default:
// Fallback in case we get something invalid to show a nice error
throw new Error('Invalid expression');
Expand Down

1 comment on commit 3cd89df

@vercel
Copy link

@vercel vercel bot commented on 3cd89df Aug 21, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.