LanceNamespaceName.java

// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you 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 org.apache.doris.datasource.lance;

import org.apache.doris.common.DdlException;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/** Reversible mapping between a hierarchical Lance namespace and one Doris database name. */
final class LanceNamespaceName {
    private static final char ESCAPE = '\\';

    private LanceNamespaceName() {
    }

    static String encode(List<String> relativeNamespace, String delimiter, String rootDatabase) {
        if (relativeNamespace.isEmpty()) {
            return rootDatabase;
        }
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < relativeNamespace.size(); i++) {
            if (i > 0) {
                result.append(delimiter);
            }
            result.append(escape(relativeNamespace.get(i), delimiter));
        }
        // The local root database represents the empty Lance namespace. Escape
        // the rare, colliding one-level namespace so the mapping stays reversible.
        return rootDatabase.contentEquals(result) ? ESCAPE + result.toString() : result.toString();
    }

    static List<String> decode(String database, String delimiter, String rootDatabase) throws DdlException {
        if (rootDatabase.equals(database)) {
            return Collections.emptyList();
        }
        if (database.length() > 1 && database.charAt(0) == ESCAPE
                && rootDatabase.equals(database.substring(1))) {
            return splitEscaped(database.substring(1), delimiter);
        }
        return splitEscaped(database, delimiter);
    }

    static List<String> parseParent(String value, String delimiter) throws DdlException {
        if (value == null || value.isEmpty()) {
            return Collections.emptyList();
        }
        return splitEscaped(value, delimiter);
    }

    private static List<String> splitEscaped(String value, String delimiter) throws DdlException {
        List<String> result = new ArrayList<>();
        StringBuilder current = new StringBuilder();
        for (int i = 0; i < value.length();) {
            if (value.charAt(i) == ESCAPE) {
                int escapedIndex = i + 1;
                if (escapedIndex < value.length() && value.charAt(escapedIndex) == ESCAPE) {
                    current.append(ESCAPE);
                    i += 2;
                } else if (value.startsWith(delimiter, escapedIndex)) {
                    current.append(delimiter);
                    i = escapedIndex + delimiter.length();
                } else {
                    throw new DdlException("Invalid escape in Lance namespace '" + value + "'");
                }
            } else if (value.startsWith(delimiter, i)) {
                addComponent(result, current, value);
                i += delimiter.length();
            } else {
                current.append(value.charAt(i++));
            }
        }
        addComponent(result, current, value);
        return result;
    }

    private static String escape(String component, String delimiter) {
        return component.replace("\\", "\\\\").replace(delimiter, "\\" + delimiter);
    }

    private static void addComponent(List<String> result, StringBuilder current, String encoded)
            throws DdlException {
        if (current.length() == 0) {
            throw new DdlException("Empty Lance namespace component in '" + encoded + "'");
        }
        result.add(current.toString());
        current.setLength(0);
    }
}