package mil.nga.geopackage.io; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Types; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Scanner; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import mil.nga.geopackage.GeoPackage; import mil.nga.geopackage.core.contents.ContentsDataType; import mil.nga.geopackage.db.CoreSQLUtils; import mil.nga.geopackage.db.SQLUtils; import mil.nga.geopackage.db.master.SQLiteMaster; import mil.nga.geopackage.db.master.SQLiteMasterColumn; import mil.nga.geopackage.db.master.SQLiteMasterQuery; import mil.nga.geopackage.db.master.SQLiteMasterType; import mil.nga.geopackage.db.table.TableColumn; import mil.nga.geopackage.db.table.TableInfo; import mil.nga.geopackage.extension.RTreeIndexExtension; import mil.nga.geopackage.manager.GeoPackageManager; import mil.nga.geopackage.validate.GeoPackageValidate; /** * Executes SQL on a SQLite database * * To run from command line, build with the standalone profile: * * mvn clean install -Pstandalone * * java -jar name.jar +usage_arguments * * java -classpath name.jar mil.nga.geopackage.io.SQLExec +usage_arguments * * @author osbornb * @since 3.3.0 */ public class SQLExec { /** * Argument prefix */ public static final String ARGUMENT_PREFIX = "-"; /** * Max Rows argument */ public static final String ARGUMENT_MAX_ROWS = "m"; /** * Default max rows */ public static final int DEFAULT_MAX_ROWS = 100; /** * History pattern */ public static final Pattern HISTORY_PATTERN = Pattern.compile("^!-?\\d+$"); /** * Command prompt */ public static final String COMMAND_PROMPT = "sql> "; /** * Help command */ public static final String COMMAND_HELP = "help"; /** * Tables command */ public static final String COMMAND_TABLES = "tables"; /** * Indexes command */ public static final String COMMAND_INDEXES = "indexes"; /** * Views command */ public static final String COMMAND_VIEWS = "views"; /** * Triggers command */ public static final String COMMAND_TRIGGERS = "triggers"; /** * Command with all rows */ public static final int COMMAND_ALL_ROWS = 2147483646; /** * History command */ public static final String COMMAND_HISTORY = "history"; /** * Previous command */ public static final String COMMAND_PREVIOUS = "!!"; /** * Write blobs command */ public static final String COMMAND_WRITE_BLOBS = "blobs"; /** * Max rows command */ public static final String COMMAND_MAX_ROWS = "rows"; /** * Table Info command */ public static final String COMMAND_TABLE_INFO = "info"; /** * SQLite Master command */ public static final String COMMAND_SQLITE_MASTER = "sqlite_master"; /** * GeoPackage contents command */ public static final String COMMAND_CONTENTS = "contents"; /** * GeoPackage Info command */ public static final String COMMAND_GEOPACKAGE_INFO = "ginfo"; /** * GeoPackage extensions command */ public static final String COMMAND_EXTENSIONS = "extensions"; /** * Blob display value */ public static final String BLOB_DISPLAY_VALUE = "BLOB"; /** * Default write directory for blobs */ public static final String BLOBS_WRITE_DEFAULT_DIRECTORY = "blobs"; /** * Blobs extension argument */ public static final String BLOBS_ARGUMENT_EXTENSION = "e"; /** * Blobs directory argument */ public static final String BLOBS_ARGUMENT_DIRECTORY = "d"; /** * Blobs pattern argument */ public static final String BLOBS_ARGUMENT_PATTERN = "p"; /** * Blobs column start regex */ public static final String BLOBS_COLUMN_START_REGEX = "\\("; /** * Blobs column end regex */ public static final String BLOBS_COLUMN_END_REGEX = "\\)"; /** * Blobs column pattern */ public static final Pattern BLOBS_COLUMN_PATTERN = Pattern .compile(BLOBS_COLUMN_START_REGEX + "([^" + BLOBS_COLUMN_END_REGEX + "]+)" + BLOBS_COLUMN_END_REGEX); /** * Blobs column pattern group */ public static final int BLOBS_COLUMN_PATTERN_GROUP = 1; /** * Main method to execute SQL in a SQLite database * * @param args * arguments * @throws Exception * upon failure */ public static void main(String[] args) throws Exception { boolean valid = true; boolean requiredArguments = false; File sqliteFile = null; Integer maxRows = null; StringBuilder sql = null; for (int i = 0; valid && i < args.length; i++) { String arg = args[i]; // Handle optional arguments if (arg.startsWith(ARGUMENT_PREFIX)) { String argument = arg.substring(ARGUMENT_PREFIX.length()); switch (argument) { case ARGUMENT_MAX_ROWS: if (i < args.length) { String maxRowsString = args[++i]; try { maxRows = Integer.valueOf(maxRowsString); } catch (NumberFormatException e) { valid = false; System.out.println("Error: Max Rows argument '" + arg + "' must be followed by a valid number. Invalid: " + maxRowsString); } } else { valid = false; System.out.println("Error: Max Rows argument '" + arg + "' must be followed by a valid number"); } break; default: valid = false; System.out.println("Error: Unsupported arg: '" + arg + "'"); } } else { // Set required arguments in order if (sqliteFile == null) { sqliteFile = new File(arg); requiredArguments = true; } else if (sql == null) { sql = new StringBuilder(arg); } else { sql.append(" ").append(arg); } } } if (!valid || !requiredArguments) { printUsage(); } else { GeoPackage database = GeoPackageManager.open(sqliteFile, false); try { if (isGeoPackage(database)) { System.out.print("GeoPackage"); } else { System.out.print("Database"); } System.out.println(": " + database.getName()); System.out.println("Path: " + database.getPath()); System.out.println("Max Rows: " + (maxRows != null ? maxRows : DEFAULT_MAX_ROWS)); if (sql != null) { try { SQLExecResult result = executeSQL(database, sql.toString(), maxRows); result.printResults(); } catch (Exception e) { System.out.println(e); } } else { commandPrompt(database, maxRows); } } finally { database.close(); } } } /** * Command prompt accepting SQL statements * * @param database * open database */ private static void commandPrompt(GeoPackage database, Integer maxRows) { printHelp(database); List<String> history = new ArrayList<>(); Scanner scanner = new Scanner(System.in); try { StringBuilder sqlBuilder = new StringBuilder(); resetCommandPrompt(sqlBuilder); while (scanner.hasNextLine()) { try { String sqlLine = scanner.nextLine().trim(); int semicolon = sqlLine.indexOf(";"); boolean executeSql = semicolon >= 0; if (executeSql) { sqlLine = sqlLine.substring(0, semicolon + 1); } boolean singleLine = sqlBuilder.length() == 0; if (!sqlLine.isEmpty()) { if (!singleLine) { sqlBuilder.append(" "); } sqlBuilder.append(sqlLine); } if (singleLine) { if (executeSql) { sqlLine = sqlLine.substring(0, sqlLine.length() - 1) .trim(); } boolean command = true; if (sqlLine.isEmpty()) { break; } else if (sqlLine.equalsIgnoreCase(COMMAND_HELP)) { printHelp(database); resetCommandPrompt(sqlBuilder); } else if (sqlLine.toLowerCase() .startsWith(COMMAND_TABLES)) { String name = sqlLine .substring(COMMAND_TABLES.length(), sqlLine.length()) .trim(); String sql = buildSqlMasterQuery(false, SQLiteMasterType.TABLE, name); executeSQL(database, sqlBuilder, sql, COMMAND_ALL_ROWS, history); } else if (sqlLine.toLowerCase() .startsWith(COMMAND_INDEXES)) { String name = sqlLine .substring(COMMAND_INDEXES.length(), sqlLine.length()) .trim(); String sql = buildSqlMasterQuery(true, SQLiteMasterType.INDEX, name); executeSQL(database, sqlBuilder, sql, COMMAND_ALL_ROWS, history); } else if (sqlLine.toLowerCase() .startsWith(COMMAND_VIEWS)) { String name = sqlLine .substring(COMMAND_VIEWS.length(), sqlLine.length()) .trim(); String sql = buildSqlMasterQuery(false, SQLiteMasterType.VIEW, name); executeSQL(database, sqlBuilder, sql, COMMAND_ALL_ROWS, history); } else if (sqlLine.toLowerCase() .startsWith(COMMAND_TRIGGERS)) { String name = sqlLine .substring(COMMAND_TRIGGERS.length(), sqlLine.length()) .trim(); String sql = buildSqlMasterQuery(true, SQLiteMasterType.TRIGGER, name); executeSQL(database, sqlBuilder, sql, COMMAND_ALL_ROWS, history); } else if (sqlLine.equalsIgnoreCase(COMMAND_HISTORY)) { for (int i = 0; i < history.size(); i++) { System.out.println( " " + String.format("%4d", i + 1) + " " + history.get(i)); } resetCommandPrompt(sqlBuilder); } else if (sqlLine.equalsIgnoreCase(COMMAND_PREVIOUS)) { executeSQL(database, sqlBuilder, history.size(), maxRows, history); } else if (sqlLine.toLowerCase() .startsWith(COMMAND_WRITE_BLOBS)) { writeBlobs(database, sqlBuilder, maxRows, history, sqlLine.substring( COMMAND_WRITE_BLOBS.length())); } else if (HISTORY_PATTERN.matcher(sqlLine).matches()) { int historyNumber = Integer.parseInt( sqlLine.substring(1, sqlLine.length())); executeSQL(database, sqlBuilder, historyNumber, maxRows, history); } else if (sqlLine.toLowerCase() .startsWith(COMMAND_MAX_ROWS)) { maxRows = Integer.parseInt( sqlLine.substring(COMMAND_MAX_ROWS.length(), sqlLine.length()).trim()); System.out.println("Max Rows: " + maxRows); resetCommandPrompt(sqlBuilder); } else if (sqlLine.toLowerCase() .startsWith(COMMAND_TABLE_INFO)) { String tableName = sqlLine .substring(COMMAND_TABLE_INFO.length(), sqlLine.length()) .trim(); if (!tableName.isEmpty()) { executeSQL(database, sqlBuilder, "PRAGMA table_info(\"" + tableName + "\");", COMMAND_ALL_ROWS, history); } else { resetCommandPrompt(sqlBuilder); } } else if (sqlLine .equalsIgnoreCase(COMMAND_SQLITE_MASTER) || SQLiteMaster.count(database.getDatabase(), new SQLiteMasterType[] { SQLiteMasterType.TABLE, SQLiteMasterType.VIEW }, SQLiteMasterQuery.create( SQLiteMasterColumn.NAME, sqlLine)) > 0) { executeSQL(database, sqlBuilder, "SELECT * FROM \"" + sqlLine + "\";", maxRows, history); } else if (isGeoPackage(database)) { if (sqlLine.toLowerCase() .startsWith(COMMAND_CONTENTS)) { String tableName = sqlLine .substring(COMMAND_CONTENTS.length(), sqlLine.length()) .trim(); StringBuilder sql = new StringBuilder( "SELECT table_name, data_type FROM gpkg_contents"); if (!tableName.isEmpty()) { sql.append(" WHERE table_name LIKE "); sql.append( CoreSQLUtils.quoteWrap(tableName)); } sql.append(" ORDER BY table_name;"); executeSQL(database, sqlBuilder, sql.toString(), COMMAND_ALL_ROWS, history); } else if (sqlLine.toLowerCase() .startsWith(COMMAND_GEOPACKAGE_INFO)) { String tableName = sqlLine.substring( COMMAND_GEOPACKAGE_INFO.length(), sqlLine.length()).trim(); if (!tableName.isEmpty()) { executeSQL(database, sqlBuilder, "SELECT * FROM gpkg_contents WHERE LOWER(table_name) = '" + tableName.toLowerCase() + "';", COMMAND_ALL_ROWS, history, false); ContentsDataType dataType = database .getTableDataType(tableName); if (dataType != null) { switch (dataType) { case ATTRIBUTES: break; case FEATURES: executeSQL(database, sqlBuilder, "SELECT * FROM gpkg_geometry_columns WHERE table_name = '" + tableName + "';", COMMAND_ALL_ROWS, history, false); break; case GRIDDED_COVERAGE: executeSQL(database, sqlBuilder, "SELECT * FROM gpkg_2d_gridded_coverage_ancillary WHERE tile_matrix_set_name = '" + tableName + "';", COMMAND_ALL_ROWS, history, false); executeSQL(database, sqlBuilder, "SELECT * FROM gpkg_2d_gridded_tile_ancillary WHERE tpudt_name = '" + tableName + "';", COMMAND_ALL_ROWS, history, false); case TILES: executeSQL(database, sqlBuilder, "SELECT * FROM gpkg_tile_matrix_set WHERE table_name = '" + tableName + "';", COMMAND_ALL_ROWS, history, false); executeSQL(database, sqlBuilder, "SELECT * FROM gpkg_tile_matrix WHERE table_name = '" + tableName + "';", COMMAND_ALL_ROWS, history, false); break; } } executeSQL(database, sqlBuilder, "PRAGMA table_info(\"" + tableName + "\");", COMMAND_ALL_ROWS, history, false); } resetCommandPrompt(sqlBuilder); } else if (sqlLine.toLowerCase() .startsWith(COMMAND_EXTENSIONS)) { String tableName = sqlLine .substring(COMMAND_EXTENSIONS.length(), sqlLine.length()) .trim(); StringBuilder sql = new StringBuilder( "SELECT table_name, column_name, extension_name, definition FROM gpkg_extensions"); if (!tableName.isEmpty()) { sql.append( " WHERE LOWER(table_name) LIKE "); sql.append(CoreSQLUtils.quoteWrap( tableName.toLowerCase())); } sql.append(";"); executeSQL(database, sqlBuilder, sql.toString(), COMMAND_ALL_ROWS, history); } else { String[] parts = sqlLine.split("\\s+"); String dataType = parts[0]; if (ContentsDataType.fromName( dataType.toLowerCase()) != null || !database .getTables( dataType.toLowerCase()) .isEmpty() || !database.getTables(dataType) .isEmpty()) { StringBuilder sql = new StringBuilder( "SELECT table_name FROM gpkg_contents WHERE LOWER(data_type) = '"); sql.append(dataType.toLowerCase()); sql.append("'"); if (parts.length > 0) { String tableName = sqlLine .substring(dataType.length(), sqlLine.length()) .trim(); if (!tableName.isEmpty()) { sql.append(" AND table_name LIKE "); sql.append(CoreSQLUtils .quoteWrap(tableName)); } } sql.append(" ORDER BY table_name;"); executeSQL(database, sqlBuilder, sql.toString(), COMMAND_ALL_ROWS, history); } else { command = false; } } } else { command = false; } if (command) { executeSql = false; } } if (executeSql) { executeSQL(database, sqlBuilder, sqlBuilder.toString(), maxRows, history); } } catch (Exception e) { System.out.println(e); resetCommandPrompt(sqlBuilder); } } } finally { scanner.close(); } } /** * Build a SQLite Master table query * * @param tableName * true to include table name * @param type * SQLite Master type * @param name * name LIKE value * @return SQL */ private static String buildSqlMasterQuery(boolean tableName, SQLiteMasterType type, String name) { StringBuilder sql = new StringBuilder("SELECT "); sql.append(SQLiteMasterColumn.NAME.name().toLowerCase()); if (tableName) { sql.append(", "); sql.append(SQLiteMasterColumn.TBL_NAME.name().toLowerCase()); } sql.append(" FROM "); sql.append(SQLiteMaster.TABLE_NAME); sql.append(" WHERE "); sql.append(SQLiteMasterColumn.TYPE.name().toLowerCase()); sql.append(" = '"); sql.append(type.name().toLowerCase()); sql.append("' AND "); sql.append(SQLiteMasterColumn.NAME.name().toLowerCase()); sql.append(" NOT LIKE 'sqlite_%'"); if (name != null) { name = name.trim(); if (!name.isEmpty()) { sql.append(" AND "); sql.append(SQLiteMasterColumn.NAME.name().toLowerCase()); sql.append(" LIKE "); sql.append(CoreSQLUtils.quoteWrap(name)); } } sql.append(" ORDER BY "); sql.append(SQLiteMasterColumn.NAME.name().toLowerCase()); sql.append(";"); return sql.toString(); } /** * Print the command prompt help * * @param database * database */ private static void printHelp(GeoPackage database) { boolean isGeoPackage = isGeoPackage(database); System.out.println(); System.out.println("- Supports most SQLite statements including:"); System.out.println( "\tSELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, DROP, PRAGMA, VACUUM, etc"); System.out.println("- Terminate SQL statements with a ;"); System.out.println("- Exit with a single empty line"); System.out.println(); System.out.println("Commands:"); System.out.println(); System.out.println("\t" + COMMAND_HELP + " - print this help information"); System.out.println("\t" + COMMAND_TABLES + " [name] - list database tables (all or LIKE table name)"); System.out.println("\t" + COMMAND_INDEXES + " [name] - list database indexes (all or LIKE index name)"); System.out.println("\t" + COMMAND_VIEWS + " [name] - list database views (all or LIKE view name)"); System.out.println("\t" + COMMAND_TRIGGERS + " [name] - list database triggers (all or LIKE trigger name)"); System.out.println("\t" + COMMAND_MAX_ROWS + " n - set the max rows per query to n"); System.out.println("\t" + COMMAND_HISTORY + " - list successfully executed sql commands"); System.out.println("\t" + COMMAND_PREVIOUS + " - re-execute the previous successful sql command"); System.out.println( "\t!n - re-execute a sql statement by history id n"); System.out.println( "\t!-n - re-execute a sql statement n commands back in history"); System.out.println("\t" + COMMAND_WRITE_BLOBS + " [" + ARGUMENT_PREFIX + BLOBS_ARGUMENT_EXTENSION + " file_extension] [" + ARGUMENT_PREFIX + BLOBS_ARGUMENT_DIRECTORY + " directory] [" + ARGUMENT_PREFIX + BLOBS_ARGUMENT_PATTERN + " pattern]"); System.out.println( "\t - write blobs from the previous successful sql command to the file system"); System.out.println( "\t ([directory]|blobs)/table_name/column_name/(pk_values|result_index|[pattern])[.file_extension]"); System.out.println( "\t file_extension - file extension added to each saved blob file"); System.out.println( "\t directory - base directory to save table_name/column_name/blobs (default is ./" + BLOBS_WRITE_DEFAULT_DIRECTORY + ")"); System.out.println( "\t pattern - file directory and/or name pattern consisting of column names in parentheses"); System.out.println( "\t (column_name)-(column_name2)"); System.out.println( "\t (column_name)/(column_name2)"); System.out.println("\t" + COMMAND_TABLE_INFO + " <name> - PRAGMA table_info(<name>);"); System.out.println("\t<name> - SELECT * FROM <name>;"); if (isGeoPackage) { System.out.println("\t" + COMMAND_CONTENTS + " [name] - List GeoPackage contents (all or LIKE table name)"); System.out.println("\t" + ContentsDataType.ATTRIBUTES.getName() + " [name] - List GeoPackage attributes tables (all or LIKE table name)"); System.out.println("\t" + ContentsDataType.FEATURES.getName() + " [name] - List GeoPackage feature tables (all or LIKE table name)"); System.out.println("\t" + ContentsDataType.TILES.getName() + " [name] - List GeoPackage tile tables (all or LIKE table name)"); System.out.println("\t" + COMMAND_GEOPACKAGE_INFO + " <name> - Query GeoPackage metadata for the table name"); System.out.println("\t" + COMMAND_EXTENSIONS + " [name] - List GeoPackage extensions (all or LIKE table name)"); } System.out.println(); System.out.println("Special Supported Cases:"); System.out.println(); System.out.println("\tDrop Column - Not natively supported in SQLite"); System.out.println( "\t * ALTER TABLE table_name DROP column_name"); System.out.println( "\t * ALTER TABLE table_name DROP COLUMN column_name"); System.out.println("\tCopy Table - Not a traditional SQL statment"); System.out.println( "\t * ALTER TABLE table_name COPY TO new_table_name"); if (isGeoPackage) { System.out.println( "\tRename Table - User tables are updated throughout the GeoPackage"); System.out.println( "\t * ALTER TABLE table_name RENAME TO new_table_name"); System.out.println( "\tDrop Table - User tables are dropped throughout the GeoPackage"); System.out.println("\t * DROP TABLE table_name"); } } /** * Execute the SQL * * @param database * database * @param sqlBuilder * SQL builder * @param historyNumber * history number * @param maxRows * max rows * @param history * history * @throws SQLException * upon error */ private static void executeSQL(GeoPackage database, StringBuilder sqlBuilder, int historyNumber, Integer maxRows, List<String> history) throws SQLException { int number = historyNumber; if (number < 0) { number += history.size(); } else { number--; } if (number >= 0 && number < history.size()) { String sql = history.get(number); System.out.println(sql); executeSQL(database, sqlBuilder, sql, maxRows, history); } else { System.out.println("No History at " + historyNumber); resetCommandPrompt(sqlBuilder); } } /** * Execute the SQL * * @param database * database * @param sqlBuilder * SQL builder * @param sql * SQL statement * @param maxRows * max rows * @param history * history * @throws SQLException * upon error */ private static void executeSQL(GeoPackage database, StringBuilder sqlBuilder, String sql, Integer maxRows, List<String> history) throws SQLException { executeSQL(database, sqlBuilder, sql, maxRows, history, true); } /** * Execute the SQL * * @param database * database * @param sqlBuilder * SQL builder * @param sql * SQL statement * @param maxRows * max rows * @param history * history * @param resetCommandPrompt * reset command prompt * @throws SQLException * upon error */ private static void executeSQL(GeoPackage database, StringBuilder sqlBuilder, String sql, Integer maxRows, List<String> history, boolean resetCommandPrompt) throws SQLException { SQLExecResult result = executeSQL(database, sql, maxRows); result.printResults(); history.add(sql); if (resetCommandPrompt) { resetCommandPrompt(sqlBuilder); } } /** * Reset the command prompt * * @param sqlBuilder * sql builder */ private static void resetCommandPrompt(StringBuilder sqlBuilder) { sqlBuilder.setLength(0); System.out.println(); System.out.print(COMMAND_PROMPT); } /** * Execute the SQL on the database * * @param databaseFile * database file * @param sql * SQL statement * @return results * @throws SQLException * upon SQL error */ public static SQLExecResult executeSQL(File databaseFile, String sql) throws SQLException { return executeSQL(databaseFile, sql, null); } /** * Execute the SQL on the database * * @param databaseFile * database file * @param sql * SQL statement * @param maxRows * max rows * @return results * @throws SQLException * upon SQL error */ public static SQLExecResult executeSQL(File databaseFile, String sql, Integer maxRows) throws SQLException { SQLExecResult result = null; GeoPackage database = GeoPackageManager.open(databaseFile); try { result = executeSQL(database, sql, maxRows); } finally { database.close(); } return result; } /** * Execute the SQL on the database * * @param database * open database * @param sql * SQL statement * @return results * @throws SQLException * upon SQL error */ public static SQLExecResult executeSQL(GeoPackage database, String sql) throws SQLException { return executeSQL(database, sql, null); } /** * Execute the SQL on the GeoPackage database * * @param database * open database * @param sql * SQL statement * @param maxRows * max rows * @return results * @throws SQLException * upon SQL error */ public static SQLExecResult executeSQL(GeoPackage database, String sql, Integer maxRows) throws SQLException { // If no max number of results, use the default if (maxRows == null) { maxRows = DEFAULT_MAX_ROWS; } sql = sql.trim(); RTreeIndexExtension rtree = new RTreeIndexExtension(database); if (rtree.has()) { rtree.createAllFunctions(); } SQLExecResult result = SQLExecAlterTable.alterTable(database, sql); if (result == null) { result = executeQuery(database, sql, maxRows); } return result; } /** * Execute the query against the database * * @param database * open database * @param sql * SQL statement * @param maxRows * max rows * @return results * @throws SQLException * upon SQL error */ private static SQLExecResult executeQuery(GeoPackage database, String sql, int maxRows) throws SQLException { SQLExecResult result = new SQLExecResult(); if (!sql.equals(";")) { PreparedStatement statement = null; try { statement = database.getConnection().getConnection() .prepareStatement(sql); statement.setMaxRows(maxRows); result.setMaxRows(maxRows); boolean hasResultSet = statement.execute(); if (hasResultSet) { ResultSet resultSet = statement.getResultSet(); ResultSetMetaData metadata = resultSet.getMetaData(); int numColumns = metadata.getColumnCount(); int[] columnWidths = new int[numColumns]; int[] columnTypes = new int[numColumns]; for (int col = 1; col <= numColumns; col++) { result.addTable(metadata.getTableName(col)); String columnName = metadata.getColumnName(col); result.addColumn(columnName); columnTypes[col - 1] = metadata.getColumnType(col); columnWidths[col - 1] = columnName.length(); } while (resultSet.next()) { List<String> row = new ArrayList<>(); result.addRow(row); for (int col = 1; col <= numColumns; col++) { String stringValue = resultSet.getString(col); if (stringValue != null) { switch (columnTypes[col - 1]) { case Types.BLOB: stringValue = BLOB_DISPLAY_VALUE; break; default: stringValue = stringValue.replaceAll( "\\s*[\\r\\n]+\\s*", " "); } int valueLength = stringValue.length(); if (valueLength > columnWidths[col - 1]) { columnWidths[col - 1] = valueLength; } } row.add(stringValue); } } result.addColumnWidths(columnWidths); } else { int updateCount = statement.getUpdateCount(); if (updateCount >= 0) { result.setUpdateCount(updateCount); } } } finally { SQLUtils.closeStatement(statement, sql); } } return result; } /** * Write blobs from the query * * @param database * database * @param sqlBuilder * SQL builder * @param maxRows * max rows * @param history * history * @param args * write blob arguments * @throws SQLException * upon error * @throws IOException * upon error */ private static void writeBlobs(GeoPackage database, StringBuilder sqlBuilder, Integer maxRows, List<String> history, String args) throws SQLException, IOException { if (history.isEmpty()) { System.out.println("No previous query with blobs"); } else { boolean valid = true; String extension = null; String directory = null; String pattern = null; List<String> patternColumns = new ArrayList<>(); if (args != null && !args.isEmpty()) { String[] argParts = args.trim().split("\\s+"); for (int i = 0; valid && i < argParts.length; i++) { String arg = argParts[i]; if (arg.startsWith(ARGUMENT_PREFIX)) { String argument = arg .substring(ARGUMENT_PREFIX.length()); switch (argument) { case BLOBS_ARGUMENT_EXTENSION: if (i < argParts.length) { extension = argParts[++i]; } else { valid = false; System.out.println( "Error: Blobs extension argument '" + arg + "' must be followed by a file extension"); } break; case BLOBS_ARGUMENT_DIRECTORY: if (i < argParts.length) { directory = argParts[++i]; } else { valid = false; System.out.println( "Error: Blobs directory argument '" + arg + "' must be followed by a directory location"); } break; case BLOBS_ARGUMENT_PATTERN: if (i < argParts.length) { pattern = argParts[++i]; Matcher matcher = BLOBS_COLUMN_PATTERN .matcher(pattern); while (matcher.find()) { String columnName = matcher .group(BLOBS_COLUMN_PATTERN_GROUP); patternColumns.add(columnName); } if (patternColumns.isEmpty()) { valid = false; System.out.println( "Error: Blobs pattern argument '" + arg + "' must be followed by a save pattern with at least one column surrounded by parentheses"); } } else { valid = false; System.out.println( "Error: Blobs pattern argument '" + arg + "' must be followed by a save pattern"); } break; default: valid = false; System.out.println( "Error: Unsupported arg: '" + arg + "'"); } } else { valid = false; System.out.println( "Error: Unsupported arg: '" + arg + "'"); } } } if (valid) { String sql = history.get(history.size() - 1); if (maxRows == null) { maxRows = DEFAULT_MAX_ROWS; } Set<String> blobsWritten = new LinkedHashSet<>(); int blobsWrittenCount = 0; PreparedStatement statement = null; try { statement = database.getConnection().getConnection() .prepareStatement(sql); statement.setMaxRows(maxRows); boolean hasResultSet = statement.execute(); if (hasResultSet) { ResultSet resultSet = statement.getResultSet(); ResultSetMetaData metadata = resultSet.getMetaData(); int numColumns = metadata.getColumnCount(); List<Integer> blobColumns = new ArrayList<>(); List<String> tables = new ArrayList<>(); List<String> columnNames = new ArrayList<>(); Map<String, List<Integer>> tableNameColumns = new HashMap<>(); Map<String, Integer> columnNameIndexes = new HashMap<>(); for (int col = 1; col <= numColumns; col++) { columnNameIndexes.put(metadata.getColumnName(col), col); } for (int col = 1; col <= numColumns; col++) { if (metadata.getColumnType(col) == Types.BLOB) { blobColumns.add(col); String tableName = metadata.getTableName(col); List<Integer> nameColumns = tableNameColumns .get(tableName); if (nameColumns == null) { nameColumns = new ArrayList<>(); TableInfo tableInfo = TableInfo.info( database.getConnection(), tableName); List<String> nameColumnNames = null; if (pattern != null) { nameColumnNames = patternColumns; } else if (tableInfo.hasPrimaryKey()) { nameColumnNames = new ArrayList<>(); for (TableColumn tableColumn : tableInfo .getPrimaryKeys()) { nameColumnNames .add(tableColumn.getName()); } } if (nameColumnNames != null) { for (String columnName : nameColumnNames) { Integer columnIndex = columnNameIndexes .get(columnName); if (columnIndex == null && pattern != null) { throw new IllegalArgumentException( "Pattern column not found in query: " + columnName); } nameColumns.add(columnIndex); } } tableNameColumns.put(tableName, nameColumns); } tables.add(tableName); columnNames.add(metadata.getColumnName(col)); } } if (!blobColumns.isEmpty()) { if (extension != null && !extension.startsWith(".")) { extension = "." + extension; } if (directory == null) { directory = BLOBS_WRITE_DEFAULT_DIRECTORY; } File blobsDirectory = new File(directory); int resultCount = 0; while (resultSet.next()) { resultCount++; for (int i = 0; i < blobColumns.size(); i++) { int col = blobColumns.get(i); byte[] blobBytes = resultSet.getBytes(col); if (blobBytes != null) { String tableName = tables.get(i); File tableDirectory = new File( blobsDirectory, tableName); File columnDirectory = new File( tableDirectory, columnNames.get(i)); String name = null; if (pattern != null) { name = pattern; } List<Integer> nameColumns = tableNameColumns .get(tableName); if (!nameColumns.isEmpty()) { for (int j = 0; j < nameColumns .size(); j++) { Integer nameColumn = nameColumns .get(j); if (nameColumn != null) { String columnValue = resultSet .getString( nameColumn); if (columnValue != null) { if (pattern != null) { String columnName = patternColumns .get(j); name = name .replaceAll( BLOBS_COLUMN_START_REGEX + columnName + BLOBS_COLUMN_END_REGEX, columnValue); } else if (name == null) { name = columnValue; } else { name += "-" + columnValue; } } } } } if (name == null) { name = String.valueOf(resultCount); } if (extension != null) { name += extension; } File blobFile = new File( columnDirectory, name); blobFile.getParentFile().mkdirs(); FileOutputStream fos = new FileOutputStream( blobFile); fos.write(blobBytes); fos.close(); blobsWrittenCount++; blobsWritten.add(columnDirectory .getAbsolutePath()); } } } } } } finally { SQLUtils.closeStatement(statement, sql); } if (blobsWrittenCount <= 0) { System.out.println("No Blobs in previous query: " + sql); } else { System.out .println(blobsWrittenCount + " Blobs written to:"); for (String location : blobsWritten) { System.out.println(location); } } } } resetCommandPrompt(sqlBuilder); } /** * Print usage for the main method */ private static void printUsage() { System.out.println(); System.out.println("USAGE"); System.out.println(); System.out.println("\t[" + ARGUMENT_PREFIX + ARGUMENT_MAX_ROWS + " max_rows] sqlite_file [sql]"); System.out.println(); System.out.println("DESCRIPTION"); System.out.println(); System.out.println("\tExecutes SQL on a SQLite database"); System.out.println(); System.out.println( "\tProvide the SQL to execute a single statement. Omit to start an interactive session."); System.out.println(); System.out.println("ARGUMENTS"); System.out.println(); System.out.println( "\t" + ARGUMENT_PREFIX + ARGUMENT_MAX_ROWS + " max_rows"); System.out.println("\t\tMax rows to query and display" + " (Default is " + DEFAULT_MAX_ROWS + ")"); System.out.println(); System.out.println("\tsqlite_file"); System.out.println("\t\tpath to the SQLite database file"); System.out.println(); System.out.println("\tsql"); System.out.println("\t\tSQL statement to execute"); System.out.println(); } /** * Check if the SQLite database is a GeoPackage * * @param database * SQLite database * @return true if a GeoPackage */ public static boolean isGeoPackage(GeoPackage database) { return GeoPackageValidate.hasMinimumTables(database); } }