Mercurial > hg > monetdb-java
diff src/main/java/nl/cwi/monetdb/util/SQLExporter.java @ 0:a5a898f6886c
Copy of MonetDB java directory changeset e6e32756ad31.
author | Sjoerd Mullender <sjoerd@acm.org> |
---|---|
date | Wed, 21 Sep 2016 09:34:48 +0200 (2016-09-21) |
parents | |
children | b07b4940c3ed |
line wrap: on
line diff
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/util/SQLExporter.java @@ -0,0 +1,503 @@ +/* + * 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 - 2016 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); + ResultSetMetaData rsmd = cols.getMetaData(); + int colwidth = rsmd.getColumnDisplaySize(cols.findColumn("COLUMN_NAME")); + int typewidth = rsmd.getColumnDisplaySize(cols.findColumn("TYPE_NAME")); + for (i = 0; cols.next(); i++) { + if (i > 0) out.println(","); + // print column name + s = dq(cols.getString("COLUMN_NAME")); + out.print("\t" + s + repeat(' ', (colwidth - s.length()) + 3)); + + s = cols.getString("TYPE_NAME"); + int ctype = cols.getInt("DATA_TYPE"); + int size = cols.getInt("COLUMN_SIZE"); + int digits = cols.getInt("DECIMAL_DIGITS"); + // small hack to get desired behaviour: set digits when we + // have a time or timestamp with time zone and at the same + // time masking the internal types + if (s.equals("timetz")) { + digits = 1; + s = "time"; + } else if (s.equals("timestamptz")) { + digits = 1; + s = "timestamp"; + } + // print column type + out.print(s + repeat(' ', typewidth - s.length())); + + // do some MonetDB type specifics + switch (ctype) { + case Types.CHAR: + case Types.VARCHAR: + case Types.LONGVARCHAR: + case Types.CLOB: + case Types.BLOB: + case Types.FLOAT: + if (size > 0) + out.print("(" + size + ")"); + break; + case Types.TIME: + case Types.TIMESTAMP: + if (size > 1) + out.print("(" + (size - 1) + ")"); + if (digits != 0) + out.print(" WITH TIME ZONE"); + break; + case Types.DECIMAL: + case Types.NUMERIC: + if (digits != 0) + out.print("(" + size + "," + digits + ")"); + else + out.print("(" + size + ")"); + break; + } + if (cols.getInt("NULLABLE") == DatabaseMetaData.columnNoNulls) + out.print("\tNOT NULL"); + if ((s = cols.getString("COLUMN_DEF")) != null) + out.print("\tDEFAULT " + q(s)); + } + 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); + while (cols.next()) { + String idxname = cols.getString("INDEX_NAME"); + out.println(","); + out.print("\tCONSTRAINT " + dq(idxname) + " UNIQUE (" + + dq(cols.getString("COLUMN_NAME"))); + + boolean next; + while ((next = cols.next()) && idxname != null && + idxname.equals(cols.getString("INDEX_NAME"))) { + out.print(", " + dq(cols.getString("COLUMN_NAME"))); + } + // 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(") "); + + 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); + while (cols.next()) { + if (!cols.getBoolean("NON_UNIQUE")) { + // we already covered this one as UNIQUE constraint in the CREATE TABLE + continue; + } else { + String idxname = cols.getString("INDEX_NAME"); + out.print("CREATE INDEX " + dq(idxname) + " ON " + + dq(cols.getString("TABLE_NAME")) + " (" + + dq(cols.getString("COLUMN_NAME"))); + + boolean next; + while ((next = cols.next()) && idxname != null && + idxname.equals(cols.getString("INDEX_NAME"))) + { + out.print(", " + dq(cols.getString("COLUMN_NAME"))); + } + // 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 + */ + 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"); + } + } +}