diff --git a/.github/workflows/maven-test.yml b/.github/workflows/maven-test.yml new file mode 100644 index 0000000..95be220 --- /dev/null +++ b/.github/workflows/maven-test.yml @@ -0,0 +1,34 @@ +name: Maven Test +on: + push: + branches: + - '**' + - '!master' + +jobs: + test: + runs-on: ubuntu-latest + services: + mysql: + image: mariadb:latest + env: + MYSQL_ALLOW_EMPTY_PASSWORD: yes + MYSQL_DATABASE: test + MYSQL_USER: test + MYSQL_PASSWORD: test + MYSQL_RANDOM_ROOT_PASSWORD: yes + ports: + - 3306 + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Test + run: mvn -B test + env: + MYSQL_PORT: ${{ job.services.mysql.ports[3306] }} + MYSQL_USERNAME: test + MYSQL_PASSWORD: test \ No newline at end of file diff --git a/pom.xml b/pom.xml index 9b5ef29..7ccd700 100644 --- a/pom.xml +++ b/pom.xml @@ -63,11 +63,6 @@ reflections 0.9.12 - - org.javawebstack - Injector - 1.0-SNAPSHOT - org.javawebstack AbstractData diff --git a/src/main/java/org/javawebstack/orm/Model.java b/src/main/java/org/javawebstack/orm/Model.java index 4a8ef7c..9619dbd 100644 --- a/src/main/java/org/javawebstack/orm/Model.java +++ b/src/main/java/org/javawebstack/orm/Model.java @@ -1,6 +1,5 @@ package org.javawebstack.orm; -import org.javawebstack.injector.Injector; import org.javawebstack.orm.query.Query; import java.lang.reflect.Field; @@ -22,7 +21,6 @@ public class Model { private static final Method refreshMethod; { - inject(); updateOriginal(); } @@ -41,6 +39,7 @@ public class Model { private transient boolean internalEntryExists = false; private transient final Map, Object> internalJoinedModels = new HashMap<>(); private transient Map internalOriginalValues = new HashMap<>(); + private transient Map internalExtraFields = new HashMap<>(); void internalAddJoinedModel(Class type, Object entity) { internalJoinedModels.put(type, entity); @@ -62,6 +61,14 @@ public Map getFieldValues() { return values; } + public Map getExtraFields() { + return internalExtraFields; + } + + public T getExtraField(String key) { + return (T) internalExtraFields.get(key); + } + public Map getOriginalValues() { return internalOriginalValues; } @@ -114,12 +121,6 @@ void setEntryExists(boolean exists) { this.internalEntryExists = exists; } - public void inject() { - Injector injector = Repo.get(getClass()).getInfo().getConfig().getInjector(); - if (injector != null) - injector.inject(this); - } - public void save() { try { saveMethod.invoke(ORM.repo(getClass()), this); diff --git a/src/main/java/org/javawebstack/orm/ORMConfig.java b/src/main/java/org/javawebstack/orm/ORMConfig.java index 1558d95..38c14a6 100644 --- a/src/main/java/org/javawebstack/orm/ORMConfig.java +++ b/src/main/java/org/javawebstack/orm/ORMConfig.java @@ -1,6 +1,5 @@ package org.javawebstack.orm; -import org.javawebstack.injector.Injector; import org.javawebstack.orm.exception.ORMConfigurationException; import org.javawebstack.orm.mapper.DefaultMapper; import org.javawebstack.orm.mapper.TypeMapper; @@ -15,7 +14,6 @@ public class ORMConfig { private boolean idPrimaryKey = true; private boolean idAutoIncrement = true; private final List typeMappers = new ArrayList<>(); - private Injector injector; private boolean preventUnnecessaryUpdates = true; public ORMConfig() { @@ -55,11 +53,6 @@ public ORMConfig setIdAutoIncrement(boolean idAutoIncrement) { return this; } - public ORMConfig setInjector(Injector injector) { - this.injector = injector; - return this; - } - public boolean isCamelToSnakeCase() { return camelToSnakeCase; } @@ -84,10 +77,6 @@ public boolean isIdAutoIncrement() { return idAutoIncrement; } - public Injector getInjector() { - return injector; - } - public TypeMapper getTypeMapper(Class type, int size) { for (TypeMapper mapper : getTypeMappers()) { SQLType sqlType = mapper.getType(type, size); diff --git a/src/main/java/org/javawebstack/orm/Repo.java b/src/main/java/org/javawebstack/orm/Repo.java index d05687f..018f7f9 100644 --- a/src/main/java/org/javawebstack/orm/Repo.java +++ b/src/main/java/org/javawebstack/orm/Repo.java @@ -7,6 +7,7 @@ import org.javawebstack.orm.migration.AutoMigrator; import org.javawebstack.orm.query.Query; import org.javawebstack.orm.wrapper.SQL; +import org.javawebstack.orm.wrapper.builder.SQLQueryString; import java.lang.reflect.Field; import java.sql.SQLException; @@ -100,8 +101,6 @@ public void save(T entry) { } public void create(T entry) { - if (info.getConfig().getInjector() != null) - info.getConfig().getInjector().inject(entry); observers.forEach(o -> o.saving(entry)); observers.forEach(o -> o.creating(entry)); executeCreate(entry); @@ -124,28 +123,14 @@ private void executeCreate(T entry) { if (field.get(entry) == null) field.set(entry, UUID.randomUUID()); } - List params = new ArrayList<>(); - StringBuilder sb = new StringBuilder("INSERT INTO `"); - sb.append(info.getTableName()); - sb.append("` ("); - List cols = new ArrayList<>(); - List values = new ArrayList<>(); Map map = SQLMapper.map(this, entry); if (info.isAutoIncrement()) { String idCol = info.getColumnName(info.getIdField()); if (map.containsKey(idCol) && map.get(idCol) == null) map.remove(idCol); } - for (String columnName : map.keySet()) { - cols.add("`" + columnName + "`"); - values.add("?"); - params.add(map.get(columnName)); - } - sb.append(String.join(",", cols)); - sb.append(") VALUES ("); - sb.append(String.join(",", values)); - sb.append(");"); - int id = connection.write(sb.toString(), params.toArray()); + SQLQueryString qs = getConnection().builder().buildInsert(info, map); + int id = connection.write(qs.getQuery(), qs.getParameters().toArray()); if (info.isAutoIncrement()) info.getField(info.getIdField()).set(entry, id); entry.setEntryExists(true); @@ -237,8 +222,6 @@ public Object getId(Object entity) { } public Repo observe(Observer observer) { - if (info.getConfig().getInjector() != null) - info.getConfig().getInjector().inject(observer); observers.add(observer); return this; } diff --git a/src/main/java/org/javawebstack/orm/SQLMapper.java b/src/main/java/org/javawebstack/orm/SQLMapper.java index 6d63411..dd3ebc8 100644 --- a/src/main/java/org/javawebstack/orm/SQLMapper.java +++ b/src/main/java/org/javawebstack/orm/SQLMapper.java @@ -1,6 +1,5 @@ package org.javawebstack.orm; -import org.javawebstack.injector.Injector; import org.javawebstack.orm.exception.ORMQueryException; import org.javawebstack.orm.mapper.DefaultMapper; import org.javawebstack.orm.mapper.TypeMapper; @@ -27,14 +26,9 @@ public static List map(Repo repo, ResultSet rs, List model : joinedModels) { Repo r = Repo.get((Class) model); Model o = (Model) r.getInfo().getModelConstructor().newInstance(); - if (injector != null) - injector.inject(o); t.internalAddJoinedModel(model, mapBack(r, rs, o)); } list.add(mapBack(repo, rs, t)); diff --git a/src/main/java/org/javawebstack/orm/migration/DB.java b/src/main/java/org/javawebstack/orm/migration/DB.java deleted file mode 100644 index e7892e6..0000000 --- a/src/main/java/org/javawebstack/orm/migration/DB.java +++ /dev/null @@ -1,282 +0,0 @@ -package org.javawebstack.orm.migration; - -import org.javawebstack.orm.SQLType; -import org.javawebstack.orm.exception.ORMQueryException; -import org.javawebstack.orm.wrapper.SQL; - -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; -import java.util.stream.Collectors; - -public class DB { - - private final SQL sql; - private final String tablePrefix; - - public DB(SQL sql, String tablePrefix) { - this.sql = sql; - this.tablePrefix = tablePrefix; - } - - public void table(String name, Consumer consumer) { - consumer.accept(new Table(name)); - } - - public class Table { - - private String name; - private final List columns = new ArrayList<>(); - - public Table(String name) { - this.name = name; - } - - public String fullName() { - return tablePrefix + name; - } - - public Column column(String name, SQLType type, String size) { - for (Column column : columns) { - if (column.name.equals(name)) - return column; - } - Column column = new Column(name, type, size); - columns.add(column); - return column; - } - - public Column column(String name, SQLType type) { - return column(name, type, null); - } - - public Column string(String name) { - return string(name, 255); - } - - public Column string(String name, int size) { - return column(name, SQLType.VARCHAR, String.valueOf(size)); - } - - public Column text(String name, int size) { - return column(name, SQLType.TEXT, String.valueOf(size)); - } - - public Column text(String name) { - return column(name, SQLType.TEXT); - } - - public Column id() { - return integer("id").autoIncrement().primary(); - } - - public Column integer(String name, int size) { - return column(name, SQLType.INT, String.valueOf(size)); - } - - public Column integer(String name) { - return column(name, SQLType.INT); - } - - public Column bool(String name) { - return column(name, SQLType.TINYINT, "1"); - } - - public Column uuid(String name) { - return column(name, SQLType.VARCHAR, "36"); - } - - public Column uuid() { - return uuid("uuid").primary(); - } - - public Column enums(String name, String... values) { - return column(name, SQLType.ENUM, String.join(",", values)); - } - - public Column timestamp(String name) { - return column(name, SQLType.TIMESTAMP); - } - - public void timestamps(String... names) { - for (String name : names) - timestamp(name); - } - - public void dates() { - timestamps("created_at", "updated_at"); - } - - public void softDelete() { - timestamp("deleted_at").nullable(); - } - - public void rename(String to) { - try { - sql.write("RENAME TABLE `" + fullName() + "` TO `" + tablePrefix + to + "`;"); - } catch (SQLException throwables) { - throw new ORMQueryException(throwables); - } - this.name = to; - } - - public void drop() { - try { - sql.write("DROP TABLE `" + fullName() + "`;"); - } catch (SQLException throwables) { - throw new ORMQueryException(throwables); - } - } - - public void create() { - create(false); - } - - public void create(boolean ifNotExists) { - StringBuilder sb = new StringBuilder("CREATE TABLE "); - if (ifNotExists) - sb.append("IF NOT EXISTS "); - sb.append('`'); - sb.append(fullName()); - sb.append("` ("); - List entries = new ArrayList<>(columns.stream().map(Column::definition).collect(Collectors.toList())); - columns.forEach(c -> entries.addAll(c.contraints())); - sb.append(String.join(",", entries)); - sb.append(") DEFAULT CHARSET=utf8mb4;"); - try { - sql.write(sb.toString()); - } catch (SQLException throwables) { - throw new ORMQueryException(throwables); - } - } - - public class Column { - private boolean primary = false; - private boolean unique = false; - private boolean autoIncrement = false; - private boolean nullable = false; - private String after; - private String name; - private final SQLType type; - private final String size; - - public Column(String name, SQLType type, String size) { - this.name = name; - this.type = type; - this.size = size; - } - - public Column primary() { - this.primary = true; - return this; - } - - public Column unique() { - this.unique = true; - return this; - } - - public Column autoIncrement() { - this.autoIncrement = true; - return this; - } - - public Column nullable() { - this.nullable = true; - return this; - } - - public Column after(String after) { - this.after = after; - return this; - } - - public Column first() { - this.after = ""; - return this; - } - - public void rename(String to) { - try { - sql.write("ALTER TABLE `" + fullName() + "` RENAME COLUMN `" + name + "` TO `" + to + "`;"); - } catch (SQLException throwables) { - throw new ORMQueryException(throwables); - } - name = to; - } - - public void drop() { - try { - sql.write("ALTER TABLE `" + fullName() + "` DROP COLUMN `" + name + "`;"); - } catch (SQLException throwables) { - throw new ORMQueryException(throwables); - } - columns.remove(this); - } - - public void add() { - StringBuilder sb = new StringBuilder("ALTER TABLE `"); - sb.append(fullName()); - sb.append("` ADD "); - sb.append(definition()); - sb.append(';'); - try { - sql.write(sb.toString()); - } catch (SQLException throwables) { - throw new ORMQueryException(throwables); - } - } - - public void modify() { - StringBuilder sb = new StringBuilder("ALTER TABLE `"); - sb.append(fullName()); - sb.append("` MODIFY "); - sb.append(definition()); - sb.append(';'); - try { - sql.write(sb.toString()); - } catch (SQLException throwables) { - throw new ORMQueryException(throwables); - } - } - - String definition() { - StringBuilder sb = new StringBuilder('`'); - sb.append(name); - sb.append("` "); - sb.append(type.name()); - if (size != null) { - sb.append('('); - sb.append(size); - sb.append(')'); - } - if (nullable) { - sb.append(" NULL"); - } else { - sb.append(" NOT NULL"); - } - if (autoIncrement) - sb.append(" AUTO_INCREMENT"); - if (after != null) { - if (after.length() != 0) { - sb.append(" AFTER `"); - sb.append(after); - sb.append('`'); - } else { - sb.append(" FIRST"); - } - } - return sb.toString(); - } - - List contraints() { - List constraints = new ArrayList<>(columns.stream().filter(c -> c.primary).map(c -> "PRIMARY KEY (`" + c.name + "`)").collect(Collectors.toList())); - contraints().addAll(columns.stream().filter(c -> c.unique).map(c -> "UNIQUE (`" + c.name + "`)").collect(Collectors.toList())); - return constraints; - } - } - - } - -} diff --git a/src/main/java/org/javawebstack/orm/migration/Migration.java b/src/main/java/org/javawebstack/orm/migration/Migration.java deleted file mode 100644 index e6da30f..0000000 --- a/src/main/java/org/javawebstack/orm/migration/Migration.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.javawebstack.orm.migration; - -public interface Migration { - - String version(); - - default String name() { - String className = getClass().getSimpleName(); - StringBuilder sb = new StringBuilder(Character.toLowerCase(className.charAt(0))); - for (int i = 1; i < className.length(); i++) { - if (Character.isUpperCase(className.charAt(i))) { - sb.append('-'); - sb.append(Character.toLowerCase(className.charAt(i))); - } else { - sb.append(className.charAt(i)); - } - } - return version().replace(" ", "-") + "-" + sb.toString(); - } - - void up(DB db); - - void down(DB db); - -} diff --git a/src/main/java/org/javawebstack/orm/migration/MigrationState.java b/src/main/java/org/javawebstack/orm/migration/MigrationState.java deleted file mode 100644 index 524d40e..0000000 --- a/src/main/java/org/javawebstack/orm/migration/MigrationState.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.javawebstack.orm.migration; - -import org.javawebstack.orm.Model; -import org.javawebstack.orm.annotation.Column; -import org.javawebstack.orm.annotation.Dates; -import org.javawebstack.orm.util.KeyType; - -import java.sql.Timestamp; - -@Dates -public class MigrationState extends Model { - - @Column(id = true, ai = false, key = KeyType.PRIMARY) - private String name; - @Column - private Timestamp createdAt; - @Column - private Timestamp updatedAt; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Timestamp getCreatedAt() { - return createdAt; - } - - public void setCreatedAt(Timestamp createdAt) { - this.createdAt = createdAt; - } - - public Timestamp getUpdatedAt() { - return updatedAt; - } - - public void setUpdatedAt(Timestamp updatedAt) { - this.updatedAt = updatedAt; - } - -} diff --git a/src/main/java/org/javawebstack/orm/migration/Migrator.java b/src/main/java/org/javawebstack/orm/migration/Migrator.java deleted file mode 100644 index c35f456..0000000 --- a/src/main/java/org/javawebstack/orm/migration/Migrator.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.javawebstack.orm.migration; - -import org.javawebstack.orm.ORM; -import org.javawebstack.orm.ORMConfig; -import org.javawebstack.orm.Repo; -import org.javawebstack.orm.exception.ORMConfigurationException; -import org.javawebstack.orm.wrapper.SQL; -import org.reflections.Reflections; - -import java.sql.Date; -import java.text.SimpleDateFormat; -import java.time.Instant; -import java.util.*; -import java.util.stream.Collectors; - -public class Migrator { - - private final DB db; - private final List migrations = new ArrayList<>(); - - public Migrator(SQL sql, ORMConfig config) { - this.db = new DB(sql, config.getTablePrefix()); - try { - ORM.register(MigrationState.class, sql, config); - } catch (ORMConfigurationException ignored) { - } - } - - public Migrator add(Migration... migrations) { - this.migrations.addAll(Arrays.asList(migrations)); - return this; - } - - public Migrator add(Package p) { - Reflections reflections = new Reflections(p.getName()); - reflections.getSubTypesOf(Migration.class).forEach(c -> { - try { - add(c.newInstance()); - } catch (InstantiationException | IllegalAccessException e) { - } - }); - return this; - } - - public void migrate() { - migrate(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(Date.from(Instant.now()))); - } - - public void migrate(String version) { - Map states = new HashMap<>(); - Repo.get(MigrationState.class).stream().forEach(m -> states.put(m.getName(), m)); - after(version).stream().sorted(Comparator.comparing(Migration::version).reversed()).forEach(m -> { - if (states.containsKey(m.name())) { - m.down(db); - states.get(m.name()).delete(); - states.remove(m.name()); - } - }); - before(version).stream().sorted(Comparator.comparing(Migration::version)).forEach(m -> { - if (!states.containsKey(m.name())) { - m.up(db); - MigrationState state = new MigrationState(); - state.setName(m.name()); - state.save(); - states.put(state.getName(), state); - } - }); - } - - private List before(String version) { - return new ArrayList<>(migrations.stream().filter(m -> m.version().compareTo(version) < 0).collect(Collectors.toList())); - } - - private List after(String version) { - return new ArrayList<>(migrations.stream().filter(m -> m.version().compareTo(version) >= 0).collect(Collectors.toList())); - } - -} diff --git a/src/main/java/org/javawebstack/orm/query/Query.java b/src/main/java/org/javawebstack/orm/query/Query.java index 4a98294..9bbd617 100644 --- a/src/main/java/org/javawebstack/orm/query/Query.java +++ b/src/main/java/org/javawebstack/orm/query/Query.java @@ -4,6 +4,7 @@ import org.javawebstack.orm.Repo; import org.javawebstack.orm.SQLMapper; import org.javawebstack.orm.exception.ORMQueryException; +import org.javawebstack.orm.wrapper.builder.SQLQueryString; import java.sql.ResultSet; import java.sql.SQLException; @@ -23,10 +24,9 @@ public class Query { private final QueryGroup where; private Integer offset; private Integer limit; - private QueryColumn order; - private boolean desc = false; + private QueryOrderBy order; private boolean withDeleted = false; - private final Map, QueryCondition> leftJoins = new HashMap<>(); + private final List withs = new ArrayList<>(); public Query(Class model) { this(Repo.get(model), model); @@ -36,14 +36,47 @@ public Query(Repo repo, Class model) { this.repo = repo; this.model = model; this.where = new QueryGroup<>(); + this.order = new QueryOrderBy(); + } + + public boolean isWithDeleted() { + return withDeleted; + } + + public QueryGroup getWhereGroup() { + return where; + } + + public List getWiths() { + return withs; + } + + public Integer getLimit() { + return limit; + } + + public Integer getOffset() { + return offset; + } + + public QueryOrderBy getOrder() { + return order; + } + + public Repo getRepo() { + return repo; } public Class getModel() { return model; } - public Query leftJoin(Class model, String self, String other) { - leftJoins.put(model, new QueryCondition(new QueryColumn(repo.getInfo().getTableName() + "." + self), "=", new QueryColumn(Repo.get(model).getInfo().getTableName() + "." + other))); + public Query with(String extra) { + return with(extra, null); + } + + public Query with(String extra, String as) { + withs.add(new QueryWith(extra, as)); return this; } @@ -263,20 +296,50 @@ public Query search(String search) { return this; } - public Query order(String orderBy, boolean desc) { - return order(new QueryColumn(orderBy), desc); - } + /** + * Sorts the results by the given column name ascendingly. + * + * @param columnName The name of the column to sort ascendingly by. + * @return The Query object with the given order by information added. + * @throws ORMQueryException if the order operation is called twice on a column specification with the same name. + */ + public Query order(String columnName) throws ORMQueryException { + return order(columnName, false); + } + + /** + * Sorts the results by the given column name with the given order direction. + * + * @param columnName The name of the column to sort ascendingly by. + * @param desc If true it will order descendingly, if false it will order ascendingly. + * @return The Query object with the given order by information added. + * @throws ORMQueryException if the order operation is called twice on a column specification with the same name. + */ + public Query order(String columnName, boolean desc) throws ORMQueryException { + return order(new QueryColumn(columnName), desc); + } + + /** + * Sorts the results by the given column with the given order direction. + * + * @param column The column encoded as QueryColumn object. + * @param desc If true it will order descendingly, if false it will order ascendingly. + * @return The Query object with the given order by information added. + * @throws ORMQueryException if the order operation is called twice on a column specification with the same name. + */ + public Query order(QueryColumn column, boolean desc) throws ORMQueryException{ + boolean success = this.order.add(column, desc); + if(!success) { + throw new ORMQueryException(String.format( + "The column %s could not be ordered %s. This is probably caused by calling .order() on this column twice.", + column.toString(), + desc ? "descendingly" : "ascendingly" + )); + } - public Query order(QueryColumn orderBy, boolean desc) { - this.order = orderBy; - this.desc = desc; return this; } - public Query order(String orderBy) { - return order(orderBy, false); - } - public Query limit(int offset, int limit) { return offset(offset).limit(limit); } @@ -296,59 +359,10 @@ public Query withDeleted() { return this; } - public QueryString getQueryString() { - return getQueryString(false); - } - - public QueryString getQueryString(boolean count) { - List parameters = new ArrayList<>(); - StringBuilder sb = new StringBuilder("SELECT ") - .append(count ? "COUNT(*)" : "*") - .append(" FROM `") - .append(repo.getInfo().getTableName()) - .append('`'); - for (Class type : leftJoins.keySet()) { - sb.append(" LEFT JOIN `") - .append(Repo.get(type).getInfo().getTableName()) - .append("` ON ") - .append(leftJoins.get(type).getQueryString(repo.getInfo()).getQuery()); - } - considerSoftDelete(); - if (where.getQueryElements().size() > 0) { - QueryString qs = where.getQueryString(repo.getInfo()); - sb.append(" WHERE ").append(qs.getQuery()); - parameters.addAll(qs.getParameters()); - } - if (order != null) { - sb.append(" ORDER BY ").append(order.toString(repo.getInfo())); - if (desc) - sb.append(" DESC"); - } - if (offset != null && limit == null) - limit = Integer.MAX_VALUE; - if (limit != null) { - sb.append(" LIMIT ?"); - if (offset != null) { - sb.append(",?"); - parameters.add(offset); - } - parameters.add(limit); - } - return new QueryString(sb.toString(), SQLMapper.mapParams(repo, parameters)); - } - public void finalDelete() { - List parameters = new ArrayList<>(); - StringBuilder sb = new StringBuilder("DELETE FROM `") - .append(repo.getInfo().getTableName()) - .append('`'); - if (where.getQueryElements().size() > 0) { - QueryString qs = where.getQueryString(repo.getInfo()); - sb.append(" WHERE ").append(qs.getQuery()); - parameters = qs.getParameters(); - } + SQLQueryString qs = repo.getConnection().builder().buildDelete(this); try { - repo.getConnection().write(sb.toString(), SQLMapper.mapParams(repo, parameters).toArray()); + repo.getConnection().write(qs.getQuery(), qs.getParameters().toArray()); } catch (SQLException throwables) { throw new ORMQueryException(throwables); } @@ -374,18 +388,10 @@ public void restore() { withDeleted().update(values); } - private void considerSoftDelete() { - if (repo.getInfo().isSoftDelete() && !withDeleted) { - if (where.getQueryElements().size() > 0) - where.getQueryElements().add(0, QueryConjunction.AND); - where.getQueryElements().add(0, new QueryCondition(new QueryColumn(repo.getInfo().getColumnName(repo.getInfo().getSoftDeleteField())), "IS NULL", null)); - } - } - public T refresh(T entity) { - QueryString qs = getQueryString(false); + SQLQueryString qs = repo.getConnection().builder().buildQuery(this, false); try { - ResultSet rs = repo.getConnection().read(qs.getQuery(), SQLMapper.mapParams(repo, SQLMapper.mapParams(repo, qs.getParameters())).toArray()); + ResultSet rs = repo.getConnection().read(qs.getQuery(), qs.getParameters().toArray()); SQLMapper.mapBack(repo, rs, entity); repo.getConnection().close(rs); return entity; @@ -399,39 +405,19 @@ public void update(T entity) { } public void update(Map values) { - if (repo.getInfo().hasUpdated()) - values.put(repo.getInfo().getColumnName(repo.getInfo().getUpdatedField()), Timestamp.from(Instant.now())); - List parameters = new ArrayList<>(); - List sets = new ArrayList<>(); - values.forEach((key, value) -> { - sets.add("`" + key + "`=?"); - parameters.add(value); - }); - StringBuilder sb = new StringBuilder("UPDATE `") - .append(repo.getInfo().getTableName()) - .append("` SET ") - .append(String.join(",", sets)); - considerSoftDelete(); - if (where.getQueryElements().size() > 0) { - QueryString qs = where.getQueryString(repo.getInfo()); - sb.append(" WHERE ").append(qs.getQuery()); - parameters.addAll(qs.getParameters()); - } - sb.append(';'); + SQLQueryString queryString = repo.getConnection().builder().buildUpdate(this, values); try { - repo.getConnection().write(sb.toString(), SQLMapper.mapParams(repo, parameters).toArray()); + repo.getConnection().write(queryString.getQuery(), queryString.getParameters().toArray()); } catch (SQLException throwables) { throw new ORMQueryException(throwables); } } public List all() { - QueryString qs = getQueryString(false); + SQLQueryString qs = repo.getConnection().builder().buildQuery(this, false); try { - ResultSet rs = repo.getConnection().read(qs.getQuery(), SQLMapper.mapParams(repo, qs.getParameters()).toArray()); - List> joinedModels = new ArrayList<>(); - joinedModels.addAll(leftJoins.keySet()); - List list = SQLMapper.map(repo, rs, joinedModels); + ResultSet rs = repo.getConnection().read(qs.getQuery(), qs.getParameters().toArray()); + List list = SQLMapper.map(repo, rs, new ArrayList<>()); repo.getConnection().close(rs); return list; } catch (SQLException throwables) { @@ -455,9 +441,9 @@ public Stream stream() { } public int count() { - QueryString qs = getQueryString(true); + SQLQueryString qs = repo.getConnection().builder().buildQuery(this, true); try { - ResultSet rs = repo.getConnection().read(qs.getQuery(), SQLMapper.mapParams(repo, qs.getParameters()).toArray()); + ResultSet rs = repo.getConnection().read(qs.getQuery(), qs.getParameters().toArray()); int c = 0; if (rs.next()) c = rs.getInt(1); diff --git a/src/main/java/org/javawebstack/orm/query/QueryColumn.java b/src/main/java/org/javawebstack/orm/query/QueryColumn.java index 1cee6ed..651c19d 100644 --- a/src/main/java/org/javawebstack/orm/query/QueryColumn.java +++ b/src/main/java/org/javawebstack/orm/query/QueryColumn.java @@ -1,12 +1,17 @@ package org.javawebstack.orm.query; import org.javawebstack.orm.TableInfo; +import org.javawebstack.orm.exception.ORMQueryException; import java.util.Arrays; +import java.util.Objects; +import java.util.regex.Pattern; import java.util.stream.Collectors; public class QueryColumn { + private static final Pattern NAME_PATTERN = Pattern.compile("[A-Za-z0-9_\\-.]+"); + private final String name; private final boolean raw; @@ -15,6 +20,8 @@ public QueryColumn(String name) { } public QueryColumn(String name, boolean raw) { + if(!raw) + validateName(name); this.name = name; this.raw = raw; } @@ -37,4 +44,21 @@ public String toString(TableInfo info) { return Arrays.stream((info != null ? info.getColumnName(name) : name).split("\\.")).map(s -> "`" + s + "`").collect(Collectors.joining(".")); } + private static void validateName(String name) { + if(!NAME_PATTERN.matcher(name).matches()) + throw new ORMQueryException("Invalid column name '" + name + "' (Use raw in case you know what you're doing)"); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + QueryColumn that = (QueryColumn) o; + return toString().equals(that.toString()); + } + + @Override + public int hashCode() { + return Objects.hash(toString()); + } } diff --git a/src/main/java/org/javawebstack/orm/query/QueryCondition.java b/src/main/java/org/javawebstack/orm/query/QueryCondition.java index c547452..4f3901c 100644 --- a/src/main/java/org/javawebstack/orm/query/QueryCondition.java +++ b/src/main/java/org/javawebstack/orm/query/QueryCondition.java @@ -1,23 +1,41 @@ package org.javawebstack.orm.query; -import org.javawebstack.orm.TableInfo; +import org.javawebstack.orm.exception.ORMQueryException; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.IntStream; +import java.util.Locale; public class QueryCondition implements QueryElement { + private static final List VALID_OPERATORS = Arrays.asList( + "=", + "<=>", + "!=", + "<>", + "<=", + ">=", + "<", + ">", + "is null", + "is not null", + "is", + "is not", + "in", + "not in", + "like", + "not like" + ); + private final Object left; private final String operator; private final Object right; private final boolean not; public QueryCondition(Object left, String operator, Object right, boolean not) { + validateOperator(operator); this.left = left; - this.operator = operator; // TODO Validate and throw exception + this.operator = operator; this.right = right; this.not = not; } @@ -38,37 +56,21 @@ public String getOperator() { return operator; } - private boolean hasRight() { + public boolean hasRight() { return !(operator.equalsIgnoreCase("IS NULL") || operator.equalsIgnoreCase("IS NOT NULL")); } - public QueryString getQueryString(TableInfo info) { - StringBuilder sb = new StringBuilder(); - if (not) - sb.append("NOT "); - List parameters = new ArrayList<>(); - if (left instanceof QueryColumn) { - sb.append(((QueryColumn) left).toString(info)); - } else { - sb.append('?'); - parameters.add(left); - } - sb.append(' '); - sb.append(operator); - if (hasRight()) { - sb.append(' '); - if (operator.endsWith("IN")) { - Object[] values = (Object[]) right; - sb.append("(").append(IntStream.range(0, values.length).mapToObj(i -> "?").collect(Collectors.joining(","))).append(")"); - parameters.addAll(Arrays.asList(values)); - } else if (right instanceof QueryColumn) { - sb.append(((QueryColumn) right).toString(info)); - } else { - sb.append('?'); - parameters.add(right); - } - } - return new QueryString(sb.toString(), parameters); + public boolean isNot() { + return not; + } + + private static void validateOperator(String operator) { + if(!VALID_OPERATORS.contains(operator.toLowerCase(Locale.ROOT))) + throw new ORMQueryException("The given operator '" + operator + "' is invalid or not supported"); + } + + public enum Operator { + } } diff --git a/src/main/java/org/javawebstack/orm/query/QueryConjunction.java b/src/main/java/org/javawebstack/orm/query/QueryConjunction.java index 5b4b1da..4edb308 100644 --- a/src/main/java/org/javawebstack/orm/query/QueryConjunction.java +++ b/src/main/java/org/javawebstack/orm/query/QueryConjunction.java @@ -1,13 +1,7 @@ package org.javawebstack.orm.query; -import org.javawebstack.orm.TableInfo; - public enum QueryConjunction implements QueryElement { AND, OR, - XOR; - - public QueryString getQueryString(TableInfo info) { - return new QueryString(name()); - } + XOR } diff --git a/src/main/java/org/javawebstack/orm/query/QueryElement.java b/src/main/java/org/javawebstack/orm/query/QueryElement.java index 8b81bbe..8b960ff 100644 --- a/src/main/java/org/javawebstack/orm/query/QueryElement.java +++ b/src/main/java/org/javawebstack/orm/query/QueryElement.java @@ -1,9 +1,5 @@ package org.javawebstack.orm.query; -import org.javawebstack.orm.TableInfo; - public interface QueryElement { - QueryString getQueryString(TableInfo info); - } diff --git a/src/main/java/org/javawebstack/orm/query/QueryExists.java b/src/main/java/org/javawebstack/orm/query/QueryExists.java index ad1529d..a175f51 100644 --- a/src/main/java/org/javawebstack/orm/query/QueryExists.java +++ b/src/main/java/org/javawebstack/orm/query/QueryExists.java @@ -2,6 +2,7 @@ import org.javawebstack.orm.Model; import org.javawebstack.orm.TableInfo; +import org.javawebstack.orm.wrapper.builder.SQLQueryString; public class QueryExists implements QueryElement { @@ -13,9 +14,12 @@ public QueryExists(Query query, boolean not) { this.not = not; } - public QueryString getQueryString(TableInfo info) { - QueryString qs = query.getQueryString(); - return new QueryString((not ? "NOT " : "") + "EXISTS (" + qs.getQuery() + ")", qs.getParameters()); + public Query getQuery() { + return query; + } + + public boolean isNot() { + return not; } } diff --git a/src/main/java/org/javawebstack/orm/query/QueryGroup.java b/src/main/java/org/javawebstack/orm/query/QueryGroup.java index 3249288..870b94c 100644 --- a/src/main/java/org/javawebstack/orm/query/QueryGroup.java +++ b/src/main/java/org/javawebstack/orm/query/QueryGroup.java @@ -3,12 +3,22 @@ import org.javawebstack.orm.Model; import org.javawebstack.orm.Repo; import org.javawebstack.orm.TableInfo; +import org.javawebstack.orm.wrapper.builder.SQLQueryString; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.function.Function; +/** + * Queries grouped via the QueryGroup class will be put inside parenthesis. + * This makes expressions as the following possible (MySQL example): + * ... `column_a` = 'A' OR (`column_b` = 'B' AND `column_c´ = 'C') ... + * + * In the above example `column_b` = 'B' AND `column_c´ = 'C' would be in a QueryGroup. + * + * @param The model under which the QueryGroups functions. Currently purely semantic without functionality. + */ public class QueryGroup implements QueryElement { private final List queryElements = new ArrayList<>(); @@ -203,18 +213,4 @@ public QueryGroup orWhereNotIn(Object left, Object... values) { return orWhere(left, "NOT IN", values); } - public QueryString getQueryString(TableInfo info) { - StringBuilder sb = new StringBuilder("("); - List parameters = new ArrayList<>(); - for (QueryElement element : queryElements) { - if (sb.length() > 1) - sb.append(' '); - QueryString s = element.getQueryString(info); - sb.append(s.getQuery()); - parameters.addAll(s.getParameters()); - } - sb.append(')'); - return new QueryString(sb.toString(), parameters); - } - } diff --git a/src/main/java/org/javawebstack/orm/query/QueryOrderBy.java b/src/main/java/org/javawebstack/orm/query/QueryOrderBy.java new file mode 100644 index 0000000..a58925c --- /dev/null +++ b/src/main/java/org/javawebstack/orm/query/QueryOrderBy.java @@ -0,0 +1,70 @@ +package org.javawebstack.orm.query; + +import org.javawebstack.orm.TableInfo; + +import java.util.LinkedList; +import java.util.stream.Collectors; + +/** + * The QueryOrderBy class serves as an aggregation of order by elements. It extends a list, because the order of the + * order by statements is of relevance. + */ +public class QueryOrderBy extends LinkedList{ + + /** + * Add a new order by statement. If a statement with the same column name already exists it will not add the + * statement. + * + * @param columnName The column name to order by. + * @param desc If the column should be order descendingly. + * @return True if adding the statement was successful. False otherwise. + */ + public boolean add(String columnName, boolean desc) { + return this.add(new QueryColumn(columnName), desc); + } + + /** + * Add a new order by statement. If a statement with the same column name already exists it will not add the + * statement. + * + * @param column The column to be ordered by. It will retrieve the name from the QueryColumn. + * @param desc If the column should be order descendingly. + * @return True if adding the statement was successful. False otherwise. + */ + public boolean add(QueryColumn column, boolean desc) { + return this.add(new QueryOrderByElement(column, desc)); + } + + @Override + /** + * Add a new order by statement. If a statement with the same column name already exists it will not add the + * statement. + * + * @param element The direct QueryOrderByElement which encodes the order by statement. + * @return True if adding the statement was successful. False otherwise. + */ + public boolean add(QueryOrderByElement element) { + boolean hasBeenAdded = false; + if(!willOverwrite(element)) + hasBeenAdded = super.add(element); + + return hasBeenAdded; + } + + private boolean willOverwrite(QueryOrderByElement element) { + return this.stream().anyMatch(element::hasEqualColumn); + } + + + // The toString methods are specific to MySQL so they might have to be replaced later on. + @Override + public String toString() { + return toString(null); + } + + public String toString(TableInfo info) { + return this.stream() + .map(singleOrderByElement -> singleOrderByElement.toString(info)) + .collect(Collectors.joining(",")); + } +} diff --git a/src/main/java/org/javawebstack/orm/query/QueryOrderByElement.java b/src/main/java/org/javawebstack/orm/query/QueryOrderByElement.java new file mode 100644 index 0000000..915284f --- /dev/null +++ b/src/main/java/org/javawebstack/orm/query/QueryOrderByElement.java @@ -0,0 +1,80 @@ +package org.javawebstack.orm.query; + +import org.javawebstack.orm.TableInfo; + +import java.util.Objects; + +/** + * The QueryOrderByElement class encodes an Order By Statement. + */ +public class QueryOrderByElement { + private final QueryColumn queryColumn; + private final boolean desc; + + QueryOrderByElement(String columnName, boolean desc) { + queryColumn = new QueryColumn(columnName); + this.desc = desc; + } + + QueryOrderByElement(QueryColumn column, boolean desc) { + this.queryColumn = column; + this.desc = desc; + } + + /** + * Retrieves the QueryColumn of the statement which encodes the column name. + * + * @return The encoding QueryColumn object. + */ + public QueryColumn getQueryColumn() { + return queryColumn; + } + + /** + * Retrieves the information if this column is ordered ascendingly or descendingly. + * + * @return false if ascending, true if descending. + */ + public boolean isDesc() { + return desc; + } + + /** + * Compares the encoded column name. + * + * @param o An object to compare to. + * @return True if the object is a QueryOrderByElement with a QueryColumn with generates the same identifier. + */ + public boolean hasEqualColumn(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + QueryOrderByElement that = (QueryOrderByElement) o; + return getQueryColumn().equals(that.getQueryColumn()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + QueryOrderByElement that = (QueryOrderByElement) o; + return isDesc() == that.isDesc() && getQueryColumn().equals(that.getQueryColumn()); + } + + @Override + public int hashCode() { + return Objects.hash(getQueryColumn(), isDesc()); + } + + @Override + public String toString() { + return this.toString(null); + } + + public String toString(TableInfo info) { + String stringifiedOrderBy = getQueryColumn().toString(info); + if (isDesc()) + stringifiedOrderBy += " DESC"; + + return stringifiedOrderBy; + } +} diff --git a/src/main/java/org/javawebstack/orm/query/QueryWith.java b/src/main/java/org/javawebstack/orm/query/QueryWith.java new file mode 100644 index 0000000..653fcbc --- /dev/null +++ b/src/main/java/org/javawebstack/orm/query/QueryWith.java @@ -0,0 +1,20 @@ +package org.javawebstack.orm.query; + +public class QueryWith { + + private final String expression; + private final String as; + + public QueryWith(String expression, String as) { + this.expression = expression; + this.as = as; + } + + public String getExpression() { + return expression; + } + + public String getAs() { + return as; + } +} diff --git a/src/main/java/org/javawebstack/orm/wrapper/MySQL.java b/src/main/java/org/javawebstack/orm/wrapper/MySQL.java index c933b0a..21dda73 100644 --- a/src/main/java/org/javawebstack/orm/wrapper/MySQL.java +++ b/src/main/java/org/javawebstack/orm/wrapper/MySQL.java @@ -1,6 +1,8 @@ package org.javawebstack.orm.wrapper; import org.javawebstack.orm.exception.ORMQueryException; +import org.javawebstack.orm.wrapper.builder.MySQLQueryStringBuilder; +import org.javawebstack.orm.wrapper.builder.QueryStringBuilder; import java.sql.Connection; import java.sql.DriverManager; @@ -65,6 +67,10 @@ public Connection getConnection() { return c; } + public QueryStringBuilder builder() { + return MySQLQueryStringBuilder.INSTANCE; + } + } diff --git a/src/main/java/org/javawebstack/orm/wrapper/SQL.java b/src/main/java/org/javawebstack/orm/wrapper/SQL.java index 5288dbf..04b37ad 100644 --- a/src/main/java/org/javawebstack/orm/wrapper/SQL.java +++ b/src/main/java/org/javawebstack/orm/wrapper/SQL.java @@ -1,5 +1,7 @@ package org.javawebstack.orm.wrapper; +import org.javawebstack.orm.wrapper.builder.QueryStringBuilder; + import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; @@ -14,6 +16,8 @@ public interface SQL { void close(ResultSet resultSet); + QueryStringBuilder builder(); + void addQueryLogger(QueryLogger logger); void removeQueryLogger(QueryLogger logger); diff --git a/src/main/java/org/javawebstack/orm/wrapper/SQLite.java b/src/main/java/org/javawebstack/orm/wrapper/SQLite.java index 9234b5f..1ae7a40 100644 --- a/src/main/java/org/javawebstack/orm/wrapper/SQLite.java +++ b/src/main/java/org/javawebstack/orm/wrapper/SQLite.java @@ -1,5 +1,8 @@ package org.javawebstack.orm.wrapper; +import org.javawebstack.orm.wrapper.builder.MySQLQueryStringBuilder; +import org.javawebstack.orm.wrapper.builder.QueryStringBuilder; + import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; @@ -35,6 +38,10 @@ public Connection getConnection() { return c; } + public QueryStringBuilder builder() { + return MySQLQueryStringBuilder.INSTANCE; // TODO Build a custom one for SQLite + } + } diff --git a/src/main/java/org/javawebstack/orm/wrapper/builder/MySQLQueryStringBuilder.java b/src/main/java/org/javawebstack/orm/wrapper/builder/MySQLQueryStringBuilder.java new file mode 100644 index 0000000..5e85191 --- /dev/null +++ b/src/main/java/org/javawebstack/orm/wrapper/builder/MySQLQueryStringBuilder.java @@ -0,0 +1,180 @@ +package org.javawebstack.orm.wrapper.builder; + +import org.javawebstack.orm.Repo; +import org.javawebstack.orm.SQLMapper; +import org.javawebstack.orm.TableInfo; +import org.javawebstack.orm.query.*; + +import java.sql.Timestamp; +import java.time.Instant; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class MySQLQueryStringBuilder implements QueryStringBuilder { + + public static final MySQLQueryStringBuilder INSTANCE = new MySQLQueryStringBuilder(); + + public SQLQueryString buildInsert(TableInfo info, Map values) { + List params = new ArrayList<>(); + StringBuilder sb = new StringBuilder("INSERT INTO `"); + sb.append(info.getTableName()); + sb.append("` ("); + List cols = new ArrayList<>(); + List vals = new ArrayList<>(); + for (Map.Entry columnValueMapping : values.entrySet()) { + cols.add("`" + columnValueMapping.getKey() + "`"); + vals.add("?"); + params.add(columnValueMapping.getValue()); + } + sb.append(String.join(",", cols)); + sb.append(") VALUES ("); + sb.append(String.join(",", vals)); + sb.append(");"); + return new SQLQueryString(sb.toString(), params); + } + + public SQLQueryString buildQuery(Query query, boolean count) { + Repo repo = query.getRepo(); + List parameters = new ArrayList<>(); + StringBuilder sb = new StringBuilder("SELECT ") + .append(count ? "COUNT(*)" : "*") + .append(" FROM `") + .append(repo.getInfo().getTableName()) + .append('`'); + QueryGroup where = query.getWhereGroup(); + checkWithDeleted(repo, query.isWithDeleted(), where); + if (!where.getQueryElements().isEmpty()) { + SQLQueryString qs = convertGroup(repo.getInfo(), where); + sb.append(" WHERE ").append(qs.getQuery()); + parameters.addAll(qs.getParameters()); + } + + QueryOrderBy orderBy = query.getOrder(); + if (!orderBy.isEmpty()) { + sb.append(" ORDER BY ") + .append(orderBy.toString(repo.getInfo())); + } + + Integer offset = query.getOffset(); + Integer limit = query.getLimit(); + if (offset != null && limit == null) + limit = Integer.MAX_VALUE; + if (limit != null) { + sb.append(" LIMIT ?"); + if (offset != null) { + sb.append(",?"); + parameters.add(offset); + } + parameters.add(limit); + } + return new SQLQueryString(sb.toString(), SQLMapper.mapParams(repo, parameters)); + } + + public SQLQueryString buildUpdate(Query query, Map values) { + Repo repo = query.getRepo(); + if (repo.getInfo().hasUpdated()) + values.put(repo.getInfo().getColumnName(repo.getInfo().getUpdatedField()), Timestamp.from(Instant.now())); + List parameters = new ArrayList<>(); + List sets = new ArrayList<>(); + values.forEach((key, value) -> { + sets.add("`" + key + "`=?"); + parameters.add(value); + }); + StringBuilder sb = new StringBuilder("UPDATE `") + .append(repo.getInfo().getTableName()) + .append("` SET ") + .append(String.join(",", sets)); + QueryGroup where = query.getWhereGroup(); + checkWithDeleted(repo, query.isWithDeleted(), where); + if (!where.getQueryElements().isEmpty()) { + SQLQueryString qs = convertGroup(repo.getInfo(), where); + sb.append(" WHERE ").append(qs.getQuery()); + parameters.addAll(qs.getParameters()); + } + sb.append(';'); + return new SQLQueryString(sb.toString(), SQLMapper.mapParams(repo, parameters).toArray()); + } + + public SQLQueryString buildDelete(Query query) { + Repo repo = query.getRepo(); + QueryGroup where = query.getWhereGroup(); + List parameters = new ArrayList<>(); + StringBuilder sb = new StringBuilder("DELETE FROM `") + .append(repo.getInfo().getTableName()) + .append('`'); + if (!where.getQueryElements().isEmpty()) { + SQLQueryString qs = convertGroup(repo.getInfo(), where); + sb.append(" WHERE ").append(qs.getQuery()); + parameters = qs.getParameters(); + } + return new SQLQueryString(sb.toString(), SQLMapper.mapParams(repo, parameters)); + } + + private void checkWithDeleted(Repo repo, boolean withDeleted, QueryGroup where) { + if (repo.getInfo().isSoftDelete() && !withDeleted) { + if (!where.getQueryElements().isEmpty()) + where.getQueryElements().add(0, QueryConjunction.AND); + where.getQueryElements().add(0, new QueryCondition(new QueryColumn(repo.getInfo().getColumnName(repo.getInfo().getSoftDeleteField())), "IS NULL", null)); + } + } + + private SQLQueryString convertElement(TableInfo info, QueryElement element) { + if(element instanceof QueryCondition) + return convertCondition(info, (QueryCondition) element); + if(element instanceof QueryConjunction) + return new SQLQueryString(((QueryConjunction) element).name()); + if(element instanceof QueryExists) { + QueryExists queryExists = (QueryExists) element; + SQLQueryString qs = buildQuery(queryExists.getQuery(), false); + return new SQLQueryString((queryExists.isNot() ? "NOT " : "") + "EXISTS (" + qs.getQuery() + ")", qs.getParameters()); + } + if(element instanceof QueryGroup) + return convertGroup(info, (QueryGroup) element); + return null; + } + + private SQLQueryString convertGroup(TableInfo info, QueryGroup group) { + StringBuilder sb = new StringBuilder("("); + List parameters = new ArrayList<>(); + for (QueryElement element : group.getQueryElements()) { + if (sb.length() > 1) + sb.append(' '); + SQLQueryString s = convertElement(info, element); + sb.append(s.getQuery()); + parameters.addAll(s.getParameters()); + } + sb.append(')'); + return new SQLQueryString(sb.toString(), parameters); + } + + private SQLQueryString convertCondition(TableInfo info, QueryCondition condition) { + StringBuilder sb = new StringBuilder(); + if (condition.isNot()) + sb.append("NOT "); + List parameters = new ArrayList<>(); + if (condition.getLeft() instanceof QueryColumn) { + sb.append(((QueryColumn) condition.getLeft()).toString(info)); + } else { + sb.append('?'); + parameters.add(condition.getLeft()); + } + sb.append(' '); + sb.append(condition.getOperator()); + if (condition.hasRight()) { + sb.append(' '); + if (condition.getOperator().endsWith("IN")) { + Object[] values = (Object[]) condition.getRight(); + sb.append("(").append(IntStream.range(0, values.length).mapToObj(i -> "?").collect(Collectors.joining(","))).append(")"); + parameters.addAll(Arrays.asList(values)); + } else if (condition.getRight() instanceof QueryColumn) { + sb.append(((QueryColumn) condition.getRight()).toString(info)); + } else { + sb.append('?'); + parameters.add(condition.getRight()); + } + } + return new SQLQueryString(sb.toString(), parameters); + } + +} diff --git a/src/main/java/org/javawebstack/orm/wrapper/builder/QueryStringBuilder.java b/src/main/java/org/javawebstack/orm/wrapper/builder/QueryStringBuilder.java new file mode 100644 index 0000000..46e93c5 --- /dev/null +++ b/src/main/java/org/javawebstack/orm/wrapper/builder/QueryStringBuilder.java @@ -0,0 +1,15 @@ +package org.javawebstack.orm.wrapper.builder; + +import org.javawebstack.orm.TableInfo; +import org.javawebstack.orm.query.*; + +import java.util.Map; + +public interface QueryStringBuilder { + + SQLQueryString buildInsert(TableInfo info, Map values); + SQLQueryString buildQuery(Query query, boolean count); + SQLQueryString buildUpdate(Query query, Map values); + SQLQueryString buildDelete(Query query); + +} diff --git a/src/main/java/org/javawebstack/orm/query/QueryString.java b/src/main/java/org/javawebstack/orm/wrapper/builder/SQLQueryString.java similarity index 66% rename from src/main/java/org/javawebstack/orm/query/QueryString.java rename to src/main/java/org/javawebstack/orm/wrapper/builder/SQLQueryString.java index 069a17a..3c60a42 100644 --- a/src/main/java/org/javawebstack/orm/query/QueryString.java +++ b/src/main/java/org/javawebstack/orm/wrapper/builder/SQLQueryString.java @@ -1,24 +1,24 @@ -package org.javawebstack.orm.query; +package org.javawebstack.orm.wrapper.builder; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -public class QueryString { +public class SQLQueryString { private final String query; private final List parameters; - public QueryString(String query, List parameters) { + public SQLQueryString(String query, List parameters) { this.query = query; this.parameters = parameters; } - public QueryString(String query, Object... parameters) { + public SQLQueryString(String query, Object... parameters) { this(query, new ArrayList<>(Arrays.asList(parameters))); } - public QueryString(String query) { + public SQLQueryString(String query) { this(query, new ArrayList<>()); } diff --git a/src/test/java/org/javawebstack/orm/test/exception/SectionIndexOutOfBoundException.java b/src/test/java/org/javawebstack/orm/test/exception/SectionIndexOutOfBoundException.java new file mode 100644 index 0000000..1a40ee7 --- /dev/null +++ b/src/test/java/org/javawebstack/orm/test/exception/SectionIndexOutOfBoundException.java @@ -0,0 +1,17 @@ +package org.javawebstack.orm.test.exception; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +/** + * Only to be used for tests. + * This exception should be thrown when a SQL Query String is manually parsed and sections and section types are defined, and + * a type of section is attempted to be retrieved which does not exist in this number. + */ +public class SectionIndexOutOfBoundException extends Exception { + private int sectionCount; + private int attemptedIndex; + private String topLevelKeyword; +} diff --git a/src/test/java/org/javawebstack/orm/test/querybuilding/FromClauseTest.java b/src/test/java/org/javawebstack/orm/test/querybuilding/FromClauseTest.java index f03f754..0963e9b 100644 --- a/src/test/java/org/javawebstack/orm/test/querybuilding/FromClauseTest.java +++ b/src/test/java/org/javawebstack/orm/test/querybuilding/FromClauseTest.java @@ -1,6 +1,5 @@ package org.javawebstack.orm.test.querybuilding; -import org.atteo.evo.inflector.English; import org.javawebstack.orm.Model; import org.javawebstack.orm.ORM; import org.javawebstack.orm.Repo; @@ -55,7 +54,7 @@ void testOneWordAlreadyInPluralDoesntWork() throws ORMConfigurationException { @Test void testOverwrittenTableName() throws ORMConfigurationException { - String query = getBaseQuery(OverwritteTableName.class); + String query = getBaseQuery(OverwrittenTableName.class); assertTrue(query.contains("FROM `oVer_writtenValue`")); } @@ -69,6 +68,6 @@ void testOverwrittenTableName() throws ORMConfigurationException { */ private String getBaseQuery(Class clazz) throws ORMConfigurationException { ORM.register(clazz, sql()); - return Repo.get(clazz).query().getQueryString().getQuery(); + return Repo.get(clazz).getConnection().builder().buildQuery(Repo.get(clazz).query(), false).getQuery(); } } diff --git a/src/test/java/org/javawebstack/orm/test/querybuilding/OrderByClauseTest.java b/src/test/java/org/javawebstack/orm/test/querybuilding/OrderByClauseTest.java new file mode 100644 index 0000000..d3aadb2 --- /dev/null +++ b/src/test/java/org/javawebstack/orm/test/querybuilding/OrderByClauseTest.java @@ -0,0 +1,165 @@ +package org.javawebstack.orm.test.querybuilding; + +import org.javawebstack.orm.exception.ORMQueryException; +import org.javawebstack.orm.query.Query; +import org.javawebstack.orm.test.exception.SectionIndexOutOfBoundException; +import org.javawebstack.orm.test.shared.models.Datatype; +import org.javawebstack.orm.test.shared.models.columnnames.OverwrittenColumnName; +import org.javawebstack.orm.test.shared.verification.QueryVerification; +import org.junit.jupiter.api.Test; +import java.util.*; +import static org.javawebstack.orm.test.shared.setup.ModelSetup.setUpModel; +import static org.junit.jupiter.api.Assertions.*; + +// This class tests the query generation for order by statements an MySQL +class OrderByClauseTest { + + @Test + void testOneExistingColumnDefaultOrderBy() { + Query query = setUpModel(Datatype.class).query() + .order("wrapperInteger"); + new QueryVerification(query).assertSectionEquals("ORDER BY", "`wrapper_integer`"); + } + + @Test + void testOneNonExistingColumnDefaultOrderBy() { + Query query = setUpModel(Datatype.class).query() + .order("doesNotExist"); + + // Not in snake case as it is not in the mapping + new QueryVerification(query).assertSectionEquals("ORDER BY", "`doesNotExist`"); + } + + @Test + void testOneExistingColumnASCOrderBy() { + Query query = setUpModel(Datatype.class).query() + .order("wrapperInteger", false); + new QueryVerification(query).assertSectionEquals("ORDER BY", "`wrapper_integer`"); + } + + @Test + void testOneNonExistingColumnASCOrderBy() { + Query query = setUpModel(Datatype.class).query() + .order("doesNotExist", false); + + // Not in snake case as it is not in the mapping + new QueryVerification(query).assertSectionEquals("ORDER BY", "`doesNotExist`"); + } + + @Test + void testOneExistingColumnDESCOrderBy() { + Query query = setUpModel(Datatype.class).query() + .order("wrapperInteger", true); + new QueryVerification(query).assertSectionEquals("ORDER BY", "`wrapper_integer` DESC"); + } + + @Test + void testOneNonExistingColumnDESCOrderBy() { + Query query = setUpModel(Datatype.class).query() + .order("doesNotExist", true); + // Not in snake case as it is not in the mapping + new QueryVerification(query).assertSectionEquals("ORDER BY", "`doesNotExist` DESC"); + } + + @Test + void testMultipleOrderByClausesOfASCOrder() { + Query query = setUpModel(Datatype.class).query() + .order("wrapperInteger") + .order("primitiveInteger"); + + new QueryVerification(query) + .assertSectionContains("ORDER BY", "`wrapper_integer`") + .assertSectionContains("ORDER BY", "`primitive_integer`"); + } + + @Test + void testMultipleOrderByClausesOfDESCOrder() { + Query query = setUpModel(Datatype.class).query() + .order("wrapperInteger", true) + .order("primitiveInteger", true); + + new QueryVerification(query) + .assertSectionContains("ORDER BY", "`wrapper_integer` DESC") + .assertSectionContains("ORDER BY", "`primitive_integer` DESC"); + } + + @Test + void testMultipleOrderByClausesOfMixedOrder() { + Query query = setUpModel(Datatype.class).query() + .order("wrapperInteger", false) + .order("primitiveInteger", true); + + new QueryVerification(query) + .assertSectionContains("ORDER BY", "`wrapper_integer`") + .assertSectionContains("ORDER BY", "`primitive_integer` DESC"); + } + + @Test + void testMultipleOrderByClausesOfMixedOrderReversed() { + Query query = setUpModel(Datatype.class).query() + .order("primitiveInteger", true) + .order("wrapperInteger", false); + + new QueryVerification(query) + .assertSectionContains("ORDER BY", "`primitive_integer` DESC") + .assertSectionContains("ORDER BY", "`wrapper_integer`"); + } + + + @Test + // This test is important because putting the order by statements in different order is relevant (they set priorities) + void testMultipleOrderByClausesOfRandomOrderForCorrectOrder() throws SectionIndexOutOfBoundException { + // This test does not use camel cases as input + Query query = setUpModel(Datatype.class).query(); + ArrayList columnNames = new ArrayList<>(Datatype.columnNames); + + LinkedList callOrder = new LinkedList<>(); + + Random r = new Random(); + columnNames.stream().unordered().forEach((singleColumn) -> { + query.order(singleColumn, r.nextBoolean()); + callOrder.add(singleColumn); + }); + + String queryString = new QueryVerification(query).getSection("ORDER BY"); + int lastIndex = 0; + int foundIndex; + for (String nextInCallOrder : callOrder) { + foundIndex = queryString.indexOf("`" + nextInCallOrder + "`"); + if(foundIndex < lastIndex) { + if (foundIndex == -1) + fail("Not all columns occurred in the query string."); + else + fail("The columns did not appear an the correct order."); + + break; + } + + lastIndex = foundIndex; + } + + // If it came until here the test should count as passed. + assertTrue(true); + + } + + @Test + void testWillUseOverwrittenColumnName() { + Query query = setUpModel(OverwrittenColumnName.class).query() + .order("dummyString"); + new QueryVerification(query).assertSectionEquals("ORDER BY", "`oVer_writtenColumn-name`"); + } + + /* + * Error Cases + */ + + // This test might not be correct here as it does not purely look at the query + @Test + void testCannotCallOrderOnSameColumnTwice() { + Query query = setUpModel(Datatype.class).query() + .order("primitiveInteger", true); + + assertThrows(ORMQueryException.class, () -> query.order("primitiveInteger")); + } +} diff --git a/src/test/java/org/javawebstack/orm/test/queryexecution/OrderByTest.java b/src/test/java/org/javawebstack/orm/test/queryexecution/OrderByTest.java new file mode 100644 index 0000000..cdc223a --- /dev/null +++ b/src/test/java/org/javawebstack/orm/test/queryexecution/OrderByTest.java @@ -0,0 +1,69 @@ +package org.javawebstack.orm.test.queryexecution; + +import org.javawebstack.orm.ORM; +import org.javawebstack.orm.Repo; +import org.javawebstack.orm.test.ORMTestCase; +import org.javawebstack.orm.test.shared.models.OnlyIdModel; +import org.javawebstack.orm.test.shared.setup.ModelSetup; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class OrderByTest extends ORMTestCase { + + @Test + void testOrderByCanPullResults() { + ModelSetup.setUpModel(OnlyIdModel.class); + ORM.autoMigrate(true); + + new OnlyIdModel().save(); + new OnlyIdModel().save(); + new OnlyIdModel().save(); + + assertDoesNotThrow(() -> Repo.get(OnlyIdModel.class) + .query() + .order("id") + .get() + ); + } + + @Test + void testOrderByWorksWithAsc() { + ModelSetup.setUpModel(OnlyIdModel.class); + ORM.autoMigrate(true); + + new OnlyIdModel().save(); + new OnlyIdModel().save(); + new OnlyIdModel().save(); + + List orderedList = Repo.get(OnlyIdModel.class) + .query() + .order("id", false) + .get(); + + for(int i = 1; i <= 3; i++) { + assertEquals(i, orderedList.get(i - 1).getId()); + } + } + + @Test + void testOrderByWorksWithDesc() { + ModelSetup.setUpModel(OnlyIdModel.class); + ORM.autoMigrate(true); + + new OnlyIdModel().save(); + new OnlyIdModel().save(); + new OnlyIdModel().save(); + + List orderedList = Repo.get(OnlyIdModel.class) + .query() + .order("id", true) + .get(); + + for(int i = 3; i >= 1; i--) { + assertEquals(i, orderedList.get(3 - i).getId()); + } + } +} diff --git a/src/test/java/org/javawebstack/orm/test/shared/models/Datatype.java b/src/test/java/org/javawebstack/orm/test/shared/models/Datatype.java index 7b4aa9d..5d79a8d 100644 --- a/src/test/java/org/javawebstack/orm/test/shared/models/Datatype.java +++ b/src/test/java/org/javawebstack/orm/test/shared/models/Datatype.java @@ -5,6 +5,8 @@ import java.sql.Date; import java.sql.Timestamp; +import java.util.Arrays; +import java.util.List; import java.util.UUID; /* @@ -84,4 +86,30 @@ public enum OptionEnum { OPTION1, OPTION2, } + + public static final List columnNames = Arrays.asList( + "id", + "primitive_boolean", + "wrapper_boolean", + "primitive_byte", + "wrapper_byte", + "primitive_short", + "wrapper_short", + "primitive_integer", + "wrapper_integer", + "primitive_long", + "wrapper_long", + "primitive_float", + "wrapper_float", + "primitive_double", + "wrapper_double", + "primitive_char", + "wrapper_string", + "char_array", + "byte_array", + "timestamp", + "date", + "uuid", + "option_enum" + ); } \ No newline at end of file diff --git a/src/test/java/org/javawebstack/orm/test/shared/models/OnlyIdModel.java b/src/test/java/org/javawebstack/orm/test/shared/models/OnlyIdModel.java index e69de29..9579265 100644 --- a/src/test/java/org/javawebstack/orm/test/shared/models/OnlyIdModel.java +++ b/src/test/java/org/javawebstack/orm/test/shared/models/OnlyIdModel.java @@ -0,0 +1,13 @@ +package org.javawebstack.orm.test.shared.models; + +import lombok.Getter; +import org.javawebstack.orm.Model; +import org.javawebstack.orm.annotation.Column; + +@Getter +public class OnlyIdModel extends Model { + + @Column + int id; + +} diff --git a/src/test/java/org/javawebstack/orm/test/shared/models/columnnames/OverwrittenColumnName.java b/src/test/java/org/javawebstack/orm/test/shared/models/columnnames/OverwrittenColumnName.java new file mode 100644 index 0000000..d4608e6 --- /dev/null +++ b/src/test/java/org/javawebstack/orm/test/shared/models/columnnames/OverwrittenColumnName.java @@ -0,0 +1,12 @@ +package org.javawebstack.orm.test.shared.models.columnnames; + +import org.javawebstack.orm.Model; +import org.javawebstack.orm.annotation.Column; + +public class OverwrittenColumnName extends Model { + @Column + int id; + + @Column(name = "oVer_writtenColumn-name") + String dummyString; +} diff --git a/src/test/java/org/javawebstack/orm/test/shared/models/tablenames/OverwritteTableName.java b/src/test/java/org/javawebstack/orm/test/shared/models/tablenames/OverwrittenTableName.java similarity index 87% rename from src/test/java/org/javawebstack/orm/test/shared/models/tablenames/OverwritteTableName.java rename to src/test/java/org/javawebstack/orm/test/shared/models/tablenames/OverwrittenTableName.java index 7b4413b..a1d139e 100644 --- a/src/test/java/org/javawebstack/orm/test/shared/models/tablenames/OverwritteTableName.java +++ b/src/test/java/org/javawebstack/orm/test/shared/models/tablenames/OverwrittenTableName.java @@ -8,7 +8,7 @@ * This class overwrites the model name to a seemingly random word to test multiple cases at once. */ @Table("oVer_writtenValue") -public class OverwritteTableName extends Model { +public class OverwrittenTableName extends Model { @Column int id; } diff --git a/src/test/java/org/javawebstack/orm/test/shared/verification/QueryVerification.java b/src/test/java/org/javawebstack/orm/test/shared/verification/QueryVerification.java index 0c2f5ec..442d7de 100644 --- a/src/test/java/org/javawebstack/orm/test/shared/verification/QueryVerification.java +++ b/src/test/java/org/javawebstack/orm/test/shared/verification/QueryVerification.java @@ -1,12 +1,13 @@ package org.javawebstack.orm.test.shared.verification; import org.javawebstack.orm.query.Query; +import org.javawebstack.orm.test.exception.SectionIndexOutOfBoundException; import org.javawebstack.orm.test.shared.util.QueryStringUtil; import java.util.HashSet; +import java.util.List; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.*; /** * The QueryVerification class wraps an JWS Query Object and provides assertion methods regarding the raw query string. @@ -22,7 +23,6 @@ public class QueryVerification { public QueryVerification(Query query) { this.query = query; - } /** @@ -32,8 +32,9 @@ public QueryVerification(Query query) { * @param topLevelKeyword The top level keyword that prefaces the section. * @param containedSubstring The substring which should be contained in the first section of the given type. */ - public void assertSectionContains(String topLevelKeyword, String containedSubstring) { + public QueryVerification assertSectionContains(String topLevelKeyword, String containedSubstring) { this.assertSectionContains(topLevelKeyword, containedSubstring, 0); + return this; } /** @@ -42,30 +43,119 @@ public void assertSectionContains(String topLevelKeyword, String containedSubstr * * @param topLevelKeyword The top level keyword that prefaces the section. * @param containedSubstring The substring which should be contained in the first section of the given type. - * @param sectionIndex The index of the section to be checked, thusly 0 refers to the first occurrence etc. + * @param sectionIndex The index of the section to be checked, thus 0 refers to the first occurrence etc. */ - public void assertSectionContains(String topLevelKeyword, String containedSubstring, int sectionIndex) { - String sectionString; - + public QueryVerification assertSectionContains(String topLevelKeyword, String containedSubstring, int sectionIndex) { + String sectionString = null; try { - sectionString = new QueryStringUtil(this.query.getQueryString().getQuery()) - .getTopLevelSectionsByKeyword(topLevelKeyword) - .get(sectionIndex); - } catch (IndexOutOfBoundsException ignored) { - fail(String.format( - "A top level section of type %s and index %d was tested but only %d sections of that type existed.", - sectionIndex, - topLevelKeyword, - sectionIndex + 1 - )); - - return; + sectionString = getSection(topLevelKeyword, sectionIndex); + } catch (SectionIndexOutOfBoundException e) { + this.failDueToSectionIndexOutOfBounds(e); + return this; } assertTrue( - sectionString.contains(containedSubstring), - String.format("The occurrence of index %d of %s section of the query did not contain a substring %s but looked like this: %s. Note that the match is case-sensitive.", sectionIndex, topLevelKeyword, containedSubstring, sectionString) + sectionString.contains(containedSubstring), + String.format("The occurrence of index %d of %s section of the query did not contain a substring %s but looked like this: %s. Note that the match is case-sensitive.", sectionIndex, topLevelKeyword, containedSubstring, sectionString) + ); + + return this; + } + + /** + * Asserts that the first occurring section of a given top level keyword is equal to the given string. + * This method uses the String.equals method internally and is therefore case sensitive. + * + * @param topLevelKeyword The top level keyword that prefaces the section. + * @param expectedString The substring which the first section of the given type should be equal to. + */ + public QueryVerification assertSectionEquals(String topLevelKeyword, String expectedString) { + this.assertSectionEquals(topLevelKeyword, expectedString, 0); + return this; + } + + /** + * Asserts that the i-th occurring section of a given top level keyword equal to the given string. + * This method uses the String.equals method internally and is therefore case sensitive. + * + * @param topLevelKeyword The top level keyword that prefaces the section. + * @param expectedString The substring which the specified section of the given type should be equal to. + * @param sectionIndex The index of the section to be checked, thus 0 refers to the first occurrence etc. + */ + public QueryVerification assertSectionEquals(String topLevelKeyword, String expectedString, int sectionIndex) { + String sectionString = null; + try { + sectionString = getSection(topLevelKeyword, sectionIndex); + } catch (SectionIndexOutOfBoundException e) { + this.failDueToSectionIndexOutOfBounds(e); + return this; + } + + assertEquals( + expectedString, + sectionString, + String.format("The occurrence of index %d of %s section of the query was not equal to the string %s but looked like this: %s. Note that the match is case-sensitive.", sectionIndex, topLevelKeyword, expectedString, sectionString) ); + + return this; + } + + /** + * Retrieves the inner part of a section by its keyword. With multiple occurrences it will only retrieve the first + * one. It does not include the keyword and one whitespaces at start and end. + * + * @param topLevelKeyword The top level keyword that prefaces the section. + * @return The inner part of the first section as specified. + * @throws SectionIndexOutOfBoundException if no section by that top level keyword exists. + */ + public String getSection(String topLevelKeyword) throws SectionIndexOutOfBoundException { + return this.getSection(topLevelKeyword, 0); + } + + /** + * Retrieves the inner part of a section by its keyword and index specifying which occurrence should be retrieved. + * It does not include the keyword and one whitespaces at start and end. + * + * @param topLevelKeyword The top level keyword that prefaces the section. + * @param sectionIndex The index of the section to be retrieved, thus 0 refers to the first occurrence etc. + * @return The inner part of the first section as specified. + * @throws SectionIndexOutOfBoundException if there are less than sectionIndex + 1 elements + */ + public String getSection(String topLevelKeyword, int sectionIndex) throws SectionIndexOutOfBoundException { + List sectionList = this.getSectionList(topLevelKeyword); + try { + return sectionList.get(sectionIndex); + } catch (IndexOutOfBoundsException converted) { + + SectionIndexOutOfBoundException exception = new SectionIndexOutOfBoundException(); + + exception.setSectionCount(sectionList.size()); + exception.setAttemptedIndex(sectionIndex); + exception.setTopLevelKeyword(topLevelKeyword); + + throw exception; + } + } + + /** + * Retrieve list of all sections prefaced with the specified top level keyword. The list will have the same order + * as the occurrences of each section. + * + * @param topLevelKeyword The top level keyword that prefaces the sections. + * @return The order sensitive string list of inner sections. + */ + public List getSectionList(String topLevelKeyword) { + return new QueryStringUtil(this.query.getRepo().getConnection().builder().buildQuery(this.query, false).getQuery()) + .getTopLevelSectionsByKeyword(topLevelKeyword); + } + + private void failDueToSectionIndexOutOfBounds(SectionIndexOutOfBoundException exception) { + fail(String.format( + "A top level section of type %s and index %d was tested but only %d sections of that type existed.", + exception.getTopLevelKeyword(), + exception.getAttemptedIndex(), + exception.getSectionCount() + )); } } diff --git a/start_mariadb_test_server.sh b/start_mariadb_test_server.sh new file mode 100644 index 0000000..23e4b97 --- /dev/null +++ b/start_mariadb_test_server.sh @@ -0,0 +1 @@ +docker run -p 3306:3306 -e MYSQL_DATABASE=ormtest -e MYSQL_ROOT_PASSWORD=testpassword -d mariadb:10.5.9 \ No newline at end of file