Mercurial > hg > monetdb-java
view src/main/java/nl/cwi/monetdb/util/SQLExporter.java @ 261:d4baf8a4b43a
Update Copyright year to 2019
author | Martin van Dinther <martin.van.dinther@monetdbsolutions.com> |
---|---|
date | Thu, 03 Jan 2019 14:43:44 +0100 (2019-01-03) |
parents | c38d4eaf5479 |
children | 4face9f42efc 7c79ef41b840 |
line wrap: on
line source
/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * Copyright 1997 - July 2008 CWI, August 2008 - 2019 MonetDB B.V. */ package nl.cwi.monetdb.util; import java.io.PrintWriter; import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Types; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.Stack; import java.util.TreeMap; public class SQLExporter extends Exporter { private int outputMode; private Stack<String> lastSchema; public final static int TYPE_OUTPUT = 1; public final static int VALUE_INSERT = 0; public final static int VALUE_COPY = 1; public final static int VALUE_TABLE = 2; public SQLExporter(PrintWriter out) { super(out); } /** * A helper method to generate SQL CREATE code for a given table. * This method performs all required lookups to find all relations and * column information, as well as additional indices. * * @param dbmd a DatabaseMetaData object to query on (not null) * @param type the type of the object, e.g. VIEW, TABLE (not null) * @param catalog the catalog the object is in * @param schema the schema the object is in (not null) * @param name the table to describe in SQL CREATE format (not null) * @throws SQLException if a database related error occurs */ public void dumpSchema( DatabaseMetaData dbmd, String type, String catalog, String schema, String name) throws SQLException { assert dbmd != null; assert type != null; assert schema != null; assert name != null; String fqname = (!useSchema ? dq(schema) + "." : "") + dq(name); if (useSchema) changeSchema(schema); // handle views directly if (type.indexOf("VIEW") != -1) { String[] types = new String[1]; types[0] = type; ResultSet tbl = dbmd.getTables(catalog, schema, name, types); if (!tbl.next()) throw new SQLException("Whoops no meta data for view " + fqname); // This will probably only work for MonetDB String remarks = tbl.getString("REMARKS"); // for MonetDB driver this contains the view definition if (remarks == null) { out.println("-- invalid " + type + " " + fqname + ": no definition found"); } else { out.print("CREATE " + type + " " + fqname + " AS "); out.println(remarks.replaceFirst("create view [^ ]+ as", "").trim()); } return; } int i; String s; out.println("CREATE " + type + " " + fqname + " ("); // add all columns with their type, nullability and default definition ResultSet cols = dbmd.getColumns(catalog, schema, name, null); int colNmIndex = cols.findColumn("COLUMN_NAME"); int colTypeNmIndex = cols.findColumn("TYPE_NAME"); ResultSetMetaData rsmd = cols.getMetaData(); int colwidth = rsmd.getColumnDisplaySize(colNmIndex); int typewidth = rsmd.getColumnDisplaySize(colTypeNmIndex); if (typewidth < 13) typewidth = 13; // use minimal 13 characters for the typename (same as used in mclient) StringBuilder sb = new StringBuilder(128); for (i = 0; cols.next(); i++) { if (i > 0) out.println(","); // print column name (with double quotes) s = dq(cols.getString(colNmIndex)); out.print("\t" + s + repeat(' ', (colwidth - s.length() + 3))); int ctype = cols.getInt("DATA_TYPE"); int size = cols.getInt("COLUMN_SIZE"); int digits = cols.getInt("DECIMAL_DIGITS"); boolean isNotNull = cols.getInt("NULLABLE") == DatabaseMetaData.columnNoNulls; String defaultValue = cols.getString("COLUMN_DEF"); boolean hasDefault = (defaultValue != null && !defaultValue.isEmpty()); s = cols.getString(colTypeNmIndex).toUpperCase(); // do some data type substitutions to match SQL standard if (s.equals("INT")) { s = "INTEGER"; } else if (s.equals("SEC_INTERVAL")) { s = "INTERVAL SECOND"; } else if (s.equals("MONTH_INTERVAL")) { s = "INTERVAL MONTH"; } else if (s.equals("TIMETZ")) { s = "TIME"; // small hack to get desired behaviour: set digits when we have // a time with time zone and at the same time masking the internal types digits = 1; } else if (s.equals("TIMESTAMPTZ")) { s = "TIMESTAMP"; // small hack to get desired behaviour: set digits when we have // a timestamp with time zone and at the same time masking the internal types digits = 1; } sb.append(s); // add the data type for this column // do some SQL/MonetDB type length/precision and scale specifics switch (ctype) { case Types.CHAR: case Types.VARCHAR: case Types.LONGVARCHAR: case Types.CLOB: case Types.BLOB: case Types.FLOAT: if (size > 0) sb.append('(').append(size).append(')'); break; case Types.TIME: case Types.TIMESTAMP: if (size > 1) sb.append('(').append(size -1).append(')'); if (digits != 0) sb.append(" WITH TIME ZONE"); break; case Types.DECIMAL: case Types.NUMERIC: sb.append('(').append(size); if (digits != 0) sb.append(',').append(digits); sb.append(')'); break; } if (isNotNull || hasDefault) { int spaces = typewidth - sb.length(); if (spaces > 0) sb.append(repeat(' ', spaces)); if (isNotNull) sb.append(" NOT NULL"); if (hasDefault) sb.append(" DEFAULT ").append(defaultValue); } // print column type, optional length and scale, optional Not NULL, optional default value out.print(sb.toString()); sb.delete(0, sb.length()); // clear the stringbuffer for next column } cols.close(); // add the primary key constraint definition // unfortunately some idiot defined that getPrimaryKeys() // returns the primary key columns sorted by column name, not // key sequence order. So we have to sort ourself :( cols = dbmd.getPrimaryKeys(catalog, schema, name); // first make an 'index' of the KEY_SEQ column SortedMap<Integer, Integer> seqIndex = new TreeMap<Integer, Integer>(); for (i = 1; cols.next(); i++) { seqIndex.put(Integer.valueOf(cols.getInt("KEY_SEQ")), Integer.valueOf(i)); } if (seqIndex.size() > 0) { // terminate the previous line out.println(","); cols.absolute(1); out.print("\tCONSTRAINT " + dq(cols.getString("PK_NAME")) + " PRIMARY KEY ("); i = 0; for (Iterator<Map.Entry<Integer, Integer>> it = seqIndex.entrySet().iterator(); it.hasNext(); i++) { Map.Entry<Integer, Integer> e = it.next(); cols.absolute(e.getValue().intValue()); if (i > 0) out.print(", "); out.print(dq(cols.getString("COLUMN_NAME"))); } out.print(")"); } cols.close(); // add unique constraint definitions cols = dbmd.getIndexInfo(catalog, schema, name, true, true); int colIndexNm = cols.findColumn("INDEX_NAME"); int colIndexColNm = cols.findColumn("COLUMN_NAME"); while (cols.next()) { String idxname = cols.getString(colIndexNm); if (idxname != null) { out.println(","); out.print("\tCONSTRAINT " + dq(idxname) + " UNIQUE (" + dq(cols.getString(colIndexColNm))); boolean next; while ((next = cols.next()) && idxname.equals(cols.getString(colIndexNm))) { out.print(", " + dq(cols.getString(colIndexColNm))); } // go back one, we've gone one too far if (next) cols.previous(); out.print(")"); } } cols.close(); // add foreign keys definitions cols = dbmd.getImportedKeys(catalog, schema, name); while (cols.next()) { out.println(","); out.print("\tCONSTRAINT " + dq(cols.getString("FK_NAME")) + " FOREIGN KEY ("); boolean next; Set<String> fk = new LinkedHashSet<String>(); fk.add(cols.getString("FKCOLUMN_NAME").intern()); Set<String> pk = new LinkedHashSet<String>(); pk.add(cols.getString("PKCOLUMN_NAME").intern()); while ((next = cols.next()) && cols.getInt("KEY_SEQ") != 1) { fk.add(cols.getString("FKCOLUMN_NAME").intern()); pk.add(cols.getString("PKCOLUMN_NAME").intern()); } // go back one if (next) cols.previous(); Iterator<String> it = fk.iterator(); for (i = 0; it.hasNext(); i++) { if (i > 0) out.print(", "); out.print(dq(it.next())); } out.print(") REFERENCES " + dq(cols.getString("PKTABLE_SCHEM")) + "." + dq(cols.getString("PKTABLE_NAME")) + " ("); it = pk.iterator(); for (i = 0; it.hasNext(); i++) { if (i > 0) out.print(", "); out.print(dq(it.next())); } out.print(")"); } cols.close(); out.println(); // end the create table statement out.println(");"); // create the non unique indexes defined for this table cols = dbmd.getIndexInfo(catalog, schema, name, false, true); colIndexNm = cols.findColumn("INDEX_NAME"); colIndexColNm = cols.findColumn("COLUMN_NAME"); while (cols.next()) { if (cols.getBoolean("NON_UNIQUE")) { // We only process non-unique indexes here. // The unique indexes are already covered as UNIQUE constraints in the CREATE TABLE above String idxname = cols.getString(colIndexNm); if (idxname != null) { out.print("CREATE INDEX " + dq(idxname) + " ON " + dq(cols.getString("TABLE_NAME")) + " (" + dq(cols.getString(colIndexColNm))); boolean next; while ((next = cols.next()) && idxname.equals(cols.getString(colIndexNm))) { out.print(", " + dq(cols.getString(colIndexColNm))); } // go back one if (next) cols.previous(); out.println(");"); } } } cols.close(); } /** * Dumps the given ResultSet as specified in the form variable. * * @param rs the ResultSet to dump * @throws SQLException if a database error occurs */ public void dumpResultSet(ResultSet rs) throws SQLException { switch (outputMode) { case VALUE_INSERT: resultSetToSQL(rs); break; case VALUE_COPY: resultSetToSQLDump(rs); break; case VALUE_TABLE: resultSetToTable(rs); break; } } public void setProperty(int type, int value) throws Exception { switch (type) { case TYPE_OUTPUT: switch (value) { case VALUE_INSERT: case VALUE_COPY: case VALUE_TABLE: outputMode = value; break; default: throw new Exception("Illegal value " + value + " for TYPE_OUTPUT"); } break; default: throw new Exception("Illegal type " + type); } } public int getProperty(int type) throws Exception { switch (type) { case TYPE_OUTPUT: return outputMode; default: throw new Exception("Illegal type " + type); } } private final static int AS_IS = 0; private final static int QUOTE = 1; /** * Helper method to dump the contents of a table in SQL INSERT INTO * format. * * @param rs the ResultSet to convert into INSERT INTO statements * @param absolute if true, dumps table name prepended with schema name * @throws SQLException if a database related error occurs */ private void resultSetToSQL(ResultSet rs) throws SQLException { ResultSetMetaData rsmd = rs.getMetaData(); String statement = "INSERT INTO "; if (!useSchema) { String schema = rsmd.getSchemaName(1); if (schema != null && schema.length() > 0) statement += dq(schema) + "."; } statement += dq(rsmd.getTableName(1)) + " VALUES ("; int cols = rsmd.getColumnCount(); short[] types = new short[cols +1]; for (int i = 1; i <= cols; i++) { switch (rsmd.getColumnType(i)) { case Types.CHAR: case Types.VARCHAR: case Types.LONGVARCHAR: case Types.CLOB: case Types.BLOB: case Types.DATE: case Types.TIME: case Types.TIMESTAMP: types[i] = QUOTE; break; case Types.NUMERIC: case Types.DECIMAL: case Types.BIT: // we don't use type BIT, it's here for completeness case Types.BOOLEAN: case Types.TINYINT: case Types.SMALLINT: case Types.INTEGER: case Types.BIGINT: case Types.REAL: case Types.FLOAT: case Types.DOUBLE: types[i] = AS_IS; break; default: types[i] = AS_IS; } } StringBuilder strbuf = new StringBuilder(1024); strbuf.append(statement); while (rs.next()) { for (int i = 1; i <= cols; i++) { String val = rs.getString(i); if (i > 1) strbuf.append(", "); if (val == null || rs.wasNull()) { strbuf.append("NULL"); } else { strbuf.append((types[i] == QUOTE) ? q(val) : val); } } strbuf.append(");"); out.println(strbuf.toString()); // clear the variable part of the buffer contents for next data row strbuf.setLength(statement.length()); } } public void resultSetToSQLDump(ResultSet rs) { // TODO: write copy into statement } /** * Helper method to write a ResultSet in a convenient table format * to the output writer. * * @param rs the ResultSet to write out * @throws SQLException if a database related error occurs */ public void resultSetToTable(ResultSet rs) throws SQLException { ResultSetMetaData md = rs.getMetaData(); int cols = md.getColumnCount(); // find the optimal display widths of the columns int[] width = new int[cols + 1]; boolean[] isSigned = new boolean[cols + 1]; // used for controlling left or right alignment of data for (int j = 1; j < width.length; j++) { int coldisplaysize = md.getColumnDisplaySize(j); int collabellength = md.getColumnLabel(j).length(); int maxwidth = (coldisplaysize > collabellength) ? coldisplaysize : collabellength; // the minimum width should be 4 to represent: "NULL" width[j] = (maxwidth > 4) ? maxwidth : 4; isSigned[j] = md.isSigned(j); } // use a buffer to construct the text lines StringBuilder strbuf = new StringBuilder(1024); // construct the frame lines and header text strbuf.append('+'); for (int j = 1; j < width.length; j++) strbuf.append(repeat('-', width[j] + 1) + "-+"); String outsideLine = strbuf.toString(); strbuf.setLength(0); // clear the buffer strbuf.append('|'); for (int j = 1; j < width.length; j++) { String colLabel = md.getColumnLabel(j); strbuf.append(' '); strbuf.append(colLabel); strbuf.append(repeat(' ', width[j] - colLabel.length())); strbuf.append(" |"); } // print the header text out.println(outsideLine); out.println(strbuf.toString()); out.println(outsideLine.replace('-', '=')); // print formatted data of each row from resultset long count = 0; for (; rs.next(); count++) { strbuf.setLength(0); // clear the buffer strbuf.append('|'); for (int j = 1; j < width.length; j++) { String data = rs.getString(j); if (data == null || rs.wasNull()) { data = "NULL"; } int filler_length = width[j] - data.length(); if (filler_length <= 0) { if (filler_length == 0) { strbuf.append(' '); } strbuf.append(data); } else { strbuf.append(' '); if (isSigned[j]) { // we have a numeric type here, right align strbuf.append(repeat(' ', filler_length)); strbuf.append(data); } else { // all other left align strbuf.append(data); strbuf.append(repeat(' ', filler_length)); } } strbuf.append(" |"); } out.println(strbuf.toString()); } // print the footer text out.println(outsideLine); out.println(count + " row" + (count != 1 ? "s" : "")); } private void changeSchema(String schema) { if (lastSchema == null) { lastSchema = new Stack<String>(); lastSchema.push(null); } if (!schema.equals(lastSchema.peek())) { if (!lastSchema.contains(schema)) { // create schema out.print("CREATE SCHEMA "); out.print(dq(schema)); out.println(";\n"); lastSchema.push(schema); } out.print("SET SCHEMA "); out.print(dq(schema)); out.println(";\n"); } } }