• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dataunitylab / relational-playground / #107

09 Sep 2025 03:15PM UTC coverage: 78.436% (-7.9%) from 86.296%
#107

push

michaelmior
Update Node to 24

Signed-off-by: Michael Mior <mmior@mail.rit.edu>

517 of 731 branches covered (70.73%)

Branch coverage included in aggregate %.

1018 of 1226 relevant lines covered (83.03%)

14584.19 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.17
/src/RelExpr.js
1
// @flow
2
import React, {useEffect, useRef, useState} from 'react';
3
import {
4
  UnaryRelOp,
5
  Projection,
6
  Rename,
7
  Selection,
8
  BinaryRelOp,
9
  Except,
10
  OrderBy,
11
  Product,
12
  Join,
13
  Intersect,
14
  Union,
15
  GroupBy,
16
} from './RelOp';
17
import Relation from './Relation';
18
import {exprToString} from './util';
19
import {changeExpr} from './modules/data';
20
import ReactDOM from 'react-dom';
21

22
import './RelExpr.css';
23

24
import type {Node, StatelessFunctionalComponent} from 'react';
25
import {useSelector} from 'react-redux';
26
import type {State} from './modules/relexp';
27

28
type Props = {
29
  changeExpr: typeof changeExpr,
30
  expr: {[string]: any},
31
  ReactGA: any,
32
};
33

34
/** A graphical representation of a relational algebra expression */
35
const RelExpr: StatelessFunctionalComponent<Props> = (props) => {
3✔
36
  const nodeRef = useRef<?HTMLSpanElement>();
34✔
37
  const currentNode = useSelector<{data: State}, _>((state) => state.data.expr);
60✔
38
  const [isHovering, setIsHovering] = useState(false);
33✔
39
  const [isSelected, setIsSelected] = useState(false);
33✔
40

41
  useEffect(() => {
33✔
42
    // Adjust 'clicked' highlighting based on any changes to currentNode selected
43
    const clicked = props.expr === currentNode;
26✔
44
    if (!clicked) {
26!
45
      setIsSelected(false);
26✔
46
      setIsHovering(false);
26✔
47
    }
48
  }, [currentNode, props.expr]);
49

50
  /**
51
   * @param expr - a relational algebra expression object to render
52
   * @return a component representing the top-most expression
53
   */
54
  const buildExpr = (expr: {[string]: any}): Node => {
33✔
55
    // Don't try to render empty expressions
56
    if (!expr || Object.keys(expr).length === 0) {
32!
57
      return '';
×
58
    }
59
    const type = Object.keys(expr)[0];
32✔
60
    switch (type) {
32!
61
      case 'projection':
62
        return (
1✔
63
          <span>
64
            <UnaryRelOp
65
              operator={
66
                <Projection project={expr.projection.arguments.project} />
67
              }
68
            >
69
              <RelExpr
70
                expr={expr.projection.children[0]}
71
                changeExpr={props.changeExpr}
72
                ReactGA={props.ReactGA}
73
              />
74
            </UnaryRelOp>
75
          </span>
76
        );
77

78
      case 'selection':
79
        return (
14✔
80
          <span>
81
            <UnaryRelOp
82
              operator={
83
                <Selection
84
                  select={exprToString(expr.selection.arguments.select)}
85
                />
86
              }
87
            >
88
              <RelExpr
89
                expr={expr.selection.children[0]}
90
                changeExpr={props.changeExpr}
91
                ReactGA={props.ReactGA}
92
              />
93
            </UnaryRelOp>
94
          </span>
95
        );
96

97
      case 'rename':
98
        return (
1✔
99
          <span>
100
            <UnaryRelOp
101
              operator={<Rename rename={expr.rename.arguments.rename} />}
102
            >
103
              <RelExpr
104
                expr={expr.rename.children[0]}
105
                changeExpr={props.changeExpr}
106
                ReactGA={props.ReactGA}
107
              />
108
            </UnaryRelOp>
109
          </span>
110
        );
111

112
      case 'relation':
113
        return <Relation name={expr.relation} />;
15✔
114

115
      case 'order_by':
116
        return (
×
117
          <OrderBy
118
            columns={expr.order_by.arguments.order_by}
119
            relation={buildExpr(expr.order_by.children[0])}
120
          />
121
        );
122

123
      case 'group_by':
124
        return (
×
125
          <span>
126
            <UnaryRelOp
127
              operator={
128
                <GroupBy
129
                  groupBy={expr.group_by.arguments.groupBy}
130
                  aggregates={expr.group_by.arguments.aggregates.map(
131
                    (agg) =>
132
                      `${agg.aggregate.function}(${agg.aggregate.column})`
×
133
                  )}
134
                  selectColumns={expr.group_by.arguments.selectColumns}
135
                />
136
              }
137
            >
138
              <RelExpr
139
                expr={expr.group_by.children[0]}
140
                changeExpr={props.changeExpr}
141
                ReactGA={props.ReactGA}
142
              />
143
            </UnaryRelOp>
144
          </span>
145
        );
146

147
      case 'join':
148
        return (
×
149
          <Join
150
            type={expr.join.type}
151
            condition={exprToString(expr.join.condition)}
152
            left={buildExpr(expr.join.left)}
153
            right={buildExpr(expr.join.right)}
154
          />
155
        );
156

157
      case 'except':
158
      case 'intersect':
159
      case 'product':
160
      case 'union':
161
        const operator = {
×
162
          except: <Except />,
163
          intersect: <Intersect />,
164
          product: <Product />,
165
          union: <Union />,
166
        }[type];
167
        return (
×
168
          <BinaryRelOp
169
            operator={operator}
170
            left={
171
              <RelExpr
172
                expr={expr[type].left}
173
                ReactGA={props.ReactGA}
174
                changeExpr={props.changeExpr}
175
              />
176
            }
177
            right={
178
              <RelExpr
179
                expr={expr[type].right}
180
                ReactGA={props.ReactGA}
181
                changeExpr={props.changeExpr}
182
              />
183
            }
184
          />
185
        );
186

187
      default:
188
        throw new Error('Invalid expression ' + JSON.stringify(expr) + '.');
1✔
189
    }
190
  };
191

192
  /**
193
   * @param e - the event object which generated the click
194
   */
195
  const handleExprClick = (e: SyntheticMouseEvent<HTMLElement>): void => {
33✔
196
    e.stopPropagation();
1✔
197
    const node =
198
      ReactDOM.findDOMNode(nodeRef.current) instanceof HTMLElement
1!
199
        ? ReactDOM.findDOMNode(nodeRef.current)
200
        : undefined;
201

202
    const clicked = e.type === 'click' && !isSelected;
1✔
203

204
    if (props.changeExpr && node instanceof HTMLElement) {
1!
205
      if (clicked) {
1!
206
        props.changeExpr(props.expr, node);
1✔
207
      } else {
208
        props.changeExpr({}, node);
×
209
      }
210

211
      setIsHovering(false);
1✔
212
      setIsSelected(clicked);
1✔
213
    }
214

215
    if (props.ReactGA) {
1!
216
      props.ReactGA.event({
1✔
217
        category: 'User Selecting Relational Algebra Enclosure',
218
        action: Object.keys(props.expr)[0],
219
      });
220
    }
221
  };
222

223
  const handleExprHover = (e: SyntheticMouseEvent<HTMLElement>): void => {
33✔
224
    e.stopPropagation();
2✔
225
    setIsHovering(e.type === 'mouseover');
2✔
226
  };
227

228
  if (!props.expr || Object.keys(props.expr).length === 0) {
33✔
229
    return '';
1✔
230
  }
231
  const type = Object.keys(props.expr)[0];
32✔
232
  let className = 'RelExpr';
32✔
233
  if (isHovering) {
32✔
234
    className += ' hovering';
1✔
235
  }
236
  if (isSelected) {
32✔
237
    className += ' clicked';
1✔
238
  }
239
  if (type === 'relation') {
32✔
240
    return (
15✔
241
      <span className="RelExpr" style={{margin: '.4em'}}>
242
        {buildExpr(props.expr)}
243
      </span>
244
    );
245
  } else {
246
    return (
17✔
247
      <span
248
        className={className}
249
        onMouseOver={handleExprHover}
250
        onMouseOut={handleExprHover}
251
        onClick={handleExprClick}
252
        style={{margin: '.4em'}}
253
        ref={nodeRef}
254
      >
255
        {buildExpr(props.expr)}
256
      </span>
257
    );
258
  }
259
};
260

261
export default RelExpr;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc