001/*
002 * Stallion Core: A Modern Web Framework
003 *
004 * Copyright (C) 2015 - 2016 Stallion Software LLC.
005 *
006 * This program is free software: you can redistribute it and/or modify it under the terms of the
007 * GNU General Public License as published by the Free Software Foundation, either version 2 of
008 * the License, or (at your option) any later version. This program is distributed in the hope that
009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
011 * License for more details. You should have received a copy of the GNU General Public License
012 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
013 *
014 *
015 *
016 */
017
018package io.stallion.dataAccess.db;
019
020import io.stallion.boot.AppContextLoader;
021import io.stallion.boot.SqlGenerateCommandOptions;
022import io.stallion.boot.StallionRunAction;
023import io.stallion.exceptions.ConfigException;
024import io.stallion.exceptions.UsageException;
025import io.stallion.services.Log;
026import io.stallion.settings.Settings;
027import io.stallion.utils.GeneralUtils;
028import io.stallion.utils.Prompter;
029import org.apache.commons.lang3.StringUtils;
030import org.parboiled.common.FileUtils;
031
032import javax.script.ScriptEngine;
033import java.io.File;
034import java.nio.charset.Charset;
035import java.time.ZonedDateTime;
036import java.util.List;
037import java.util.Map;
038
039import static io.stallion.utils.Literals.*;
040
041
042public class SqlGenerationAction  implements StallionRunAction<SqlGenerateCommandOptions> {
043    private ScriptEngine scriptEngine;
044
045    @Override
046    public String getActionName() {
047        return "sql-generate";
048    }
049
050    @Override
051    public String getHelp() {
052        return "Generates new SQL scripts from the models in the project.";
053    }
054
055
056    @Override
057    public SqlGenerateCommandOptions newCommandOptions() {
058        return new SqlGenerateCommandOptions();
059    }
060
061    @Override
062    public void loadApp(SqlGenerateCommandOptions options) {
063        //AppContextLoader.loadWithSettingsOnly(options);
064        //DB.load();
065
066        DB.setUseDummyPersisterForSqlGenerationMode(true);
067        AppContextLoader.loadCompletely(options);
068        if (!DB.available()) {
069            if (Settings.instance().getDatabase() == null || empty(Settings.instance().getDatabase().getUrl())) {
070                throw new ConfigException("No database url defined in your settings");
071            } else {
072                throw new ConfigException("Database is not available.");
073            }
074        }
075    }
076
077    @Override
078    public void execute(SqlGenerateCommandOptions options) throws Exception {
079        //List<ClassLoader> loaders = list();
080        //ConfigurationBuilder builder = new ConfigurationBuilder();
081        //if (!empty(options.getPackageName())) {
082        //    builder = builder.forPackages("io.stallion");
083        //} else {
084        //    builder = builder.forPackages(options.getPackageName());
085        //}
086        //for(StallionJavaPlugin plugin: PluginRegistry.instance().getJavaPluginByName().values()) {
087        //    builder = builder.addClassLoader(plugin.getClass().getClassLoader());
088        //}
089
090        //Reflections reflections = new Reflections(builder);
091
092        //Reflections reflections = new Reflections("org.mycabal");
093
094        //Set<Class<? extends ModelBase>> classes = reflections.getSubTypesOf(ModelBase.class);
095        //Log.info("Model Count {0} {1}", classes.size(), classes);
096        //classes = set(User.class);
097        boolean hasNewMigrations = false;
098       // for (Class cls: classes) {
099        List<Schema> schemas = DB.instance().getSchemas();
100        Log.info("Schema count {0}", DB.instance().getSchemas().size());
101        Integer lastMigrationNumber = getLastMigrationNumber();
102        for (Schema schema: schemas)   {
103            Log.info("Try generate for schema ", schema.getName());
104            //GenerateResult result = generateForModel(cls);
105            GenerateResult result = generateForSchema(schema);
106            if (result == null) {
107                Log.info("No change needed for {0}", schema.getName());
108                continue;
109            }
110            Log.info("SQL GENERATED\n{0}", result.getSql());
111            boolean shouldWrite = new Prompter("Write this script to file? ").yesNo();
112            if (shouldWrite) {
113                lastMigrationNumber += 10;
114                String prefix = StringUtils.leftPad(lastMigrationNumber.toString(), 5, '0');
115                String file = Settings.instance().getTargetFolder() + "/sql/" + prefix + "-" + result.getChangePrefix()
116                        + ".mysql.js";
117                FileUtils.writeAllText(result.getSqlJs(), new File(file), Charset.forName("UTF-8"));
118                hasNewMigrations = true;
119            }
120        }
121        if (hasNewMigrations) {
122            boolean runMigrations = new Prompter("Execute all new SQL migrations? ").yesNo();
123            if (runMigrations) {
124                SqlMigrationAction action = new SqlMigrationAction();
125                action.execute(options);
126            }
127        }
128    }
129
130    public int getLastMigrationNumber() {
131        Integer max = 0;
132        for (SqlMigration migration: new SqlMigrationAction().getUserMigrations()) {
133            if (migration.getVersionNumber() > max) {
134                max = migration.getVersionNumber();
135            }
136        }
137        return max;
138    }
139
140    public GenerateResult generateForModel(Class cls) {
141        Schema schema = DB.instance().modelToSchema(cls);
142        return generateForSchema(schema);
143    }
144
145    public GenerateResult generateForSchema(Schema schema) {
146        if (!DB.instance().tableExists(schema.getName())) {
147            return createTableSqlForSchema(schema);
148        } else {
149            List<Col> missingColumns = list();
150            for(Col col: schema.getColumns()) {
151                if (!DB.instance().columnExists(schema.getName(), col.getName())) {
152                    missingColumns.add(col);
153                }
154            }
155            if (missingColumns.size() > 0) {
156                return createAlterTableForNewColumns(schema, missingColumns);
157            }
158        }
159        return null;
160    }
161
162    public GenerateResult createTableSqlForSchema(Schema schema) {
163        StringBuilder sql = new StringBuilder();
164        sql.append("CREATE TABLE IF NOT EXISTS `" + schema.getName() + "` (\n");
165        sql.append("`id` bigint(20) unsigned NOT NULL,\n");
166        for (Col col: schema.getColumns()) {
167            sql.append("    `" + col.getName() + "` ");
168            sql.append(" " + dbTypeForColumn(col) + " ");
169            if (col.getNullable()) {
170                sql.append(" NULL ");
171            } else {
172                sql.append(" NOT NULL ");
173            }
174            if (col.getDefaultValue() != null) {
175                Object defaultVal = col.getDefaultValue();
176                if (defaultVal instanceof Boolean) {
177                    if ((Boolean)defaultVal == true){
178                        defaultVal = 1;
179                    } else {
180                        defaultVal = 0;
181                    }
182                } else {
183                    defaultVal = "'" + defaultVal.toString().replace("'", "\'") + "'";
184                }
185                sql.append(" DEFAULT " + defaultVal + " ");
186            }
187            sql.append(",\n");
188        }
189        sql.append("  `row_updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n");
190        sql.append("  PRIMARY KEY (`id`),\n");
191        sql.append("  KEY `row_updated_at_key` (`row_updated_at`),\n");
192
193        for(Col col: schema.getColumns()) {
194            if (col.getUniqueKey()) {
195                sql.append("  UNIQUE KEY `" + col.getName() + "_key` (`" + col.getName() + "`),\n");
196
197            }
198            if (col.getAlternativeKey()) {
199                sql.append("  KEY `" + col.getName() + "_key` (`" + col.getName() + "`),\n");
200            }
201        }
202        for(String def: schema.getExtraKeyDefinitions()) {
203            sql.append("    " + def + ",\n");
204        }
205        String sqlString = sql.toString();
206        sqlString = StringUtils.strip(StringUtils.strip(sqlString, "\n"), ",") + "\n";
207        sqlString += ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;";
208
209        return new GenerateResult()
210                .setChangePrefix("create-table-" + schema.getName())
211                .setSql(sqlString)
212                .setSqlJs("db.execute('''\n" + sqlString + " \n''');")
213                .setTableName(schema.getName())
214                ;
215    }
216
217    public GenerateResult createAlterTableForNewColumns(Schema schema, List<Col> columns) {
218        StringBuilder sql = new StringBuilder();
219        sql.append("ALTER TABLE `" + schema.getName() + "` ");
220
221        int i = 0;
222        for (Col col: columns) {
223            sql.append("\n    ADD COLUMN `" + col.getName() + "` ");
224            sql.append(" " + dbTypeForColumn(col) + " ");
225            if (col.getNullable()) {
226                sql.append(" NULL");
227            } else {
228                sql.append(" NOT NULL");
229            }
230            if (col.getDefaultValue() != null) {
231                sql.append(" DEFAULT '" + col.getDefaultValue().toString().replace("'", "\'") + "'");
232            }
233            i++;
234            if (i != columns.size()) {
235                sql.append(",");
236            }
237        }
238        sql.append(";");
239        String sqlString = sql.toString();
240
241        GenerateResult generateResult = new GenerateResult()
242                .setChangePrefix(GeneralUtils.slugify("alter-" + schema.getName() + "-add-" + columns.size()))
243                .setSql(sqlString)
244                .setSqlJs("db.execute('''\n" + sqlString + "\n''');")
245                .setTableName(schema.getName())
246                ;
247        return generateResult;
248    }
249
250    protected String dbTypeForColumn(Col column) {
251        if (!empty(column.getDbType())) {
252            String dbType = column.getDbType();
253            if (column.getLength() > 0 && !dbType.contains("(") && !dbType.contains("text") && !dbType.contains("(")) {
254                dbType = dbType + "(" + column.getLength() + ")";
255            }
256            return dbType;
257        }
258        if (column.getjType() == String.class) {
259            int length = column.getLength();
260            if (length < 1) {
261                length = 250;
262            }
263            return "varchar(" + length + ")";
264        } else if (column.getjType() == Boolean.class) {
265            return "bit(1)";
266        } else if (column.getjType() == boolean.class) {
267            return "bit(1)";
268        } else if (column.getjType() == Long.class) {
269            return "bigint(20)";
270        } else if (column.getjType() == long.class) {
271            return "bigint(20)";
272        } else if (column.getjType() == Integer.class) {
273            return "int";
274        } else if (column.getjType() == int.class) {
275            return "int";
276        } else if (column.getjType() == float.class) {
277            return "float";
278        } else if (column.getjType() == Float.class) {
279            return "float";
280        } else if (column.getjType() == double.class) {
281            return "double";
282        } else if (column.getjType() == Double.class) {
283            return "double";
284        } else if (column.getjType() == ZonedDateTime.class) {
285            return "datetime";
286        } else if (List.class.isAssignableFrom(column.getjType())) {
287            return "longtext";
288        } else if (Map.class.isAssignableFrom(column.getjType())) {
289            return "longtext";
290        } else if (Enum.class.isAssignableFrom(column.getjType())) {
291            return "varchar(30)";
292        } else {
293            throw new UsageException("Could not guess database column type for column " + column.getName());
294        }
295    }
296
297    public static class GenerateResult {
298        private String sql = "";
299        private String sqlJs = "";
300        private String tableName = "";
301        private String changePrefix = "";
302
303        public String getSql() {
304            return sql;
305        }
306
307        public GenerateResult setSql(String sql) {
308            this.sql = sql;
309            return this;
310        }
311
312        public String getSqlJs() {
313            return sqlJs;
314        }
315
316        public GenerateResult setSqlJs(String sqlJs) {
317            this.sqlJs = sqlJs;
318            return this;
319        }
320
321        public String getTableName() {
322            return tableName;
323        }
324
325        public GenerateResult setTableName(String tableName) {
326            this.tableName = tableName;
327            return this;
328        }
329
330        public String getChangePrefix() {
331            return changePrefix;
332        }
333
334        public GenerateResult setChangePrefix(String changePrefix) {
335            this.changePrefix = changePrefix;
336            return this;
337        }
338    }
339
340}