Refactor matching filter abstraction level.

master
Sander Vocke 6 years ago
parent 4bbe692548
commit 0715bc1b10
  1. 136
      src/queries.js
  2. 27
      src/userquerywidget.js

@ -62,13 +62,13 @@ export function do_tag_query(query, database) {
export const MatchTypeEnum = { export const MatchTypeEnum = {
MATCH_EQUALS: 1, MATCH_EQUALS: 1,
MATCH_REGEXP_CASEINSENSITIVE: 2, MATCH_NATURAL: 2,
}; };
export const MatchAgainstEnum = { export const MatchAgainstEnum = {
MATCH_RESULT_NAME: 1, // Match on the name of whatever object type we are querying for MATCH_IMAGE_NAME: 1, // Match on the name of the relevant image(s), if any
MATCH_IMAGE_NAME: 2, // Match on the name of the relevant image(s), if any MATCH_ALBUM_NAME: 2, // Match on the name of the relevant album(s), if any
MATCH_ALBUM_NAME: 3, // Match on the name of the relevant album(s), if any MATCH_ALBUM_NAME_IN_TREE: 3, // Match on any album name in the album tree branch, including parents
MATCH_TAG_NAME: 4, // Match on the name of the relevant tag(s), if any MATCH_TAG_NAME: 4, // Match on the name of the relevant tag(s), if any
} }
@ -102,17 +102,40 @@ export class ConstFilter extends ResultFilter {
simplify() { return this; } simplify() { return this; }
} }
export class NegationFilter extends ResultFilter {
constructor(rtype, body) {
super(rtype);
this.body = body;
}
body = new ConstFilter(this.return_type, false);
to_sql_where() {
return "NOT (" + this.body.to_sql_where + ")";
}
simplify() {
var f = this.body.simplify();
if(f.is_true()) {
return new ConstFilter(this.result_type, false);
}
if(f.is_false()) {
return new ConstFilter(this.result_type, true);
}
return this;
}
}
export class MatchingFilter extends ResultFilter { export class MatchingFilter extends ResultFilter {
constructor(rtype, against, from, mtype, negate) { constructor(rtype, against, from, mtype) {
super(rtype); super(rtype);
this.match_against = against; this.match_against = against;
this.match_from = from; this.match_from = from;
this.match_type = mtype; this.match_type = mtype;
this.negate = negate;
} }
// What string to match against, e.g. "Image.name" (field) or "\"Image.name\"" (literal) // What string to match against.
match_against = ""; match_against = MatchAgainstEnum.MATCH_IMAGE_NAME;
// optional string used in the filtering // optional string used in the filtering
match_from = ""; match_from = "";
@ -120,26 +143,39 @@ export class MatchingFilter extends ResultFilter {
// How to use the matching string // How to use the matching string
match_type = MatchTypeEnum.MATCH_EQUALS; match_type = MatchTypeEnum.MATCH_EQUALS;
// If true, negates the filter
// TODO: make negation its own filter type
negate = false;
to_sql_where() { to_sql_where() {
var match_type_str = false; if(this.match_against == MatchAgainstEnum.MATCH_IMAGE_NAME) {
var match_against = this.match_against; if(this.match_type == MatchTypeEnum.MATCH_EQUALS) {
var match_from = this.match_from; return '(Images.name="' + this.match_from + '")';
} else if(this.match_type == MatchTypeEnum.MATCH_NATURAL) {
if (this.match_type == MatchTypeEnum.MATCH_EQUALS) { var expr = '"' + '.*' + escape_regex(this.match_from) + '.*' + '"';
match_type_str = "="; return "(LOWER(Images.name) REGEXP LOWER(" + expr +"))";
} else if (this.match_type == MatchTypeEnum.MATCH_REGEXP_CASEINSENSITIVE) { }
match_type_str = " REGEXP "; } else if(this.match_against == MatchAgainstEnum.MATCH_TAG_NAME) {
match_from = "LOWER(" + match_from + ")"; if(this.match_type == MatchTypeEnum.MATCH_EQUALS) {
match_against = "LOWER(" + match_against + ")"; return '(Tags.name="' + this.match_from + '")';
} else { } else if(this.match_type == MatchTypeEnum.MATCH_NATURAL) {
throw new Error('Unsupported match type: ' + this.match_type); var expr = '"' + '.*' + escape_regex(this.match_from) + '.*' + '"';
return '(LOWER(Tags.name) REGEXP LOWER("' + expr + '"))';
}
} else if(this.match_against == MatchAgainstEnum.MATCH_ALBUM_NAME) {
if(this.match_type == MatchTypeEnum.MATCH_EQUALS) {
var expr = '\/(.*\/)*' + escape_regex(this.match_from);
return '(Albums.relativePath REGEXP "' + expr + '")';
} else if(this.match_type == MatchTypeEnum.MATCH_NATURAL) {
throw new Error("Natural matching on album names is not yet supported.");
}
} else if(this.match_against == MatchAgainstEnum.MATCH_ALBUM_NAME_IN_TREE) {
if(this.match_type == MatchTypeEnum.MATCH_EQUALS) {
var expr = escape_regex(this.match_from) + '(\/[^\/]+)*';
return '(Albums.relativePath REGEXP "' + expr + '")';
} else if(this.match_type == MatchTypeEnum.MATCH_NATURAL) {
throw new Error("Natural matching on album tree names is not yet supported.");
}
} }
return "(" + (this.negate ? "NOT " : "") + match_against + match_type_str + match_from + ")"; console.log(this);
throw new Error("Unsupported matching filter for SQL generation.");
} }
simplify() { return this; } simplify() { return this; }
} }
@ -255,43 +291,47 @@ function filter_from_text_segment(result_type, segment) {
// Option 1: match on image name // Option 1: match on image name
var name_filter = new MatchingFilter( var name_filter = new MatchingFilter(
result_type, result_type,
"Images.name", MatchAgainstEnum.MATCH_IMAGE_NAME,
'"' + '.*' + escape_regex(segment['text']) + '.*' + '"', segment['text'],
MatchTypeEnum.MATCH_REGEXP_CASEINSENSITIVE, MatchTypeEnum.MATCH_NATURAL
segment['negated']
); );
// Option 2: match on album path // Option 2: match on album path
var album_filter = new MatchingFilter( var album_filter = new MatchingFilter(
result_type, result_type,
"Albums.relativePath", MatchAgainstEnum.MATCH_ALBUM_NAME,
'"' + escape_regex(segment['text']) + '(\/[^\/]+)*' + '"', segment['text'],
MatchTypeEnum.MATCH_REGEXP_CASEINSENSITIVE, MatchTypeEnum.MATCH_EQUALS
segment['negated']
); );
filter = new LogicalOperatorFilter(result_type, name_filter, album_filter, LogicalOperatorEnum.OR); filter = new LogicalOperatorFilter(result_type, name_filter, album_filter, LogicalOperatorEnum.OR);
if(segment['negated']) {
filter = new NegationFilter(result_type, filter);
}
} else if (result_type == ResultTypeEnum.ALBUM) { } else if (result_type == ResultTypeEnum.ALBUM) {
// Match against the album "name", which is the basename of its relative path. // TODO: We need a natural matcher for album names
// TODO is it nicer to match on relative path?
var name_filter = new MatchingFilter( var name_filter = new MatchingFilter(
result_type, result_type,
"Albums.relativePath", MatchAgainstEnum.MATCH_ALBUM_NAME,
'"' + '\/(.*\/)*' + escape_regex(segment['text']) + '"', segment['text'],
MatchTypeEnum.MATCH_REGEXP_CASEINSENSITIVE, MatchTypeEnum.MATCH_EQUALS
segment['negated']
); );
filter = name_filter; filter = name_filter;
if(segment['negated']) {
filter = new NegationFilter(result_type, filter);
}
} else if (result_type == ResultTypeEnum.TAG) { } else if (result_type == ResultTypeEnum.TAG) {
// Match against the tag name. // Match against the tag name.
var name_filter = new MatchingFilter( var name_filter = new MatchingFilter(
result_type, result_type,
"Tags.name", MatchAgainstEnum.MATCH_TAG_NAME,
'"' + '.*' + escape_regex(segment['text']) + '.*' + '"', segment['text'],
MatchTypeEnum.MATCH_REGEXP_CASEINSENSITIVE, MatchTypeEnum.MATCH_EQUALS
segment['negated']
); );
filter = name_filter; filter = name_filter;
if(segment['negated']) {
filter = new NegationFilter(result_type, filter);
}
} }
return filter.simplify(); return filter.simplify();
@ -320,9 +360,9 @@ export function user_query_from_search_string(search_string) {
export function user_query_from_browsed_album(album_path) { export function user_query_from_browsed_album(album_path) {
var r = new UserQuery(); var r = new UserQuery();
var match_type = MatchTypeEnum.MATCH_REGEXP_CASEINSENSITIVE; var match_type = MatchTypeEnum.MATCH_EQUALS;
var match_text = '"' + escape_regex(album_path) + '(\/[^\/]+)*' + '"'; var match_against = MatchAgainstEnum.MATCH_ALBUM_NAME_IN_TREE;
var match_against = "Albums.relativePath"; var match_text = album_path;
r.image_filter = new MatchingFilter(ResultTypeEnum.IMAGE, match_against, match_text, match_type, false).simplify(); r.image_filter = new MatchingFilter(ResultTypeEnum.IMAGE, match_against, match_text, match_type, false).simplify();
r.album_filter = new ConstFilter(ResultTypeEnum.ALBUM, false).simplify(); r.album_filter = new ConstFilter(ResultTypeEnum.ALBUM, false).simplify();
@ -332,8 +372,8 @@ export function user_query_from_browsed_album(album_path) {
export function user_query_from_browsed_tag(name) { export function user_query_from_browsed_tag(name) {
var r = new UserQuery(); var r = new UserQuery();
var match_type = MatchTypeEnum.MATCH_EQUALS; var match_type = MatchTypeEnum.MATCH_EQUALS;
var match_text = '"' + name + '"'; var match_text = name;
var match_against = "Tags.name"; var match_against = MatchAgainstEnum.MATCH_TAG_NAME;
r.image_filter = new MatchingFilter(ResultTypeEnum.IMAGE, match_against, match_text, match_type, false).simplify(); r.image_filter = new MatchingFilter(ResultTypeEnum.IMAGE, match_against, match_text, match_type, false).simplify();
r.album_filter = new ConstFilter(ResultTypeEnum.ALBUM, false).simplify(); r.album_filter = new ConstFilter(ResultTypeEnum.ALBUM, false).simplify();
r.tag_filter = new ConstFilter(ResultTypeEnum.TAG, false).simplify(); r.tag_filter = new ConstFilter(ResultTypeEnum.TAG, false).simplify();

@ -6,6 +6,7 @@ import FormControlLabel from '@material-ui/core/FormControlLabel';
import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline'; import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import LabelIcon from '@material-ui/icons/Label';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
@ -28,10 +29,36 @@ const useStyles = makeStyles(theme => ({
}, },
})); }));
export function TagEqualsExpressionControl(props) {
const classes = useStyles();
const { name } = props;
var pretty_name = name
.replace(/^"/g, '')
.replace(/"$/g, '');
return (
<>
<Button
variant="outlined"
className={classes.filterexpcontrol}
startIcon={<LabelIcon />}>
{pretty_name}
</Button>
</>
);
}
export function MatchingFilterExpressionControl(props) { export function MatchingFilterExpressionControl(props) {
const classes = useStyles(); const classes = useStyles();
const { expr } = props; const { expr } = props;
if(expr.match_type == MatchTypeEnum.MATCH_EQUALS &&
expr.match_against == "Tags.name") {
// This is an exact tag match.
return <TagEqualsExpressionControl name={expr.match_from} />
}
var opstr = ""; var opstr = "";
if (expr.match_type == MatchTypeEnum.MATCH_EQUALS) { if (expr.match_type == MatchTypeEnum.MATCH_EQUALS) {
opstr = " = "; opstr = " = ";

Loading…
Cancel
Save