diff --git a/build.sbt b/build.sbt index 496aac36..0c009a04 100644 --- a/build.sbt +++ b/build.sbt @@ -101,7 +101,7 @@ libraryDependencies += "com.vdurmont" % "semver4j" % "3.1.0" libraryDependencies += "com.github.jsqlparser" % "jsqlparser" % "4.9" -libraryDependencies += "org.postgresql" % "postgresql" % "42.7.3" +libraryDependencies += "org.liquibase" % "liquibase-core" % "4.28.0" % Test buildInfoPackage := "dev.mongocamp.driver.mongodb" diff --git a/src/main/scala/dev/mongocamp/driver/mongodb/database/DatabaseProvider.scala b/src/main/scala/dev/mongocamp/driver/mongodb/database/DatabaseProvider.scala index 2ba1451b..9e500a75 100644 --- a/src/main/scala/dev/mongocamp/driver/mongodb/database/DatabaseProvider.scala +++ b/src/main/scala/dev/mongocamp/driver/mongodb/database/DatabaseProvider.scala @@ -16,7 +16,17 @@ class DatabaseProvider(val config: MongoConfig, val registry: CodecRegistry) ext private val cachedMongoDAOMap = new mutable.HashMap[String, MongoDAO[Document]]() private var cachedClient: Option[MongoClient] = None - val DefaultDatabaseName: String = config.database + private var defaultDatabaseName: String = config.database + + def DefaultDatabaseName: String = defaultDatabaseName + + def connectionString = { + s"mongodb://${config.host}:${config.port}/${config.database}" + } + + def setDefaultDatabaseName(databaseName: String): Unit = { + defaultDatabaseName = databaseName + } def client: MongoClient = { if (isClosed) { diff --git a/src/main/scala/dev/mongocamp/driver/mongodb/database/MongoConfig.scala b/src/main/scala/dev/mongocamp/driver/mongodb/database/MongoConfig.scala index 7f2d3368..b8402430 100644 --- a/src/main/scala/dev/mongocamp/driver/mongodb/database/MongoConfig.scala +++ b/src/main/scala/dev/mongocamp/driver/mongodb/database/MongoConfig.scala @@ -16,7 +16,7 @@ case class MongoConfig( database: String, host: String = DefaultHost, port: Int = DefaultPort, - applicationName: String = DefaultApplicationName, + var applicationName: String = DefaultApplicationName, userName: Option[String] = None, password: Option[String] = None, authDatabase: String = DefaultAuthenticationDatabaseName, diff --git a/src/main/scala/dev/mongocamp/driver/mongodb/database/MongoIndex.scala b/src/main/scala/dev/mongocamp/driver/mongodb/database/MongoIndex.scala index 8318842f..4d0f3e89 100644 --- a/src/main/scala/dev/mongocamp/driver/mongodb/database/MongoIndex.scala +++ b/src/main/scala/dev/mongocamp/driver/mongodb/database/MongoIndex.scala @@ -51,7 +51,7 @@ object MongoIndex extends ObservableIncludes with LazyLogging { indexOptions.getOrElse("weights", Map()).asInstanceOf[Map[String, _]].keys.toList else indexOptions.getOrElse("key", Map).asInstanceOf[Map[String, _]].keys.toList, - indexOptions.getOrElse("unique", false).asInstanceOf[Boolean], + indexOptions.getOrElse("unique", indexOptions("name").toString.equalsIgnoreCase("_id_")).asInstanceOf[Boolean], indexOptions.getOrElse("v", -1).asInstanceOf[Int], indexOptions.getOrElse("ns", "").toString, indexOptions.getOrElse("key", Map).asInstanceOf[Map[String, _]], diff --git a/src/main/scala/dev/mongocamp/driver/mongodb/exception/SqlCommandNotSupportedException.scala b/src/main/scala/dev/mongocamp/driver/mongodb/exception/SqlCommandNotSupportedException.scala new file mode 100644 index 00000000..c43c65f2 --- /dev/null +++ b/src/main/scala/dev/mongocamp/driver/mongodb/exception/SqlCommandNotSupportedException.scala @@ -0,0 +1,3 @@ +package dev.mongocamp.driver.mongodb.exception + +class SqlCommandNotSupportedException(message: String) extends Exception(message) diff --git a/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/MongoDatabaseMetaData.scala b/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/MongoDatabaseMetaData.scala index a1eed1b0..01ca0974 100644 --- a/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/MongoDatabaseMetaData.scala +++ b/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/MongoDatabaseMetaData.scala @@ -1,358 +1,629 @@ package dev.mongocamp.driver.mongodb.jdbc -import java.sql.{Connection, DatabaseMetaData, ResultSet, RowIdLifetime} +import com.vdurmont.semver4j.Semver +import dev.mongocamp.driver.mongodb.database.DatabaseProvider.CollectionSeparator +import dev.mongocamp.driver.mongodb.{ BuildInfo, Converter, GenericObservable } +import dev.mongocamp.driver.mongodb.jdbc.resultSet.MongoDbResultSet +import dev.mongocamp.driver.mongodb.schema.SchemaExplorer +import org.mongodb.scala.bson.{ BsonNull, BsonString } +import org.mongodb.scala.bson.collection.immutable.Document -class MongoDatabaseMetaData extends DatabaseMetaData{ +import java.sql.{ Connection, DatabaseMetaData, ResultSet, RowIdLifetime, Types } +import scala.collection.mutable.ArrayBuffer - override def allProceduresAreCallable(): Boolean = ??? +class MongoDatabaseMetaData(connection: MongoJdbcConnection) extends DatabaseMetaData { + private lazy val semVer = new Semver(BuildInfo.version) + private lazy val jdbcSemVer = new Semver("4.2") + private lazy val DatabaseNameKey = "mongodb" - override def allTablesAreSelectable(): Boolean = ??? + override def allProceduresAreCallable() = false - override def getURL: String = ??? + override def allTablesAreSelectable(): Boolean = false - override def getUserName: String = ??? + override def getURL: String = { + connection.getDatabaseProvider.connectionString + } - override def isReadOnly: Boolean = ??? + override def getUserName: String = connection.getDatabaseProvider.config.userName.getOrElse("not set") - override def nullsAreSortedHigh(): Boolean = ??? + override def isReadOnly: Boolean = false - override def nullsAreSortedLow(): Boolean = ??? + override def nullsAreSortedHigh(): Boolean = false - override def nullsAreSortedAtStart(): Boolean = ??? + override def nullsAreSortedLow(): Boolean = false - override def nullsAreSortedAtEnd(): Boolean = ??? + override def nullsAreSortedAtStart(): Boolean = false - override def getDatabaseProductName: String = ??? + override def nullsAreSortedAtEnd(): Boolean = false - override def getDatabaseProductVersion: String = ??? + override def getDatabaseProductName: String = DatabaseNameKey - override def getDriverName: String = ??? + override def getDatabaseProductVersion: String = { + connection.getDatabaseProvider.runCommand(Document("buildInfo" -> 1)).map(doc => doc.getString("version")).result(10) + } - override def getDriverVersion: String = ??? + override def getDriverName: String = BuildInfo.name - override def getDriverMajorVersion: Int = ??? + override def getDriverVersion: String = semVer.getValue - override def getDriverMinorVersion: Int = ??? + override def getDriverMajorVersion: Int = semVer.getMajor - override def usesLocalFiles(): Boolean = ??? + override def getDriverMinorVersion: Int = semVer.getMinor - override def usesLocalFilePerTable(): Boolean = ??? + override def usesLocalFiles(): Boolean = false - override def supportsMixedCaseIdentifiers(): Boolean = ??? + override def usesLocalFilePerTable(): Boolean = false - override def storesUpperCaseIdentifiers(): Boolean = ??? + override def supportsMixedCaseIdentifiers(): Boolean = false - override def storesLowerCaseIdentifiers(): Boolean = ??? + override def storesUpperCaseIdentifiers(): Boolean = false - override def storesMixedCaseIdentifiers(): Boolean = ??? + override def storesLowerCaseIdentifiers(): Boolean = false - override def supportsMixedCaseQuotedIdentifiers(): Boolean = ??? + override def storesMixedCaseIdentifiers(): Boolean = false - override def storesUpperCaseQuotedIdentifiers(): Boolean = ??? + override def supportsMixedCaseQuotedIdentifiers(): Boolean = false - override def storesLowerCaseQuotedIdentifiers(): Boolean = ??? + override def storesUpperCaseQuotedIdentifiers(): Boolean = false - override def storesMixedCaseQuotedIdentifiers(): Boolean = ??? + override def storesLowerCaseQuotedIdentifiers(): Boolean = false - override def getIdentifierQuoteString: String = ??? + override def storesMixedCaseQuotedIdentifiers(): Boolean = false - override def getSQLKeywords: String = ??? + override def getIdentifierQuoteString: String = null - override def getNumericFunctions: String = ??? + override def getSQLKeywords: String = "" - override def getStringFunctions: String = ??? + override def getNumericFunctions: String = null - override def getSystemFunctions: String = ??? + override def getStringFunctions: String = null - override def getTimeDateFunctions: String = ??? + override def getSystemFunctions: String = null - override def getSearchStringEscape: String = ??? + override def getTimeDateFunctions: String = "date" - override def getExtraNameCharacters: String = ??? + override def getSearchStringEscape: String = "\\" - override def supportsAlterTableWithAddColumn(): Boolean = ??? + override def getExtraNameCharacters: String = null - override def supportsAlterTableWithDropColumn(): Boolean = ??? + override def supportsAlterTableWithAddColumn(): Boolean = false - override def supportsColumnAliasing(): Boolean = ??? + override def supportsAlterTableWithDropColumn(): Boolean = false - override def nullPlusNonNullIsNull(): Boolean = ??? + override def supportsColumnAliasing(): Boolean = true - override def supportsConvert(): Boolean = ??? + override def nullPlusNonNullIsNull(): Boolean = false - override def supportsConvert(fromType: Int, toType: Int): Boolean = ??? + override def supportsConvert(): Boolean = false - override def supportsTableCorrelationNames(): Boolean = ??? + override def supportsConvert(fromType: Int, toType: Int): Boolean = false - override def supportsDifferentTableCorrelationNames(): Boolean = ??? + override def supportsTableCorrelationNames(): Boolean = false - override def supportsExpressionsInOrderBy(): Boolean = ??? + override def supportsDifferentTableCorrelationNames(): Boolean = false - override def supportsOrderByUnrelated(): Boolean = ??? + override def supportsExpressionsInOrderBy(): Boolean = false - override def supportsGroupBy(): Boolean = ??? + override def supportsOrderByUnrelated(): Boolean = true - override def supportsGroupByUnrelated(): Boolean = ??? + override def supportsGroupBy(): Boolean = true - override def supportsGroupByBeyondSelect(): Boolean = ??? + override def supportsGroupByUnrelated(): Boolean = true - override def supportsLikeEscapeClause(): Boolean = ??? + override def supportsGroupByBeyondSelect(): Boolean = true - override def supportsMultipleResultSets(): Boolean = ??? + override def supportsLikeEscapeClause(): Boolean = true - override def supportsMultipleTransactions(): Boolean = ??? + override def supportsMultipleResultSets(): Boolean = true - override def supportsNonNullableColumns(): Boolean = ??? + override def supportsMultipleTransactions(): Boolean = false - override def supportsMinimumSQLGrammar(): Boolean = ??? + override def supportsNonNullableColumns(): Boolean = true - override def supportsCoreSQLGrammar(): Boolean = ??? + override def supportsMinimumSQLGrammar(): Boolean = false - override def supportsExtendedSQLGrammar(): Boolean = ??? + override def supportsCoreSQLGrammar(): Boolean = false - override def supportsANSI92EntryLevelSQL(): Boolean = ??? + override def supportsExtendedSQLGrammar(): Boolean = false - override def supportsANSI92IntermediateSQL(): Boolean = ??? + override def supportsANSI92EntryLevelSQL(): Boolean = false - override def supportsANSI92FullSQL(): Boolean = ??? + override def supportsANSI92IntermediateSQL(): Boolean = false - override def supportsIntegrityEnhancementFacility(): Boolean = ??? + override def supportsANSI92FullSQL(): Boolean = false - override def supportsOuterJoins(): Boolean = ??? + override def supportsIntegrityEnhancementFacility(): Boolean = false - override def supportsFullOuterJoins(): Boolean = ??? + override def supportsOuterJoins(): Boolean = false - override def supportsLimitedOuterJoins(): Boolean = ??? + override def supportsFullOuterJoins(): Boolean = false - override def getSchemaTerm: String = ??? + override def supportsLimitedOuterJoins(): Boolean = false - override def getProcedureTerm: String = ??? + override def getSchemaTerm: String = "database" - override def getCatalogTerm: String = ??? + override def getProcedureTerm: String = null - override def isCatalogAtStart: Boolean = ??? + override def getCatalogTerm: String = "database" - override def getCatalogSeparator: String = ??? + override def isCatalogAtStart: Boolean = true - override def supportsSchemasInDataManipulation(): Boolean = ??? + override def getCatalogSeparator: String = "." - override def supportsSchemasInProcedureCalls(): Boolean = ??? + override def supportsSchemasInDataManipulation(): Boolean = false - override def supportsSchemasInTableDefinitions(): Boolean = ??? + override def supportsSchemasInProcedureCalls(): Boolean = false - override def supportsSchemasInIndexDefinitions(): Boolean = ??? + override def supportsSchemasInTableDefinitions(): Boolean = false - override def supportsSchemasInPrivilegeDefinitions(): Boolean = ??? + override def supportsSchemasInIndexDefinitions(): Boolean = false - override def supportsCatalogsInDataManipulation(): Boolean = ??? + override def supportsSchemasInPrivilegeDefinitions(): Boolean = false - override def supportsCatalogsInProcedureCalls(): Boolean = ??? + override def supportsCatalogsInDataManipulation(): Boolean = true - override def supportsCatalogsInTableDefinitions(): Boolean = ??? + override def supportsCatalogsInProcedureCalls(): Boolean = false - override def supportsCatalogsInIndexDefinitions(): Boolean = ??? + override def supportsCatalogsInTableDefinitions(): Boolean = false - override def supportsCatalogsInPrivilegeDefinitions(): Boolean = ??? + override def supportsCatalogsInIndexDefinitions(): Boolean = false - override def supportsPositionedDelete(): Boolean = ??? + override def supportsCatalogsInPrivilegeDefinitions(): Boolean = false - override def supportsPositionedUpdate(): Boolean = ??? + override def supportsPositionedDelete(): Boolean = false - override def supportsSelectForUpdate(): Boolean = ??? + override def supportsPositionedUpdate(): Boolean = false - override def supportsStoredProcedures(): Boolean = ??? + override def supportsSelectForUpdate(): Boolean = false - override def supportsSubqueriesInComparisons(): Boolean = ??? + override def supportsStoredProcedures(): Boolean = false - override def supportsSubqueriesInExists(): Boolean = ??? + override def supportsSubqueriesInComparisons(): Boolean = false - override def supportsSubqueriesInIns(): Boolean = ??? + override def supportsSubqueriesInExists(): Boolean = false - override def supportsSubqueriesInQuantifieds(): Boolean = ??? + override def supportsSubqueriesInIns(): Boolean = false - override def supportsCorrelatedSubqueries(): Boolean = ??? + override def supportsSubqueriesInQuantifieds(): Boolean = false - override def supportsUnion(): Boolean = ??? + override def supportsCorrelatedSubqueries(): Boolean = false - override def supportsUnionAll(): Boolean = ??? + override def supportsUnion(): Boolean = true - override def supportsOpenCursorsAcrossCommit(): Boolean = ??? + override def supportsUnionAll(): Boolean = true - override def supportsOpenCursorsAcrossRollback(): Boolean = ??? + override def supportsOpenCursorsAcrossCommit(): Boolean = false - override def supportsOpenStatementsAcrossCommit(): Boolean = ??? + override def supportsOpenCursorsAcrossRollback(): Boolean = false - override def supportsOpenStatementsAcrossRollback(): Boolean = ??? + override def supportsOpenStatementsAcrossCommit(): Boolean = false - override def getMaxBinaryLiteralLength: Int = ??? + override def supportsOpenStatementsAcrossRollback(): Boolean = false - override def getMaxCharLiteralLength: Int = ??? + override def getMaxBinaryLiteralLength: Int = 0 - override def getMaxColumnNameLength: Int = ??? + override def getMaxCharLiteralLength: Int = 0 - override def getMaxColumnsInGroupBy: Int = ??? + override def getMaxColumnNameLength: Int = 0 - override def getMaxColumnsInIndex: Int = ??? + override def getMaxColumnsInGroupBy: Int = 0 - override def getMaxColumnsInOrderBy: Int = ??? + override def getMaxColumnsInIndex: Int = 0 - override def getMaxColumnsInSelect: Int = ??? + override def getMaxColumnsInOrderBy: Int = 0 - override def getMaxColumnsInTable: Int = ??? + override def getMaxColumnsInSelect: Int = 0 - override def getMaxConnections: Int = ??? + override def getMaxColumnsInTable: Int = 0 - override def getMaxCursorNameLength: Int = ??? + override def getMaxConnections: Int = 0 - override def getMaxIndexLength: Int = ??? + override def getMaxCursorNameLength: Int = 0 - override def getMaxSchemaNameLength: Int = ??? + override def getMaxIndexLength: Int = 0 - override def getMaxProcedureNameLength: Int = ??? + override def getMaxSchemaNameLength: Int = 0 - override def getMaxCatalogNameLength: Int = ??? + override def getMaxProcedureNameLength: Int = 0 - override def getMaxRowSize: Int = ??? + override def getMaxCatalogNameLength: Int = 0 - override def doesMaxRowSizeIncludeBlobs(): Boolean = ??? + override def getMaxRowSize: Int = 0 - override def getMaxStatementLength: Int = ??? + override def doesMaxRowSizeIncludeBlobs(): Boolean = false - override def getMaxStatements: Int = ??? + override def getMaxStatementLength: Int = 0 - override def getMaxTableNameLength: Int = ??? + override def getMaxStatements: Int = 0 - override def getMaxTablesInSelect: Int = ??? + override def getMaxTableNameLength: Int = 90 - override def getMaxUserNameLength: Int = ??? + override def getMaxTablesInSelect: Int = 0 - override def getDefaultTransactionIsolation: Int = ??? + override def getMaxUserNameLength: Int = 0 - override def supportsTransactions(): Boolean = ??? + override def getDefaultTransactionIsolation: Int = Connection.TRANSACTION_NONE - override def supportsTransactionIsolationLevel(level: Int): Boolean = ??? + override def supportsTransactions(): Boolean = false - override def supportsDataDefinitionAndDataManipulationTransactions(): Boolean = ??? + override def supportsTransactionIsolationLevel(level: Int): Boolean = false - override def supportsDataManipulationTransactionsOnly(): Boolean = ??? + override def supportsDataDefinitionAndDataManipulationTransactions(): Boolean = false - override def dataDefinitionCausesTransactionCommit(): Boolean = ??? + override def supportsDataManipulationTransactionsOnly(): Boolean = false - override def dataDefinitionIgnoredInTransactions(): Boolean = ??? + override def dataDefinitionCausesTransactionCommit(): Boolean = false - override def getProcedures(catalog: String, schemaPattern: String, procedureNamePattern: String): ResultSet = ??? + override def dataDefinitionIgnoredInTransactions(): Boolean = false - override def getProcedureColumns(catalog: String, schemaPattern: String, procedureNamePattern: String, columnNamePattern: String): ResultSet = ??? + override def getProcedures(catalog: String, schemaPattern: String, procedureNamePattern: String): ResultSet = { new MongoDbResultSet(null, List.empty, 10) } - override def getTables(catalog: String, schemaPattern: String, tableNamePattern: String, types: Array[String]): ResultSet = ??? + override def getProcedureColumns(catalog: String, schemaPattern: String, procedureNamePattern: String, columnNamePattern: String): ResultSet = { + new MongoDbResultSet(null, List.empty, 10) + } - override def getSchemas: ResultSet = ??? + override def getTables(catalog: String, schemaPattern: String, tableNamePattern: String, types: Array[String]): ResultSet = { + val internalSchemaPattern = Option(schemaPattern).getOrElse("(.*?)") + val internalTableNamePattern = Option(tableNamePattern).getOrElse("(.*?)") + val documents: List[Document] = connection.getDatabaseProvider.databaseNames + .filter(s => internalSchemaPattern.r.findFirstMatchIn(s).nonEmpty) + .flatMap(dbName => { + val collDocuments: List[Document] = connection.getDatabaseProvider + .collectionNames(dbName) + .filter(s => internalTableNamePattern.r.findFirstMatchIn(s).nonEmpty) + .map(collName => { + Document( + "TABLE_CAT" -> BsonString(DatabaseNameKey), + "TABLE_SCHEM" -> BsonString(dbName), + "TABLE_NAME" -> BsonString(collName), + "TABLE_TYPE" -> BsonString("TABLE"), + "REMARKS" -> BsonString("COLLECTION"), + "TYPE_CAT" -> BsonString(DatabaseNameKey), + "TYPE_SCHEM" -> BsonString(dbName), + "TYPE_NAME" -> BsonString("COLLECTION"), + "SELF_REFERENCING_COL_NAME" -> BsonNull(), + "REF_GENERATION" -> BsonNull() + ) + }) + collDocuments + }) + new MongoDbResultSet(null, documents, 10) + } + + override def getSchemas: ResultSet = getSchemas("", "(.*?)") + + override def getCatalogs: ResultSet = { + val documents = List( + Document( + "TABLE_CAT" -> DatabaseNameKey + ) + ) + new MongoDbResultSet(null, documents, 10) + } + + override def getTableTypes: ResultSet = { + val documents = List( + Document( + "TABLE_TYPE" -> "COLLECTION" + ) + ) + new MongoDbResultSet(null, documents, 10) + } + + override def getColumns(catalog: String, schemaPattern: String, tableNamePattern: String, columnNamePattern: String): ResultSet = { + val schemaRegex = schemaPattern.replace("%", "(.*?)").r + val tableNameRegex = tableNamePattern.replace("%", "(.*?)").r + val columnNameRegex = columnNamePattern.replace("%", "(.*?)").r + val databaseNames = connection.getDatabaseProvider.databaseNames.filter(s => schemaRegex.findFirstMatchIn(s).nonEmpty) + val documents = ArrayBuffer[Document]() + val schemaExplorer = new SchemaExplorer() + var i = 0 + databaseNames.map(dbName => { + val allCollections = connection.getDatabaseProvider.collectionNames(dbName) + val filtered = allCollections.filter(tbl => tableNameRegex.findFirstMatchIn(tbl).nonEmpty) + filtered.map(table => { + val dao = connection.getDatabaseProvider.dao(s"$dbName$CollectionSeparator$table") + val schemaAnalysis = schemaExplorer.analyzeSchema(dao) + val relevantColumns = schemaAnalysis.fields.filter(field => columnNameRegex.findFirstMatchIn(field.name).nonEmpty) + relevantColumns.foreach(schemaAnalysis => { + val fieldTypeName = schemaAnalysis.fieldTypes.head.fieldType + var decimalDigits: Option[Int] = None + val fieldType = fieldTypeName match { + case "string" => Types.LONGVARCHAR + case "null" => Types.VARCHAR + case "objectId" => Types.VARCHAR + case "date" => Types.DATE + case "int" => + decimalDigits = Some(0) + Types.INTEGER + case "long" => + decimalDigits = Some(0) + Types.BIGINT + case "number" => + decimalDigits = Some(Int.MaxValue) + Types.DOUBLE + case "double" => + decimalDigits = Some(Int.MaxValue) + Types.DOUBLE + case "array" => Types.ARRAY + case "bool" => Types.BOOLEAN + case "object" => Types.JAVA_OBJECT + case _ => + Types.VARCHAR + } + documents += Converter.toDocument( + Map( + "TABLE_CAT" -> DatabaseNameKey, + "TABLE_SCHEM" -> dbName, + "TABLE_NAME" -> table, + "COLUMN_NAME" -> schemaAnalysis.name, + "DATA_TYPE" -> fieldType, + "TYPE_NAME" -> fieldTypeName, + "COLUMN_SIZE" -> null, + "BUFFER_LENGTH" -> null, + "DECIMAL_DIGITS" -> decimalDigits.getOrElse(null), + "NUM_PREC_RADIX" -> null, + "NULLABLE" -> DatabaseMetaData.columnNullable, // how to check + "REMARKS" -> null, + "COLUMN_DEF" -> null, + "SQL_DATA_TYPE" -> null, + "SQL_DATETIME_SUB" -> null, + "CHAR_OCTET_LENGTH" -> null, + "ORDINAL_POSITION" -> i, + "IS_NULLABLE" -> "YES", + "SCOPE_CATLOG" -> null, + "SCOPE_SCHEMA" -> null, + "SCOPE_TABLE" -> null, + "SOURCE_DATA_TYPE" -> null, + "IS_AUTOINCREMENT" -> "NO" + ) + ) + i = i + 1 + }) + }) + }) + new MongoDbResultSet(null, documents.toList, 10) + } + + override def getColumnPrivileges(catalog: String, schema: String, table: String, columnNamePattern: String): ResultSet = { + null + } + + override def getTablePrivileges(catalog: String, schemaPattern: String, tableNamePattern: String): ResultSet = { + null + } + + override def getBestRowIdentifier(catalog: String, schema: String, table: String, scope: Int, nullable: Boolean): ResultSet = { + null + } + + override def getVersionColumns(catalog: String, schema: String, table: String): ResultSet = { + null + } + + override def getPrimaryKeys(catalog: String, schema: String, table: String): ResultSet = { + val dao = connection.getDatabaseProvider.dao(s"$schema$CollectionSeparator$table") + val uniqueIndices = dao.indexList().filter(_.unique) + val pkDocuments = uniqueIndices.map(i => + Map( + "TABLE_CAT" -> DatabaseNameKey, + "TABLE_SCHEM" -> schema, + "TABLE_NAME" -> table, + "COLUMN_NAME" -> i.fields.head, + "KEY_SEQ" -> 0, + "PK_NAME" -> i.name + ) + ) + new MongoDbResultSet(null, pkDocuments.map(i => Converter.toDocument(i)), 10) + + } + + override def getImportedKeys(catalog: String, schema: String, table: String): ResultSet = { + null + } + + override def getExportedKeys(catalog: String, schema: String, table: String): ResultSet = { + null + } + + override def getCrossReference( + parentCatalog: String, + parentSchema: String, + parentTable: String, + foreignCatalog: String, + foreignSchema: String, + foreignTable: String + ): ResultSet = { + null + } + + override def getTypeInfo: ResultSet = { + val objectIdValue = "OBJECT_ID" + val documentValue = "DOCUMENT" + val types = List( + Map( + "TYPE_NAME" -> objectIdValue, + "DATA_TYPE" -> Types.VARCHAR, + "PRECISION" -> "800", + "LITERAL_PREFIX" -> "'", + "LITERAL_SUFFIX" -> "'", + "CREATE_PARAMS" -> null, + "NULLABLE" -> DatabaseMetaData.typeNullable, + "CASE_SENSITIVE" -> true, + "SEARCHABLE" -> DatabaseMetaData.typeSearchable, + "UNSIGNED_ATTRIBUTE" -> false, + "FIXED_PREC_SCALE" -> false, + "AUTO_INCREMENT" -> false, + "LOCAL_TYPE_NAME" -> objectIdValue, + "MINIMUM_SCALE" -> 0, + "MAXIMUM_SCALE" -> 0, + "SQL_DATA_TYPE" -> null, + "SQL_DATETIME_SUB" -> null, + "NUM_PREC_RADIX" -> 10 + ), + Map( + "TYPE_NAME" -> documentValue, + "DATA_TYPE" -> Types.CLOB, + "PRECISION" -> "16777216", + "LITERAL_PREFIX" -> "'", + "LITERAL_SUFFIX" -> "'", + "CREATE_PARAMS" -> null, + "NULLABLE" -> DatabaseMetaData.typeNullable, + "CASE_SENSITIVE" -> true, + "SEARCHABLE" -> DatabaseMetaData.typeSearchable, + "UNSIGNED_ATTRIBUTE" -> false, + "FIXED_PREC_SCALE" -> false, + "AUTO_INCREMENT" -> false, + "LOCAL_TYPE_NAME" -> documentValue, + "MINIMUM_SCALE" -> 0, + "MAXIMUM_SCALE" -> 0, + "SQL_DATA_TYPE" -> null, + "SQL_DATETIME_SUB" -> null, + "NUM_PREC_RADIX" -> 10 + ) + ) + new MongoDbResultSet(null, types.map(i => Converter.toDocument(i)), 10) + } + + override def getIndexInfo(catalog: String, schema: String, table: String, unique: Boolean, approximate: Boolean): ResultSet = { + val schemaRegex = schema.r + val tableNameRegex = table.r + val databaseNames = connection.getDatabaseProvider.databaseNames.filter(s => schemaRegex.findFirstMatchIn(s).nonEmpty) + val documents = ArrayBuffer[Document]() + databaseNames.map(dbName => { + val allCollections = connection.getDatabaseProvider.collectionNames(dbName) + allCollections + .filter(tbl => tableNameRegex.findFirstMatchIn(tbl).nonEmpty) + .map(table => { + val dao = connection.getDatabaseProvider.dao(s"$dbName$CollectionSeparator$table") + dao + .indexList() + .map(index => { + val fields = index.fields + fields.zipWithIndex.foreach { case (field, i) => + documents += Converter.toDocument( + Map( + "TABLE_CAT" -> DatabaseNameKey, + "TABLE_SCHEM" -> dbName, + "TABLE_NAME" -> table, + "NON_UNIQUE" -> (if (!index.unique) "YES" else "NO"), + "INDEX_QUALIFIER" -> dbName, + "INDEX_NAME" -> index.name, + "TYPE" -> 0, + "ORDINAL_POSITION" -> i, + "COLUMN_NAME" -> field, + "ASC_OR_DESC" -> "A", + "CARDINALITY" -> "0", + "PAGES" -> "0", + "FILTER_CONDITION" -> "" + ) + ) + } + }) + }) + }) + new MongoDbResultSet(null, documents.toList, 10) + } + + override def supportsResultSetType(`type`: Int): Boolean = { + `type` == ResultSet.TYPE_FORWARD_ONLY + } + + override def supportsResultSetConcurrency(`type`: Int, concurrency: Int): Boolean = false + + override def ownUpdatesAreVisible(`type`: Int): Boolean = false + + override def ownDeletesAreVisible(`type`: Int): Boolean = false + + override def ownInsertsAreVisible(`type`: Int): Boolean = false + + override def othersUpdatesAreVisible(`type`: Int): Boolean = false + + override def othersDeletesAreVisible(`type`: Int): Boolean = false + + override def othersInsertsAreVisible(`type`: Int): Boolean = false + + override def updatesAreDetected(`type`: Int): Boolean = false + + override def deletesAreDetected(`type`: Int): Boolean = false + + override def insertsAreDetected(`type`: Int): Boolean = false + + override def supportsBatchUpdates(): Boolean = false + + override def getUDTs(catalog: String, schemaPattern: String, typeNamePattern: String, types: Array[Int]): ResultSet = { + new MongoDbResultSet(null, List.empty, 10) + } + + override def getConnection: Connection = connection - override def getCatalogs: ResultSet = ??? + override def supportsSavepoints(): Boolean = false - override def getTableTypes: ResultSet = ??? + override def supportsNamedParameters(): Boolean = false - override def getColumns(catalog: String, schemaPattern: String, tableNamePattern: String, columnNamePattern: String): ResultSet = ??? + override def supportsMultipleOpenResults(): Boolean = false - override def getColumnPrivileges(catalog: String, schema: String, table: String, columnNamePattern: String): ResultSet = ??? + override def supportsGetGeneratedKeys(): Boolean = false - override def getTablePrivileges(catalog: String, schemaPattern: String, tableNamePattern: String): ResultSet = ??? + override def getSuperTypes(catalog: String, schemaPattern: String, typeNamePattern: String): ResultSet = { new MongoDbResultSet(null, List.empty, 10) } - override def getBestRowIdentifier(catalog: String, schema: String, table: String, scope: Int, nullable: Boolean): ResultSet = ??? + override def getSuperTables(catalog: String, schemaPattern: String, tableNamePattern: String): ResultSet = { new MongoDbResultSet(null, List.empty, 10) } - override def getVersionColumns(catalog: String, schema: String, table: String): ResultSet = ??? + override def getAttributes(catalog: String, schemaPattern: String, typeNamePattern: String, attributeNamePattern: String): ResultSet = { + new MongoDbResultSet(null, List.empty, 10) + } - override def getPrimaryKeys(catalog: String, schema: String, table: String): ResultSet = ??? + override def supportsResultSetHoldability(holdability: Int): Boolean = false - override def getImportedKeys(catalog: String, schema: String, table: String): ResultSet = ??? + override def getResultSetHoldability: Int = ResultSet.HOLD_CURSORS_OVER_COMMIT - override def getExportedKeys(catalog: String, schema: String, table: String): ResultSet = ??? + override def getDatabaseMajorVersion: Int = semVer.getMajor - override def getCrossReference(parentCatalog: String, parentSchema: String, parentTable: String, foreignCatalog: String, foreignSchema: String, foreignTable: String): ResultSet = ??? + override def getDatabaseMinorVersion: Int = semVer.getMinor - override def getTypeInfo: ResultSet = ??? + override def getJDBCMajorVersion: Int = jdbcSemVer.getMajor - override def getIndexInfo(catalog: String, schema: String, table: String, unique: Boolean, approximate: Boolean): ResultSet = ??? + override def getJDBCMinorVersion: Int = jdbcSemVer.getMinor - override def supportsResultSetType(`type`: Int): Boolean = ??? + override def getSQLStateType: Int = DatabaseMetaData.sqlStateXOpen - override def supportsResultSetConcurrency(`type`: Int, concurrency: Int): Boolean = ??? + override def locatorsUpdateCopy(): Boolean = false - override def ownUpdatesAreVisible(`type`: Int): Boolean = ??? + override def supportsStatementPooling(): Boolean = false - override def ownDeletesAreVisible(`type`: Int): Boolean = ??? + override def getRowIdLifetime: RowIdLifetime = null - override def ownInsertsAreVisible(`type`: Int): Boolean = ??? + override def getSchemas(catalog: String, schemaPattern: String): ResultSet = { + val documents = connection.getDatabaseProvider.databaseNames + .filter(s => schemaPattern.r.findFirstMatchIn(s).nonEmpty) + .map(dbName => { + Document( + "TABLE_SCHEM" -> dbName, + "TABLE_CATALOG" -> DatabaseNameKey + ) + }) + new MongoDbResultSet(null, documents, 10) + } - override def othersUpdatesAreVisible(`type`: Int): Boolean = ??? + override def supportsStoredFunctionsUsingCallSyntax(): Boolean = false - override def othersDeletesAreVisible(`type`: Int): Boolean = ??? + override def autoCommitFailureClosesAllResultSets(): Boolean = false - override def othersInsertsAreVisible(`type`: Int): Boolean = ??? + override def getClientInfoProperties: ResultSet = { new MongoDbResultSet(null, List.empty, 10) } - override def updatesAreDetected(`type`: Int): Boolean = ??? + override def getFunctions(catalog: String, schemaPattern: String, functionNamePattern: String): ResultSet = { new MongoDbResultSet(null, List.empty, 10) } - override def deletesAreDetected(`type`: Int): Boolean = ??? + override def getFunctionColumns(catalog: String, schemaPattern: String, functionNamePattern: String, columnNamePattern: String): ResultSet = { + new MongoDbResultSet(null, List.empty, 10) + } - override def insertsAreDetected(`type`: Int): Boolean = ??? + override def getPseudoColumns(catalog: String, schemaPattern: String, tableNamePattern: String, columnNamePattern: String): ResultSet = { + new MongoDbResultSet(null, List.empty, 10) + } - override def supportsBatchUpdates(): Boolean = ??? + override def generatedKeyAlwaysReturned(): Boolean = false - override def getUDTs(catalog: String, schemaPattern: String, typeNamePattern: String, types: Array[Int]): ResultSet = ??? + override def unwrap[T](iface: Class[T]): T = null.asInstanceOf[T] - override def getConnection: Connection = ??? - - override def supportsSavepoints(): Boolean = ??? - - override def supportsNamedParameters(): Boolean = ??? - - override def supportsMultipleOpenResults(): Boolean = ??? - - override def supportsGetGeneratedKeys(): Boolean = ??? - - override def getSuperTypes(catalog: String, schemaPattern: String, typeNamePattern: String): ResultSet = ??? - - override def getSuperTables(catalog: String, schemaPattern: String, tableNamePattern: String): ResultSet = ??? - - override def getAttributes(catalog: String, schemaPattern: String, typeNamePattern: String, attributeNamePattern: String): ResultSet = ??? - - override def supportsResultSetHoldability(holdability: Int): Boolean = ??? - - override def getResultSetHoldability: Int = ??? - - override def getDatabaseMajorVersion: Int = ??? - - override def getDatabaseMinorVersion: Int = ??? - - override def getJDBCMajorVersion: Int = ??? - - override def getJDBCMinorVersion: Int = ??? - - override def getSQLStateType: Int = ??? - - override def locatorsUpdateCopy(): Boolean = ??? - - override def supportsStatementPooling(): Boolean = ??? - - override def getRowIdLifetime: RowIdLifetime = ??? - - override def getSchemas(catalog: String, schemaPattern: String): ResultSet = ??? - - override def supportsStoredFunctionsUsingCallSyntax(): Boolean = ??? - - override def autoCommitFailureClosesAllResultSets(): Boolean = ??? - - override def getClientInfoProperties: ResultSet = ??? - - override def getFunctions(catalog: String, schemaPattern: String, functionNamePattern: String): ResultSet = ??? - - override def getFunctionColumns(catalog: String, schemaPattern: String, functionNamePattern: String, columnNamePattern: String): ResultSet = ??? - - override def getPseudoColumns(catalog: String, schemaPattern: String, tableNamePattern: String, columnNamePattern: String): ResultSet = ??? - - override def generatedKeyAlwaysReturned(): Boolean = ??? - - override def unwrap[T](iface: Class[T]): T = ??? - - override def isWrapperFor(iface: Class[_]): Boolean = ??? + override def isWrapperFor(iface: Class[_]): Boolean = false } diff --git a/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/MongoJdbcCloseable.scala b/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/MongoJdbcCloseable.scala new file mode 100644 index 00000000..f09d75a2 --- /dev/null +++ b/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/MongoJdbcCloseable.scala @@ -0,0 +1,34 @@ +package dev.mongocamp.driver.mongodb.jdbc + +import java.sql.{SQLException, SQLFeatureNotSupportedException} + +trait MongoJdbcCloseable extends AutoCloseable { + + protected def checkClosed(): Unit = { + if (closed) { + throw new SQLException("Closed " + this.getClass.getSimpleName) + } + } + + private var closed: Boolean = false + + override def close(): Unit = { + checkClosed() + closed = true + } + + def isClosed: Boolean = closed + + def sqlFeatureNotSupported[A <: Any](message: String): A = { + sqlFeatureNotSupported(Option(message).filter(_.trim.nonEmpty)) + } + + def sqlFeatureNotSupported[A <: Any](message: Option[String] = None): A = { + checkClosed() + if (message.nonEmpty) { + throw new SQLFeatureNotSupportedException(message.get) + } + throw new SQLFeatureNotSupportedException() + } + +} diff --git a/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/MongoJdbcConnection.scala b/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/MongoJdbcConnection.scala index 04931edb..6b5e5075 100644 --- a/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/MongoJdbcConnection.scala +++ b/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/MongoJdbcConnection.scala @@ -1,17 +1,22 @@ package dev.mongocamp.driver.mongodb.jdbc -import org.mongodb.scala.MongoClient +import dev.mongocamp.driver.mongodb.Converter +import dev.mongocamp.driver.mongodb.bson.BsonConverter +import dev.mongocamp.driver.mongodb.database.DatabaseProvider +import dev.mongocamp.driver.mongodb.jdbc.statement.MongoPreparedStatement import java.{sql, util} -import java.sql.{Blob, CallableStatement, Clob, Connection, DatabaseMetaData, NClob, PreparedStatement, SQLWarning, SQLXML, Savepoint, Statement, Struct} +import java.sql.{Blob, CallableStatement, Clob, Connection, DatabaseMetaData, NClob, PreparedStatement, SQLException, SQLWarning, SQLXML, Savepoint, Statement, Struct} import java.util.Properties import java.util.concurrent.Executor +import scala.jdk.CollectionConverters._ -class MongoJdbcConnection(client: MongoClient) extends Connection { - private var _isClosed = false +class MongoJdbcConnection(databaseProvider: DatabaseProvider) extends Connection with MongoJdbcCloseable { private var _isReadOnly = false - override def createStatement(): Statement = new MongoPreparedStatement(this, null) + def getDatabaseProvider: DatabaseProvider = databaseProvider + + override def createStatement(): Statement = MongoPreparedStatement(this) override def prepareStatement(sql: String): PreparedStatement = { new MongoPreparedStatement(this, sql) @@ -19,10 +24,14 @@ class MongoJdbcConnection(client: MongoClient) extends Connection { override def prepareCall(sql: String): CallableStatement = { checkClosed() - null + createMongoStatement(Some(sql)) } - override def nativeSQL(sql: String): String = ??? + override def nativeSQL(sql: String): String = { + checkClosed() + // todo: return debug string + sql + } override def setAutoCommit(autoCommit: Boolean): Unit = { checkClosed() @@ -33,25 +42,20 @@ class MongoJdbcConnection(client: MongoClient) extends Connection { true } - override def commit(): Unit = { checkClosed() } - override def rollback(): Unit = { checkClosed() } - override def close(): Unit = { - _isClosed = true - client.close() + super.close() + databaseProvider.client.close() } - override def isClosed: Boolean = _isClosed - - override def getMetaData: DatabaseMetaData = ??? + override def getMetaData: DatabaseMetaData = new MongoDatabaseMetaData(this) override def setReadOnly(readOnly: Boolean): Unit = { checkClosed() @@ -67,13 +71,9 @@ class MongoJdbcConnection(client: MongoClient) extends Connection { override def getCatalog: String = null override def setTransactionIsolation(level: Int): Unit = { - checkClosed() - // Since the only valid value for MongoDB is Connection.TRANSACTION_NONE, and the javadoc for this method - // indicates that this is not a valid value for level here, throw unsupported operation exception. - throw new UnsupportedOperationException("MongoDB provides no support for transactions.") + sqlFeatureNotSupported() } - override def getTransactionIsolation: Int = { checkClosed() Connection.TRANSACTION_NONE @@ -84,91 +84,190 @@ class MongoJdbcConnection(client: MongoClient) extends Connection { null } - override def clearWarnings(): Unit = checkClosed() + override def clearWarnings(): Unit = { + checkClosed() + } - override def createStatement(resultSetType: Int, resultSetConcurrency: Int): Statement = ??? + def createMongoStatement(sqlOption: Option[String] = None): MongoPreparedStatement = { + checkClosed() + val stmt = statement.MongoPreparedStatement(this) + sqlOption.foreach(stmt.setSql) + stmt + } - override def prepareStatement(sql: String, resultSetType: Int, resultSetConcurrency: Int): PreparedStatement = ??? + override def createStatement(resultSetType: Int, resultSetConcurrency: Int): Statement = { + checkClosed() + createMongoStatement() + } - override def prepareCall(sql: String, resultSetType: Int, resultSetConcurrency: Int): CallableStatement = ??? + override def prepareStatement(sql: String, resultSetType: Int, resultSetConcurrency: Int): PreparedStatement = { + checkClosed() + createMongoStatement(Some(sql)) + } - override def getTypeMap: util.Map[String, Class[_]] = ??? + override def prepareCall(sql: String, resultSetType: Int, resultSetConcurrency: Int): CallableStatement = { + checkClosed() + createMongoStatement(Some(sql)) + } - override def setTypeMap(map: util.Map[String, Class[_]]): Unit = ??? + override def getTypeMap: util.Map[String, Class[_]] = { + checkClosed() + null + } - override def setHoldability(holdability: Int): Unit = ??? + override def setTypeMap(map: util.Map[String, Class[_]]): Unit = { + checkClosed() + } - override def getHoldability: Int = ??? + override def setHoldability(holdability: Int): Unit = { + checkClosed() + } - override def setSavepoint(): Savepoint = ??? + override def getHoldability: Int = { + checkClosed() + 0 + } - override def setSavepoint(name: String): Savepoint = ??? + override def setSavepoint(): Savepoint = { + checkClosed() + null + } - override def rollback(savepoint: Savepoint): Unit = ??? + override def setSavepoint(name: String): Savepoint = { + checkClosed() + null + } - override def releaseSavepoint(savepoint: Savepoint): Unit = ??? + override def rollback(savepoint: Savepoint): Unit = { + checkClosed() + } - override def createStatement(resultSetType: Int, resultSetConcurrency: Int, resultSetHoldability: Int): Statement = ??? + override def releaseSavepoint(savepoint: Savepoint): Unit = { + checkClosed() + } - override def prepareStatement(sql: String, resultSetType: Int, resultSetConcurrency: Int, resultSetHoldability: Int): PreparedStatement = ??? + override def createStatement(resultSetType: Int, resultSetConcurrency: Int, resultSetHoldability: Int): Statement = { + createMongoStatement() + } - override def prepareCall(sql: String, resultSetType: Int, resultSetConcurrency: Int, resultSetHoldability: Int): CallableStatement = ??? + override def prepareStatement(sql: String, resultSetType: Int, resultSetConcurrency: Int, resultSetHoldability: Int): PreparedStatement = { + createMongoStatement(Option(sql)) + } - override def prepareStatement(sql: String, autoGeneratedKeys: Int): PreparedStatement = ??? + override def prepareCall(sql: String, resultSetType: Int, resultSetConcurrency: Int, resultSetHoldability: Int): CallableStatement = { + checkClosed() + createMongoStatement(Some(sql)) + } - override def prepareStatement(sql: String, columnIndexes: Array[Int]): PreparedStatement = ??? + override def prepareStatement(sql: String, autoGeneratedKeys: Int): PreparedStatement = { + createMongoStatement(Option(sql)) + } - override def prepareStatement(sql: String, columnNames: Array[String]): PreparedStatement = ??? + override def prepareStatement(sql: String, columnIndexes: Array[Int]): PreparedStatement = { + createMongoStatement(Option(sql)) + } - override def createClob(): Clob = ??? + override def prepareStatement(sql: String, columnNames: Array[String]): PreparedStatement = { + createMongoStatement(Option(sql)) + } - override def createBlob(): Blob = ??? + override def createClob(): Clob = { + checkClosed() + null + } - override def createNClob(): NClob = ??? + override def createBlob(): Blob = { + checkClosed() + null + } + + override def createNClob(): NClob = { + checkClosed() + null + } - override def createSQLXML(): SQLXML = ??? + override def createSQLXML(): SQLXML = { + checkClosed() + null + } - override def isValid(timeout: Int): Boolean = ??? + override def isValid(timeout: Int): Boolean = { + checkClosed() + true + } - override def setClientInfo(name: String, value: String): Unit = ??? + override def setClientInfo(name: String, value: String): Unit = { + checkClosed() + if ("ApplicationName".equalsIgnoreCase(name) || "appName".equalsIgnoreCase(name) || "name".equalsIgnoreCase(name)) { + if (value != null) { + databaseProvider.closeClient() + databaseProvider.config.applicationName = value + } + } + } - override def setClientInfo(properties: Properties): Unit = ??? + override def setClientInfo(properties: Properties): Unit = { + properties.asScala.foreach(entry => setClientInfo(entry._1, entry._2)) + } - override def getClientInfo(name: String): String = ??? + override def getClientInfo(name: String): String = { + checkClosed() + if ("ApplicationName".equalsIgnoreCase(name) || "appName".equalsIgnoreCase(name) || "name".equalsIgnoreCase(name)) { + databaseProvider.config.applicationName + } else { + null + } + } - override def getClientInfo: Properties = ??? + override def getClientInfo: Properties = { + val properties = new Properties() + properties.setProperty("ApplicationName", databaseProvider.config.applicationName) + val document = Converter.toDocument(databaseProvider.config) + BsonConverter.asMap(document).foreach(entry => properties.setProperty(entry._1, entry._2.toString)) + properties + } - override def createArrayOf(typeName: String, elements: Array[AnyRef]): sql.Array = ??? + override def createArrayOf(typeName: String, elements: Array[AnyRef]): sql.Array = { + checkClosed() + null + } - override def createStruct(typeName: String, attributes: Array[AnyRef]): Struct = ??? + override def createStruct(typeName: String, attributes: Array[AnyRef]): Struct = { + checkClosed() + null + } - override def setSchema(schema: String): Unit = ??? + override def setSchema(schema: String): Unit = { + checkClosed() + databaseProvider.setDefaultDatabaseName(schema) + } - override def getSchema: String = ??? + override def getSchema: String = { + checkClosed() + databaseProvider.DefaultDatabaseName + } - override def abort(executor: Executor): Unit = ??? + override def abort(executor: Executor): Unit = { + checkClosed() + } - override def setNetworkTimeout(executor: Executor, milliseconds: Int): Unit = ??? + override def setNetworkTimeout(executor: Executor, milliseconds: Int): Unit = { + checkClosed() + } - override def getNetworkTimeout: Int = ??? + override def getNetworkTimeout: Int = { + checkClosed() + 0 + } - @throws[SQLAlreadyClosedException] override def unwrap[T](iface: Class[T]): T = { checkClosed() null.asInstanceOf[T] } - @throws[SQLAlreadyClosedException] override def isWrapperFor(iface: Class[_]): Boolean = { checkClosed() false } - - @throws[SQLAlreadyClosedException] - private def checkClosed(): Unit = { - if (isClosed) { - throw new SQLAlreadyClosedException(this.getClass.getSimpleName) - } - } } diff --git a/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/MongoJdbcDriver.scala b/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/MongoJdbcDriver.scala index 5b48b83d..ddbc856e 100644 --- a/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/MongoJdbcDriver.scala +++ b/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/MongoJdbcDriver.scala @@ -11,10 +11,12 @@ import java.util.logging.Logger import scala.jdk.CollectionConverters.CollectionHasAsScala class MongoJdbcDriver extends java.sql.Driver { + private val propertyInfoHelper = new MongodbJdbcDriverPropertyInfoHelper() private lazy val semVer = new Semver(BuildInfo.version) - /** Connect to the database using a URL like : jdbc:mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]] The + /** + * Connect to the database using a URL like : jdbc:mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]] The * URL excepting the jdbc: prefix is passed as it is to the MongoDb native Java driver. */ override def connect(url: String, info: Properties): Connection = { @@ -23,19 +25,21 @@ class MongoJdbcDriver extends java.sql.Driver { } val connectionUrl = url.replaceFirst("^jdbc:", "") - val username = Option(info.getProperty("user")).filter(_.trim.nonEmpty) - val password = Option(info.getProperty("password")).filter(_.trim.nonEmpty) + val username = Option(info.getProperty(MongodbJdbcDriverPropertyInfoHelper.AuthUser)).filter(_.trim.nonEmpty) + val password = Option(info.getProperty(MongodbJdbcDriverPropertyInfoHelper.AuthPassword)).filter(_.trim.nonEmpty) val string = new ConnectionString(connectionUrl) + val database = Option(string.getDatabase).getOrElse(Option(info.getProperty(MongodbJdbcDriverPropertyInfoHelper.Database)).getOrElse("admin")) + val authDb = Option(info.getProperty(MongodbJdbcDriverPropertyInfoHelper.AuthDatabase)).getOrElse(Option(string.getDatabase).getOrElse("admin")) val provider = DatabaseProvider( MongoConfig( - string.getDatabase, + database, MongoConfig.DefaultHost, MongoConfig.DefaultPort, - string.getApplicationName, + Option(string.getApplicationName).filter(_.trim.nonEmpty).getOrElse(info.getProperty(MongodbJdbcDriverPropertyInfoHelper.ApplicationName)), username, password, - string.getDatabase, + authDb, serverAddressList = string.getHosts.asScala.toList.map(h => new ServerAddress(h)) ) ) @@ -47,7 +51,7 @@ class MongoJdbcDriver extends java.sql.Driver { internalUrl.startsWith("mongodb://") || internalUrl.startsWith("mongodb+srv://") } - override def getPropertyInfo(url: String, info: Properties): Array[DriverPropertyInfo] = ??? + override def getPropertyInfo(url: String, info: Properties): Array[DriverPropertyInfo] = propertyInfoHelper.getPropertyInfo override def getMajorVersion: Int = semVer.getMajor diff --git a/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/MongoPreparedStatement.scala b/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/MongoPreparedStatement.scala deleted file mode 100644 index b0f182f5..00000000 --- a/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/MongoPreparedStatement.scala +++ /dev/null @@ -1,306 +0,0 @@ -package dev.mongocamp.driver.mongodb.jdbc - -import java.io.{InputStream, Reader} -import java.net.URL -import java.sql -import java.sql.{Blob, Clob, Connection, Date, NClob, ParameterMetaData, PreparedStatement, Ref, ResultSet, ResultSetMetaData, RowId, SQLException, SQLFeatureNotSupportedException, SQLWarning, SQLXML, Time, Timestamp} -import java.util.Calendar - -class MongoPreparedStatement(connection: MongoJdbcConnection, private var query: String) extends PreparedStatement { - private var lastResultSet: ResultSet = null - private var _isClosed = false - private var maxRows = -1 - private var fetchSize = -1 - - override def executeQuery(sql: String): ResultSet = { - checkClosed() - query = sql - if (lastResultSet != null && !lastResultSet.isClosed) { - lastResultSet.close(); - } - if (query == null) { - throw new SQLException("Null statement."); - } - // todo: execute and generate result set - // lastResultSet = connection.getScriptEngine().execute(query, fetchSize); - lastResultSet - } - - override def executeUpdate(sql: String): Int = ??? - - override def executeQuery(): ResultSet = { - execute(query) - lastResultSet - } - - override def execute(sql: String): Boolean = { - executeQuery(sql) - lastResultSet != null - } - - override def executeUpdate(): Int = executeUpdate(query) - - override def setNull(parameterIndex: Int, sqlType: Int): Unit = {} - - override def setBoolean(parameterIndex: Int, x: Boolean): Unit = {} - - override def setByte(parameterIndex: Int, x: Byte): Unit = {} - - override def setShort(parameterIndex: Int, x: Short): Unit = {} - - override def setInt(parameterIndex: Int, x: Int): Unit = {} - - override def setLong(parameterIndex: Int, x: Long): Unit = {} - - override def setFloat(parameterIndex: Int, x: Float): Unit = {} - - override def setDouble(parameterIndex: Int, x: Double): Unit = {} - - override def setBigDecimal(parameterIndex: Int, x: java.math.BigDecimal): Unit = {} - - override def setString(parameterIndex: Int, x: String): Unit = {} - - override def setBytes(parameterIndex: Int, x: Array[Byte]): Unit = {} - - override def setDate(parameterIndex: Int, x: Date): Unit = {} - - override def setTime(parameterIndex: Int, x: Time): Unit = {} - - override def setTimestamp(parameterIndex: Int, x: Timestamp): Unit = {} - - override def setAsciiStream(parameterIndex: Int, x: InputStream, length: Int): Unit = {} - - override def setUnicodeStream(parameterIndex: Int, x: InputStream, length: Int): Unit = {} - - override def setBinaryStream(parameterIndex: Int, x: InputStream, length: Int): Unit = {} - - override def clearParameters(): Unit = {} - - override def setObject(parameterIndex: Int, x: Any, targetSqlType: Int): Unit = {} - - override def setObject(parameterIndex: Int, x: Any): Unit = {} - - override def execute(): Boolean = { - query != null && execute(query) - } - - override def addBatch(): Unit = {} - - override def setCharacterStream(parameterIndex: Int, reader: Reader, length: Int): Unit = {} - - override def setRef(parameterIndex: Int, x: Ref): Unit = {} - - override def setBlob(parameterIndex: Int, x: Blob): Unit = {} - - override def setClob(parameterIndex: Int, x: Clob): Unit = {} - - override def setArray(parameterIndex: Int, x: sql.Array): Unit = {} - - override def getMetaData: ResultSetMetaData = { - null - } - - override def setDate(parameterIndex: Int, x: Date, cal: Calendar): Unit = {} - - override def setTime(parameterIndex: Int, x: Time, cal: Calendar): Unit = {} - - override def setTimestamp(parameterIndex: Int, x: Timestamp, cal: Calendar): Unit = {} - - override def setNull(parameterIndex: Int, sqlType: Int, typeName: String): Unit = {} - - override def setURL(parameterIndex: Int, x: URL): Unit = {} - - override def getParameterMetaData: ParameterMetaData = null - - override def setRowId(parameterIndex: Int, x: RowId): Unit = {} - - override def setNString(parameterIndex: Int, value: String): Unit = {} - - override def setNCharacterStream(parameterIndex: Int, value: Reader, length: Long): Unit = {} - - override def setNClob(parameterIndex: Int, value: NClob): Unit = {} - - override def setClob(parameterIndex: Int, reader: Reader, length: Long): Unit = {} - - override def setBlob(parameterIndex: Int, inputStream: InputStream, length: Long): Unit = {} - - override def setNClob(parameterIndex: Int, reader: Reader, length: Long): Unit = {} - - override def setSQLXML(parameterIndex: Int, xmlObject: SQLXML): Unit = {} - - override def setObject(parameterIndex: Int, x: Any, targetSqlType: Int, scaleOrLength: Int): Unit = {} - - override def setAsciiStream(parameterIndex: Int, x: InputStream, length: Long): Unit = {} - - override def setBinaryStream(parameterIndex: Int, x: InputStream, length: Long): Unit = {} - - override def setCharacterStream(parameterIndex: Int, reader: Reader, length: Long): Unit = {} - - override def setAsciiStream(parameterIndex: Int, x: InputStream): Unit = {} - - override def setBinaryStream(parameterIndex: Int, x: InputStream): Unit = {} - - override def setCharacterStream(parameterIndex: Int, reader: Reader): Unit = {} - - override def setNCharacterStream(parameterIndex: Int, value: Reader): Unit = {} - - override def setClob(parameterIndex: Int, reader: Reader): Unit = {} - - override def setBlob(parameterIndex: Int, inputStream: InputStream): Unit = {} - - override def setNClob(parameterIndex: Int, reader: Reader): Unit = {} - - override def close(): Unit = { - _isClosed = true - if (lastResultSet == null || lastResultSet.isClosed) { - return - } - lastResultSet.close() - } - - override def getMaxFieldSize: Int = { - 0 - } - - override def setMaxFieldSize(max: Int): Unit = {} - - override def getMaxRows: Int = maxRows - - override def setMaxRows(max: Int): Unit = maxRows = max - - override def setEscapeProcessing(enable: Boolean): Unit = {} - - override def getQueryTimeout: Int = { - checkClosed() - throw new SQLFeatureNotSupportedException("MongoDB provides no support for query timeouts.") - } - - override def setQueryTimeout(seconds: Int): Unit = { - checkClosed() - throw new SQLFeatureNotSupportedException("MongoDB provides no support for query timeouts.") - } - - override def cancel(): Unit = { - checkClosed() - throw new SQLFeatureNotSupportedException("MongoDB provides no support for query timeouts.") - } - - override def getWarnings: SQLWarning = { - checkClosed() - null - } - - override def clearWarnings(): Unit = { - checkClosed() - } - - override def setCursorName(name: String): Unit = { - checkClosed() - } - - override def getResultSet: ResultSet = { - checkClosed() - lastResultSet; - } - - override def getUpdateCount: Int = { - checkClosed() - -1 - } - - override def getMoreResults: Boolean = false - - override def setFetchDirection(direction: Int): Unit = {} - - override def getFetchDirection: Int = ResultSet.FETCH_FORWARD - - override def setFetchSize(rows: Int): Unit = { - if (rows <= 1) { - throw new SQLException("Fetch size must be > 1. Actual: " + rows) - } - fetchSize = rows - } - - override def getFetchSize: Int = fetchSize - - override def getResultSetConcurrency: Int = throw new SQLFeatureNotSupportedException(); - - override def getResultSetType: Int = ResultSet.TYPE_FORWARD_ONLY - - override def addBatch(sql: String): Unit = {} - - - override def clearBatch(): Unit = {} - - override def executeBatch(): Array[Int] = { - checkClosed() - null - } - - override def getConnection: Connection = { - checkClosed() - connection - } - - override def getMoreResults(current: Int): Boolean = { - checkClosed() - false - } - - override def getGeneratedKeys: ResultSet = { - checkClosed() - null - } - - override def executeUpdate(sql: String, autoGeneratedKeys: Int): Int = { - checkClosed() - 0 - } - - override def executeUpdate(sql: String, columnIndexes: Array[Int]): Int = { - checkClosed() - 0 - } - - override def executeUpdate(sql: String, columnNames: Array[String]): Int = { - checkClosed() - 0 - } - - override def execute(sql: String, autoGeneratedKeys: Int): Boolean = { - checkClosed() - false - } - - override def execute(sql: String, columnIndexes: Array[Int]): Boolean = { - checkClosed() - false - } - - override def execute(sql: String, columnNames: Array[String]): Boolean = { - checkClosed() - false - } - - override def getResultSetHoldability: Int = 0 - - override def isClosed: Boolean = _isClosed - - override def setPoolable(poolable: Boolean): Unit = {} - - override def isPoolable: Boolean = false - - override def closeOnCompletion(): Unit = {} - - override def isCloseOnCompletion: Boolean = false - - override def unwrap[T](iface: Class[T]): T = null.asInstanceOf[T] - - override def isWrapperFor(iface: Class[_]): Boolean = false - - private def checkClosed(): Unit = { - if (isClosed) { - throw new SQLAlreadyClosedException(this.getClass.getSimpleName) - } - } -} diff --git a/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/MongodbJdbcDriverPropertyInfoHelper.scala b/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/MongodbJdbcDriverPropertyInfoHelper.scala new file mode 100644 index 00000000..89ce812c --- /dev/null +++ b/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/MongodbJdbcDriverPropertyInfoHelper.scala @@ -0,0 +1,32 @@ +package dev.mongocamp.driver.mongodb.jdbc +import MongodbJdbcDriverPropertyInfoHelper._ + +object MongodbJdbcDriverPropertyInfoHelper { + val ApplicationName = "appName" + val Database = "database" + + val AuthUser = "user" + val AuthPassword = "password" + val AuthDatabase = "auth_database" + val DefaultAuthDB = "admin" + val DefaultAppName = "mongodb-driver" +} + +case class MongodbJdbcDriverPropertyInfoHelper() { + + def getPropertyInfo: Array[java.sql.DriverPropertyInfo] = { + Array( + createPropertyInfo(AuthUser, null, Some("The username to authenticate")), + createPropertyInfo(AuthPassword, null, Some("The password to authenticate")), + createPropertyInfo(AuthDatabase, DefaultAuthDB, Some("The database where user info is stored. (most cases 'admin')")), + createPropertyInfo(ApplicationName, DefaultAppName, Some("The application name witch is visible in the MongoDB logs.")), + createPropertyInfo(Database, null, Some("The default database to connect to for the driver")) + ) + } + + private def createPropertyInfo(key: String, value: String, description: Option[String] = None): java.sql.DriverPropertyInfo = { + val prop = new java.sql.DriverPropertyInfo(key, value) + description.foreach(prop.description = _) + prop + } +} diff --git a/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/SQLAlreadyClosedException.scala b/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/SQLAlreadyClosedException.scala deleted file mode 100644 index 4657c02e..00000000 --- a/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/SQLAlreadyClosedException.scala +++ /dev/null @@ -1,5 +0,0 @@ -package dev.mongocamp.driver.mongodb.jdbc - -import java.sql.SQLException - -class SQLAlreadyClosedException(name: String) extends SQLException(name + " has already been closed.") \ No newline at end of file diff --git a/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/resultSet/MongoDbResultSet.scala b/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/resultSet/MongoDbResultSet.scala new file mode 100644 index 00000000..88a28dd3 --- /dev/null +++ b/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/resultSet/MongoDbResultSet.scala @@ -0,0 +1,897 @@ +package dev.mongocamp.driver.mongodb.jdbc.resultSet + +import dev.mongocamp.driver.mongodb.MongoDAO +import dev.mongocamp.driver.mongodb.bson.BsonConverter +import org.mongodb.scala.bson.{BsonArray, BsonBoolean, BsonDateTime, BsonInt32, BsonInt64, BsonNull, BsonNumber, BsonObjectId, BsonString} +import org.mongodb.scala.bson.collection.immutable.Document + +import java.io.{InputStream, Reader} +import java.net.{URI, URL} +import java.{sql, util} +import java.sql.{Blob, Clob, Date, NClob, Ref, ResultSet, ResultSetMetaData, RowId, SQLException, SQLWarning, SQLXML, Statement, Time, Timestamp} +import java.util.Calendar +import dev.mongocamp.driver.mongodb._ +import dev.mongocamp.driver.mongodb.jdbc.MongoJdbcCloseable + +import java.nio.charset.StandardCharsets + +class MongoDbResultSet(collectionDao: MongoDAO[Document], data: List[Document], queryTimeOut: Int) extends ResultSet with MongoJdbcCloseable { + private var currentRow: Document = _ + private var index: Int = 0 + + private lazy val metaData = new MongoDbResultSetMetaData(collectionDao, data) + + def getDocument: Document = currentRow + + override def next(): Boolean = { + checkClosed() + if (data == null || data.isEmpty) { + false + } + else { + if (index == 0 || (currentRow != null && index < data.size)) { + currentRow = data(index) + index += 1 + true + } + else { + currentRow = null + false + } + } + } + + override def wasNull(): Boolean = { + checkClosed() + false + } + + override def getString(columnIndex: Int): String = { + checkClosed() + currentRow.getString(metaData.getColumnName(columnIndex)) + } + + override def getBoolean(columnIndex: Int): Boolean = { + checkClosed() + currentRow.getBoolean(metaData.getColumnName(columnIndex)) + } + + override def getByte(columnIndex: Int): Byte = { + checkClosed() + getInt(columnIndex).toByte + } + + override def getShort(columnIndex: Int): Short = { + checkClosed() + getInt(columnIndex).toShort + } + + override def getInt(columnIndex: Int): Int = { + checkClosed() + getLong(columnIndex).toInt + } + + override def getLong(columnIndex: Int): Long = { + checkClosed() + val value = currentRow.getValue(metaData.getColumnName(columnIndex)) + value match { + case b : BsonInt32 => b.longValue() + case b : BsonInt64 => b.longValue() + case _ => Option(value).flatMap(_.toString.toLongOption).getOrElse(0) + } + } + + override def getFloat(columnIndex: Int): Float = { + checkClosed() + getDouble(columnIndex).toFloat + } + + override def getDouble(columnIndex: Int): Double = { + checkClosed() + currentRow.getDouble(metaData.getColumnName(columnIndex)) + } + + override def getBigDecimal(columnIndex: Int, scale: Int): java.math.BigDecimal = { + checkClosed() + new java.math.BigDecimal(getDouble(columnIndex)).setScale(scale) + } + + override def getBytes(columnIndex: Int): Array[Byte] = { + checkClosed() + null + } + + override def getDate(columnIndex: Int): Date = { + checkClosed() + val javaDate = currentRow.getDateValue(metaData.getColumnName(columnIndex)) + new Date(javaDate.getTime) + } + + override def getTime(columnIndex: Int): Time = { + checkClosed() + val javaDate = currentRow.getDateValue(metaData.getColumnName(columnIndex)) + new Time(javaDate.getTime) + } + + override def getTimestamp(columnIndex: Int): Timestamp = { + checkClosed() + val javaDate = currentRow.getDateValue(metaData.getColumnName(columnIndex)) + new Timestamp(javaDate.getTime) + } + + override def getAsciiStream(columnIndex: Int): InputStream = { + checkClosed() + null + } + + override def getUnicodeStream(columnIndex: Int): InputStream = { + checkClosed() + null + } + + override def getBinaryStream(columnIndex: Int): InputStream = { + checkClosed() + null + } + + override def getString(columnLabel: String): String = { + checkClosed() + currentRow.get(columnLabel) match { + case Some(value) => + value match { + case v: BsonString => v.getValue + case v: BsonObjectId => v.asObjectId().getValue.toHexString + case _ => BsonConverter.fromBson(value).toString + } + case None => "" + } + } + + override def getBoolean(columnLabel: String): Boolean = { + checkClosed() + currentRow.getBoolean(columnLabel) + } + + override def getByte(columnLabel: String): Byte = { + checkClosed() + getInt(columnLabel).toByte + } + + override def getShort(columnLabel: String): Short = { + checkClosed() + getInt(columnLabel).toShort + } + + override def getInt(columnLabel: String): Int = { + checkClosed() + currentRow.getIntValue(columnLabel) + } + + override def getLong(columnLabel: String): Long = { + checkClosed() + currentRow.getLong(columnLabel) + } + + override def getFloat(columnLabel: String): Float = { + checkClosed() + getDouble(columnLabel).toFloat + } + + override def getDouble(columnLabel: String): Double = { + checkClosed() + currentRow.getDouble(columnLabel) + } + + override def getBigDecimal(columnLabel: String, scale: Int): java.math.BigDecimal = { + checkClosed() + new java.math.BigDecimal(getDouble(columnLabel)).setScale(scale) + } + + override def getBytes(columnLabel: String): Array[Byte] = { + checkClosed() + null + } + + override def getDate(columnLabel: String): Date = { + checkClosed() + val javaDate = currentRow.getDateValue(columnLabel) + new Date(javaDate.getTime) + } + + override def getTime(columnLabel: String): Time = { + checkClosed() + val javaDate = currentRow.getDateValue(columnLabel) + new Time(javaDate.getTime) + } + + override def getTimestamp(columnLabel: String): Timestamp = { + checkClosed() + val javaDate = currentRow.getDateValue(columnLabel) + new Timestamp(javaDate.getTime) + } + + override def getAsciiStream(columnLabel: String): InputStream = { + checkClosed() + null + } + + override def getUnicodeStream(columnLabel: String): InputStream = { + checkClosed() + null + } + + override def getBinaryStream(columnLabel: String): InputStream = { + checkClosed() + null + } + + override def getWarnings: SQLWarning = { + checkClosed() + null + } + + override def clearWarnings(): Unit = { + checkClosed() + } + + override def getCursorName: String = { + checkClosed() + null + } + + override def getMetaData: ResultSetMetaData = { + checkClosed() + new MongoDbResultSetMetaData(collectionDao, data) + } + + override def getObject(columnIndex: Int): AnyRef = { + checkClosed() + currentRow.get(metaData.getColumnName(columnIndex)) match { + case Some(value) => BsonConverter.fromBson(value).asInstanceOf[AnyRef] + case None => null + } + } + + override def getObject(columnLabel: String): AnyRef = { + checkClosed() + currentRow.get(columnLabel) match { + case Some(value) => BsonConverter.fromBson(value).asInstanceOf[AnyRef] + case None => null + } + } + + override def findColumn(columnLabel: String): Int = { + checkClosed() + metaData.getColumnIndex(columnLabel) + } + + override def getCharacterStream(columnIndex: Int): Reader = { + checkClosed() + null + } + + override def getCharacterStream(columnLabel: String): Reader = { + checkClosed() + null + } + + override def getBigDecimal(columnIndex: Int): java.math.BigDecimal = { + checkClosed() + new java.math.BigDecimal(getDouble(columnIndex)) + } + + override def getBigDecimal(columnLabel: String): java.math.BigDecimal = { + checkClosed() + new java.math.BigDecimal(getDouble(columnLabel)) + } + + override def isBeforeFirst: Boolean = { + checkClosed() + index == 0 + } + + override def isAfterLast: Boolean = { + checkClosed() + index >= data.size + } + + override def isFirst: Boolean = { + checkClosed() + index == 1 + } + + override def isLast: Boolean = { + checkClosed() + index == data.size + } + + override def beforeFirst(): Unit = { + checkClosed() + } + + override def afterLast(): Unit = { + checkClosed() + } + + override def first(): Boolean = isBeforeFirst + + override def last(): Boolean = isLast + + override def getRow: Int = { + checkClosed() + if (currentRow == null) { + 0 + } + else { + index + } + } + + override def absolute(row: Int): Boolean = { + checkClosed() + false + } + + override def relative(rows: Int): Boolean = { + checkClosed() + false + } + + override def previous(): Boolean = { + checkClosed() + false + } + + override def setFetchDirection(direction: Int): Unit = sqlFeatureNotSupported() + + override def getFetchDirection: Int = { + checkClosed() + ResultSet.FETCH_FORWARD + } + + override def setFetchSize(rows: Int): Unit = { + checkClosed() + } + + override def getFetchSize: Int = { + checkClosed() + 1 + } + + override def getType: Int = { + checkClosed() + ResultSet.TYPE_FORWARD_ONLY + } + + override def getConcurrency: Int = sqlFeatureNotSupported() + + override def rowUpdated(): Boolean = { + checkClosed() + false + } + + override def rowInserted(): Boolean = { + checkClosed() + false + } + + override def rowDeleted(): Boolean = { + checkClosed() + false + } + + override def updateNull(columnIndex: Int): Unit = { + checkClosed() + currentRow.updated(metaData.getColumnName(columnIndex), BsonNull()) + } + + override def updateNull(columnLabel: String): Unit = { + checkClosed() + currentRow.updated(columnLabel, BsonNull()) + } + + override def updateBoolean(columnIndex: Int, x: Boolean): Unit = { + checkClosed() + currentRow.updated(metaData.getColumnName(columnIndex), BsonBoolean(x)) + } + + override def updateBoolean(columnLabel: String, x: Boolean): Unit = { + checkClosed() + currentRow.updated(columnLabel, BsonBoolean(x)) + } + + override def updateByte(columnIndex: Int, x: Byte): Unit = { + checkClosed() + currentRow.updated(metaData.getColumnName(columnIndex), BsonNumber(x)) + } + + override def updateByte(columnLabel: String, x: Byte): Unit = { + checkClosed() + currentRow.updated(columnLabel, BsonNumber(x)) + } + + override def updateShort(columnIndex: Int, x: Short): Unit = { + checkClosed() + currentRow.updated(metaData.getColumnName(columnIndex), BsonNumber(x)) + } + + override def updateShort(columnLabel: String, x: Short): Unit = { + checkClosed() + currentRow.updated(columnLabel, BsonNumber(x)) + } + + override def updateInt(columnIndex: Int, x: Int): Unit = { + checkClosed() + currentRow.updated(metaData.getColumnName(columnIndex), BsonNumber(x)) + } + + override def updateInt(columnLabel: String, x: Int): Unit = { + checkClosed() + currentRow.updated(columnLabel, BsonNumber(x)) + } + + override def updateLong(columnIndex: Int, x: Long): Unit = { + checkClosed() + currentRow.updated(metaData.getColumnName(columnIndex), BsonNumber(x)) + } + + override def updateLong(columnLabel: String, x: Long): Unit = { + checkClosed() + currentRow.updated(columnLabel, BsonNumber(x)) + } + + override def updateFloat(columnIndex: Int, x: Float): Unit = { + checkClosed() + currentRow.updated(metaData.getColumnName(columnIndex), BsonNumber(x)) + } + + override def updateFloat(columnLabel: String, x: Float): Unit = { + checkClosed() + currentRow.updated(columnLabel, BsonNumber(x)) + } + + override def updateDouble(columnIndex: Int, x: Double): Unit = { + checkClosed() + currentRow.updated(metaData.getColumnName(columnIndex), BsonNumber(x)) + } + + override def updateDouble(columnLabel: String, x: Double): Unit = { + checkClosed() + currentRow.updated(columnLabel, BsonNumber(x)) + } + + override def updateBigDecimal(columnIndex: Int, x: java.math.BigDecimal): Unit = { + checkClosed() + currentRow.updated(metaData.getColumnName(columnIndex), BsonNumber(x.doubleValue())) + } + + override def updateBigDecimal(columnLabel: String, x: java.math.BigDecimal): Unit = { + checkClosed() + currentRow.updated(columnLabel, BsonNumber(x.doubleValue())) + } + + override def updateString(columnIndex: Int, x: String): Unit = { + checkClosed() + currentRow.updated(metaData.getColumnName(columnIndex), BsonString(x)) + } + + override def updateString(columnLabel: String, x: String): Unit = { + checkClosed() + currentRow.updated(columnLabel, BsonString(x)) + } + + override def updateBytes(columnIndex: Int, x: Array[Byte]): Unit = { + checkClosed() + currentRow.updated(metaData.getColumnName(columnIndex), BsonArray(x)) + } + + override def updateBytes(columnLabel: String, x: Array[Byte]): Unit = { + checkClosed() + currentRow.updated(columnLabel, BsonArray(x)) + } + + override def updateDate(columnIndex: Int, x: Date): Unit = { + checkClosed() + currentRow.updated(metaData.getColumnName(columnIndex), BsonDateTime(x)) + } + + override def updateDate(columnLabel: String, x: Date): Unit = { + checkClosed() + currentRow.updated(columnLabel, BsonDateTime(x)) + } + + override def updateTime(columnIndex: Int, x: Time): Unit = { + checkClosed() + currentRow.updated(metaData.getColumnName(columnIndex), BsonDateTime(x)) + } + + override def updateTime(columnLabel: String, x: Time): Unit = { + checkClosed() + currentRow.updated(columnLabel, BsonDateTime(x)) + } + + override def updateTimestamp(columnIndex: Int, x: Timestamp): Unit = { + checkClosed() + currentRow.updated(metaData.getColumnName(columnIndex), BsonDateTime(x)) + } + + override def updateTimestamp(columnLabel: String, x: Timestamp): Unit = { + checkClosed() + currentRow.updated(columnLabel, BsonDateTime(x)) + } + + override def updateAsciiStream(columnIndex: Int, x: InputStream, length: Int): Unit = sqlFeatureNotSupported() + + override def updateAsciiStream(columnLabel: String, x: InputStream, length: Int): Unit = sqlFeatureNotSupported() + + override def updateBinaryStream(columnIndex: Int, x: InputStream, length: Int): Unit = sqlFeatureNotSupported() + + override def updateBinaryStream(columnLabel: String, x: InputStream, length: Int): Unit = sqlFeatureNotSupported() + + override def updateCharacterStream(columnIndex: Int, x: Reader, length: Int): Unit = sqlFeatureNotSupported() + + override def updateCharacterStream(columnLabel: String, reader: Reader, length: Int): Unit = sqlFeatureNotSupported() + + override def updateObject(columnIndex: Int, x: Any, scaleOrLength: Int): Unit = { + checkClosed() + updateObject(columnIndex, x) + } + + override def updateObject(columnLabel: String, x: Any, scaleOrLength: Int): Unit = { + checkClosed() + updateObject(columnLabel, x) + } + + override def updateObject(columnIndex: Int, x: Any): Unit = { + checkClosed() + currentRow.updated(metaData.getColumnName(columnIndex), BsonConverter.toBson(x)) + } + + override def updateObject(columnLabel: String, x: Any): Unit = { + checkClosed() + currentRow.updated(columnLabel, BsonConverter.toBson(x)) + } + + override def insertRow(): Unit = { + checkClosed() + collectionDao.insertOne(currentRow).resultOption(queryTimeOut) + } + + override def updateRow(): Unit = { + checkClosed() + collectionDao.replaceOne(currentRow).resultOption(queryTimeOut) + } + + override def deleteRow(): Unit = { + checkClosed() + collectionDao.deleteOne(currentRow).resultOption(queryTimeOut) + } + + override def refreshRow(): Unit = { + checkClosed() + currentRow.get("_id") match { + case Some(id) => + collectionDao.find(Map("_id" -> id)).resultOption(queryTimeOut) match { + case Some(document) => currentRow = document + case None => throw new SQLException("Row not found") + } + case None => throw new SQLException("No _id field in current row") + } + } + + override def cancelRowUpdates(): Unit = sqlFeatureNotSupported() + + override def moveToInsertRow(): Unit = sqlFeatureNotSupported() + + override def moveToCurrentRow(): Unit = sqlFeatureNotSupported() + + override def getStatement: Statement = { + checkClosed() + null + } + + override def getObject(columnIndex: Int, map: util.Map[String, Class[_]]): AnyRef = { + checkClosed() + if (map == null || map.isEmpty) { + getObject(columnIndex) + } + else { + sqlFeatureNotSupported() + } + } + + override def getObject(columnLabel: String, map: util.Map[String, Class[_]]): AnyRef = { + checkClosed() + if (map == null || map.isEmpty) { + getObject(columnLabel) + } + else { + sqlFeatureNotSupported() + } + } + + override def getObject[T](columnIndex: Int, `type`: Class[T]): T = { + checkClosed() + val ref = getObject(columnIndex) + ref match { + case t: T => t + case _ => throw new SQLException("Invalid type") + } + } + + override def getObject[T](columnLabel: String, `type`: Class[T]): T = { + checkClosed() + val ref = getObject(columnLabel) + ref match { + case t: T => t + case _ => throw new SQLException("Invalid type") + } + } + + override def getRef(columnIndex: Int): Ref = sqlFeatureNotSupported() + + override def getRef(columnLabel: String): Ref = sqlFeatureNotSupported() + + override def updateRef(columnIndex: Int, x: Ref): Unit = sqlFeatureNotSupported() + + override def updateRef(columnLabel: String, x: Ref): Unit = sqlFeatureNotSupported() + + override def getDate(columnIndex: Int, cal: Calendar): Date = { + checkClosed() + val date = getDate(columnIndex) + convertDateWithCalendar(cal, date) + } + + override def getDate(columnLabel: String, cal: Calendar): Date = { + checkClosed() + val date = getDate(columnLabel) + convertDateWithCalendar(cal, date) + } + + override def getTime(columnIndex: Int, cal: Calendar): Time = { + checkClosed() + val date = getDate(columnIndex, cal) + new Time(date.getTime) + } + + override def getTime(columnLabel: String, cal: Calendar): Time = { + checkClosed() + val date = getDate(columnLabel, cal) + new Time(date.getTime) + } + + override def getTimestamp(columnIndex: Int, cal: Calendar): Timestamp = { + checkClosed() + val date = getDate(columnIndex, cal) + new Timestamp(date.getTime) + } + + override def getTimestamp(columnLabel: String, cal: Calendar): Timestamp = { + checkClosed() + val date = getDate(columnLabel, cal) + new Timestamp(date.getTime) + } + + override def getURL(columnIndex: Int): URL = { + checkClosed() + new URI(getString(columnIndex)).toURL + } + + override def getURL(columnLabel: String): URL = { + checkClosed() + new URI(getString(columnLabel)).toURL + } + + override def getRowId(columnIndex: Int): RowId = sqlFeatureNotSupported() + override def getRowId(columnLabel: String): RowId = sqlFeatureNotSupported() + override def updateRowId(columnIndex: Int, x: RowId): Unit = sqlFeatureNotSupported() + override def updateRowId(columnLabel: String, x: RowId): Unit = sqlFeatureNotSupported() + + override def getHoldability: Int = sqlFeatureNotSupported() + + override def updateNString(columnIndex: Int, nString: String): Unit = sqlFeatureNotSupported() + override def updateNString(columnLabel: String, nString: String): Unit = sqlFeatureNotSupported() + override def getNString(columnIndex: Int): String = sqlFeatureNotSupported() + override def getNString(columnLabel: String): String = sqlFeatureNotSupported() + + override def getNClob(columnIndex: Int): NClob = sqlFeatureNotSupported() + override def getNClob(columnLabel: String): NClob = sqlFeatureNotSupported() + override def updateNClob(columnIndex: Int, nClob: NClob): Unit = sqlFeatureNotSupported() + override def updateNClob(columnLabel: String, nClob: NClob): Unit = sqlFeatureNotSupported() + + override def getSQLXML(columnIndex: Int): SQLXML = sqlFeatureNotSupported() + override def getSQLXML(columnLabel: String): SQLXML = sqlFeatureNotSupported() + override def updateSQLXML(columnIndex: Int, xmlObject: SQLXML): Unit = sqlFeatureNotSupported() + override def updateSQLXML(columnLabel: String, xmlObject: SQLXML): Unit = sqlFeatureNotSupported() + + override def getNCharacterStream(columnIndex: Int): Reader = sqlFeatureNotSupported() + override def getNCharacterStream(columnLabel: String): Reader = sqlFeatureNotSupported() + override def updateNCharacterStream(columnIndex: Int, x: Reader, length: Long): Unit = sqlFeatureNotSupported() + override def updateNCharacterStream(columnLabel: String, reader: Reader, length: Long): Unit = sqlFeatureNotSupported() + + override def updateAsciiStream(columnIndex: Int, x: InputStream, length: Long): Unit = { + checkClosed() + val text = new String(x.readAllBytes, StandardCharsets.UTF_8) + updateString(columnIndex, text) + } + + override def updateAsciiStream(columnLabel: String, x: InputStream, length: Long): Unit = { + checkClosed() + val text = new String(x.readAllBytes, StandardCharsets.UTF_8) + updateString(columnLabel, text) + } + + override def updateAsciiStream(columnIndex: Int, x: InputStream): Unit = { + checkClosed() + val text = new String(x.readAllBytes, StandardCharsets.UTF_8) + updateString(columnIndex, text) + } + + override def updateAsciiStream(columnLabel: String, x: InputStream): Unit = { + checkClosed() + val text = new String(x.readAllBytes, StandardCharsets.UTF_8) + updateString(columnLabel, text) + } + + override def updateBinaryStream(columnIndex: Int, x: InputStream, length: Long): Unit = { + checkClosed() + val text = new String(x.readAllBytes, StandardCharsets.UTF_8) + updateString(columnIndex, text) + } + + override def updateBinaryStream(columnLabel: String, x: InputStream, length: Long): Unit = { + checkClosed() + val text = new String(x.readAllBytes, StandardCharsets.UTF_8) + updateString(columnLabel, text) + } + + override def updateBinaryStream(columnIndex: Int, x: InputStream): Unit = { + checkClosed() + val text = new String(x.readAllBytes, StandardCharsets.UTF_8) + updateString(columnIndex, text) + } + + override def updateBinaryStream(columnLabel: String, x: InputStream): Unit = { + checkClosed() + val text = new String(x.readAllBytes, StandardCharsets.UTF_8) + updateString(columnLabel, text) + } + + override def updateCharacterStream(columnIndex: Int, x: Reader, length: Long): Unit = sqlFeatureNotSupported() + override def updateCharacterStream(columnLabel: String, reader: Reader, length: Long): Unit = sqlFeatureNotSupported() + override def updateCharacterStream(columnIndex: Int, x: Reader): Unit = sqlFeatureNotSupported() + override def updateCharacterStream(columnLabel: String, reader: Reader): Unit = sqlFeatureNotSupported() + + override def updateNCharacterStream(columnIndex: Int, x: Reader): Unit = sqlFeatureNotSupported() + override def updateNCharacterStream(columnLabel: String, reader: Reader): Unit = sqlFeatureNotSupported() + + override def updateBlob(columnIndex: Int, inputStream: InputStream, length: Long): Unit = { + checkClosed() + val text = new String(inputStream.readAllBytes, StandardCharsets.UTF_8) + updateString(columnIndex, text) + } + override def updateBlob(columnLabel: String, inputStream: InputStream, length: Long): Unit = { + checkClosed() + val text = new String(inputStream.readAllBytes, StandardCharsets.UTF_8) + updateString(columnLabel, text) + } + override def updateBlob(columnIndex: Int, inputStream: InputStream): Unit = { + checkClosed() + val text = new String(inputStream.readAllBytes, StandardCharsets.UTF_8) + updateString(columnIndex, text) + } + override def updateBlob(columnLabel: String, inputStream: InputStream): Unit = { + checkClosed() + val text = new String(inputStream.readAllBytes, StandardCharsets.UTF_8) + updateString(columnLabel, text) + } + + override def updateClob(columnIndex: Int, reader: Reader, length: Long): Unit = { + checkClosed() + val text = convertReaderToString(reader) + updateString(columnIndex, text) + } + + override def updateNClob(columnIndex: Int, reader: Reader, length: Long): Unit = { + checkClosed() + val text = convertReaderToString(reader) + updateString(columnIndex, text) + } + + override def updateNClob(columnLabel: String, reader: Reader, length: Long): Unit = { + checkClosed() + val text = convertReaderToString(reader) + updateString(columnLabel, text) + } + + override def updateNClob(columnIndex: Int, reader: Reader): Unit = { + checkClosed() + val text = convertReaderToString(reader) + updateString(columnIndex, text) + } + + override def updateNClob(columnLabel: String, reader: Reader): Unit = { + checkClosed() + val text = convertReaderToString(reader) + updateString(columnLabel, text) + } + + override def getBlob(columnIndex: Int): Blob = sqlFeatureNotSupported() + + override def getBlob(columnLabel: String): Blob = sqlFeatureNotSupported() + + override def updateBlob(columnIndex: Int, x: Blob): Unit = { + checkClosed() + val text = new String(x.getBinaryStream.readAllBytes(), StandardCharsets.UTF_8) + updateString(columnIndex, text) + } + + override def updateBlob(columnLabel: String, x: Blob): Unit = { + checkClosed() + val text = new String(x.getBinaryStream.readAllBytes(), StandardCharsets.UTF_8) + updateString(columnLabel, text) + } + + override def getClob(columnIndex: Int): Clob = sqlFeatureNotSupported() + + override def getClob(columnLabel: String): Clob = sqlFeatureNotSupported() + + override def updateClob(columnIndex: Int, x: Clob): Unit = { + val text = new String(x.getAsciiStream.readAllBytes(), StandardCharsets.UTF_8) + updateString(columnIndex, text) + } + + override def updateClob(columnLabel: String, x: Clob): Unit = { + val text = new String(x.getAsciiStream.readAllBytes(), StandardCharsets.UTF_8) + updateString(columnLabel, text) + } + + override def updateClob(columnLabel: String, reader: Reader, length: Long): Unit = { + val text = convertReaderToString(reader) + updateString(columnLabel, text) + } + + override def updateClob(columnIndex: Int, reader: Reader): Unit = { + val text = convertReaderToString(reader) + updateString(columnIndex, text) + } + + override def updateClob(columnLabel: String, reader: Reader): Unit = { + val text = convertReaderToString(reader) + updateString(columnLabel, text) + } + + override def getArray(columnIndex: Int): sql.Array = sqlFeatureNotSupported() + + override def getArray(columnLabel: String): sql.Array = sqlFeatureNotSupported() + + override def updateArray(columnIndex: Int, x: sql.Array): Unit = sqlFeatureNotSupported() + + override def updateArray(columnLabel: String, x: sql.Array): Unit = sqlFeatureNotSupported() + + override def unwrap[T](iface: Class[T]): T = null.asInstanceOf[T] + + override def isWrapperFor(iface: Class[_]): Boolean = false + + private def convertDateWithCalendar(cal: Calendar, date: Date) = { + if (cal != null) { + val calDate = cal.getTime + calDate.setTime(date.getTime) + new Date(calDate.getTime) + } + else { + date + } + } + + private def convertReaderToString(reader: Reader): String = { + val buffer = new StringBuilder + var c = reader.read() + while (c != -1) { + buffer.append(c.toChar) + c = reader.read() + } + buffer.toString() + } + +} diff --git a/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/resultSet/MongoDbResultSetMetaData.scala b/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/resultSet/MongoDbResultSetMetaData.scala new file mode 100644 index 00000000..06b1e774 --- /dev/null +++ b/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/resultSet/MongoDbResultSetMetaData.scala @@ -0,0 +1,125 @@ +package dev.mongocamp.driver.mongodb.jdbc.resultSet + +import dev.mongocamp.driver.mongodb.MongoDAO +import org.mongodb.scala.Document +import org.mongodb.scala.bson.{BsonBoolean, BsonInt32, BsonInt64, BsonNumber, BsonString} +import dev.mongocamp.driver.mongodb._ + +import java.sql.{ResultSetMetaData, SQLException} + +class MongoDbResultSetMetaData extends ResultSetMetaData { + private var document: Document = _ + private var collectionDao: MongoDAO[Document] = _ + + def this(dao: MongoDAO[Document]) = { + this() + val row: Document = extractDocumentFromDataList(dao.findAggregated(List(Map("$sample" -> Map("size" -> 500)))).resultList()) + this.document = row + this.collectionDao = dao + } + + def this(dao: MongoDAO[Document], document: Document) = { + this() + this.document = document + this.collectionDao = dao + } + + def this(dao: MongoDAO[Document], data: List[Document]) = { + this() + val row: Document = extractDocumentFromDataList(data) + this.document = row + this.collectionDao = dao + } + + private def extractDocumentFromDataList(data: List[Document]) = { + var row = data.headOption.getOrElse(throw new SQLException("No data in ResultSet")).copy() + val distinctKeys = data.flatMap(_.keys).distinct + val missingKeys = distinctKeys.diff(row.keys.toSeq) + missingKeys.foreach(key => { + data + .find(_.get(key).nonEmpty) + .map(doc => row = row.updated(key, doc.get(key).get)) + }) + row + } + + override def getColumnCount: Int = document.size + + override def isAutoIncrement(column: Int): Boolean = false + + override def isCaseSensitive(column: Int): Boolean = true + + override def isSearchable(column: Int): Boolean = true + + override def isCurrency(column: Int): Boolean = false + + override def isNullable(column: Int): Int = ResultSetMetaData.columnNullable + + override def isSigned(column: Int): Boolean = false + + override def getColumnDisplaySize(column: Int): Int = Int.MaxValue + + override def getColumnLabel(column: Int): String = document.keys.toList(column - 1) + + override def getColumnName(column: Int): String = getColumnLabel(column) + + override def getSchemaName(column: Int): String = collectionDao.databaseName + + override def getPrecision(column: Int): Int = 0 + + override def getScale(column: Int): Int = 0 + + override def getTableName(column: Int): String = collectionDao.name + + override def getCatalogName(column: Int): String = collectionDao.name + + override def getColumnType(column: Int): Int = { + document.values.toList(column - 1) match { + case _: BsonInt32 => java.sql.Types.INTEGER + case _: BsonInt64 => java.sql.Types.BIGINT + case _: BsonNumber => java.sql.Types.DOUBLE + case _: BsonString => java.sql.Types.VARCHAR + case _: BsonBoolean => java.sql.Types.BOOLEAN + case _: Document => java.sql.Types.STRUCT + case _ => java.sql.Types.NULL + } + } + + override def getColumnTypeName(column: Int): String = { + getColumnType(column) match { + case java.sql.Types.INTEGER => "INTEGER" + case java.sql.Types.BIGINT => "BIGINT" + case java.sql.Types.DOUBLE => "DOUBLE" + case java.sql.Types.VARCHAR => "VARCHAR" + case java.sql.Types.BOOLEAN => "BOOLEAN" + case java.sql.Types.STRUCT => "STRUCT" + case _ => "NULL" + } + } + + override def isReadOnly(column: Int): Boolean = false + + override def isWritable(column: Int): Boolean = true + + override def isDefinitelyWritable(column: Int): Boolean = true + + override def getColumnClassName(column: Int): String = { + getColumnType(column) match { + case java.sql.Types.INTEGER => classOf[java.lang.Integer].getName + case java.sql.Types.BIGINT => classOf[java.lang.Long].getName + case java.sql.Types.DOUBLE => classOf[java.lang.Double].getName + case java.sql.Types.VARCHAR => classOf[java.lang.String].getName + case java.sql.Types.BOOLEAN => classOf[java.lang.Boolean].getName + case java.sql.Types.STRUCT => classOf[java.lang.Object].getName + case _ => classOf[java.lang.String].getName + } + } + + override def unwrap[T](iface: Class[T]): T = null.asInstanceOf[T] + + override def isWrapperFor(iface: Class[_]): Boolean = false + + def getColumnIndex(columnLabel: String): Int = { + document.keys.toList.indexOf(columnLabel) + } +} diff --git a/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/statement/MongoPreparedStatement.scala b/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/statement/MongoPreparedStatement.scala new file mode 100644 index 00000000..3251812f --- /dev/null +++ b/src/main/scala/dev/mongocamp/driver/mongodb/jdbc/statement/MongoPreparedStatement.scala @@ -0,0 +1,740 @@ +package dev.mongocamp.driver.mongodb.jdbc.statement + +import com.typesafe.scalalogging.LazyLogging +import dev.mongocamp.driver.mongodb.{Converter, GenericObservable} +import dev.mongocamp.driver.mongodb.exception.SqlCommandNotSupportedException +import dev.mongocamp.driver.mongodb.jdbc.{MongoJdbcCloseable, MongoJdbcConnection} +import dev.mongocamp.driver.mongodb.jdbc.resultSet.MongoDbResultSet +import dev.mongocamp.driver.mongodb.sql.MongoSqlQueryHolder +import org.mongodb.scala.bson.collection.immutable.Document + +import java.io.{InputStream, Reader} +import java.net.URL +import java.{sql, util} +import java.sql.{Blob, CallableStatement, Clob, Connection, Date, NClob, ParameterMetaData, PreparedStatement, Ref, ResultSet, ResultSetMetaData, RowId, SQLWarning, SQLXML, Time, Timestamp} +import java.util.Calendar +import scala.collection.mutable + +case class MongoPreparedStatement(connection: MongoJdbcConnection) extends CallableStatement with MongoJdbcCloseable with LazyLogging{ + + def this(connection: MongoJdbcConnection, sql: String) = { + this(connection) + setSql(sql) + } + + private var _queryTimeout: Int = 10 + private var _sql: String = null + private var _org_sql: String = null + private var _lastResultSet: ResultSet = null + private var _lastUpdateCount: Int = -1 + + override def execute(sql: String): Boolean = { + checkClosed() + if (sql != null) { + try { + val response = MongoSqlQueryHolder(sql).run(connection.getDatabaseProvider).results(getQueryTimeout) + true + } + catch { + case e: Exception => + false + } + } + else { + false + } + } + + override def executeQuery(sql: String): ResultSet = { + checkClosed() + val queryHolder: MongoSqlQueryHolder = try { + MongoSqlQueryHolder(sql) + } + catch { + case e: SqlCommandNotSupportedException => + logger.error(e.getMessage, e) + null + } + if (queryHolder == null) { + new MongoDbResultSet(null, List.empty, 0) + } else { + var response = queryHolder.run(connection.getDatabaseProvider).results(getQueryTimeout) + if (response.isEmpty && queryHolder.selectFunctionCall) { + val emptyDocument = mutable.Map[String, Any]() + queryHolder.getKeysForEmptyDocument.foreach(key => { + emptyDocument.put(key, null) + }) + val doc = Converter.toDocument(emptyDocument.toMap) + response = Seq(doc) + } + val collectionName = Option(queryHolder.getCollection).map(c => connection.getDatabaseProvider.dao(c)) + if (!sql.toLowerCase().contains("_id")){ + response = response.map(doc => { + val newDoc = Document(doc - "_id") + newDoc + }) + } + val resultSet = new MongoDbResultSet(collectionName.orNull, response.toList, getQueryTimeout) + _lastResultSet = resultSet + resultSet + } + } + + + def setSql(sql: String): Unit = { + _sql = sql + } + + override def executeQuery(): ResultSet = { + checkClosed() + executeQuery(_sql) + } + + override def executeUpdate(): Int = { + executeUpdate(_sql) + } + + override def setNull(parameterIndex: Int, sqlType: Int): Unit = { + checkClosed() + setObject(parameterIndex, null) + } + + override def setArray(parameterIndex: Int, x: java.sql.Array): Unit = { + checkClosed() + } + + override def setBoolean(parameterIndex: Int, x: Boolean): Unit = { + checkClosed() + setObject(parameterIndex, x) + } + + override def setByte(parameterIndex: Int, x: Byte): Unit = { + checkClosed() + setObject(parameterIndex, x) + } + + override def setShort(parameterIndex: Int, x: Short): Unit = { + checkClosed() + setObject(parameterIndex, x) + } + + override def setInt(parameterIndex: Int, x: Int): Unit = { + checkClosed() + setObject(parameterIndex, x) + } + + override def setLong(parameterIndex: Int, x: Long): Unit = { + checkClosed() + setObject(parameterIndex, x) + } + + override def setFloat(parameterIndex: Int, x: Float): Unit = { + checkClosed() + setObject(parameterIndex, x) + } + + override def setDouble(parameterIndex: Int, x: Double): Unit = { + checkClosed() + setObject(parameterIndex, x) + } + + override def setBigDecimal(parameterIndex: Int, x: java.math.BigDecimal): Unit = { + checkClosed() + setObject(parameterIndex, x.doubleValue()) + } + + override def setString(parameterIndex: Int, x: String): Unit = { + checkClosed() + setObject(parameterIndex, s"'$x'") + } + + override def setBytes(parameterIndex: Int, x: Array[Byte]): Unit = { + checkClosed() + } + + override def setDate(parameterIndex: Int, x: Date): Unit = { + checkClosed() + setObject(parameterIndex, s"'${x.toInstant.toString}'") + } + + override def setTime(parameterIndex: Int, x: Time): Unit = { + checkClosed() + setObject(parameterIndex, s"'${x.toInstant.toString}'") + } + + override def setTimestamp(parameterIndex: Int, x: Timestamp): Unit = { + checkClosed() + setObject(parameterIndex, s"'${x.toInstant.toString}'") + } + + override def setAsciiStream(parameterIndex: Int, x: InputStream, length: Int): Unit = { + checkClosed() + } + + override def setUnicodeStream(parameterIndex: Int, x: InputStream, length: Int): Unit = { + checkClosed() + } + + override def setBinaryStream(parameterIndex: Int, x: InputStream, length: Int): Unit = { + checkClosed() + } + + override def clearParameters(): Unit = { + checkClosed() + _sql = _org_sql + } + + override def setObject(parameterIndex: Int, x: Any, targetSqlType: Int): Unit = { + setObject(parameterIndex, x) + } + + override def setObject(parameterIndex: Int, x: Any): Unit = { + checkClosed() + var newSql = "" + var paramCount = 0 + _org_sql = _sql + _sql.foreach(c => { + var replace = false + if (c == '?') { + if (paramCount == parameterIndex) { + replace = true + } + paramCount += 1 + } + if (replace) { + newSql += x.toString + } + else { + newSql += c + } + }) + _sql = newSql + } + + override def execute(): Boolean = { + execute(_sql) + } + + override def addBatch(): Unit = { + checkClosed() + } + + override def setCharacterStream(parameterIndex: Int, reader: Reader, length: Int): Unit = { + checkClosed() + } + + override def setRef(parameterIndex: Int, x: Ref): Unit = { + checkClosed() + } + + override def setBlob(parameterIndex: Int, x: Blob): Unit = { + checkClosed() + } + + override def setClob(parameterIndex: Int, x: Clob): Unit = { + checkClosed() + } + + override def getMetaData: ResultSetMetaData = { + checkClosed() + null + } + + override def setDate(parameterIndex: Int, x: Date, cal: Calendar): Unit = { + setDate(parameterIndex, x) + } + + override def setTime(parameterIndex: Int, x: Time, cal: Calendar): Unit = { + setTime(parameterIndex, x) + } + + override def setTimestamp(parameterIndex: Int, x: Timestamp, cal: Calendar): Unit = { + setTimestamp(parameterIndex, x) + } + + override def setNull(parameterIndex: Int, sqlType: Int, typeName: String): Unit = { + setNull(parameterIndex, sqlType) + } + + override def setURL(parameterIndex: Int, x: URL): Unit = { + sqlFeatureNotSupported() + } + + override def getParameterMetaData: ParameterMetaData = { + sqlFeatureNotSupported() + } + + override def setRowId(parameterIndex: Int, x: RowId): Unit = { + checkClosed() + } + + override def setNString(parameterIndex: Int, value: String): Unit = { + checkClosed() + } + + override def setNCharacterStream(parameterIndex: Int, value: Reader, length: Long): Unit = { + checkClosed() + } + + override def setNClob(parameterIndex: Int, value: NClob): Unit = { + checkClosed() + } + + override def setClob(parameterIndex: Int, reader: Reader, length: Long): Unit = { + checkClosed() + } + + override def setBlob(parameterIndex: Int, inputStream: InputStream, length: Long): Unit = { + checkClosed() + } + + override def setNClob(parameterIndex: Int, reader: Reader, length: Long): Unit = { + checkClosed() + } + + override def setSQLXML(parameterIndex: Int, xmlObject: SQLXML): Unit = { + checkClosed() + } + + override def setObject(parameterIndex: Int, x: Any, targetSqlType: Int, scaleOrLength: Int): Unit = { + setObject(parameterIndex, x) + } + + override def setAsciiStream(parameterIndex: Int, x: InputStream, length: Long): Unit = { + checkClosed() + } + + override def setBinaryStream(parameterIndex: Int, x: InputStream, length: Long): Unit = { + checkClosed() + } + + override def setCharacterStream(parameterIndex: Int, reader: Reader, length: Long): Unit = { + checkClosed() + } + + override def setAsciiStream(parameterIndex: Int, x: InputStream): Unit = { + checkClosed() + } + + override def setBinaryStream(parameterIndex: Int, x: InputStream): Unit = { + checkClosed() + } + + override def setCharacterStream(parameterIndex: Int, reader: Reader): Unit = { + checkClosed() + } + + override def setNCharacterStream(parameterIndex: Int, value: Reader): Unit = { + checkClosed() + } + + override def setClob(parameterIndex: Int, reader: Reader): Unit = { + checkClosed() + } + + override def setBlob(parameterIndex: Int, inputStream: InputStream): Unit = { + checkClosed() + } + + override def setNClob(parameterIndex: Int, reader: Reader): Unit = { + checkClosed() + } + + override def executeUpdate(sql: String): Int = { + checkClosed() + val updateResponse = executeQuery(sql) + updateResponse.next() + val updateCount = updateResponse.getInt("matchedCount") + _lastUpdateCount = updateCount + updateCount + } + + override def getMaxFieldSize: Int = { + checkClosed() + 0 + } + + override def setMaxFieldSize(max: Int): Unit = { + checkClosed() + } + + override def getMaxRows: Int = { + sqlFeatureNotSupported() + } + + override def setMaxRows(max: Int): Unit = { + sqlFeatureNotSupported() + } + + override def setEscapeProcessing(enable: Boolean): Unit = { + checkClosed() + } + + override def getQueryTimeout: Int = _queryTimeout + + override def setQueryTimeout(seconds: Int): Unit = _queryTimeout = seconds + + override def cancel(): Unit = { + sqlFeatureNotSupported("cancel not supported at MongoDb Driver") + } + + override def getWarnings: SQLWarning = { + checkClosed() + null + } + + override def clearWarnings(): Unit = { + checkClosed() + } + + override def setCursorName(name: String): Unit = { + checkClosed() + } + + override def getResultSet: ResultSet = { + checkClosed() + _lastResultSet + } + + override def getUpdateCount: Int = { + checkClosed() + _lastUpdateCount + } + + override def getMoreResults: Boolean = { + checkClosed() + false + } + + override def setFetchDirection(direction: Int): Unit = { + sqlFeatureNotSupported() + } + + override def getFetchDirection: Int = { + checkClosed() + ResultSet.FETCH_FORWARD + } + + override def setFetchSize(rows: Int): Unit = { + + } + + override def getFetchSize: Int = { + -1 + } + + override def getResultSetConcurrency: Int = { + sqlFeatureNotSupported() + } + + override def getResultSetType: Int = { + checkClosed() + ResultSet.TYPE_FORWARD_ONLY + } + + override def addBatch(sql: String): Unit = { + checkClosed() + } + + override def clearBatch(): Unit = { + checkClosed() + } + + override def executeBatch(): Array[Int] = { + checkClosed() + null + } + + override def getConnection: Connection = { + checkClosed() + connection + } + + override def getMoreResults(current: Int): Boolean = { + checkClosed() + false + } + + override def getGeneratedKeys: ResultSet = { + checkClosed() + null + } + + override def executeUpdate(sql: String, autoGeneratedKeys: Int): Int = { + executeUpdate(sql) + } + + override def executeUpdate(sql: String, columnIndexes: Array[Int]): Int = { + executeUpdate(sql) + } + + override def executeUpdate(sql: String, columnNames: Array[String]): Int = { + executeUpdate(sql) + } + + override def execute(sql: String, autoGeneratedKeys: Int): Boolean = { + execute(sql) + } + + override def execute(sql: String, columnIndexes: Array[Int]): Boolean = { + execute(sql) + } + + override def execute(sql: String, columnNames: Array[String]): Boolean = { + execute(sql) + } + + override def getResultSetHoldability: Int = { + checkClosed() + 0 + } + + override def setPoolable(poolable: Boolean): Unit = { + checkClosed() + 0 + } + + override def isPoolable: Boolean = { + checkClosed() + false + } + + override def closeOnCompletion(): Unit = { + checkClosed() + } + + override def isCloseOnCompletion: Boolean = { + checkClosed() + false + } + + override def unwrap[T](iface: Class[T]): T = null.asInstanceOf[T] + + override def isWrapperFor(iface: Class[_]): Boolean = false + + override def registerOutParameter(parameterIndex: Int, sqlType: Int): Unit = ??? + + override def registerOutParameter(parameterIndex: Int, sqlType: Int, scale: Int): Unit = ??? + + override def wasNull(): Boolean = ??? + + override def getString(parameterIndex: Int): String = ??? + + override def getBoolean(parameterIndex: Int): Boolean = ??? + + override def getByte(parameterIndex: Int): Byte = ??? + + override def getShort(parameterIndex: Int): Short = ??? + + override def getInt(parameterIndex: Int): Int = ??? + + override def getLong(parameterIndex: Int): Long = ??? + + override def getFloat(parameterIndex: Int): Float = ??? + + override def getDouble(parameterIndex: Int): Double = ??? + + override def getBigDecimal(parameterIndex: Int, scale: Int): java.math.BigDecimal = ??? + + override def getBytes(parameterIndex: Int): Array[Byte] = ??? + + override def getDate(parameterIndex: Int): Date = ??? + + override def getTime(parameterIndex: Int): Time = ??? + + override def getTimestamp(parameterIndex: Int): Timestamp = ??? + + override def getObject(parameterIndex: Int): AnyRef = ??? + + override def getBigDecimal(parameterIndex: Int): java.math.BigDecimal = ??? + + override def getObject(parameterIndex: Int, map: util.Map[String, Class[_]]): AnyRef = ??? + + override def getRef(parameterIndex: Int): Ref = ??? + + override def getBlob(parameterIndex: Int): Blob = ??? + + override def getClob(parameterIndex: Int): Clob = ??? + + override def getArray(parameterIndex: Int): sql.Array = ??? + + override def getDate(parameterIndex: Int, cal: Calendar): Date = ??? + + override def getTime(parameterIndex: Int, cal: Calendar): Time = ??? + + override def getTimestamp(parameterIndex: Int, cal: Calendar): Timestamp = ??? + + override def registerOutParameter(parameterIndex: Int, sqlType: Int, typeName: String): Unit = ??? + + override def registerOutParameter(parameterName: String, sqlType: Int): Unit = ??? + + override def registerOutParameter(parameterName: String, sqlType: Int, scale: Int): Unit = ??? + + override def registerOutParameter(parameterName: String, sqlType: Int, typeName: String): Unit = ??? + + override def getURL(parameterIndex: Int): URL = ??? + + override def setURL(parameterName: String, `val`: URL): Unit = ??? + + override def setNull(parameterName: String, sqlType: Int): Unit = ??? + + override def setBoolean(parameterName: String, x: Boolean): Unit = ??? + + override def setByte(parameterName: String, x: Byte): Unit = ??? + + override def setShort(parameterName: String, x: Short): Unit = ??? + + override def setInt(parameterName: String, x: Int): Unit = ??? + + override def setLong(parameterName: String, x: Long): Unit = ??? + + override def setFloat(parameterName: String, x: Float): Unit = ??? + + override def setDouble(parameterName: String, x: Double): Unit = ??? + + override def setBigDecimal(parameterName: String, x: java.math.BigDecimal): Unit = ??? + + override def setString(parameterName: String, x: String): Unit = ??? + + override def setBytes(parameterName: String, x: Array[Byte]): Unit = ??? + + override def setDate(parameterName: String, x: Date): Unit = ??? + + override def setTime(parameterName: String, x: Time): Unit = ??? + + override def setTimestamp(parameterName: String, x: Timestamp): Unit = ??? + + override def setAsciiStream(parameterName: String, x: InputStream, length: Int): Unit = ??? + + override def setBinaryStream(parameterName: String, x: InputStream, length: Int): Unit = ??? + + override def setObject(parameterName: String, x: Any, targetSqlType: Int, scale: Int): Unit = ??? + + override def setObject(parameterName: String, x: Any, targetSqlType: Int): Unit = ??? + + override def setObject(parameterName: String, x: Any): Unit = ??? + + override def setCharacterStream(parameterName: String, reader: Reader, length: Int): Unit = ??? + + override def setDate(parameterName: String, x: Date, cal: Calendar): Unit = ??? + + override def setTime(parameterName: String, x: Time, cal: Calendar): Unit = ??? + + override def setTimestamp(parameterName: String, x: Timestamp, cal: Calendar): Unit = ??? + + override def setNull(parameterName: String, sqlType: Int, typeName: String): Unit = ??? + + override def getString(parameterName: String): String = ??? + + override def getBoolean(parameterName: String): Boolean = ??? + + override def getByte(parameterName: String): Byte = ??? + + override def getShort(parameterName: String): Short = ??? + + override def getInt(parameterName: String): Int = ??? + + override def getLong(parameterName: String): Long = ??? + + override def getFloat(parameterName: String): Float = ??? + + override def getDouble(parameterName: String): Double = ??? + + override def getBytes(parameterName: String): Array[Byte] = ??? + + override def getDate(parameterName: String): Date = ??? + + override def getTime(parameterName: String): Time = ??? + + override def getTimestamp(parameterName: String): Timestamp = ??? + + override def getObject(parameterName: String): AnyRef = ??? + + override def getBigDecimal(parameterName: String): java.math.BigDecimal = ??? + + override def getObject(parameterName: String, map: util.Map[String, Class[_]]): AnyRef = ??? + + override def getRef(parameterName: String): Ref = ??? + + override def getBlob(parameterName: String): Blob = ??? + + override def getClob(parameterName: String): Clob = ??? + + override def getArray(parameterName: String): sql.Array = ??? + + override def getDate(parameterName: String, cal: Calendar): Date = ??? + + override def getTime(parameterName: String, cal: Calendar): Time = ??? + + override def getTimestamp(parameterName: String, cal: Calendar): Timestamp = ??? + + override def getURL(parameterName: String): URL = ??? + + override def getRowId(parameterIndex: Int): RowId = ??? + + override def getRowId(parameterName: String): RowId = ??? + + override def setRowId(parameterName: String, x: RowId): Unit = ??? + + override def setNString(parameterName: String, value: String): Unit = ??? + + override def setNCharacterStream(parameterName: String, value: Reader, length: Long): Unit = ??? + + override def setNClob(parameterName: String, value: NClob): Unit = ??? + + override def setClob(parameterName: String, reader: Reader, length: Long): Unit = ??? + + override def setBlob(parameterName: String, inputStream: InputStream, length: Long): Unit = ??? + + override def setNClob(parameterName: String, reader: Reader, length: Long): Unit = ??? + + override def getNClob(parameterIndex: Int): NClob = ??? + + override def getNClob(parameterName: String): NClob = ??? + + override def setSQLXML(parameterName: String, xmlObject: SQLXML): Unit = ??? + + override def getSQLXML(parameterIndex: Int): SQLXML = ??? + + override def getSQLXML(parameterName: String): SQLXML = ??? + + override def getNString(parameterIndex: Int): String = ??? + + override def getNString(parameterName: String): String = ??? + + override def getNCharacterStream(parameterIndex: Int): Reader = ??? + + override def getNCharacterStream(parameterName: String): Reader = ??? + + override def getCharacterStream(parameterIndex: Int): Reader = ??? + + override def getCharacterStream(parameterName: String): Reader = ??? + + override def setBlob(parameterName: String, x: Blob): Unit = ??? + + override def setClob(parameterName: String, x: Clob): Unit = ??? + + override def setAsciiStream(parameterName: String, x: InputStream, length: Long): Unit = ??? + + override def setBinaryStream(parameterName: String, x: InputStream, length: Long): Unit = ??? + + override def setCharacterStream(parameterName: String, reader: Reader, length: Long): Unit = ??? + + override def setAsciiStream(parameterName: String, x: InputStream): Unit = ??? + + override def setBinaryStream(parameterName: String, x: InputStream): Unit = ??? + + override def setCharacterStream(parameterName: String, reader: Reader): Unit = ??? + + override def setNCharacterStream(parameterName: String, value: Reader): Unit = ??? + + override def setClob(parameterName: String, reader: Reader): Unit = ??? + + override def setBlob(parameterName: String, inputStream: InputStream): Unit = ??? + + override def setNClob(parameterName: String, reader: Reader): Unit = ??? + + override def getObject[T](parameterIndex: Int, `type`: Class[T]): T = ??? + + override def getObject[T](parameterName: String, `type`: Class[T]): T = ??? +} diff --git a/src/main/scala/dev/mongocamp/driver/mongodb/sql/MongoSqlQueryHolder.scala b/src/main/scala/dev/mongocamp/driver/mongodb/sql/MongoSqlQueryHolder.scala index d993461e..614ae986 100644 --- a/src/main/scala/dev/mongocamp/driver/mongodb/sql/MongoSqlQueryHolder.scala +++ b/src/main/scala/dev/mongocamp/driver/mongodb/sql/MongoSqlQueryHolder.scala @@ -4,6 +4,7 @@ import com.mongodb.client.model.DropIndexOptions import dev.mongocamp.driver.mongodb._ import dev.mongocamp.driver.mongodb.database.DatabaseProvider import dev.mongocamp.driver.mongodb.database.DatabaseProvider.CollectionSeparator +import dev.mongocamp.driver.mongodb.exception.SqlCommandNotSupportedException import dev.mongocamp.driver.mongodb.sql.SQLCommandType.SQLCommandType import net.sf.jsqlparser.expression.operators.conditional.{ AndExpression, OrExpression } import net.sf.jsqlparser.expression.operators.relational._ @@ -11,20 +12,25 @@ import net.sf.jsqlparser.expression.{ Expression, Parenthesis } import net.sf.jsqlparser.parser.{ CCJSqlParser, StreamProvider } import net.sf.jsqlparser.schema.Table import net.sf.jsqlparser.statement.UnsupportedStatement +import net.sf.jsqlparser.statement.alter.Alter import net.sf.jsqlparser.statement.create.index.CreateIndex +import net.sf.jsqlparser.statement.create.table.CreateTable import net.sf.jsqlparser.statement.delete.Delete import net.sf.jsqlparser.statement.drop.Drop +import net.sf.jsqlparser.statement.execute.Execute import net.sf.jsqlparser.statement.insert.Insert import net.sf.jsqlparser.statement.select.{ FromItem, PlainSelect, Select, SelectItem } import net.sf.jsqlparser.statement.show.ShowTablesStatement import net.sf.jsqlparser.statement.truncate.Truncate import net.sf.jsqlparser.statement.update.Update import org.bson.conversions.Bson +import org.h2.command.ddl.AlterTable import org.mongodb.scala.model.IndexOptions import org.mongodb.scala.model.Sorts.ascending -import org.mongodb.scala.{ Document, Observable } +import org.mongodb.scala.{ Document, Observable, SingleObservable } import java.sql.SQLException +import java.util.Date import java.util.concurrent.TimeUnit import scala.collection.mutable import scala.collection.mutable.ArrayBuffer @@ -39,6 +45,9 @@ class MongoSqlQueryHolder { private var setElement: Option[Bson] = None private val documentsToInsert: ArrayBuffer[Document] = ArrayBuffer.empty private var indexOptions: Option[IndexOptions] = None + private var callFunction: Option[String] = None + private var keepOneDocument: Boolean = false + private val keysForEmptyDocument: mutable.Set[String] = mutable.Set.empty def this(statement: net.sf.jsqlparser.statement.Statement) = { this() @@ -72,14 +81,14 @@ class MongoSqlQueryHolder { case "INDEX" => sqlCommandType = SQLCommandType.DropIndex sqlTable = drop.getName - if (!getCollection.contains(".")) { - throw new IllegalArgumentException("not supported drop index without collection specified in the name") + if (!getCollection.contains(CollectionSeparator)) { + throw new SqlCommandNotSupportedException("not supported drop index without collection specified in the name") } case "DATABASE" => sqlCommandType = SQLCommandType.DropDatabase sqlTable = drop.getName case _ => - throw new IllegalArgumentException("not supported drop command type") + throw new SqlCommandNotSupportedException("not supported drop command type") } } else if (classOf[Truncate].isAssignableFrom(statement.getClass)) { @@ -90,6 +99,18 @@ class MongoSqlQueryHolder { else if (classOf[ShowTablesStatement].isAssignableFrom(statement.getClass)) { sqlCommandType = SQLCommandType.ShowTables } + else if (classOf[Execute].isAssignableFrom(statement.getClass)) { + sqlCommandType = SQLCommandType.Execute + callFunction = Some(statement.asInstanceOf[Execute].getName) + } + else if (classOf[CreateTable].isAssignableFrom(statement.getClass)) { + val createTable = statement.asInstanceOf[CreateTable] + sqlCommandType = SQLCommandType.CreateTable + sqlTable = createTable.getTable + } + else if (classOf[Alter].isAssignableFrom(statement.getClass)) { + sqlCommandType = SQLCommandType.AlterTable + } else if (classOf[UnsupportedStatement].isAssignableFrom(statement.getClass)) { val unsupportedStatement = statement.asInstanceOf[UnsupportedStatement] val isShowDatabases = unsupportedStatement.toString.toLowerCase.contains("show databases") @@ -98,17 +119,17 @@ class MongoSqlQueryHolder { sqlCommandType = SQLCommandType.ShowDatabases } else { - throw new IllegalArgumentException("not supported sql command type") + throw new SqlCommandNotSupportedException(s"not supported sql command type <${statement.getClass.getSimpleName}>") } } else { - throw new IllegalArgumentException("not supported sql command type") + throw new SqlCommandNotSupportedException(s"not supported sql command type <${statement.getClass.getSimpleName}>") } "" } def getCollection: String = { - sqlTable.getFullyQualifiedName.replace(".", CollectionSeparator).replace("'", "").replace("`", "") + Option(sqlTable).map(_.getFullyQualifiedName.replace(".", CollectionSeparator).replace("'", "").replace("\"", "").replace("`", "")).orNull } def run(provider: DatabaseProvider, allowDiskUsage: Boolean = true): Observable[Document] = { @@ -124,7 +145,8 @@ class MongoSqlQueryHolder { }) case SQLCommandType.Select => - provider.dao(getCollection).findAggregated(aggregatePipeline.toList, allowDiskUsage) + val originalObservable = provider.dao(getCollection).findAggregated(aggregatePipeline.toList, allowDiskUsage) + originalObservable case SQLCommandType.Update => val updateSet = setElement.getOrElse(throw new IllegalArgumentException("update set element must be defined")) @@ -158,16 +180,41 @@ class MongoSqlQueryHolder { case SQLCommandType.ShowTables => provider.collections() + case SQLCommandType.ShowDatabases => provider.databases + case SQLCommandType.DropTable => provider.dao(getCollection).drop().map(_ => org.mongodb.scala.Document("wasAcknowledged" -> true)) + case SQLCommandType.CreateTable => + provider.dao(getCollection).createIndex(Map("_id" -> 1)).map(_ => org.mongodb.scala.Document("created" -> true)) + + case SQLCommandType.AlterTable => + SingleObservable(org.mongodb.scala.Document("changed" -> "true")) + + case SQLCommandType.Execute => + SingleObservable( + callFunction + .map(function => { + if (function.equalsIgnoreCase("current_schema")) { + org.mongodb.scala.Document("currentSchema" -> provider.DefaultDatabaseName) + } + else { + throw new SqlCommandNotSupportedException("not supported function") + } + }) + .getOrElse(Document()) + ) case _ => - throw new IllegalArgumentException("not supported sql command type") + throw new SqlCommandNotSupportedException("not supported sql command type") } } + def getKeysForEmptyDocument: Set[String] = keysForEmptyDocument.toSet + + def selectFunctionCall: Boolean = keepOneDocument + private def getUpdateOrDeleteFilter: Bson = { updateOrDeleteFilter.getOrElse(Map.empty).toMap } @@ -180,7 +227,16 @@ class MongoSqlQueryHolder { case e: net.sf.jsqlparser.expression.DateValue => e.getValue case e: net.sf.jsqlparser.expression.TimeValue => e.getValue case e: net.sf.jsqlparser.expression.TimestampValue => e.getValue - case e: net.sf.jsqlparser.expression.NullValue => null + case _: net.sf.jsqlparser.expression.NullValue => null + case t: net.sf.jsqlparser.expression.TimeKeyExpression => + t.getStringValue.toUpperCase match { + case "CURRENT_TIMESTAMP" => new Date() + case "NOW" => new Date() + case _ => t.getStringValue + } + case e: net.sf.jsqlparser.schema.Column => + val name = e.getColumnName + name.toIntOption.getOrElse(name.toBooleanOption.getOrElse(name)) case _ => throw new IllegalArgumentException("not supported value type") } @@ -241,6 +297,14 @@ class MongoSqlQueryHolder { } val functionName = if (e.isNot) "$nin" else "$in" queryMap.put(e.getLeftExpression.toString, Map(functionName -> value)) + case e: LikeExpression => + val value = Map("$regex" -> e.getRightExpression.toString.replace("%", "(.*?)"), "$options" -> "i") + if (e.isNot) { + queryMap.put(e.getLeftExpression.toString, Map("$not" -> value)) + } + else { + queryMap.put(e.getLeftExpression.toString, value) + } case e: IsNullExpression => if (e.isNot) { queryMap.put(e.getLeftExpression.toString, Map("$ne" -> null)) @@ -257,28 +321,65 @@ class MongoSqlQueryHolder { select.getSelectBody match { case plainSelect: PlainSelect => val selectItems = Option(plainSelect.getSelectItems).map(_.asScala).getOrElse(List.empty) - val aliasList = ArrayBuffer[String]() + val maybeDistinct = Option(plainSelect.getDistinct) + + selectItems.foreach(sI => { + if (classOf[net.sf.jsqlparser.expression.Function].isAssignableFrom(sI.getExpression.getClass)) { + keepOneDocument = maybeDistinct.isEmpty + } + }) + val aliasList = ArrayBuffer[String]() sqlCommandType = SQLCommandType.Select - Option(plainSelect.getGroupBy).foreach(gbEl => { + val maybeGroupByElement = Option(plainSelect.getGroupBy) + maybeGroupByElement.foreach(gbEl => { val groupBy = gbEl.getGroupByExpressionList.getExpressions.asScala.map(_.toString).toList val groupId = mutable.Map[String, Any]() val group = mutable.Map[String, Any]() groupBy.foreach(g => groupId += g -> ("$" + g)) selectItems.foreach { case e: SelectItem[Expression] => val expressionName = e.getExpression.toString - if (expressionName.contains("count")) { + if (expressionName.toLowerCase().contains("count")) { group += expressionName -> Map("$sum" -> 1) } else { if (!groupBy.contains(expressionName)) { val espr = expressionName.split('(').map(_.trim.replace(")", "")).map(s => ("$" + s)) - group += expressionName -> Map(espr.head -> espr.last) + if (espr.head.equalsIgnoreCase("max")) { + group += expressionName -> Map(espr.head -> espr.last) + } + else { + group += expressionName -> Map(espr.head -> espr.last) + } } } } val groupMap = Map("_id" -> groupId) ++ group.toMap ++ groupId.keys.map(s => s -> Map("$first" -> ("$" + s))).toMap aggregatePipeline += Map("$group" -> groupMap) }) + if (maybeGroupByElement.isEmpty && keepOneDocument) { + val group = mutable.Map[String, Any]() + val idGroupMap = mutable.Map() + selectItems.foreach { case se: SelectItem[Expression] => + val expressionName = se.getExpression.toString + if (expressionName.toLowerCase().contains("count")) { + group += expressionName -> Map("$sum" -> 1) + } + else { + val espr = expressionName.split('(').map(_.trim.replace(")", "")).map(s => ("$" + s)) + val functionName: String = espr.head.toLowerCase match { + case "$max" => "$last" + case "$min" => "$first" + case _ => espr.head + } + val expression = if (functionName.equalsIgnoreCase(espr.last)) Map("$first" -> espr.last) else Map(functionName -> espr.last) + group += expressionName -> expression + } + keysForEmptyDocument += Option(se.getAlias).map(_.getName).getOrElse(expressionName) + } + + val groupMap = Map("_id" -> idGroupMap) ++ group.toMap + aggregatePipeline += Map("$group" -> groupMap) + } def convertFromItemToTable(fromItem: FromItem): Table = { val tableName = Option(fromItem.getAlias).map(a => fromItem.toString.replace(a.toString, "")).getOrElse(fromItem).toString new Table(tableName) @@ -394,7 +495,7 @@ class MongoSqlQueryHolder { "$replaceWith" -> Map("$mergeObjects" -> aliasList.map(string => if (string.startsWith("$")) string else "$" + string).toList) ) } - Option(plainSelect.getDistinct).foreach { distinct => + maybeDistinct.foreach { distinct => val groupMap: mutable.Map[String, Any] = mutable.Map() selectItems.foreach { case e: SelectItem[Expression] => val expressionName = e.getExpression.toString @@ -451,7 +552,7 @@ class MongoSqlQueryHolder { singleDocumentCreated = true } catch { - case _: Throwable => + case t: Throwable => throw new IllegalArgumentException("not supported expression list") } } diff --git a/src/main/scala/dev/mongocamp/driver/mongodb/sql/SQLCommandType.scala b/src/main/scala/dev/mongocamp/driver/mongodb/sql/SQLCommandType.scala index ba60512b..f1052b67 100644 --- a/src/main/scala/dev/mongocamp/driver/mongodb/sql/SQLCommandType.scala +++ b/src/main/scala/dev/mongocamp/driver/mongodb/sql/SQLCommandType.scala @@ -6,6 +6,6 @@ object SQLCommandType extends Enumeration { type SQLCommandType = Value - val Delete, Select, Update, Insert, CreateIndex, DropTable, DropIndex, DropDatabase, ShowDatabases, ShowTables = Value + val Delete, Select, Update, Insert, CreateIndex, DropTable, DropIndex, DropDatabase, ShowDatabases, ShowTables, Execute, AlterTable, CreateTable = Value } \ No newline at end of file diff --git a/src/test/resources/json/people.json b/src/test/resources/json/people.json index 5ca35848..2c91dfa9 100644 --- a/src/test/resources/json/people.json +++ b/src/test/resources/json/people.json @@ -2,7 +2,7 @@ { "_id" : { "$oid" : "5e9ef66185c0145fa5d3c448" }, "id" : { "$numberLong" : "1" }, "guid" : "19ebe4fe-f860-4cbc-ac0a-664a418e2173", "isActive" : true, "balance" : 2316.0, "picture" : "http://placehold.it/32x32", "age" : 25, "name" : "Bowen Leon", "gender" : "male", "email" : "bowenleon@inrt.com", "phone" : "+1 (904) 457-2017", "address" : "138 Miami Court, Urbana, Kansas, 1034", "about" : "Commodo in mollit laboris incididunt excepteur nulla cillum sunt do occaecat Lorem. Excepteur esse id magna pariatur irure anim officia exercitation veniam anim dolor. Sunt irure est dolore nisi nulla nulla. Nostrud aliquip exercitation ut adipisicing esse ullamco incididunt mollit laborum duis exercitation. Ipsum commodo excepteur nulla sit irure laboris magna ipsum Lorem.\r\n", "registered" : { "$date" : "2014-01-26T16:08:40.000+0000" }, "tags" : [ "ipsum", "qui", "proident", "sunt", "cillum", "veniam", "laboris" ], "friends" : [ { "id" : { "$numberLong" : "0" }, "name" : "Reyes Velasquez" }, { "id" : { "$numberLong" : "1" }, "name" : "Rosalie Hooper" }, { "id" : { "$numberLong" : "2" }, "name" : "Alyssa David" } ], "greeting" : "Hello, Bowen Leon! You have 9 unread messages.", "favoriteFruit" : "apple" } { "_id" : { "$oid" : "5e9ef66185c0145fa5d3c449" }, "id" : { "$numberLong" : "2" }, "guid" : "6ee53e07-2e61-48cd-9bc9-b3505a0438f3", "isActive" : false, "balance" : 1527.0, "picture" : "http://placehold.it/32x32", "age" : 40, "name" : "Cecilia Lynn", "gender" : "female", "email" : "cecilialynn@medicroix.com", "phone" : "+1 (875) 525-3138", "address" : "124 Herzl Street, Greenwich, Arkansas, 5309", "about" : "Esse adipisicing ipsum esse consectetur eu ad sunt sit culpa enim velit elit velit deserunt. Aliqua nulla et laboris nulla aute excepteur Lorem. Ut aliquip non excepteur exercitation consectetur anim est ex irure dolore ut. Consequat enim enim dolor excepteur mollit consectetur. Magna sunt reprehenderit est quis.\r\n", "registered" : { "$date" : "2014-02-21T23:13:05.000+0000" }, "tags" : [ "eiusmod", "minim", "magna", "est", "laborum", "nisi", "qui" ], "friends" : [ { "id" : { "$numberLong" : "0" }, "name" : "Erika Harmon" }, { "id" : { "$numberLong" : "1" }, "name" : "Horn Larsen" }, { "id" : { "$numberLong" : "2" }, "name" : "Gertrude Fuller" }, { "id" : { "$numberLong" : "3" }, "name" : "Spencer Hutchinson" }, { "id" : { "$numberLong" : "4" }, "name" : "Beryl Buckley" } ], "greeting" : "Hello, Cecilia Lynn! You have 7 unread messages.", "favoriteFruit" : "strawberry" } { "_id" : { "$oid" : "5e9ef66185c0145fa5d3c44a" }, "id" : { "$numberLong" : "3" }, "guid" : "a01c8bb6-95ac-4235-b6b3-475734f0dd92", "isActive" : false, "balance" : 2682.0, "picture" : "http://placehold.it/32x32", "age" : 24, "name" : "Sylvia Ortega", "gender" : "female", "email" : "sylviaortega@viagrand.com", "phone" : "+1 (983) 470-3157", "address" : "617 Vernon Avenue, Advance, Connecticut, 7787", "about" : "Tempor aliquip dolor excepteur proident ex magna commodo laboris. Ullamco ex esse excepteur nostrud. Duis ex anim pariatur dolore ut irure. Consequat non Lorem laborum esse anim magna consequat voluptate dolor elit. Mollit sint consequat ipsum minim id anim aute reprehenderit eu velit voluptate commodo.\r\n", "registered" : { "$date" : "2014-01-13T07:33:15.000+0000" }, "tags" : [ "ut", "culpa", "reprehenderit", "ad", "amet", "officia", "nostrud" ], "friends" : [ { "id" : { "$numberLong" : "0" }, "name" : "Ferrell Rhodes" }, { "id" : { "$numberLong" : "1" }, "name" : "Ana Guy" }, { "id" : { "$numberLong" : "2" }, "name" : "Rosanne Griffin" }, { "id" : { "$numberLong" : "3" }, "name" : "Morrow Adams" }, { "id" : { "$numberLong" : "4" }, "name" : "Keri White" }, { "id" : { "$numberLong" : "5" }, "name" : "Tracey Sykes" } ], "greeting" : "Hello, Sylvia Ortega! You have 9 unread messages.", "favoriteFruit" : "apple" } -{ "_id" : { "$oid" : "5e9ef66185c0145fa5d3c44b" }, "id" : { "$numberLong" : "4" }, "guid" : "4ded35ef-ba63-4eef-996f-d67e38553b0d", "isActive" : false, "balance" : 1159.0, "picture" : "http://placehold.it/32x32", "age" : 31, "name" : "Howe Briggs", "gender" : "male", "email" : "howebriggs@zappix.com", "phone" : "+1 (966) 518-3246", "address" : "963 Roosevelt Court, Bowden, Oregon, 6236", "about" : "Et eu culpa elit eiusmod ea proident ad est culpa elit. Dolor eiusmod officia nisi aliquip. Ut irure laborum qui dolor ut veniam est veniam nostrud consequat voluptate velit do duis. Irure excepteur excepteur reprehenderit nostrud reprehenderit voluptate quis ex aliquip. Sunt amet commodo pariatur fugiat est laborum. Est est non aliqua nisi laboris. Irure voluptate aute deserunt commodo nostrud amet anim reprehenderit nostrud cupidatat aliqua veniam anim.\r\n", "registered" : { "$date" : "2014-03-04T23:00:36.000+0000" }, "tags" : [ "veniam", "anim", "fugiat", "dolor", "elit", "nostrud", "quis" ], "friends" : [ { "id" : { "$numberLong" : "0" }, "name" : "Hunter Garner" }, { "id" : { "$numberLong" : "1" }, "name" : "Walls Wright" }, { "id" : { "$numberLong" : "2" }, "name" : "Christie Walker" }, { "id" : { "$numberLong" : "3" }, "name" : "Powell Woods" }, { "id" : { "$numberLong" : "4" }, "name" : "Doreen Carpenter" }, { "id" : { "$numberLong" : "5" }, "name" : "Beach Harrison" }, { "id" : { "$numberLong" : "6" }, "name" : "Perkins Mullins" }, { "id" : { "$numberLong" : "7" }, "name" : "Blake Goff" }, { "id" : { "$numberLong" : "8" }, "name" : "Clarke Spears" } ], "greeting" : "Hello, Howe Briggs! You have 8 unread messages.", "favoriteFruit" : "banana" } +{ "_id" : { "$oid" : "5e9ef66185c0145fa5d3c44b" }, "id" : { "$numberLong" : "4" }, "guid" : "4ded35ef-ba63-4eef-996f-d67e38553b0d", "isActive" : false, "balance" : 1159.0, "picture" : "http://placehold.it/32x32", "age" : 31, "name" : "Howe Briggs", "gender" : "male", "email" : "howebriggs@zappix.com", "phone" : "+1 (966) 518-3246", "address" : "963 Roosevelt Court, Bowden, Oregon, 6236", "about" : "Et eu culpa elit eiusmod ea proident ad est culpa elit. Dolor eiusmod officia nisi aliquip. Ut irure laborum qui dolor ut veniam est veniam nostrud consequat voluptate velit do duis. Irure excepteur excepteur reprehenderit nostrud reprehenderit voluptate quis ex aliquip. Sunt amet commodo pariatur fugiat est laborum. Est est non aliqua nisi laboris. Irure voluptate aute deserunt commodo nostrud amet anim reprehenderit nostrud cupidatat aliqua veniam anim.\r\n", "registered" : { "$date" : "2014-03-04T23:00:36.000+0000" }, "tags" : [ "veniam", "anim", "fugiat", "dolor", "elit", "nostrud", "quis" ], "friends" : [ { "id" : { "$numberLong" : "0" }, "name" : "Hunter Garner" }, { "id" : { "$numberLong" : "1" }, "name" : "Walls Wright" }, { "id" : { "$numberLong" : "2" }, "name" : "Christie Walker" }, { "id" : { "$numberLong" : "3" }, "name" : "Powell Woods" }, { "id" : { "$numberLong" : "4" }, "name" : "Doreen Carpenter" }, { "id" : { "$numberLong" : "5" }, "name" : "Beach Harrison" }, { "id" : { "$numberLong" : "6" }, "name" : "Perkins Mullins" }, { "id" : { "$numberLong" : "7" }, "name" : "Blake Goff" }, { "id" : { "$numberLong" : "8" }, "name" : "Clarke Spears" } ], "bestFriend": { "id" : { "$numberLong" : "8" }, "name" : "Clarke Spears" }, "greeting" : "Hello, Howe Briggs! You have 8 unread messages.", "favoriteFruit" : "banana" } { "_id" : { "$oid" : "5e9ef66185c0145fa5d3c44c" }, "id" : { "$numberLong" : "5" }, "guid" : "4791d80d-0079-4dba-9f40-a1b317e1dedd", "isActive" : true, "balance" : 2132.0, "picture" : "http://placehold.it/32x32", "age" : 40, "name" : "Massey Sears", "gender" : "male", "email" : "masseysears@kog.com", "phone" : "+1 (914) 433-2474", "address" : "232 Claver Place, Omar, Kentucky, 1244", "about" : "Laboris nisi pariatur elit culpa tempor cupidatat voluptate officia labore. Magna exercitation amet sunt aliquip reprehenderit incididunt. Cupidatat id cupidatat ea officia duis ex minim. Lorem velit irure mollit non magna non consectetur tempor excepteur. Labore commodo in sit duis laborum ea nulla anim. Do voluptate adipisicing sit magna sit.\r\n", "registered" : { "$date" : "2014-04-14T11:26:33.000+0000" }, "tags" : [ "excepteur", "aliqua", "veniam", "pariatur", "incididunt", "sint", "duis" ], "friends" : [ { "id" : { "$numberLong" : "0" }, "name" : "Katie Holden" }, { "id" : { "$numberLong" : "1" }, "name" : "Payne French" }, { "id" : { "$numberLong" : "2" }, "name" : "Nannie Snyder" }, { "id" : { "$numberLong" : "3" }, "name" : "Yang Carey" }, { "id" : { "$numberLong" : "4" }, "name" : "Reilly Valdez" } ], "greeting" : "Hello, Massey Sears! You have 6 unread messages.", "favoriteFruit" : "strawberry" } { "_id" : { "$oid" : "5e9ef66185c0145fa5d3c44d" }, "id" : { "$numberLong" : "6" }, "guid" : "09552721-4ff8-4898-8066-16d4e8bbcea0", "isActive" : false, "balance" : 3872.0, "picture" : "http://placehold.it/32x32", "age" : 22, "name" : "Cecile Rogers", "gender" : "female", "email" : "cecilerogers@momentia.com", "phone" : "+1 (892) 476-2858", "address" : "149 Jerome Avenue, Crawfordsville, New Mexico, 6417", "about" : "Cupidatat ipsum enim eu nulla. Irure fugiat sint in ad dolore sunt duis sit culpa eu. Nisi ea est sint enim enim aliqua dolore labore proident ad. Ullamco cupidatat labore laboris cillum qui duis adipisicing officia cupidatat officia ullamco qui. Ut occaecat non qui labore consequat. Ex occaecat nulla sunt enim. Pariatur eu nisi ut non velit incididunt proident eu cillum culpa eu deserunt esse deserunt.\r\n", "registered" : { "$date" : "2014-03-27T19:24:33.000+0000" }, "tags" : [ "nostrud", "mollit", "ea", "eu", "consequat", "in", "veniam" ], "friends" : [ { "id" : { "$numberLong" : "0" }, "name" : "Rosario Spencer" }, { "id" : { "$numberLong" : "1" }, "name" : "Jacobson Sutton" }, { "id" : { "$numberLong" : "2" }, "name" : "Wooten Rivera" }, { "id" : { "$numberLong" : "3" }, "name" : "Guzman Johns" }, { "id" : { "$numberLong" : "4" }, "name" : "Liliana Campbell" }, { "id" : { "$numberLong" : "5" }, "name" : "Pamela Buchanan" }, { "id" : { "$numberLong" : "6" }, "name" : "Avila Dillon" } ], "greeting" : "Hello, Cecile Rogers! You have 8 unread messages.", "favoriteFruit" : "strawberry" } { "_id" : { "$oid" : "5e9ef66185c0145fa5d3c44e" }, "id" : { "$numberLong" : "7" }, "guid" : "943a13b8-bf7f-443b-bb36-280bf2328875", "isActive" : false, "balance" : 3815.0, "picture" : "http://placehold.it/32x32", "age" : 32, "name" : "Zelma Sweet", "gender" : "female", "email" : "zelmasweet@colaire.com", "phone" : "+1 (867) 535-3918", "address" : "358 Furman Street, Williams, South Dakota, 2685", "about" : "Et reprehenderit exercitation sint pariatur excepteur consectetur laboris. Minim aute esse non consectetur sunt aliquip tempor sunt magna aliqua est aliqua laboris. Excepteur do dolor sint mollit qui pariatur deserunt ipsum occaecat. Sunt enim ut mollit aliquip non do esse et nostrud dolor est occaecat. Labore ullamco ipsum ea eiusmod culpa. Cupidatat eu nisi tempor veniam consequat magna velit laborum eu incididunt minim quis. Ipsum anim cillum qui eiusmod Lorem aliqua incididunt adipisicing amet consequat velit.\r\n", "registered" : { "$date" : "2014-01-25T12:22:08.000+0000" }, "tags" : [ "minim", "dolore", "minim", "non", "velit", "mollit", "aliquip" ], "friends" : [ { "id" : { "$numberLong" : "0" }, "name" : "Erma Levine" }, { "id" : { "$numberLong" : "1" }, "name" : "Margaret Clayton" }, { "id" : { "$numberLong" : "2" }, "name" : "Norma Middleton" }, { "id" : { "$numberLong" : "3" }, "name" : "Susanne Bullock" }, { "id" : { "$numberLong" : "4" }, "name" : "Frazier Horn" }, { "id" : { "$numberLong" : "5" }, "name" : "Christy Young" }, { "id" : { "$numberLong" : "6" }, "name" : "Margarita Morales" }, { "id" : { "$numberLong" : "7" }, "name" : "Diana Hebert" }, { "id" : { "$numberLong" : "8" }, "name" : "Tonia Bell" }, { "id" : { "$numberLong" : "9" }, "name" : "Brandi Stafford" } ], "greeting" : "Hello, Zelma Sweet! You have 6 unread messages.", "favoriteFruit" : "strawberry" } diff --git a/src/test/resources/liquibase/00-init.xml b/src/test/resources/liquibase/00-init.xml new file mode 100755 index 00000000..6e85d128 --- /dev/null +++ b/src/test/resources/liquibase/00-init.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + Create table addressbook_entries for demonstrating refactorings + + + + + + + + + + + + + + + + + Create table addressbook_entries for demonstrating refactorings + + + + + + + + + + + + + + + + + + Load some Test Data + + + + + + + + + + + + + diff --git a/src/test/resources/liquibase/02-add-not-null-constraint.xml b/src/test/resources/liquibase/02-add-not-null-constraint.xml new file mode 100755 index 00000000..15b62c8d --- /dev/null +++ b/src/test/resources/liquibase/02-add-not-null-constraint.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/src/test/resources/liquibase/03-tag-database.xml b/src/test/resources/liquibase/03-tag-database.xml new file mode 100755 index 00000000..b20a0dee --- /dev/null +++ b/src/test/resources/liquibase/03-tag-database.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/src/test/resources/liquibase/04-split-table.xml b/src/test/resources/liquibase/04-split-table.xml new file mode 100755 index 00000000..7811f11a --- /dev/null +++ b/src/test/resources/liquibase/04-split-table.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/liquibase/05-add-foreign-keys.xml b/src/test/resources/liquibase/05-add-foreign-keys.xml new file mode 100755 index 00000000..8a54e186 --- /dev/null +++ b/src/test/resources/liquibase/05-add-foreign-keys.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/src/test/resources/liquibase/06-change-column-type.xml b/src/test/resources/liquibase/06-change-column-type.xml new file mode 100755 index 00000000..ccdc1aa9 --- /dev/null +++ b/src/test/resources/liquibase/06-change-column-type.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + diff --git a/src/test/resources/liquibase/07-merge-columns.xml b/src/test/resources/liquibase/07-merge-columns.xml new file mode 100755 index 00000000..89aca2b4 --- /dev/null +++ b/src/test/resources/liquibase/07-merge-columns.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/src/test/resources/liquibase/08-create-view.xml b/src/test/resources/liquibase/08-create-view.xml new file mode 100755 index 00000000..9c4d48f1 --- /dev/null +++ b/src/test/resources/liquibase/08-create-view.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + SELECT FIRSTNAME,LASTNAME,STREET,POSTCODE,CITY,PHONE FROM ADDRESSBOOK_ENTRIES + JOIN ADDRESS_DATA ON ADDRESSBOOK_ENTRIES.ID = ADDRESS_DATA.ENTRY_ID + JOIN PHONE_DATA ON ADDRESSBOOK_ENTRIES.ID = PHONE_DATA.ENTRY_ID + + + diff --git a/src/test/resources/liquibase/09-add-default-columns.xml b/src/test/resources/liquibase/09-add-default-columns.xml new file mode 100755 index 00000000..8040c9ef --- /dev/null +++ b/src/test/resources/liquibase/09-add-default-columns.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/liquibase/10-add-person.xml b/src/test/resources/liquibase/10-add-person.xml new file mode 100755 index 00000000..2cab2ee2 --- /dev/null +++ b/src/test/resources/liquibase/10-add-person.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/liquibase/11-add-note.xml b/src/test/resources/liquibase/11-add-note.xml new file mode 100755 index 00000000..96d34b56 --- /dev/null +++ b/src/test/resources/liquibase/11-add-note.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/liquibase/12-add-task-relation.xml b/src/test/resources/liquibase/12-add-task-relation.xml new file mode 100755 index 00000000..f538d2b5 --- /dev/null +++ b/src/test/resources/liquibase/12-add-task-relation.xml @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/liquibase/addressbook.csv b/src/test/resources/liquibase/addressbook.csv new file mode 100755 index 00000000..2f136d17 --- /dev/null +++ b/src/test/resources/liquibase/addressbook.csv @@ -0,0 +1,7 @@ +id,firstname,lastname,street_name,street_number,postcode,city,phone +1,Nicole,Theiss,Koenigstrasse,77,21279,Drestedt,04186952474 +2,Erik,Fried,Ollenhauer Str.,78,70499,Stuttgart Feuerbach,0711473923 +3,Torsten,Bieber,Albrechtstrasse,98,87413,Kempten,0831241900 +4,Andreas,Meyer,Neuer Jungfernstieg,91,84080,Laberweinting,08772071470 +5,Doreen,Grunewald,Güntzelstrasse,73,54472,Longkamp,06531854103 +6,Marko,Schneider,Buelowstrasse,79,57645,Nister,02662548331 diff --git a/src/test/resources/liquibase/changelog.xml b/src/test/resources/liquibase/changelog.xml new file mode 100755 index 00000000..fabe02fb --- /dev/null +++ b/src/test/resources/liquibase/changelog.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/scala/dev/mongocamp/driver/mongodb/dao/PersonDAOSpec.scala b/src/test/scala/dev/mongocamp/driver/mongodb/dao/PersonDAOSpec.scala index 3d5cccd7..04b2fcbf 100644 --- a/src/test/scala/dev/mongocamp/driver/mongodb/dao/PersonDAOSpec.scala +++ b/src/test/scala/dev/mongocamp/driver/mongodb/dao/PersonDAOSpec.scala @@ -20,7 +20,7 @@ class PersonDAOSpec extends PersonSpecification with MongoImplicits { "support columnNames" in { val columnNames = PersonDAO.columnNames(200) - columnNames.size mustEqual 19 + columnNames.size mustEqual 20 } "support results" in { diff --git a/src/test/scala/dev/mongocamp/driver/mongodb/jdbc/BaseJdbcSpec.scala b/src/test/scala/dev/mongocamp/driver/mongodb/jdbc/BaseJdbcSpec.scala new file mode 100644 index 00000000..4bfdcff0 --- /dev/null +++ b/src/test/scala/dev/mongocamp/driver/mongodb/jdbc/BaseJdbcSpec.scala @@ -0,0 +1,23 @@ +package dev.mongocamp.driver.mongodb.jdbc + +import better.files.{File, Resource} +import dev.mongocamp.driver.mongodb.dao.PersonSpecification +import dev.mongocamp.driver.mongodb.test.TestDatabase.PersonDAO + +import java.sql.{Connection, DriverManager} +import java.util.Properties + +class BaseJdbcSpec extends PersonSpecification { + var connection : Connection = _ + + override def beforeAll(): Unit = { + super.beforeAll() + val connectionProps = new Properties() + val driver = new MongoJdbcDriver() + DriverManager.registerDriver(driver) + connection = DriverManager.getConnection( + "jdbc:mongodb://localhost:27017/mongocamp-unit-test?retryWrites=true&loadBalanced=false&serverSelectionTimeoutMS=5000&connectTimeoutMS=10000", + connectionProps + ) + } +} diff --git a/src/test/scala/dev/mongocamp/driver/mongodb/jdbc/ExploreJdbcSpec.scala b/src/test/scala/dev/mongocamp/driver/mongodb/jdbc/ExploreJdbcSpec.scala new file mode 100644 index 00000000..558b14bf --- /dev/null +++ b/src/test/scala/dev/mongocamp/driver/mongodb/jdbc/ExploreJdbcSpec.scala @@ -0,0 +1,80 @@ +package dev.mongocamp.driver.mongodb.jdbc + +import dev.mongocamp.driver.mongodb.MongoDAO +import dev.mongocamp.driver.mongodb.dao.PersonSpecification +import dev.mongocamp.driver.mongodb.model.{Grade, Score} +import dev.mongocamp.driver.mongodb.test.TestDatabase +import org.bson.types.ObjectId + +import java.sql.{DriverManager, ResultSet, Types} +import java.util.Properties +import scala.collection.mutable.ArrayBuffer +import better.files.{File, Resource} +import dev.mongocamp.driver.mongodb.{GenericObservable, MongoDAO} +import dev.mongocamp.driver.mongodb.dao.PersonSpecification +import dev.mongocamp.driver.mongodb.model.{Grade, Score} +import dev.mongocamp.driver.mongodb.test.TestDatabase +import dev.mongocamp.driver.mongodb.test.TestDatabase.PersonDAO +import org.bson.types.ObjectId +import org.specs2.mutable.Specification +import org.specs2.specification.{BeforeAll, BeforeEach} + +class ExploreJdbcSpec extends BaseJdbcSpec { + + "Jdbc Connection" should { + + "get table names" in { + val tableNames = connection.getMetaData.getTables("%", "mongocamp-unit-test", "", Array.empty) + var tables = 0 + var tablePersonFound = false + while (tableNames.next()) { + tableNames.getString("TABLE_NAME") match { + case "people" => + tablePersonFound = true + tableNames.getString("TYPE_CAT") must beEqualTo("mongodb") + tableNames.getString("REMARKS") must beEqualTo("COLLECTION") + tableNames.getString("TABLE_TYPE") must beEqualTo("TABLE") + tableNames.getString("TABLE_SCHEM") must beEqualTo("mongocamp-unit-test") + case _ => + } + tables += 1 + } + tables must beGreaterThanOrEqualTo(1) + val columnNames = connection.getMetaData.getColumns("%", "mongocamp-unit-test", "people", "") + var columns = 0 + while (columnNames.next()) { + columnNames.getString("TABLE_CAT") must beEqualTo("mongodb") + columnNames.getString("TABLE_NAME") must beEqualTo("people") + columnNames.getString("TABLE_SCHEM") must beEqualTo("mongocamp-unit-test") + val KeyDataType = "DATA_TYPE" + columnNames.getString("COLUMN_NAME") match { + case "_id" => + columnNames.getInt(KeyDataType) must beEqualTo(Types.VARCHAR) + case "id" => + columnNames.getInt(KeyDataType) must beEqualTo(Types.BIGINT) + columnNames.getInt("DECIMAL_DIGITS") must beEqualTo(0) + case "guid" => + columnNames.getInt(KeyDataType) must beEqualTo(Types.LONGVARCHAR) + case "isActive" => + columnNames.getInt(KeyDataType) must beEqualTo(Types.BOOLEAN) + case "balance" => + columnNames.getInt(KeyDataType) must beEqualTo(Types.DOUBLE) + columnNames.getInt("DECIMAL_DIGITS") must beEqualTo(Int.MaxValue) + case "registered" => + columnNames.getInt(KeyDataType) must beEqualTo(Types.DATE) + case "tags" => + columnNames.getInt(KeyDataType) must beEqualTo(Types.ARRAY) + case "friends" => + columnNames.getInt(KeyDataType) must beEqualTo(Types.ARRAY) + case "bestFriend" => + columnNames.getInt(KeyDataType) must beEqualTo(Types.JAVA_OBJECT) + case _ => + } + columns += 1 + } + columns must beEqualTo(20) + tablePersonFound must beTrue + } + + } +} diff --git a/src/test/scala/dev/mongocamp/driver/mongodb/jdbc/LiquibaseJdbcSpec.scala b/src/test/scala/dev/mongocamp/driver/mongodb/jdbc/LiquibaseJdbcSpec.scala new file mode 100644 index 00000000..eb101900 --- /dev/null +++ b/src/test/scala/dev/mongocamp/driver/mongodb/jdbc/LiquibaseJdbcSpec.scala @@ -0,0 +1,41 @@ +package dev.mongocamp.driver.mongodb.jdbc + +import com.typesafe.scalalogging.LazyLogging +import liquibase.database.jvm.JdbcConnection +import liquibase.exception.LiquibaseException +import liquibase.resource.ClassLoaderResourceAccessor +import liquibase.{Contexts, LabelExpression, Liquibase} + +import scala.jdk.CollectionConverters._ +import scala.language.implicitConversions + + +class LiquibaseJdbcSpec extends BaseJdbcSpec with LazyLogging { + + "Jdbc Connection" should { + + "migrate database with liquibase" in { + val jdbcConnection = new JdbcConnection(connection) + val liquibase: Liquibase = new Liquibase("liquibase/changelog.xml", new ClassLoaderResourceAccessor(), jdbcConnection ) + val contexts = new Contexts() + val unrunChangesets = liquibase.listUnrunChangeSets(contexts, new LabelExpression()) + val changes = unrunChangesets.asScala.toList + if (changes.isEmpty) { + logger.info("liquibase - nothing to update") + true must beTrue + } + logger.info("liquibase - %s changesets to update".format(changes)) + try { + liquibase.update(contexts) + true must beTrue + } + catch { + case e: LiquibaseException => + logger.error(e.getMessage, e) + false must beTrue + } + + } + + } +} diff --git a/src/test/scala/dev/mongocamp/driver/mongodb/jdbc/SelectJDBCSpec.scala b/src/test/scala/dev/mongocamp/driver/mongodb/jdbc/SelectJDBCSpec.scala new file mode 100644 index 00000000..91cadbb7 --- /dev/null +++ b/src/test/scala/dev/mongocamp/driver/mongodb/jdbc/SelectJDBCSpec.scala @@ -0,0 +1,50 @@ +package dev.mongocamp.driver.mongodb.jdbc + +import java.sql.ResultSet +import scala.collection.mutable.ArrayBuffer + +class SelectJDBCSpec extends BaseJdbcSpec { + + "Jdbc Connection" should { + + "execute simple select" in { + val stmt = connection.createStatement() + val result = stmt.executeQuery("select id, guid, name, age, balance from people where age < 30 order by id asc") + var i = 0 + val arrayBuffer = ArrayBuffer[ResultSet]() + while (result.next()) { + i += 1 + arrayBuffer += result + } + arrayBuffer.size must beEqualTo(99) + i must beEqualTo(99) + } + + "execute prepared statement" in { + val preparedStatement = connection.prepareStatement("select * from `mongocamp-unit-test`.people where age < ? order by id asc") + preparedStatement.setLong(0, 30) + val result = preparedStatement.executeQuery() + var i = 0 + val arrayBuffer = ArrayBuffer[ResultSet]() + while (result.next()) { + i += 1 + arrayBuffer += result + } + arrayBuffer.size must beEqualTo(99) + i must beEqualTo(99) + } + + "count on empty table" in { + val stmt = connection.createStatement() + val result = stmt.executeQuery("select count(*) as tmp, sum(age) from empty;") + var i = 0 + while (result.next()) { + result.getInt("tmp") must beEqualTo(0) + result.getInt("sum(age)") must beEqualTo(0) + i += 1 + } + i must beEqualTo(1) + } + + } +} diff --git a/src/test/scala/dev/mongocamp/driver/mongodb/sql/DeleteSqlSpec.scala b/src/test/scala/dev/mongocamp/driver/mongodb/sql/DeleteSqlSpec.scala index d8015078..f92ce1f6 100644 --- a/src/test/scala/dev/mongocamp/driver/mongodb/sql/DeleteSqlSpec.scala +++ b/src/test/scala/dev/mongocamp/driver/mongodb/sql/DeleteSqlSpec.scala @@ -1,14 +1,11 @@ package dev.mongocamp.driver.mongodb.sql -import better.files.{File, Resource} -import dev.mongocamp.driver.mongodb.{GenericObservable, MongoDAO} -import dev.mongocamp.driver.mongodb.dao.PersonSpecification import dev.mongocamp.driver.mongodb.model.{Grade, Score} import dev.mongocamp.driver.mongodb.test.TestDatabase -import dev.mongocamp.driver.mongodb.test.TestDatabase.PersonDAO +import dev.mongocamp.driver.mongodb.{GenericObservable, MongoDAO} import org.bson.types.ObjectId import org.specs2.mutable.Specification -import org.specs2.specification.{BeforeAll, BeforeEach} +import org.specs2.specification.BeforeEach class DeleteSqlSpec extends Specification with BeforeEach { sequential diff --git a/src/test/scala/dev/mongocamp/driver/mongodb/sql/OtherSqlSpec.scala b/src/test/scala/dev/mongocamp/driver/mongodb/sql/OtherSqlSpec.scala index 23f3caaf..2926b9f3 100644 --- a/src/test/scala/dev/mongocamp/driver/mongodb/sql/OtherSqlSpec.scala +++ b/src/test/scala/dev/mongocamp/driver/mongodb/sql/OtherSqlSpec.scala @@ -10,6 +10,7 @@ import org.mongodb.scala.model.Sorts.ascending import org.specs2.mutable.Specification import org.specs2.specification.BeforeEach +import java.sql.SQLException import scala.concurrent.duration.DurationInt class OtherSqlSpec extends PersonSpecification with BeforeEach{ @@ -48,6 +49,17 @@ class OtherSqlSpec extends PersonSpecification with BeforeEach{ collections must not contain "universityGrades" } + "catch sql error on converting sql" in { + var errorCaught = false + try { + MongoSqlQueryHolder("blub from universityGrades;") + } catch { + case _: SQLException => + errorCaught = true + } + errorCaught mustEqual true + } + "truncate collection" in { val queryConverter = MongoSqlQueryHolder("TRUNCATE TABLE universityGrades;") val selectResponse = queryConverter.run(TestDatabase.provider).resultList() @@ -99,7 +111,8 @@ class OtherSqlSpec extends PersonSpecification with BeforeEach{ val queryConverter = MongoSqlQueryHolder("show tables;") val selectResponse = queryConverter.run(TestDatabase.provider).resultList() selectResponse.size must be greaterThanOrEqualTo(1) - selectResponse.head.getStringValue("name") mustEqual "mongo-sync-log" + val filteredDocuments = selectResponse.filter(d => d.getStringValue("name").equalsIgnoreCase("people")) + filteredDocuments.head.getStringValue("name") mustEqual "people" } "show databases" in { diff --git a/src/test/scala/dev/mongocamp/driver/mongodb/sql/SelectSqlSpec.scala b/src/test/scala/dev/mongocamp/driver/mongodb/sql/SelectSqlSpec.scala index 4f750e66..63c539e1 100644 --- a/src/test/scala/dev/mongocamp/driver/mongodb/sql/SelectSqlSpec.scala +++ b/src/test/scala/dev/mongocamp/driver/mongodb/sql/SelectSqlSpec.scala @@ -16,11 +16,12 @@ class SelectSqlSpec extends PersonSpecification { } "simple sql with schema" in { - val queryConverter = MongoSqlQueryHolder("select * from `mongocamp-unit-test`.`friend`") + val queryConverter = MongoSqlQueryHolder("select * from `mongocamp-unit-test`.`people`") val selectResponse = queryConverter.run(TestDatabase.provider).resultList() - selectResponse.size mustEqual 1327 - selectResponse.head.getString("name") mustEqual "Castaneda Mccullough" - selectResponse.head.getLong("id") mustEqual 33 + queryConverter.getCollection mustEqual "mongocamp-unit-test:people" + selectResponse.size mustEqual 200 + selectResponse.head.getString("name") mustEqual "Cheryl Hoffman" + selectResponse.head.getLong("id") mustEqual 0 } "sql with in query" in { @@ -100,6 +101,15 @@ class SelectSqlSpec extends PersonSpecification { document.getInteger("age") mustEqual 25 } + "only count" in { + val queryConverter = MongoSqlQueryHolder("select count(*) as tmp, sum(age) from people;") + val selectResponse = queryConverter.run(TestDatabase.provider).resultList() + selectResponse.size mustEqual 1 + val document = selectResponse.head + document.getInteger("tmp") mustEqual 200 + document.getInteger("sum(age)") mustEqual 5961 + } + "group by with count" in { val queryConverter = MongoSqlQueryHolder("select age, count(*) as tmp, sum(age) from people group by age order by age;") val selectResponse = queryConverter.run(TestDatabase.provider).resultList()