| /*------------------------------------------------------------------------- |
| * |
| * tsquery_rewrite.c |
| * Utilities for reconstructing tsquery |
| * |
| * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group |
| * |
| * |
| * IDENTIFICATION |
| * src/backend/utils/adt/tsquery_rewrite.c |
| * |
| *------------------------------------------------------------------------- |
| */ |
| |
| #include "postgres.h" |
| |
| #include "catalog/pg_type.h" |
| #include "executor/spi.h" |
| #include "miscadmin.h" |
| #include "tsearch/ts_utils.h" |
| #include "utils/builtins.h" |
| |
| |
| /* |
| * If "node" is equal to "ex", return a copy of "subs" instead. |
| * If "ex" matches a subset of node's children, return a modified version |
| * of "node" in which those children are replaced with a copy of "subs". |
| * Otherwise return "node" unmodified. |
| * |
| * The QTN_NOCHANGE bit is set in successfully modified nodes, so that |
| * we won't uselessly recurse into them. |
| * Also, set *isfind true if we make a replacement. |
| */ |
| static QTNode * |
| findeq(QTNode *node, QTNode *ex, QTNode *subs, bool *isfind) |
| { |
| /* Can't match unless signature matches and node type matches. */ |
| if ((node->sign & ex->sign) != ex->sign || |
| node->valnode->type != ex->valnode->type) |
| return node; |
| |
| /* Ignore nodes marked NOCHANGE, too. */ |
| if (node->flags & QTN_NOCHANGE) |
| return node; |
| |
| if (node->valnode->type == QI_OPR) |
| { |
| /* Must be same operator. */ |
| if (node->valnode->qoperator.oper != ex->valnode->qoperator.oper) |
| return node; |
| |
| if (node->nchild == ex->nchild) |
| { |
| /* |
| * Simple case: when same number of children, match if equal. |
| * (This is reliable when the children were sorted earlier.) |
| */ |
| if (QTNEq(node, ex)) |
| { |
| /* Match; delete node and return a copy of subs instead. */ |
| QTNFree(node); |
| if (subs) |
| { |
| node = QTNCopy(subs); |
| node->flags |= QTN_NOCHANGE; |
| } |
| else |
| node = NULL; |
| *isfind = true; |
| } |
| } |
| else if (node->nchild > ex->nchild && ex->nchild > 0) |
| { |
| /* |
| * AND and OR are commutative/associative, so we should check if a |
| * subset of the children match. For example, if node is A|B|C, |
| * and ex is B|C, we have a match after we notionally convert node |
| * to A|(B|C). This does not work for NOT or PHRASE nodes, but we |
| * can't get here for those node types because they have a fixed |
| * number of children. |
| * |
| * Because we expect that the children are sorted, it suffices to |
| * make one pass through the two lists to find the matches. |
| */ |
| bool *matched; |
| int nmatched; |
| int i, |
| j; |
| |
| /* Assert that the subset rule is OK */ |
| Assert(node->valnode->qoperator.oper == OP_AND || |
| node->valnode->qoperator.oper == OP_OR); |
| |
| /* matched[] will record which children of node matched */ |
| matched = (bool *) palloc0(node->nchild * sizeof(bool)); |
| nmatched = 0; |
| i = j = 0; |
| while (i < node->nchild && j < ex->nchild) |
| { |
| int cmp = QTNodeCompare(node->child[i], ex->child[j]); |
| |
| if (cmp == 0) |
| { |
| /* match! */ |
| matched[i] = true; |
| nmatched++; |
| i++, j++; |
| } |
| else if (cmp < 0) |
| { |
| /* node->child[i] has no match, ignore it */ |
| i++; |
| } |
| else |
| { |
| /* ex->child[j] has no match; we can give up immediately */ |
| break; |
| } |
| } |
| |
| if (nmatched == ex->nchild) |
| { |
| /* collapse out the matched children of node */ |
| j = 0; |
| for (i = 0; i < node->nchild; i++) |
| { |
| if (matched[i]) |
| QTNFree(node->child[i]); |
| else |
| node->child[j++] = node->child[i]; |
| } |
| |
| /* and instead insert a copy of subs */ |
| if (subs) |
| { |
| subs = QTNCopy(subs); |
| subs->flags |= QTN_NOCHANGE; |
| node->child[j++] = subs; |
| } |
| |
| node->nchild = j; |
| |
| /* |
| * At this point we might have a node with zero or one child, |
| * which should be simplified. But we leave it to our caller |
| * (dofindsubquery) to take care of that. |
| */ |
| |
| /* |
| * Re-sort the node to put new child in the right place. This |
| * is a bit bogus, because it won't matter for findsubquery's |
| * remaining processing, and it's insufficient to prepare the |
| * tree for another search (we would need to re-flatten as |
| * well, and we don't want to do that because we'd lose the |
| * QTN_NOCHANGE marking on the new child). But it's needed to |
| * keep the results the same as the regression tests expect. |
| */ |
| QTNSort(node); |
| |
| *isfind = true; |
| } |
| |
| pfree(matched); |
| } |
| } |
| else |
| { |
| Assert(node->valnode->type == QI_VAL); |
| |
| if (node->valnode->qoperand.valcrc != ex->valnode->qoperand.valcrc) |
| return node; |
| else if (QTNEq(node, ex)) |
| { |
| QTNFree(node); |
| if (subs) |
| { |
| node = QTNCopy(subs); |
| node->flags |= QTN_NOCHANGE; |
| } |
| else |
| { |
| node = NULL; |
| } |
| *isfind = true; |
| } |
| } |
| |
| return node; |
| } |
| |
| /* |
| * Recursive guts of findsubquery(): attempt to replace "ex" with "subs" |
| * at the root node, and if we failed to do so, recursively match against |
| * child nodes. |
| * |
| * Delete any void subtrees resulting from the replacement. |
| * In the following example '5' is replaced by empty operand: |
| * |
| * AND -> 6 |
| * / \ |
| * 5 OR |
| * / \ |
| * 6 5 |
| */ |
| static QTNode * |
| dofindsubquery(QTNode *root, QTNode *ex, QTNode *subs, bool *isfind) |
| { |
| /* since this function recurses, it could be driven to stack overflow. */ |
| check_stack_depth(); |
| |
| /* also, since it's a bit expensive, let's check for query cancel. */ |
| CHECK_FOR_INTERRUPTS(); |
| |
| /* match at the node itself */ |
| root = findeq(root, ex, subs, isfind); |
| |
| /* unless we matched here, consider matches at child nodes */ |
| if (root && (root->flags & QTN_NOCHANGE) == 0 && |
| root->valnode->type == QI_OPR) |
| { |
| int i, |
| j = 0; |
| |
| /* |
| * Any subtrees that are replaced by NULL must be dropped from the |
| * tree. |
| */ |
| for (i = 0; i < root->nchild; i++) |
| { |
| root->child[j] = dofindsubquery(root->child[i], ex, subs, isfind); |
| if (root->child[j]) |
| j++; |
| } |
| |
| root->nchild = j; |
| |
| /* |
| * If we have just zero or one remaining child node, simplify out this |
| * operator node. |
| */ |
| if (root->nchild == 0) |
| { |
| QTNFree(root); |
| root = NULL; |
| } |
| else if (root->nchild == 1 && root->valnode->qoperator.oper != OP_NOT) |
| { |
| QTNode *nroot = root->child[0]; |
| |
| pfree(root); |
| root = nroot; |
| } |
| } |
| |
| return root; |
| } |
| |
| /* |
| * Substitute "subs" for "ex" throughout the QTNode tree at root. |
| * |
| * If isfind isn't NULL, set *isfind to show whether we made any substitution. |
| * |
| * Both "root" and "ex" must have been through QTNTernary and QTNSort |
| * to ensure reliable matching. |
| */ |
| QTNode * |
| findsubquery(QTNode *root, QTNode *ex, QTNode *subs, bool *isfind) |
| { |
| bool DidFind = false; |
| |
| root = dofindsubquery(root, ex, subs, &DidFind); |
| |
| if (isfind) |
| *isfind = DidFind; |
| |
| return root; |
| } |
| |
| Datum |
| tsquery_rewrite_query(PG_FUNCTION_ARGS) |
| { |
| TSQuery query = PG_GETARG_TSQUERY_COPY(0); |
| text *in = PG_GETARG_TEXT_PP(1); |
| TSQuery rewrited = query; |
| MemoryContext outercontext = CurrentMemoryContext; |
| MemoryContext oldcontext; |
| QTNode *tree; |
| char *buf; |
| SPIPlanPtr plan; |
| Portal portal; |
| bool isnull; |
| |
| if (query->size == 0) |
| { |
| PG_FREE_IF_COPY(in, 1); |
| PG_RETURN_POINTER(rewrited); |
| } |
| |
| tree = QT2QTN(GETQUERY(query), GETOPERAND(query)); |
| QTNTernary(tree); |
| QTNSort(tree); |
| |
| buf = text_to_cstring(in); |
| |
| SPI_connect(); |
| |
| if ((plan = SPI_prepare(buf, 0, NULL)) == NULL) |
| elog(ERROR, "SPI_prepare(\"%s\") failed", buf); |
| |
| if ((portal = SPI_cursor_open(NULL, plan, NULL, NULL, true)) == NULL) |
| elog(ERROR, "SPI_cursor_open(\"%s\") failed", buf); |
| |
| SPI_cursor_fetch(portal, true, 100); |
| |
| if (SPI_tuptable == NULL || |
| SPI_tuptable->tupdesc->natts != 2 || |
| SPI_gettypeid(SPI_tuptable->tupdesc, 1) != TSQUERYOID || |
| SPI_gettypeid(SPI_tuptable->tupdesc, 2) != TSQUERYOID) |
| ereport(ERROR, |
| (errcode(ERRCODE_INVALID_PARAMETER_VALUE), |
| errmsg("ts_rewrite query must return two tsquery columns"))); |
| |
| while (SPI_processed > 0 && tree) |
| { |
| uint64 i; |
| |
| for (i = 0; i < SPI_processed && tree; i++) |
| { |
| Datum qdata = SPI_getbinval(SPI_tuptable->vals[i], SPI_tuptable->tupdesc, 1, &isnull); |
| Datum sdata; |
| |
| if (isnull) |
| continue; |
| |
| sdata = SPI_getbinval(SPI_tuptable->vals[i], SPI_tuptable->tupdesc, 2, &isnull); |
| |
| if (!isnull) |
| { |
| TSQuery qtex = DatumGetTSQuery(qdata); |
| TSQuery qtsubs = DatumGetTSQuery(sdata); |
| QTNode *qex, |
| *qsubs = NULL; |
| |
| if (qtex->size == 0) |
| { |
| if (qtex != (TSQuery) DatumGetPointer(qdata)) |
| pfree(qtex); |
| if (qtsubs != (TSQuery) DatumGetPointer(sdata)) |
| pfree(qtsubs); |
| continue; |
| } |
| |
| qex = QT2QTN(GETQUERY(qtex), GETOPERAND(qtex)); |
| |
| QTNTernary(qex); |
| QTNSort(qex); |
| |
| if (qtsubs->size) |
| qsubs = QT2QTN(GETQUERY(qtsubs), GETOPERAND(qtsubs)); |
| |
| oldcontext = MemoryContextSwitchTo(outercontext); |
| tree = findsubquery(tree, qex, qsubs, NULL); |
| MemoryContextSwitchTo(oldcontext); |
| |
| QTNFree(qex); |
| if (qtex != (TSQuery) DatumGetPointer(qdata)) |
| pfree(qtex); |
| QTNFree(qsubs); |
| if (qtsubs != (TSQuery) DatumGetPointer(sdata)) |
| pfree(qtsubs); |
| |
| if (tree) |
| { |
| /* ready the tree for another pass */ |
| QTNClearFlags(tree, QTN_NOCHANGE); |
| QTNTernary(tree); |
| QTNSort(tree); |
| } |
| } |
| } |
| |
| SPI_freetuptable(SPI_tuptable); |
| SPI_cursor_fetch(portal, true, 100); |
| } |
| |
| SPI_freetuptable(SPI_tuptable); |
| SPI_cursor_close(portal); |
| SPI_freeplan(plan); |
| SPI_finish(); |
| |
| if (tree) |
| { |
| QTNBinary(tree); |
| rewrited = QTN2QT(tree); |
| QTNFree(tree); |
| PG_FREE_IF_COPY(query, 0); |
| } |
| else |
| { |
| SET_VARSIZE(rewrited, HDRSIZETQ); |
| rewrited->size = 0; |
| } |
| |
| pfree(buf); |
| PG_FREE_IF_COPY(in, 1); |
| PG_RETURN_POINTER(rewrited); |
| } |
| |
| Datum |
| tsquery_rewrite(PG_FUNCTION_ARGS) |
| { |
| TSQuery query = PG_GETARG_TSQUERY_COPY(0); |
| TSQuery ex = PG_GETARG_TSQUERY(1); |
| TSQuery subst = PG_GETARG_TSQUERY(2); |
| TSQuery rewrited = query; |
| QTNode *tree, |
| *qex, |
| *subs = NULL; |
| |
| if (query->size == 0 || ex->size == 0) |
| { |
| PG_FREE_IF_COPY(ex, 1); |
| PG_FREE_IF_COPY(subst, 2); |
| PG_RETURN_POINTER(rewrited); |
| } |
| |
| tree = QT2QTN(GETQUERY(query), GETOPERAND(query)); |
| QTNTernary(tree); |
| QTNSort(tree); |
| |
| qex = QT2QTN(GETQUERY(ex), GETOPERAND(ex)); |
| QTNTernary(qex); |
| QTNSort(qex); |
| |
| if (subst->size) |
| subs = QT2QTN(GETQUERY(subst), GETOPERAND(subst)); |
| |
| tree = findsubquery(tree, qex, subs, NULL); |
| |
| QTNFree(qex); |
| QTNFree(subs); |
| |
| if (!tree) |
| { |
| SET_VARSIZE(rewrited, HDRSIZETQ); |
| rewrited->size = 0; |
| PG_FREE_IF_COPY(ex, 1); |
| PG_FREE_IF_COPY(subst, 2); |
| PG_RETURN_POINTER(rewrited); |
| } |
| else |
| { |
| QTNBinary(tree); |
| rewrited = QTN2QT(tree); |
| QTNFree(tree); |
| } |
| |
| PG_FREE_IF_COPY(query, 0); |
| PG_FREE_IF_COPY(ex, 1); |
| PG_FREE_IF_COPY(subst, 2); |
| PG_RETURN_POINTER(rewrited); |
| } |