All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.questdb.griffin.engine.functions.regex.RegexpReplaceStrFunctionFactory Maven / Gradle / Ivy

/*******************************************************************************
 *     ___                  _   ____  ____
 *    / _ \ _   _  ___  ___| |_|  _ \| __ )
 *   | | | | | | |/ _ \/ __| __| | | |  _ \
 *   | |_| | |_| |  __/\__ \ |_| |_| | |_) |
 *    \__\_\\__,_|\___||___/\__|____/|____/
 *
 *  Copyright (c) 2014-2019 Appsicle
 *  Copyright (c) 2019-2023 QuestDB
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 ******************************************************************************/

package io.questdb.griffin.engine.functions.regex;

import io.questdb.cairo.CairoConfiguration;
import io.questdb.cairo.CairoException;
import io.questdb.cairo.sql.Function;
import io.questdb.cairo.sql.Record;
import io.questdb.cairo.sql.SymbolTableSource;
import io.questdb.griffin.FunctionFactory;
import io.questdb.griffin.PlanSink;
import io.questdb.griffin.SqlException;
import io.questdb.griffin.SqlExecutionContext;
import io.questdb.griffin.engine.functions.StrFunction;
import io.questdb.griffin.engine.functions.UnaryFunction;
import io.questdb.std.Chars;
import io.questdb.std.IntList;
import io.questdb.std.ObjList;
import org.jetbrains.annotations.NotNull;

import java.util.regex.Matcher;

public class RegexpReplaceStrFunctionFactory implements FunctionFactory {

    private static final String SIGNATURE = "regexp_replace(SSS)";

    @Override
    public String getSignature() {
        return SIGNATURE;
    }

    @Override
    public Function newInstance(
            int position,
            ObjList args,
            IntList argPositions,
            CairoConfiguration configuration,
            SqlExecutionContext sqlExecutionContext
    ) throws SqlException {
        final Function value = args.getQuick(0);

        final Function pattern = args.getQuick(1);
        final int patternPos = argPositions.getQuick(1);
        if (!pattern.isConstant() && !pattern.isRuntimeConstant()) {
            throw SqlException.$(patternPos, "not implemented: dynamic pattern would be very slow to execute");
        }

        final Function replacement = args.getQuick(2);
        final int replacementPos = argPositions.getQuick(2);
        if (!replacement.isConstant() && !replacement.isRuntimeConstant()) {
            throw SqlException.$(patternPos, "not implemented: dynamic replacement would be slow to execute");
        }

        final int maxLength = configuration.getStrFunctionMaxBufferLength();
        return new Func(value, pattern, patternPos, replacement, replacementPos, maxLength, position);
    }

    private static class Func extends StrFunction implements UnaryFunction {

        private final int functionPos;
        private final int maxLength;
        private final Function pattern;
        private final int patternPos;
        private final Function replacement;
        private final int replacementPos;
        private final StringBufferSink sink = new StringBufferSink();
        private final StringBufferSink sinkB = new StringBufferSink();
        private final Function value;
        private Matcher matcher;
        private String replacementStr;

        public Func(Function value, Function pattern, int patternPos, Function replacement, int replacementPos, int maxLength, int functionPos) {
            this.value = value;
            this.pattern = pattern;
            this.patternPos = patternPos;
            this.replacement = replacement;
            this.replacementPos = replacementPos;
            this.maxLength = maxLength;
            this.functionPos = functionPos;
        }

        @Override
        public Function getArg() {
            return value;
        }

        @Override
        public CharSequence getStr(Record rec) {
            return getStr(rec, sink);
        }

        public CharSequence getStr(Record rec, StringBufferSink sink) {
            CharSequence cs = value.getStr(rec);
            if (cs == null) {
                return null;
            }

            matcher.reset(cs);
            sink.clear();

            boolean result = find();
            if (!result) {
                sink.buffer.append(cs);
            } else {
                do {
                    if (sink.length() > maxLength) {
                        throw CairoException.nonCritical()
                                .put("breached memory limit set for ").put(SIGNATURE)
                                .put(" [maxLength=").put(maxLength).put(']');
                    }
                    matcher.appendReplacement(sink.buffer, replacementStr);
                    result = find();
                } while (result);
                matcher.appendTail(sink.buffer);
            }
            return sink;
        }

        @Override
        public CharSequence getStrB(Record rec) {
            return getStr(rec, sinkB);
        }

        @Override
        public void init(SymbolTableSource symbolTableSource, SqlExecutionContext executionContext) throws SqlException {
            UnaryFunction.super.init(symbolTableSource, executionContext);
            pattern.init(symbolTableSource, executionContext);
            matcher = RegexUtils.createMatcher(pattern, patternPos);
            replacement.init(symbolTableSource, executionContext);
            CharSequence cs = replacement.getStr(null);
            if (cs == null) {
                throw SqlException.$(replacementPos, "NULL replacement");
            }
            replacementStr = cs.toString();
        }

        @Override
        public boolean isConstant() {
            return false;
        }

        @Override
        public boolean isReadThreadSafe() {
            return false;
        }

        @Override
        public boolean isRuntimeConstant() {
            return false;
        }

        @Override
        public void toPlan(PlanSink sink) {
            sink.val("regexp_replace(").val(value).val(',').val(pattern).val(',').val(replacement).val(')');
        }

        private boolean find() {
            try {
                return matcher.find();
            } catch (StackOverflowError err) {
                throw CairoException.nonCritical().put("stack overflow error [position=").put(functionPos).put(']');
            }
        }
    }

    // TODO(puzpuzpuz):
    //  Get rid of this class in favor of StringSink once we drop support for Java 8 where j.u.r.Matcher
    //  has no method overloads for StringBuilder.
    private static class StringBufferSink implements CharSequence {
        private final StringBuffer buffer = new StringBuffer();

        @Override
        public char charAt(int index) {
            return buffer.charAt(index);
        }

        public void clear() {
            buffer.setLength(0);
        }

        @Override
        public boolean equals(Object obj) {
            return obj instanceof CharSequence && Chars.equals(buffer, (CharSequence) obj);
        }

        @Override
        public int hashCode() {
            return Chars.hashCode(buffer);
        }

        @Override
        public int length() {
            return buffer.length();
        }

        @Override
        public @NotNull CharSequence subSequence(int lo, int hi) {
            return buffer.subSequence(lo, hi);
        }

        @Override
        public @NotNull String toString() {
            return buffer.toString();
        }
    }
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy