Files
bstore-j/src/main/java/com/reliancy/dbo/sql/SQLTerminal.java

309 lines
12 KiB
Java

/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo.sql;
import java.io.IOException;
import java.lang.reflect.Array;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.JDBCType;
import java.sql.SQLException;
import java.sql.Types;
import java.util.HashMap;
import java.util.Map;
import com.reliancy.dbo.Action;
import com.reliancy.dbo.ActionHero;
import com.reliancy.dbo.Entity;
import com.reliancy.dbo.Terminal;
import com.reliancy.dbo.meta.MetaTerminal;
import com.reliancy.util.Path;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
/**
* JDBC/SQL implementation of {@link Terminal} with connection pooling.
*
* <p>This class provides database connectivity and CRUD operations for SQL databases
* using JDBC. It leverages HikariCP for high-performance connection pooling and
* supports multiple database vendors (PostgreSQL, MySQL, SQL Server, Oracle, H2).
*
* <h2>Connection URL Format:</h2>
* <pre>
* protocol://username:password@host:port/database
*
* Examples:
* postgres://user:pass@localhost:5432/mydb
* mysql://root:secret@192.168.1.100:3306/app
* sqlserver://sa:pass@server:1433/testdb
* h2://sa:@mem:testdb
* </pre>
*
* <h2>Features:</h2>
* <ul>
* <li><b>Connection Pooling</b>: HikariCP for efficient connection reuse</li>
* <li><b>Type Mapping</b>: Automatic Java ↔ SQL type conversion</li>
* <li><b>Vendor Support</b>: Database-specific SQL dialect handling</li>
* <li><b>Prepared Statements</b>: Caching enabled for performance (250 statements, 2KB max)</li>
* </ul>
*
* <h2>Usage Example:</h2>
* <pre>{@code
* SQLTerminal db = new SQLTerminal("postgres://user:pass@localhost:5432/mydb");
*
* // High-level operations
* PersonDBO person = db.load(PersonDBO.class, 42);
* person.set(PersonDBO.NAME, "Jane");
* db.save(person);
*
* // Low-level Action API
* try (Action query = db.begin()
* .load(PersonDBO.class)
* .filterBy(PersonDBO.AGE.gte(21))
* .execute()) {
* for (DBO record : query) {
* // Process records
* }
* }
* }</pre>
*
* <h2>Type Mapping:</h2>
* <p>The terminal maintains bidirectional type mappings:
* <ul>
* <li>{@link #getJava2SQL()} - Java Class → JDBC Types constant</li>
* <li>{@link #getSQL2Java()} - JDBC Types constant → Java Class</li>
* <li>{@link #getTypeName(Class, String)} - Java Class → SQL type name with parameters</li>
* </ul>
*
* <h3>Common Type Mappings:</h3>
* <table>
* <caption>Java to SQL type mappings</caption>
* <tr><th>Java Type</th><th>SQL Type</th></tr>
* <tr><td>Integer</td><td>INTEGER</td></tr>
* <tr><td>Long</td><td>BIGINT</td></tr>
* <tr><td>String</td><td>VARCHAR(n)</td></tr>
* <tr><td>BigDecimal</td><td>DECIMAL(p,s)</td></tr>
* <tr><td>java.sql.Date</td><td>DATE</td></tr>
* <tr><td>java.sql.Timestamp</td><td>TIMESTAMP/DATETIME</td></tr>
* <tr><td>Boolean</td><td>BOOLEAN/BIT (vendor-specific)</td></tr>
* </table>
*
* <h2>Database-Specific Handling:</h2>
* <p>The terminal adapts to different SQL dialects:
* <ul>
* <li><b>PostgreSQL</b>: Uses ILIKE for case-insensitive matching, BYTEA for binary, TEXT for large strings</li>
* <li><b>SQL Server</b>: BIT for boolean, DATETIME for timestamp, VARCHAR(MAX) for large text</li>
* <li><b>Oracle</b>: INTEGER for boolean, CLOB for large text</li>
* <li><b>MySQL</b>: TEXT for large strings</li>
* <li><b>H2</b>: In-memory database support</li>
* </ul>
*
* <h2>Identifier Quoting:</h2>
* <p>The terminal uses database-specific identifier quotes:
* <ul>
* <li>Standard: {@code "tablename"."columnname"}</li>
* <li>Access via {@link #getQuoteLeft()} and {@link #getQuoteRight()}</li>
* </ul>
*
* @see Terminal
* @see Action
* @see SQLReader
* @see SQLWriter
* @see SQLCleaner
*/
public class SQLTerminal implements Terminal{
HikariConfig config = new HikariConfig();
HikariDataSource ds;
Path url;
String quoteLeft="\""; // quotes could be subject to sql flavour
String quoteRight="\"";
public SQLTerminal(String url){
this.url=new Path(url);
String proto=this.url.getProtocol();
if(!proto.startsWith("jdbc:")) proto="jdbc:"+proto;
String u=proto+"://"+this.url.getHost()+":"+this.url.getPort()+"/"+this.url.getDatabase();
config.setJdbcUrl(u);
config.setUsername(this.url.getUserid());
config.setPassword(this.url.getPassword());
//config.setAutoCommit(false); -- do this in batch cases only
config.addDataSourceProperty( "cachePrepStmts" , "true" );
config.addDataSourceProperty( "prepStmtCacheSize" , "250" );
config.addDataSourceProperty( "prepStmtCacheSqlLimit" , "2048" );
ds = new HikariDataSource( config );
}
public Connection getConnection() throws SQLException{
return ds.getConnection();
}
@Override
public ActionHero getExecutor(Entity ent, Action.Trait trait) {
// Default executors for regular entities
if(trait instanceof Action.Load){
return new SQLReader(ent, this);
}else if(trait instanceof Action.Save){
return new SQLWriter(ent, this);
}else if(trait instanceof Action.Delete){
return new SQLCleaner(ent, this);
}else{
throw new UnsupportedOperationException("Trait not supported:"+trait);
}
}
public String getProtocol() {
return url.getProtocol();
}
public String getQuoteLeft(){
return this.quoteLeft;
}
public String getQuoteRight(){
return this.quoteRight;
}
final HashMap<Integer,Class<?>> sql2java=new HashMap<>();
final HashMap<Class<?>,Integer> java2sql=new HashMap<>();
public Map<Class<?>,Integer> getJava2SQL(){
if(!java2sql.isEmpty()) return java2sql;
String protocol=url.getProtocol();
java2sql.put(java.math.BigDecimal.class,Types.DECIMAL);
java2sql.put(java.math.BigInteger.class,Types.DECIMAL);
java2sql.put(Boolean.class,protocol.contains(":oracle")?Types.INTEGER:Types.BOOLEAN);
java2sql.put(Byte.class,Types.TINYINT);
java2sql.put(Short.class,Types.SMALLINT);
java2sql.put(Integer.class,Types.INTEGER);
java2sql.put(Long.class,Types.BIGINT);
java2sql.put(Float.class,Types.FLOAT);
java2sql.put(Double.class,Types.DOUBLE);
java2sql.put(byte[].class,Types.VARBINARY);
java2sql.put(Blob.class,Types.BLOB);
java2sql.put(char[].class,Types.VARCHAR);
java2sql.put(String.class,Types.VARCHAR);
java2sql.put(StringBuffer.class,Types.VARCHAR);
java2sql.put(Clob.class,Types.CLOB);
java2sql.put(java.sql.Date.class,Types.DATE);
java2sql.put(java.sql.Time.class,Types.TIME);
java2sql.put(java.sql.Timestamp.class,Types.TIMESTAMP);
java2sql.put(Array.class,Types.ARRAY);
return java2sql;
}
public Map<Integer,Class<?>> getSQL2Java(){
if(!sql2java.isEmpty()) return sql2java;
//String protocol=url.getProtocol();
sql2java.put(Types.NUMERIC,java.math.BigDecimal.class);
sql2java.put(Types.DECIMAL,java.math.BigDecimal.class);
sql2java.put(Types.BIT,Boolean.class);
sql2java.put(Types.BOOLEAN,Boolean.class);
sql2java.put(Types.TINYINT,Byte.class);
sql2java.put(Types.SMALLINT,Short.class);
sql2java.put(Types.INTEGER,Integer.class);
sql2java.put(Types.BIGINT,Long.class);
sql2java.put(Types.REAL,Float.class);
sql2java.put(Types.FLOAT,Float.class);
sql2java.put(Types.DOUBLE,Double.class);
sql2java.put(Types.BINARY,byte[].class);
sql2java.put(Types.VARBINARY,byte[].class);
sql2java.put(Types.LONGVARBINARY,byte[].class);
sql2java.put(Types.CHAR,String.class);
sql2java.put(Types.NCHAR,String.class);
sql2java.put(Types.VARCHAR,String.class);
sql2java.put(Types.NVARCHAR,String.class);
sql2java.put(Types.LONGVARCHAR,String.class);
sql2java.put(Types.LONGNVARCHAR,String.class);
sql2java.put(Types.DATE,java.sql.Date.class);
sql2java.put(Types.TIME,java.sql.Time.class);
sql2java.put(Types.TIMESTAMP,java.sql.Timestamp.class);
sql2java.put(Types.BLOB,byte[].class);
sql2java.put(Types.CLOB,char[].class);
sql2java.put(Types.ARRAY,java.sql.Array.class);
sql2java.put(Types.JAVA_OBJECT,Object.class);
return sql2java;
}
/**
* Returns back java class for given id and or name.
* The name is not used in default implementation.
* @param typeid sql type to map
* @return Class matching sql typeid.
*/
public Class<?> getJavaType(int typeid) {
Class<?> ret=getSQL2Java().get(typeid);
return ret;
}
/**
* This method will correct cases when sqltype is varchar (12) but type name is date or similar.
* @param sqltype
* @param type_name
* @return tries to promote sqltype given type name to something more specific.
*/
public int getTypeId(int sqltype,String type_name){
if(type_name==null) return sqltype;
type_name=type_name.toLowerCase();
if(sqltype==Types.VARCHAR || sqltype==Types.CHAR){
if(type_name.equals("date")) sqltype=Types.DATE;
if(type_name.equals("time")) sqltype=Types.TIME;
if(type_name.equals("datetime")) sqltype=Types.TIMESTAMP;
}
return sqltype;
}
/**
* @param cls
* @param createParams
* @return SQL type given java class and create params
*/
public int getTypeId(Class<?> cls,String createParams){
int ret=getJava2SQL().get(cls);
return ret;
}
public String getTypeName(Class<?> cls,String createParams){
int id=this.getTypeId(cls, createParams);
String ret = JDBCType.valueOf(id).getName();
if(ret==null) return null;
String protocol=url.getProtocol();
if(protocol.contains(":sqlserver")){
if("boolean".equalsIgnoreCase(ret)) ret="BIT";
if("timestamp".equalsIgnoreCase(ret)) ret="DATETIME";
if("double".equalsIgnoreCase(ret)) ret="float";
if("float".equalsIgnoreCase(ret)) ret="real";
}
if(protocol.contains(":postgre")){
if("varbinary".equalsIgnoreCase(ret)) ret="bytea";
if("double".equalsIgnoreCase(ret)) ret="double precision";
}
if("varchar".equalsIgnoreCase(ret) && (createParams!=null && !createParams.isEmpty())){
long size=Long.parseLong(createParams);
if(protocol.contains(":sqlserver")) ret=size>8000?ret.concat("(").concat("MAX").concat(")"):ret.concat("(").concat(String.valueOf(size)).concat(")");
else if(protocol.contains(":oracle")) ret=size>2000?"CLOB":ret.concat("(").concat(String.valueOf(size)).concat(")");
else if(protocol.contains(":mysql")) ret=size>Character.MAX_VALUE?"TEXT":ret.concat("(").concat(String.valueOf(size)).concat(")");
else if(protocol.contains(":h2")) ret=size>Integer.MAX_VALUE?"CLOB":ret.concat("(").concat(String.valueOf(size)).concat(")");
else if(protocol.contains(":postgre")) ret=size>Character.MAX_VALUE?"TEXT":ret.concat("(").concat(String.valueOf(size)).concat(")");
else ret=(size>Character.MAX_VALUE)?"CLOB":ret.concat("(").concat(String.valueOf(size)).concat(")");
}
String args=null;
if(ret.indexOf('(')==-1 && createParams!=null && !createParams.isEmpty()){
if("decimal".equalsIgnoreCase(ret)) args=createParams;
if("numeric".equalsIgnoreCase(ret)) args=createParams;
}
if(args!=null){
ret=ret.concat("(").concat(args).concat(")");
}
return ret;
}
/**
* Returns a MetaTerminal instance for metadata operations.
*
* @param ent the entity (unused, kept for interface compatibility)
* @return a new SQLMetaTerminal instance
*/
@Override
public com.reliancy.dbo.meta.MetaTerminal meta(Entity ent) {
return new SQLMetaTerminal(this);
}
}