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

com.mongodb.jdbc.mongosql.MongoSQLTranslate Maven / Gradle / Ivy

There is a newer version: 2.2.1
Show newest version
/*
 * Copyright 2024-present MongoDB, Inc.
 *
 * 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 com.mongodb.jdbc.mongosql;

import com.mongodb.client.AggregateIterable;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Accumulators;
import com.mongodb.client.model.Aggregates;
import com.mongodb.client.model.Field;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.Projections;
import com.mongodb.jdbc.BsonUtils;
import com.mongodb.jdbc.MongoDriver;
import com.mongodb.jdbc.MongoJsonSchemaResult;
import com.mongodb.jdbc.MongoSerializationException;
import com.mongodb.jdbc.logging.AutoLoggable;
import com.mongodb.jdbc.logging.MongoLogger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.stream.Collectors;
import org.bson.*;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.conversions.Bson;

@AutoLoggable
public class MongoSQLTranslate {
    public static final String SQL_SCHEMAS_COLLECTION = "__sql_schemas";
    private final MongoLogger logger;
    private final CodecRegistry pojoCodecRegistry;

    /** Native method to send commands via JNI. */
    public native byte[] runCommand(byte[] command, int length);

    public MongoSQLTranslate(MongoLogger logger) {
        this.logger = logger;
        this.pojoCodecRegistry = MongoDriver.getCodecRegistry();
    }

    /**
     * Executes the JNI runCommand and returns the response as a POJO.
     *
     * @param command The command to be executed.
     * @param responseClass The class of the response POJO.
     * @return The response POJO.
     * @throws MongoSerializationException If an error occurs during serialization or
     *     deserialization.
     * @throws MongoSQLException If an error occurs during command execution.
     */
    public  T runCommand(BsonDocument command, Class responseClass)
            throws MongoSerializationException, MongoSQLException {

        byte[] commandBytes = BsonUtils.serialize(command);
        byte[] responseBytes = runCommand(commandBytes, commandBytes.length);
        BsonDocument responseDoc = BsonUtils.deserialize(responseBytes);

        BsonDocumentReader reader = new BsonDocumentReader(responseDoc);
        T result =
                pojoCodecRegistry
                        .get(responseClass)
                        .decode(reader, DecoderContext.builder().build());

        if (result.hasError()) {
            String errorMessage =
                    String.format(
                            "Error executing command: %s. Error is internal: %s",
                            result.getError(), result.getErrorIsInternal());
            throw new MongoSQLException(errorMessage);
        }

        return result;
    }

    /**
     * Retrieves the version of the mongosqltranslate library.
     *
     * @return GetMongosqlTranslateVersionResult containing the version string.
     * @throws MongoSQLException If an error occurs during command execution.
     * @throws MongoSerializationException If an error occurs during serialization or
     *     deserialization.
     */
    public GetMongosqlTranslateVersionResult getMongosqlTranslateVersion()
            throws MongoSQLException, MongoSerializationException {
        BsonDocument command =
                new BsonDocument("command", new BsonString("getMongosqlTranslateVersion"))
                        .append("options", new BsonDocument());

        GetMongosqlTranslateVersionResult versionResult =
                runCommand(command, GetMongosqlTranslateVersionResult.class);

        logger.log(Level.INFO, "mongosqlTranslateVersion: " + versionResult.version);
        return versionResult;
    }

    /**
     * Checks if the JDBC driver version is compatible with the mongosqltranslate library.
     *
     * @return CheckDriverVersionResult containing the compatibility status.
     * @throws MongoSQLException If an error occurs during command execution.
     * @throws MongoSerializationException If an error occurs during serialization or
     *     deserialization.
     */
    public CheckDriverVersionResult checkDriverVersion()
            throws MongoSQLException, MongoSerializationException {
        BsonDocument options =
                new BsonDocument("driverVersion", new BsonString(MongoDriver.getVersion()))
                        .append("odbcDriver", new BsonBoolean(false));
        BsonDocument command =
                new BsonDocument("command", new BsonString("checkDriverVersion"))
                        .append("options", options);

        CheckDriverVersionResult checkDriverVersionResult =
                runCommand(command, CheckDriverVersionResult.class);

        logger.log(
                Level.INFO, "Driver Compatibility Status: " + checkDriverVersionResult.compatible);
        return checkDriverVersionResult;
    }

    /**
     * Executes a translate command based on the provided SQL and returns the response.
     *
     * @param sql The SQL query to translate.
     * @param dbName The database name.
     * @param schemaCatalog schema catalog
     * @return TranslateResult
     * @throws MongoSQLException If the command execution fails.
     * @throws MongoSerializationException If an error occurs during serialization or
     *     deserialization.
     */
    public TranslateResult translate(String sql, String dbName, BsonDocument schemaCatalog)
            throws MongoSQLException, MongoSerializationException {

        // Setting excludeNamespaces to default value false and relaxSchemaChecking to default value true.
        // These options are not currently handled in the JDBC driver
        BsonDocument options =
                new BsonDocument("sql", new BsonString(sql))
                        .append("db", new BsonString(dbName))
                        .append("excludeNamespaces", new BsonBoolean(false))
                        .append("relaxSchemaChecking", new BsonBoolean(true))
                        .append("schemaCatalog", schemaCatalog);
        BsonDocument translateCommand =
                new BsonDocument("command", new BsonString("translate")).append("options", options);

        return runCommand(translateCommand, TranslateResult.class);
    }

    /**
     * Retrieves the namespaces involved in the given SQL query.
     *
     * @param dbName The name of the database.
     * @param sql The SQL query.
     * @return GetNamespacesResult containing the namespaces.
     * @throws MongoSQLException If an error occurs during command execution.
     * @throws MongoSerializationException If an error occurs during serialization or
     *     deserialization.
     */
    public GetNamespacesResult getNamespaces(String dbName, String sql)
            throws MongoSQLException, MongoSerializationException {
        BsonDocument options =
                new BsonDocument("sql", new BsonString(sql)).append("db", new BsonString(dbName));
        BsonDocument command =
                new BsonDocument("command", new BsonString("getNamespaces"))
                        .append("options", options);

        return runCommand(command, GetNamespacesResult.class);
    }

    // Builds a catalog document containing the schema information for the specified collections.
    public BsonDocument buildCatalogDocument(
            MongoDatabase mongoDatabase,
            String dbName,
            List collections)
            throws MongoSQLException {

        // Create an aggregation pipeline to fetch the schema information for the specified collections.
        // The pipeline uses $in to query all the specified collections and projects them into the desired format:
        // "dbName": { "collection1" : "Schema1", "collection2" : "Schema2", ... }
        List collectionNames =
                collections.stream().map(ns -> ns.collection).collect(Collectors.toList());

        // Filter documents where _id is in the list of collection names
        Bson matchStage = Aggregates.match(Filters.in("_id", collectionNames));

        // Include only the 'schema' and '_id' fields
        Bson projectStage =
                Aggregates.project(
                        Projections.fields(
                                Projections.include("schema"), Projections.include("_id")));

        // Accumulate collection names and their schemas into an array
        Bson groupStage =
                Aggregates.group(
                        null,
                        Accumulators.push(
                                "collections",
                                new BsonDocument("collectionName", new BsonString("$_id"))
                                        .append("schema", new BsonString("$schema"))));

        // Convert the 'collections' array into a document mapping each collection name to its schema
        // under the database name
        Bson finalProjectStage =
                Aggregates.project(
                        Projections.fields(
                                Projections.excludeId(),
                                Projections.computed(
                                        dbName,
                                        new BsonDocument(
                                                "$arrayToObject",
                                                new BsonArray(
                                                        Arrays.asList(
                                                                new BsonDocument(
                                                                        "$map",
                                                                        new BsonDocument(
                                                                                        "input",
                                                                                        new BsonString(
                                                                                                "$collections"))
                                                                                .append(
                                                                                        "as",
                                                                                        new BsonString(
                                                                                                "coll"))
                                                                                .append(
                                                                                        "in",
                                                                                        new BsonDocument(
                                                                                                        "k",
                                                                                                        new BsonString(
                                                                                                                "$$coll.collectionName"))
                                                                                                .append(
                                                                                                        "v",
                                                                                                        new BsonString(
                                                                                                                "$$coll.schema"))))))))));
        List pipeline =
                Arrays.asList(matchStage, projectStage, groupStage, finalProjectStage);

        MongoCollection collection =
                mongoDatabase.getCollection(SQL_SCHEMAS_COLLECTION, BsonDocument.class);
        AggregateIterable result = collection.aggregate(pipeline);

        BsonDocument catalog = null;
        boolean foundResult = false;

        for (BsonDocument doc : result) {
            if (foundResult) {
                throw new MongoSQLException(
                        "Multiple results returned while getting schema; expected only one.");
            }
            catalog = doc;
            foundResult = true;
        }
        if (!foundResult) {
            throw new MongoSQLException(
                    "No schema information returned for the requested collections.");
        }

        // Check that all expected collections are present in the result
        BsonDocument resultCollections = catalog.getDocument(dbName);
        List returnedCollections = new ArrayList<>(resultCollections.keySet());
        List missingCollections =
                collections
                        .stream()
                        .map(ns -> ns.collection)
                        .filter(c -> !returnedCollections.contains(c))
                        .collect(Collectors.toList());

        if (!missingCollections.isEmpty()) {
            throw new MongoSQLException(
                    "Could not retrieve schema for collections: " + missingCollections);
        }

        return catalog;
    }

    /**
     * Retrieves the schema of a specific collection from the MongoDB database.
     *
     * @param mongoDatabase MongoDB database instance.
     * @param collectionName Name of the collection to retrieve the schema.
     * @return MongoJsonSchemaResult schema result of the collection.
     * @throws MongoSQLException If no schema is found for the given collection or an error occurs
     *     during command execution.
     * @throws MongoSerializationException If an error occurs during serialization or
     *     deserialization.
     */
    public MongoJsonSchemaResult getSchema(MongoDatabase mongoDatabase, String collectionName)
            throws MongoSerializationException, MongoSQLException {

        // The pipeline formats the output to match the structure required by MongoJsonSchemaResult
        List pipeline =
                Arrays.asList(
                        Aggregates.match(Filters.eq("_id", collectionName)),
                        Aggregates.project(
                                Projections.fields(
                                        Projections.exclude("_id"),
                                        Projections.computed("schema.jsonSchema", "$schema"),
                                        Projections.computed("schema.version", new BsonInt32(1)))),
                        Aggregates.addFields(new Field<>("ok", new BsonInt32(1))));

        MongoCollection schemasCollection =
                mongoDatabase.getCollection(SQL_SCHEMAS_COLLECTION, BsonDocument.class);
        AggregateIterable result = schemasCollection.aggregate(pipeline);

        BsonDocument resultDoc = result.first();

        if (resultDoc == null) {
            throw new MongoSQLException("No schema found for collection: " + collectionName);
        }

        BsonDocumentReader reader = new BsonDocumentReader(resultDoc);
        MongoJsonSchemaResult mongoJsonSchemaResult =
                pojoCodecRegistry
                        .get(MongoJsonSchemaResult.class)
                        .decode(reader, DecoderContext.builder().build());

        return mongoJsonSchemaResult;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy