├── .gitattributes ├── assets ├── badge.png ├── logo-old.jpg ├── logo-white.png └── logo-transparent.png ├── src ├── main │ └── java │ │ └── co │ │ └── aurasphere │ │ └── gomorrasql │ │ ├── Keywords.java │ │ ├── states │ │ ├── FinalState.java │ │ ├── where │ │ │ ├── WhereFieldState.java │ │ │ ├── WhereValueState.java │ │ │ ├── WhereJoinState.java │ │ │ └── WhereOperatorState.java │ │ ├── SingleTokenMatchState.java │ │ ├── AnyTokenConsumerState.java │ │ ├── AbstractState.java │ │ ├── GreedyMatchKeywordState.java │ │ ├── query │ │ │ ├── UpdateSetState.java │ │ │ ├── SelectColumnsState.java │ │ │ └── OptionalWhereState.java │ │ ├── CommaSeparedValuesState.java │ │ └── InitialState.java │ │ ├── model │ │ ├── GomorraSqlQueryResult.java │ │ ├── WhereCondition.java │ │ ├── CaggiaFaException.java │ │ └── QueryInfo.java │ │ ├── GomorraSqlShell.java │ │ └── GomorraSqlInterpreter.java └── test │ ├── java │ └── co │ │ └── aurasphere │ │ └── gomorrasql │ │ ├── TestDelete.java │ │ ├── TestInsert.java │ │ ├── TestSelect.java │ │ ├── TestUpdate.java │ │ ├── TestTransactions.java │ │ ├── TestWrongSyntax.java │ │ ├── Config.java │ │ └── TestGomorraSqlInterpreter.java │ └── resources │ └── init-db.sql ├── .gitignore ├── .travis.yml ├── LICENSE ├── pom.xml └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | *.java linguist-language=CSS -------------------------------------------------------------------------------- /assets/badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurasphere/gomorra-sql/HEAD/assets/badge.png -------------------------------------------------------------------------------- /assets/logo-old.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurasphere/gomorra-sql/HEAD/assets/logo-old.jpg -------------------------------------------------------------------------------- /assets/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurasphere/gomorra-sql/HEAD/assets/logo-white.png -------------------------------------------------------------------------------- /assets/logo-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurasphere/gomorra-sql/HEAD/assets/logo-transparent.png -------------------------------------------------------------------------------- /src/main/java/co/aurasphere/gomorrasql/Keywords.java: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurasphere/gomorra-sql/HEAD/src/main/java/co/aurasphere/gomorrasql/Keywords.java -------------------------------------------------------------------------------- /src/test/java/co/aurasphere/gomorrasql/TestDelete.java: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurasphere/gomorra-sql/HEAD/src/test/java/co/aurasphere/gomorrasql/TestDelete.java -------------------------------------------------------------------------------- /src/test/java/co/aurasphere/gomorrasql/TestInsert.java: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurasphere/gomorra-sql/HEAD/src/test/java/co/aurasphere/gomorrasql/TestInsert.java -------------------------------------------------------------------------------- /src/test/java/co/aurasphere/gomorrasql/TestSelect.java: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurasphere/gomorra-sql/HEAD/src/test/java/co/aurasphere/gomorrasql/TestSelect.java -------------------------------------------------------------------------------- /src/test/java/co/aurasphere/gomorrasql/TestUpdate.java: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurasphere/gomorra-sql/HEAD/src/test/java/co/aurasphere/gomorrasql/TestUpdate.java -------------------------------------------------------------------------------- /src/test/java/co/aurasphere/gomorrasql/TestTransactions.java: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurasphere/gomorra-sql/HEAD/src/test/java/co/aurasphere/gomorrasql/TestTransactions.java -------------------------------------------------------------------------------- /src/test/java/co/aurasphere/gomorrasql/TestWrongSyntax.java: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurasphere/gomorra-sql/HEAD/src/test/java/co/aurasphere/gomorrasql/TestWrongSyntax.java -------------------------------------------------------------------------------- /src/test/java/co/aurasphere/gomorrasql/Config.java: -------------------------------------------------------------------------------- 1 | package co.aurasphere.gomorrasql; 2 | 3 | import java.sql.Connection; 4 | import java.sql.DriverManager; 5 | import java.sql.SQLException; 6 | 7 | public class Config { 8 | 9 | static Connection getDbConnection() throws SQLException { 10 | String jdbcString = "jdbc:h2:mem:sample;INIT=RUNSCRIPT FROM 'classpath:init-db.sql'"; 11 | return DriverManager.getConnection(jdbcString); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | /.classpath 25 | /.project 26 | /.settings/org.eclipse.jdt.core.prefs 27 | /.settings/org.eclipse.m2e.core.prefs 28 | /target/test-classes 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | sudo: false 3 | jdk: 4 | - openjdk8 5 | install: true 6 | 7 | branches: 8 | only: 9 | - main 10 | 11 | before_script: 12 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 13 | - chmod +x ./cc-test-reporter 14 | - ./cc-test-reporter before-build 15 | 16 | script: 17 | - mvn clean install cobertura:cobertura 18 | 19 | after_success: 20 | - ./cc-test-reporter format-coverage -t cobertura target/site/cobertura/coverage.xml 21 | - ./cc-test-reporter upload-coverage 22 | -------------------------------------------------------------------------------- /src/main/java/co/aurasphere/gomorrasql/states/FinalState.java: -------------------------------------------------------------------------------- 1 | package co.aurasphere.gomorrasql.states; 2 | 3 | import co.aurasphere.gomorrasql.model.CaggiaFaException; 4 | import co.aurasphere.gomorrasql.model.QueryInfo; 5 | 6 | /** 7 | * State that represents the end of a query. 8 | * 9 | * @author Donato Rimenti 10 | * 11 | */ 12 | public class FinalState extends AbstractState { 13 | 14 | public FinalState(QueryInfo queryInfo) { 15 | super(queryInfo); 16 | } 17 | 18 | @Override 19 | public AbstractState transitionToNextState(String token) throws CaggiaFaException { 20 | throw new CaggiaFaException("%END_OF_QUERY%", token); 21 | } 22 | 23 | @Override 24 | public boolean isFinalState() { 25 | return true; 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /src/test/resources/init-db.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE USER ( 2 | USER_ID INT, 3 | FIRST_NAME VARCHAR(50), 4 | LAST_NAME VARCHAR(50), 5 | ADDRESS VARCHAR(100), 6 | FK_CITY_ID INT, 7 | FESS BOOLEAN 8 | ); 9 | 10 | CREATE TABLE CITY ( 11 | CITY_ID INT, 12 | CITY_NAME VARCHAR(50) 13 | ); 14 | 15 | INSERT INTO USER VALUES (1, 'PINCO', 'PALLINO', 'VIA DEI PAZZI', 1, TRUE); 16 | INSERT INTO USER VALUES (2, 'JOHN', 'DOE', NULL, 4, TRUE); 17 | INSERT INTO USER VALUES (3, 'PAOLINO', 'PAPERINO', 'VIA DEI PAPERI', 2, FALSE); 18 | INSERT INTO USER VALUES (4, 'FRED', 'FLINSTONE', 'VIA ROSSI', 3, FALSE); 19 | 20 | INSERT INTO CITY VALUES (1, 'MILANO'); 21 | INSERT INTO CITY VALUES (2, 'ROMA'); 22 | INSERT INTO CITY VALUES (3, 'NAPOLI'); 23 | INSERT INTO CITY VALUES (4, 'NEW YORK'); 24 | -------------------------------------------------------------------------------- /src/main/java/co/aurasphere/gomorrasql/model/GomorraSqlQueryResult.java: -------------------------------------------------------------------------------- 1 | package co.aurasphere.gomorrasql.model; 2 | 3 | import java.sql.ResultSet; 4 | 5 | /** 6 | * Wrapper around JDBC query results, used by GomorraSQL. 7 | * 8 | * @author Donato Rimenti 9 | * 10 | */ 11 | public class GomorraSqlQueryResult { 12 | 13 | private Integer affectedRows; 14 | 15 | private ResultSet resultSet; 16 | 17 | public Integer getAffectedRows() { 18 | return affectedRows; 19 | } 20 | 21 | public void setAffectedRows(Integer affectedRows) { 22 | this.affectedRows = affectedRows; 23 | } 24 | 25 | public ResultSet getResultSet() { 26 | return resultSet; 27 | } 28 | 29 | public void setResultSet(ResultSet resultSet) { 30 | this.resultSet = resultSet; 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /src/main/java/co/aurasphere/gomorrasql/model/WhereCondition.java: -------------------------------------------------------------------------------- 1 | package co.aurasphere.gomorrasql.model; 2 | 3 | /** 4 | * Represents a statement in a where condition of a GomorraSQL query. 5 | * 6 | * @author Donato Rimenti 7 | * 8 | */ 9 | public class WhereCondition { 10 | 11 | private String field; 12 | private String value; 13 | private String operator; 14 | 15 | public WhereCondition(String field) { 16 | this.field = field; 17 | } 18 | 19 | public String getField() { 20 | return field; 21 | } 22 | 23 | public String getValue() { 24 | return value; 25 | } 26 | 27 | public void setValue(String value) { 28 | this.value = value; 29 | } 30 | 31 | public String getOperator() { 32 | return operator; 33 | } 34 | 35 | public void setOperator(String operator) { 36 | this.operator = operator; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/co/aurasphere/gomorrasql/model/CaggiaFaException.java: -------------------------------------------------------------------------------- 1 | package co.aurasphere.gomorrasql.model; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Exception thrown by GomorraSQL when an error occurs during query 7 | * parsing/execution. 8 | * 9 | * @author Donato Rimenti 10 | * 11 | */ 12 | public class CaggiaFaException extends RuntimeException { 13 | 14 | public CaggiaFaException(List expectedTokens, String actualToken) { 15 | super("Expected one of " + expectedTokens + " but got [" + actualToken + "]"); 16 | } 17 | 18 | public CaggiaFaException(String expectedToken, String token) { 19 | super("Expected [" + expectedToken + "] but got [" + token + "]"); 20 | } 21 | 22 | public CaggiaFaException(String message) { 23 | super(message); 24 | } 25 | 26 | public CaggiaFaException(Exception e) { 27 | super(e); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/co/aurasphere/gomorrasql/states/where/WhereFieldState.java: -------------------------------------------------------------------------------- 1 | package co.aurasphere.gomorrasql.states.where; 2 | 3 | import co.aurasphere.gomorrasql.model.CaggiaFaException; 4 | import co.aurasphere.gomorrasql.model.QueryInfo; 5 | import co.aurasphere.gomorrasql.model.WhereCondition; 6 | import co.aurasphere.gomorrasql.states.AbstractState; 7 | 8 | /** 9 | * State for creating a new WHERE subclause in the format "field operator 10 | * value" by inserting the field name. 11 | * 12 | * @author Donato Rimenti 13 | * 14 | */ 15 | public class WhereFieldState extends AbstractState { 16 | 17 | public WhereFieldState(QueryInfo queryInfo) { 18 | super(queryInfo); 19 | } 20 | 21 | @Override 22 | public AbstractState transitionToNextState(String token) throws CaggiaFaException { 23 | return new WhereOperatorState(queryInfo, new WhereCondition(token)); 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/main/java/co/aurasphere/gomorrasql/states/SingleTokenMatchState.java: -------------------------------------------------------------------------------- 1 | package co.aurasphere.gomorrasql.states; 2 | 3 | import java.util.function.Function; 4 | 5 | import co.aurasphere.gomorrasql.model.CaggiaFaException; 6 | import co.aurasphere.gomorrasql.model.QueryInfo; 7 | 8 | /** 9 | * States that proceeds if a specific token is matched exactly. 10 | * 11 | * @author Donato Rimenti 12 | * 13 | */ 14 | public class SingleTokenMatchState extends AbstractState { 15 | 16 | private String expectedToken; 17 | private Function transitionFunction; 18 | 19 | public SingleTokenMatchState(QueryInfo queryInfo, String expectedToken, 20 | Function transitionFunction) { 21 | super(queryInfo); 22 | this.expectedToken = expectedToken; 23 | this.transitionFunction = transitionFunction; 24 | } 25 | 26 | @Override 27 | public AbstractState transitionToNextState(String token) throws CaggiaFaException { 28 | if (token.equalsIgnoreCase(expectedToken)) { 29 | return transitionFunction.apply(queryInfo); 30 | } 31 | throw new CaggiaFaException(expectedToken, token); 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Donato Rimenti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/co/aurasphere/gomorrasql/states/AnyTokenConsumerState.java: -------------------------------------------------------------------------------- 1 | package co.aurasphere.gomorrasql.states; 2 | 3 | import java.util.function.Consumer; 4 | import java.util.function.Function; 5 | 6 | import co.aurasphere.gomorrasql.model.CaggiaFaException; 7 | import co.aurasphere.gomorrasql.model.QueryInfo; 8 | 9 | /** 10 | * Consumes any token applying the given function and then moves to the next 11 | * state. 12 | * 13 | * @author Donato Rimenti 14 | * 15 | */ 16 | public class AnyTokenConsumerState extends AbstractState { 17 | 18 | private Consumer tokenConsumer; 19 | private Function transitionFunction; 20 | 21 | public AnyTokenConsumerState(QueryInfo queryInfo, Consumer tokenConsumer, 22 | Function transitionFunction) { 23 | super(queryInfo); 24 | this.tokenConsumer = tokenConsumer; 25 | this.transitionFunction = transitionFunction; 26 | } 27 | 28 | @Override 29 | public AbstractState transitionToNextState(String token) throws CaggiaFaException { 30 | tokenConsumer.accept(token); 31 | return transitionFunction.apply(queryInfo); 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /src/main/java/co/aurasphere/gomorrasql/states/where/WhereValueState.java: -------------------------------------------------------------------------------- 1 | package co.aurasphere.gomorrasql.states.where; 2 | 3 | import co.aurasphere.gomorrasql.Keywords; 4 | import co.aurasphere.gomorrasql.model.CaggiaFaException; 5 | import co.aurasphere.gomorrasql.model.QueryInfo; 6 | import co.aurasphere.gomorrasql.model.WhereCondition; 7 | import co.aurasphere.gomorrasql.states.AbstractState; 8 | 9 | /** 10 | * State for completing the last WHERE subclause in the format "field operator 11 | * value" by inserting the value. 12 | * 13 | * @author Donato Rimenti 14 | * 15 | */ 16 | public class WhereValueState extends AbstractState { 17 | 18 | private WhereCondition condition; 19 | 20 | public WhereValueState(QueryInfo queryInfo, WhereCondition condition) { 21 | super(queryInfo); 22 | this.condition = condition; 23 | } 24 | 25 | @Override 26 | public AbstractState transitionToNextState(String token) throws CaggiaFaException { 27 | if(token.equalsIgnoreCase(Keywords.NULL_KEYWORD)) { 28 | condition.setValue("NULL"); 29 | } else { 30 | condition.setValue(token); 31 | } 32 | queryInfo.addWhereCondition(condition); 33 | return new WhereJoinState(queryInfo); 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /src/test/java/co/aurasphere/gomorrasql/TestGomorraSqlInterpreter.java: -------------------------------------------------------------------------------- 1 | package co.aurasphere.gomorrasql; 2 | 3 | import java.sql.Connection; 4 | import java.sql.SQLException; 5 | 6 | import org.junit.After; 7 | import org.junit.Assert; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | 11 | import co.aurasphere.gomorrasql.model.CaggiaFaException; 12 | 13 | public class TestGomorraSqlInterpreter { 14 | 15 | private Connection connection; 16 | 17 | @Before 18 | public void setup() throws SQLException { 19 | this.connection = Config.getDbConnection(); 20 | } 21 | 22 | @After 23 | public void teardown() throws SQLException { 24 | this.connection.close(); 25 | } 26 | 27 | @Test(expected = CaggiaFaException.class) 28 | public void testWrongDataTypeInsert() throws SQLException { 29 | GomorraSqlInterpreter gsi = new GomorraSqlInterpreter(connection); 30 | gsi.execute("nzipp 'ngoppa city chist 'RHO', 7"); 31 | } 32 | 33 | @Test 34 | public void testSqlConversion() throws SQLException { 35 | String gomorraQuery = "nzipp 'ngoppa city chist 6, 8"; 36 | String sqlQuery = GomorraSqlInterpreter.toSqlQuery(gomorraQuery); 37 | Assert.assertEquals("INSERT INTO city VALUES ( 6, 8 )", sqlQuery); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/co/aurasphere/gomorrasql/states/AbstractState.java: -------------------------------------------------------------------------------- 1 | package co.aurasphere.gomorrasql.states; 2 | 3 | import co.aurasphere.gomorrasql.model.CaggiaFaException; 4 | import co.aurasphere.gomorrasql.model.QueryInfo; 5 | 6 | /** 7 | * Base class for any state used by the GomorraSQL parser. 8 | * 9 | * @author Donato Rimenti 10 | * 11 | */ 12 | public abstract class AbstractState { 13 | 14 | protected QueryInfo queryInfo; 15 | 16 | public AbstractState(QueryInfo queryInfo) { 17 | this.queryInfo = queryInfo; 18 | } 19 | 20 | /** 21 | * Moves from this state to the next one, according to the token read. 22 | * 23 | * @param token the token in the query currently being parsed 24 | * @return the next state 25 | * @throws CaggiaFaException if the token is not allowed in this position 26 | */ 27 | public abstract AbstractState transitionToNextState(String token) throws CaggiaFaException; 28 | 29 | /** 30 | * Returns true if the query can end with this state (e.g. if the query can end 31 | * with a given token). 32 | * 33 | * @return true if the query can end in this state 34 | */ 35 | public boolean isFinalState() { 36 | return false; 37 | } 38 | 39 | public QueryInfo getQueryInfo() { 40 | return queryInfo; 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /src/main/java/co/aurasphere/gomorrasql/states/where/WhereJoinState.java: -------------------------------------------------------------------------------- 1 | package co.aurasphere.gomorrasql.states.where; 2 | 3 | import java.util.Arrays; 4 | 5 | import co.aurasphere.gomorrasql.Keywords; 6 | import co.aurasphere.gomorrasql.model.CaggiaFaException; 7 | import co.aurasphere.gomorrasql.model.QueryInfo; 8 | import co.aurasphere.gomorrasql.states.AbstractState; 9 | 10 | /** 11 | * State for joining two WHERE subclause using an AND or OR operator, or 12 | * finishing the WHERE clause. 13 | * 14 | * @author Donato Rimenti 15 | * 16 | */ 17 | public class WhereJoinState extends AbstractState { 18 | 19 | public WhereJoinState(QueryInfo queryInfo) { 20 | super(queryInfo); 21 | } 22 | 23 | @Override 24 | public AbstractState transitionToNextState(String token) throws CaggiaFaException { 25 | if (token.equalsIgnoreCase(Keywords.AND_KEYWORD)) { 26 | queryInfo.addWhereConditionsJoinOperator("AND"); 27 | return new WhereFieldState(queryInfo); 28 | } 29 | if (token.equalsIgnoreCase(Keywords.OR_KEYWORD)) { 30 | queryInfo.addWhereConditionsJoinOperator("OR"); 31 | return new WhereFieldState(queryInfo); 32 | } 33 | throw new CaggiaFaException(Arrays.asList(Keywords.AND_KEYWORD, Keywords.OR_KEYWORD), token); 34 | } 35 | 36 | @Override 37 | public boolean isFinalState() { 38 | return true; 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /src/main/java/co/aurasphere/gomorrasql/states/GreedyMatchKeywordState.java: -------------------------------------------------------------------------------- 1 | package co.aurasphere.gomorrasql.states; 2 | 3 | import java.util.function.Function; 4 | 5 | import co.aurasphere.gomorrasql.model.CaggiaFaException; 6 | import co.aurasphere.gomorrasql.model.QueryInfo; 7 | 8 | /** 9 | * State that proceeds to the next one if all the given keywords are matched, 10 | * otherwise throws an exception. 11 | * 12 | * @author Donato Rimenti 13 | * 14 | */ 15 | public class GreedyMatchKeywordState extends AbstractState { 16 | 17 | private int currentIndex = 1; 18 | private String[] keywords; 19 | private Function nextStateTransition; 20 | 21 | public GreedyMatchKeywordState(QueryInfo queryInfo, String[] keywords, 22 | Function nextStateTransition) { 23 | super(queryInfo); 24 | this.keywords = keywords; 25 | this.nextStateTransition = nextStateTransition; 26 | } 27 | 28 | public GreedyMatchKeywordState(QueryInfo queryInfo, String[] keywords, 29 | Function nextStateTransition, int currentIndex) { 30 | super(queryInfo); 31 | this.keywords = keywords; 32 | this.nextStateTransition = nextStateTransition; 33 | this.currentIndex = currentIndex; 34 | } 35 | 36 | @Override 37 | public AbstractState transitionToNextState(String token) throws CaggiaFaException { 38 | if (token.equalsIgnoreCase(keywords[currentIndex])) { 39 | currentIndex++; 40 | if (currentIndex == keywords.length) { 41 | return nextStateTransition.apply(queryInfo); 42 | } 43 | return this; 44 | } 45 | throw new CaggiaFaException(keywords[currentIndex], token); 46 | } 47 | } -------------------------------------------------------------------------------- /src/main/java/co/aurasphere/gomorrasql/states/where/WhereOperatorState.java: -------------------------------------------------------------------------------- 1 | package co.aurasphere.gomorrasql.states.where; 2 | 3 | import co.aurasphere.gomorrasql.Keywords; 4 | import co.aurasphere.gomorrasql.model.CaggiaFaException; 5 | import co.aurasphere.gomorrasql.model.QueryInfo; 6 | import co.aurasphere.gomorrasql.model.WhereCondition; 7 | import co.aurasphere.gomorrasql.states.AbstractState; 8 | import co.aurasphere.gomorrasql.states.GreedyMatchKeywordState; 9 | 10 | /** 11 | * State for continuing the last WHERE subclause in the format "field operator 12 | * value" by inserting the operator. 13 | * 14 | * @author Donato Rimenti 15 | * 16 | */ 17 | public class WhereOperatorState extends AbstractState { 18 | 19 | private WhereCondition condition; 20 | 21 | public WhereOperatorState(QueryInfo queryInfo, WhereCondition condition) { 22 | super(queryInfo); 23 | this.condition = condition; 24 | } 25 | 26 | @Override 27 | public AbstractState transitionToNextState(String token) throws CaggiaFaException { 28 | if (Keywords.WHERE_OPERATORS.contains(token) || token.equalsIgnoreCase(Keywords.IS_KEYWORD)) { 29 | if (token.equalsIgnoreCase(Keywords.IS_NOT_KEYWORDS[0])) { 30 | condition.setOperator("IS NOT"); 31 | return new GreedyMatchKeywordState(queryInfo, Keywords.IS_NOT_KEYWORDS, q -> new WhereValueState(q, condition)); 32 | } 33 | if (token.equalsIgnoreCase(Keywords.IS_KEYWORD)) { 34 | condition.setOperator("IS"); 35 | } else { 36 | condition.setOperator(token); 37 | } 38 | return new WhereValueState(queryInfo, condition); 39 | } 40 | throw new CaggiaFaException(Keywords.WHERE_OPERATORS, token); 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /src/main/java/co/aurasphere/gomorrasql/states/query/UpdateSetState.java: -------------------------------------------------------------------------------- 1 | package co.aurasphere.gomorrasql.states.query; 2 | 3 | import java.util.Arrays; 4 | 5 | import co.aurasphere.gomorrasql.Keywords; 6 | import co.aurasphere.gomorrasql.model.CaggiaFaException; 7 | import co.aurasphere.gomorrasql.model.QueryInfo; 8 | import co.aurasphere.gomorrasql.states.AbstractState; 9 | import co.aurasphere.gomorrasql.states.AnyTokenConsumerState; 10 | import co.aurasphere.gomorrasql.states.SingleTokenMatchState; 11 | import co.aurasphere.gomorrasql.states.where.WhereFieldState; 12 | 13 | /** 14 | * State for an update when the first value is set. Allows to continue setting 15 | * values or moving on to an optional where clause. 16 | * 17 | * @author Donato Rimenti 18 | * 19 | */ 20 | public class UpdateSetState extends AbstractState { 21 | 22 | public UpdateSetState(QueryInfo queryInfo) { 23 | super(queryInfo); 24 | } 25 | 26 | @Override 27 | public AbstractState transitionToNextState(String token) throws CaggiaFaException { 28 | // Adds another variable 29 | if (token.equalsIgnoreCase(",")) { 30 | return new AnyTokenConsumerState(queryInfo, queryInfo::addColumnName, 31 | q -> new SingleTokenMatchState(q, Keywords.SET_EQUAL_KEYWORD, 32 | q2 -> new AnyTokenConsumerState(q2, q2::addValue, UpdateSetState::new))); 33 | } 34 | // Moves to a where clause 35 | if (token.equalsIgnoreCase(Keywords.WHERE_KEYWORD)) { 36 | return new WhereFieldState(queryInfo); 37 | } 38 | throw new CaggiaFaException(Arrays.asList(",", Keywords.WHERE_KEYWORD, "%END_OF_QUERY%"), token); 39 | } 40 | 41 | @Override 42 | public boolean isFinalState() { 43 | return true; 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /src/main/java/co/aurasphere/gomorrasql/states/query/SelectColumnsState.java: -------------------------------------------------------------------------------- 1 | package co.aurasphere.gomorrasql.states.query; 2 | 3 | import co.aurasphere.gomorrasql.Keywords; 4 | import co.aurasphere.gomorrasql.model.CaggiaFaException; 5 | import co.aurasphere.gomorrasql.model.QueryInfo; 6 | import co.aurasphere.gomorrasql.states.AbstractState; 7 | import co.aurasphere.gomorrasql.states.AnyTokenConsumerState; 8 | import co.aurasphere.gomorrasql.states.CommaSeparedValuesState; 9 | import co.aurasphere.gomorrasql.states.GreedyMatchKeywordState; 10 | 11 | /** 12 | * State that allows a select to switch between the * operator and the column 13 | * names to rietrieve. 14 | * 15 | * @author Donato Rimenti 16 | * 17 | */ 18 | public class SelectColumnsState extends AbstractState { 19 | 20 | public SelectColumnsState(QueryInfo queryInfo) { 21 | super(queryInfo); 22 | } 23 | 24 | @Override 25 | public AbstractState transitionToNextState(String token) throws CaggiaFaException { 26 | if (token.equalsIgnoreCase(Keywords.ASTERISK_KEYWORDS[0])) { 27 | // Token is "*" (all columns). We proceed to from keyword 28 | queryInfo.addColumnName("*"); 29 | return new GreedyMatchKeywordState(queryInfo, Keywords.ASTERISK_KEYWORDS, 30 | q -> new GreedyMatchKeywordState(q, Keywords.FROM_KEYWORDS, 31 | q2 -> new AnyTokenConsumerState(q2, q2::setTableName, OptionalWhereState::new), 0)); 32 | } else { 33 | // Token is a column name, we continue until there are none 34 | queryInfo.addColumnName(token); 35 | return new CommaSeparedValuesState(queryInfo, queryInfo.getColumnNames(), Keywords.FROM_KEYWORDS[0], 36 | "%COLUMN_NAME%", q -> new GreedyMatchKeywordState(queryInfo, Keywords.FROM_KEYWORDS, 37 | q2 -> new AnyTokenConsumerState(q2, q2::setTableName, OptionalWhereState::new))); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/main/java/co/aurasphere/gomorrasql/states/query/OptionalWhereState.java: -------------------------------------------------------------------------------- 1 | package co.aurasphere.gomorrasql.states.query; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.List; 6 | 7 | import co.aurasphere.gomorrasql.Keywords; 8 | import co.aurasphere.gomorrasql.model.CaggiaFaException; 9 | import co.aurasphere.gomorrasql.model.QueryInfo; 10 | import co.aurasphere.gomorrasql.model.QueryInfo.QueryType; 11 | import co.aurasphere.gomorrasql.states.AbstractState; 12 | import co.aurasphere.gomorrasql.states.AnyTokenConsumerState; 13 | import co.aurasphere.gomorrasql.states.GreedyMatchKeywordState; 14 | import co.aurasphere.gomorrasql.states.where.WhereFieldState; 15 | 16 | /** 17 | * State that allows for an optional WHERE clause, JOIN clause (only when the 18 | * query is a select) or end of the query. 19 | * 20 | * @author Donato Rimenti 21 | * 22 | */ 23 | public class OptionalWhereState extends AbstractState { 24 | 25 | public OptionalWhereState(QueryInfo queryInfo) { 26 | super(queryInfo); 27 | } 28 | 29 | @Override 30 | public AbstractState transitionToNextState(String token) throws CaggiaFaException { 31 | List expectedKeywords = new ArrayList<>(Arrays.asList(Keywords.WHERE_KEYWORD)); 32 | if (token.equalsIgnoreCase(Keywords.WHERE_KEYWORD)) { 33 | return new WhereFieldState(queryInfo); 34 | } 35 | if (queryInfo.getType().equals(QueryType.SELECT)) { 36 | expectedKeywords.add(Keywords.JOIN_KEYWORDS[0]); 37 | if (token.equalsIgnoreCase(Keywords.JOIN_KEYWORDS[0])) { 38 | return new GreedyMatchKeywordState(queryInfo, Keywords.JOIN_KEYWORDS, 39 | q -> new AnyTokenConsumerState(q, q::addJoinedTable, OptionalWhereState::new)); 40 | } 41 | } 42 | throw new CaggiaFaException(expectedKeywords, token); 43 | } 44 | 45 | @Override 46 | public boolean isFinalState() { 47 | return true; 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | co.aurasphere 6 | gomorra-sql 7 | 1.0.0 8 | 9 | 10 | 1.8 11 | 1.8 12 | 13 | 14 | 15 | 16 | com.h2database 17 | h2 18 | 1.4.200 19 | test 20 | 21 | 22 | 23 | junit 24 | junit 25 | 4.13.2 26 | test 27 | 28 | 29 | 30 | 31 | 32 | org.codehaus.mojo 33 | cobertura-maven-plugin 34 | 2.7 35 | 36 | 37 | html 38 | xml 39 | 40 | 41 | 42 | 43 | org.apache.maven.plugins 44 | maven-assembly-plugin 45 | 46 | 47 | package 48 | 49 | single 50 | 51 | 52 | 53 | 54 | 55 | co.aurasphere.gomorrasql.GomorraSqlShell 56 | 57 | 58 | 59 | 60 | jar-with-dependencies 61 | 62 | false 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/main/java/co/aurasphere/gomorrasql/model/QueryInfo.java: -------------------------------------------------------------------------------- 1 | package co.aurasphere.gomorrasql.model; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /** 7 | * Context wrapper for the parsed query. 8 | * 9 | * @author Donato Rimenti 10 | * 11 | */ 12 | public class QueryInfo { 13 | 14 | public enum QueryType { 15 | SELECT, UPDATE, DELETE, INSERT, COMMIT, BEGIN_TRANSACTION, ROLLBACK; 16 | } 17 | 18 | private QueryType type; 19 | 20 | private String tableName; 21 | 22 | private List columnNames = new ArrayList<>(); 23 | 24 | private List values = new ArrayList<>(); 25 | 26 | private List whereConditions = new ArrayList<>(); 27 | 28 | private List joinedTables = new ArrayList<>(); 29 | 30 | private List whereConditionsJoinOperators = new ArrayList<>(); 31 | 32 | public QueryType getType() { 33 | return type; 34 | } 35 | 36 | public void setType(QueryType type) { 37 | this.type = type; 38 | } 39 | 40 | public String getTableName() { 41 | return tableName; 42 | } 43 | 44 | public void setTableName(String tableName) { 45 | this.tableName = tableName; 46 | } 47 | 48 | public List getColumnNames() { 49 | return columnNames; 50 | } 51 | 52 | public void addColumnName(String columnName) { 53 | this.columnNames.add(columnName); 54 | } 55 | 56 | public List getValues() { 57 | return values; 58 | } 59 | 60 | public void addValue(String value) { 61 | this.values.add(value); 62 | } 63 | 64 | public List getWhereConditions() { 65 | return whereConditions; 66 | } 67 | 68 | public void addWhereCondition(WhereCondition whereCondition) { 69 | this.whereConditions.add(whereCondition); 70 | } 71 | 72 | public List getJoinedTables() { 73 | return joinedTables; 74 | } 75 | 76 | public void addJoinedTable(String joinedTable) { 77 | this.joinedTables.add(joinedTable); 78 | } 79 | 80 | public List getWhereConditionsJoinOperators() { 81 | return whereConditionsJoinOperators; 82 | } 83 | 84 | public void addWhereConditionsJoinOperator(String whereConditionsJoinOperator) { 85 | this.whereConditionsJoinOperators.add(whereConditionsJoinOperator); 86 | } 87 | 88 | } -------------------------------------------------------------------------------- /src/main/java/co/aurasphere/gomorrasql/GomorraSqlShell.java: -------------------------------------------------------------------------------- 1 | package co.aurasphere.gomorrasql; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.sql.Connection; 5 | import java.sql.DriverManager; 6 | import java.sql.ResultSet; 7 | import java.sql.ResultSetMetaData; 8 | import java.sql.SQLException; 9 | import java.util.Scanner; 10 | 11 | import co.aurasphere.gomorrasql.model.CaggiaFaException; 12 | import co.aurasphere.gomorrasql.model.GomorraSqlQueryResult; 13 | 14 | /** 15 | * Shell wrapper on the {@link GomorraSqlInterpreter}. 16 | * 17 | * @author Donato Rimenti 18 | * 19 | */ 20 | public class GomorraSqlShell { 21 | 22 | public static void main(String[] args) throws Exception { 23 | Scanner scanner = new Scanner(System.in, StandardCharsets.ISO_8859_1.name()); 24 | System.out.print("Insert a JDBC string to connect to a database: "); 25 | String url = scanner.nextLine(); 26 | Connection connection = DriverManager.getConnection(url); 27 | GomorraSqlInterpreter gomorraSqlParser = new GomorraSqlInterpreter(connection); 28 | 29 | System.out.println("Succesfully connected to DB"); 30 | while (true) { 31 | try { 32 | System.out.print("> "); 33 | String query = scanner.nextLine(); 34 | System.out.println(query); 35 | GomorraSqlQueryResult result = gomorraSqlParser.execute(query); 36 | if (result.getResultSet() != null) { 37 | printSelectResult(result.getResultSet()); 38 | } 39 | if (result.getAffectedRows() != null) { 40 | System.out.println("Affected rows: " + result.getAffectedRows()); 41 | } 42 | if (result.getResultSet() == null && result.getAffectedRows() == null) { 43 | System.out.println("OK"); 44 | } 45 | } catch (CaggiaFaException | SQLException e) { 46 | System.err.println("Error: " + e.getMessage()); 47 | } 48 | } 49 | } 50 | 51 | private static void printSelectResult(ResultSet result) throws SQLException { 52 | ResultSetMetaData rsmd = result.getMetaData(); 53 | int columnsNumber = rsmd.getColumnCount(); 54 | for (int i = 1; i <= columnsNumber; i++) { 55 | System.out.print(rsmd.getColumnName(i) + " | "); 56 | } 57 | System.out.println(); 58 | while (result.next()) { 59 | for (int i = 1; i <= columnsNumber; i++) { 60 | System.out.print(result.getString(i) + " | "); 61 | } 62 | System.out.println(); 63 | } 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /src/main/java/co/aurasphere/gomorrasql/states/CommaSeparedValuesState.java: -------------------------------------------------------------------------------- 1 | package co.aurasphere.gomorrasql.states; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | import java.util.function.Function; 6 | 7 | import co.aurasphere.gomorrasql.model.CaggiaFaException; 8 | import co.aurasphere.gomorrasql.model.QueryInfo; 9 | 10 | /** 11 | * State that parses a list of values, separed by a comma. It stops when there's 12 | * no more comma and checks if the token after is the expected one (set in the 13 | * constructor). It can be configured to be a final state. 14 | * 15 | * @author Donato Rimenti 16 | * 17 | */ 18 | public class CommaSeparedValuesState extends AbstractState { 19 | 20 | private boolean lastWasComma = false; 21 | private List collector; 22 | private String nextToken; 23 | private Function transitionFunction; 24 | private String expectedToken; 25 | private boolean canBeFinalState = false; 26 | private boolean optionalValues = false; 27 | 28 | public CommaSeparedValuesState(QueryInfo queryInfo, List collector, String nextToken, String expectedToken, 29 | Function transitionFunction) { 30 | super(queryInfo); 31 | this.collector = collector; 32 | this.nextToken = nextToken; 33 | this.transitionFunction = transitionFunction; 34 | this.expectedToken = expectedToken; 35 | } 36 | 37 | public CommaSeparedValuesState(QueryInfo queryInfo, List collector, String nextToken, String expectedToken, 38 | boolean lastWasComma, boolean canBeFinalState, Function transitionFunction) { 39 | this(queryInfo, collector, nextToken, expectedToken, transitionFunction); 40 | 41 | // Used when the first token is not consumed by the previous state. 42 | this.lastWasComma = lastWasComma; 43 | this.canBeFinalState = canBeFinalState; 44 | this.optionalValues = true; 45 | } 46 | 47 | @Override 48 | public AbstractState transitionToNextState(String token) throws CaggiaFaException { 49 | if (token.equals(",")) { 50 | if (lastWasComma) { 51 | // Case ", ," 52 | throw new CaggiaFaException(expectedToken, token); 53 | } else { 54 | // Case "%expectedToken% ," 55 | lastWasComma = true; 56 | return this; 57 | } 58 | } 59 | 60 | // Case ", %expectedToken%" 61 | if (lastWasComma) { 62 | if (optionalValues && token.equalsIgnoreCase(nextToken)) { 63 | return transitionFunction.apply(queryInfo); 64 | } else { 65 | optionalValues = false; 66 | } 67 | collector.add(token); 68 | lastWasComma = false; 69 | return this; 70 | } 71 | 72 | // Case "%expectedToken% %nextToken%" 73 | if (token.equalsIgnoreCase(nextToken)) { 74 | return transitionFunction.apply(queryInfo); 75 | } 76 | 77 | // Case "%expectedToken% %WRONG_TOKEN%" 78 | throw new CaggiaFaException(Arrays.asList(",", nextToken), token); 79 | } 80 | 81 | @Override 82 | public boolean isFinalState() { 83 | return canBeFinalState; 84 | } 85 | } -------------------------------------------------------------------------------- /src/main/java/co/aurasphere/gomorrasql/states/InitialState.java: -------------------------------------------------------------------------------- 1 | package co.aurasphere.gomorrasql.states; 2 | 3 | import java.util.Arrays; 4 | 5 | import co.aurasphere.gomorrasql.Keywords; 6 | import co.aurasphere.gomorrasql.model.CaggiaFaException; 7 | import co.aurasphere.gomorrasql.model.QueryInfo; 8 | import co.aurasphere.gomorrasql.model.QueryInfo.QueryType; 9 | import co.aurasphere.gomorrasql.states.query.OptionalWhereState; 10 | import co.aurasphere.gomorrasql.states.query.SelectColumnsState; 11 | import co.aurasphere.gomorrasql.states.query.UpdateSetState; 12 | 13 | /** 14 | * First state when parsing a query. It switches through the various possible 15 | * queries according to the first token. 16 | * 17 | * @author Donato Rimenti 18 | * 19 | */ 20 | public class InitialState extends AbstractState { 21 | 22 | public InitialState() { 23 | super(new QueryInfo()); 24 | } 25 | 26 | @Override 27 | public AbstractState transitionToNextState(String token) throws CaggiaFaException { 28 | if (token.equalsIgnoreCase(Keywords.SELECT_KEYWORD)) { 29 | queryInfo.setType(QueryType.SELECT); 30 | return new SelectColumnsState(queryInfo); 31 | } 32 | if (token.equalsIgnoreCase(Keywords.UPDATE_KEYWORD)) { 33 | queryInfo.setType(QueryType.UPDATE); 34 | return new AnyTokenConsumerState(queryInfo, queryInfo::setTableName, 35 | q -> new SingleTokenMatchState(q, Keywords.SET_KEYWORD, 36 | q2 -> new AnyTokenConsumerState(q2, q2::addColumnName, 37 | q3 -> new SingleTokenMatchState(q3, Keywords.SET_EQUAL_KEYWORD, 38 | q4 -> new AnyTokenConsumerState(q4, q4::addValue, UpdateSetState::new))))); 39 | } 40 | if (token.equalsIgnoreCase(Keywords.DELETE_KEYWORDS[0])) { 41 | queryInfo.setType(QueryType.DELETE); 42 | return new GreedyMatchKeywordState(queryInfo, Keywords.DELETE_KEYWORDS, 43 | q -> new GreedyMatchKeywordState(q, Keywords.FROM_KEYWORDS, 44 | q2 -> new AnyTokenConsumerState(q2, q2::setTableName, OptionalWhereState::new), 0)); 45 | } 46 | if (token.equalsIgnoreCase(Keywords.INSERT_KEYWORDS[0])) { 47 | queryInfo.setType(QueryType.INSERT); 48 | return new GreedyMatchKeywordState(queryInfo, Keywords.INSERT_KEYWORDS, 49 | q -> new AnyTokenConsumerState(q, q::setTableName, 50 | q2 -> new CommaSeparedValuesState(q2, q2.getColumnNames(), Keywords.VALUES_KEYWORD, 51 | "%COLUMN_NAME%", true, false, q3 -> new CommaSeparedValuesState(q3, q3.getValues(), 52 | null, "%VALUE%", true, true, FinalState::new)))); 53 | } 54 | if (token.equalsIgnoreCase(Keywords.COMMIT_KEYWORDS[0])) { 55 | queryInfo.setType(QueryType.COMMIT); 56 | return new GreedyMatchKeywordState(queryInfo, Keywords.COMMIT_KEYWORDS, FinalState::new); 57 | } 58 | if (token.equalsIgnoreCase(Keywords.ROLLBACK_KEYWORD)) { 59 | queryInfo.setType(QueryType.ROLLBACK); 60 | return new FinalState(queryInfo); 61 | } 62 | if (token.equalsIgnoreCase(Keywords.BEGIN_TRANSACTION_KEYWORDS[0])) { 63 | queryInfo.setType(QueryType.BEGIN_TRANSACTION); 64 | return new GreedyMatchKeywordState(queryInfo, Keywords.BEGIN_TRANSACTION_KEYWORDS, FinalState::new); 65 | } 66 | throw new CaggiaFaException(Arrays.asList(Keywords.SELECT_KEYWORD, Keywords.UPDATE_KEYWORD, 67 | Keywords.INSERT_KEYWORDS[0], Keywords.DELETE_KEYWORDS[0], Keywords.BEGIN_TRANSACTION_KEYWORDS[0], 68 | Keywords.COMMIT_KEYWORDS[0], Keywords.ROLLBACK_KEYWORD), token); 69 | } 70 | 71 | } -------------------------------------------------------------------------------- /src/main/java/co/aurasphere/gomorrasql/GomorraSqlInterpreter.java: -------------------------------------------------------------------------------- 1 | package co.aurasphere.gomorrasql; 2 | 3 | import java.sql.Connection; 4 | import java.sql.ResultSet; 5 | import java.sql.SQLException; 6 | import java.sql.Statement; 7 | import java.util.List; 8 | import java.util.StringTokenizer; 9 | import java.util.stream.Collectors; 10 | 11 | import co.aurasphere.gomorrasql.model.CaggiaFaException; 12 | import co.aurasphere.gomorrasql.model.GomorraSqlQueryResult; 13 | import co.aurasphere.gomorrasql.model.QueryInfo; 14 | import co.aurasphere.gomorrasql.model.WhereCondition; 15 | import co.aurasphere.gomorrasql.states.AbstractState; 16 | import co.aurasphere.gomorrasql.states.InitialState; 17 | 18 | /** 19 | * Interpreter for the GomorraSQL. 20 | * 21 | * @author Donato Rimenti 22 | * 23 | */ 24 | public class GomorraSqlInterpreter { 25 | 26 | private Connection connection; 27 | 28 | public GomorraSqlInterpreter(Connection connection) { 29 | this.connection = connection; 30 | } 31 | 32 | /** 33 | * Converts a GomorraSQL query into a standard SQL query. 34 | * 35 | * @param gomorraQuery the query to convert 36 | * @return an equivalent SQL query 37 | */ 38 | public static String toSqlQuery(String gomorraQuery) { 39 | QueryInfo info = parseQuery(gomorraQuery); 40 | return buildSqlQuery(info); 41 | } 42 | 43 | private static QueryInfo parseQuery(String query) { 44 | AbstractState currentState = new InitialState(); 45 | // TODO: bug: whitespaces inside quotes should be ignored 46 | StringTokenizer tokenizer = new StringTokenizer(query, ", ", true); 47 | while (tokenizer.hasMoreTokens()) { 48 | String nextToken = tokenizer.nextToken().trim(); 49 | if (!nextToken.isEmpty()) { 50 | currentState = currentState.transitionToNextState(nextToken); 51 | } 52 | } 53 | 54 | if (!currentState.isFinalState()) { 55 | throw new CaggiaFaException("Unexpected end of query"); 56 | } 57 | 58 | return currentState.getQueryInfo(); 59 | } 60 | 61 | private static String buildSqlQuery(QueryInfo queryInfo) { 62 | switch (queryInfo.getType()) { 63 | case SELECT: 64 | return buildSelectQuery(queryInfo); 65 | case INSERT: 66 | return buildInsertQuery(queryInfo); 67 | case UPDATE: 68 | return buildUpdateQuery(queryInfo); 69 | case DELETE: 70 | return buildDeleteQuery(queryInfo); 71 | case BEGIN_TRANSACTION: 72 | return "START_TRANSACTION"; 73 | case COMMIT: 74 | return "COMMIT"; 75 | case ROLLBACK: 76 | return "ROLLBACK"; 77 | } 78 | throw new CaggiaFaException("Unrecognised query"); 79 | } 80 | 81 | private static String buildDeleteQuery(QueryInfo queryInfo) { 82 | return "DELETE FROM " + queryInfo.getTableName() + buildWhereClause(queryInfo); 83 | } 84 | 85 | private static String buildWhereClause(QueryInfo queryInfo) { 86 | List whereConditions = queryInfo.getWhereConditions(); 87 | List whereConditionsJoinOperators = queryInfo.getWhereConditionsJoinOperators(); 88 | if (whereConditions.isEmpty()) { 89 | return ""; 90 | } 91 | StringBuilder whereClause = new StringBuilder(" WHERE "); 92 | for (int i = 0; i < whereConditions.size(); i++) { 93 | WhereCondition whereCondition = whereConditions.get(i); 94 | whereClause.append(whereCondition.getField()).append(" ").append(whereCondition.getOperator()).append(" ") 95 | .append(whereCondition.getValue()); 96 | if (whereConditionsJoinOperators.size() >= i + 1) { 97 | whereClause.append(" ").append(whereConditionsJoinOperators.get(i)).append(" "); 98 | } 99 | } 100 | return whereClause.toString(); 101 | } 102 | 103 | private static String buildUpdateQuery(QueryInfo queryInfo) { 104 | StringBuilder query = new StringBuilder("UPDATE ").append(queryInfo.getTableName()).append(" SET "); 105 | for (int i = 0; i < queryInfo.getColumnNames().size(); i++) { 106 | query.append(queryInfo.getColumnNames().get(i)).append(" = ").append(queryInfo.getValues().get(i)) 107 | .append(", "); 108 | } 109 | query.delete(query.length() - 2, query.length()); 110 | return query.toString() + buildWhereClause(queryInfo); 111 | } 112 | 113 | private static String buildInsertQuery(QueryInfo queryInfo) { 114 | StringBuilder query = new StringBuilder("INSERT INTO ").append(queryInfo.getTableName()); 115 | if (!queryInfo.getColumnNames().isEmpty()) { 116 | query.append(" ( ").append(queryInfo.getColumnNames().stream().collect(Collectors.joining(", "))) 117 | .append(" )"); 118 | } 119 | query.append(" VALUES ( ").append(queryInfo.getValues().stream().collect(Collectors.joining(", "))) 120 | .append(" )"); 121 | return query.toString(); 122 | } 123 | 124 | private static String buildSelectQuery(QueryInfo queryInfo) { 125 | String query = "SELECT "; 126 | 127 | // Column names 128 | query += queryInfo.getColumnNames().stream().collect(Collectors.joining(", ")); 129 | 130 | // Table name 131 | query += " FROM " + queryInfo.getTableName(); 132 | 133 | // Joins 134 | List joinedTables = queryInfo.getJoinedTables(); 135 | if (!joinedTables.isEmpty()) { 136 | query += " INNER JOIN " + joinedTables.stream().collect(Collectors.joining(" INNER JOIN ")); 137 | } 138 | // Where 139 | query += buildWhereClause(queryInfo); 140 | 141 | return query; 142 | } 143 | 144 | /** 145 | * Executes the given GomorraSQL query in the connected database. 146 | * 147 | * @param gomorraSqlQuery the query to execute 148 | * @return the result of the query 149 | */ 150 | public GomorraSqlQueryResult execute(String gomorraSqlQuery) { 151 | QueryInfo queryInfo = parseQuery(gomorraSqlQuery); 152 | String sqlQuery = buildSqlQuery(queryInfo); 153 | GomorraSqlQueryResult result = new GomorraSqlQueryResult(); 154 | 155 | // Executes the query 156 | try { 157 | Statement statement = connection.createStatement(); 158 | switch (queryInfo.getType()) { 159 | case SELECT: 160 | ResultSet resultSet = statement.executeQuery(sqlQuery); 161 | result.setResultSet(resultSet); 162 | break; 163 | case BEGIN_TRANSACTION: 164 | connection.setAutoCommit(false); 165 | break; 166 | case COMMIT: 167 | connection.commit(); 168 | connection.setAutoCommit(true); 169 | break; 170 | case DELETE: 171 | case UPDATE: 172 | case INSERT: 173 | int updatedRows = statement.executeUpdate(sqlQuery); 174 | result.setAffectedRows(updatedRows); 175 | break; 176 | case ROLLBACK: 177 | connection.rollback(); 178 | connection.setAutoCommit(true); 179 | break; 180 | } 181 | } catch (SQLException e) { 182 | throw new CaggiaFaException(e); 183 | } 184 | return result; 185 | } 186 | 187 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Travis](https://img.shields.io/travis/aurasphere/gomorra-sql.svg)](https://travis-ci.org/aurasphere/gomorra-sql) 2 | [![Maintainability](https://api.codeclimate.com/v1/badges/6de2eb23249ef4c8a60c/maintainability)](https://codeclimate.com/github/aurasphere/gomorra-sql/maintainability) 3 | [![Test Coverage](https://api.codeclimate.com/v1/badges/6de2eb23249ef4c8a60c/test_coverage)](https://codeclimate.com/github/aurasphere/gomorra-sql/test_coverage) 4 | [![Join the chat at https://gitter.im/gomorra-sql/community](https://badges.gitter.im/gomorra-sql/community.svg)](https://gitter.im/gomorra-sql/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | [![Donate](https://img.shields.io/badge/Donate-PayPal-orange.svg)](https://www.paypal.com/donate/?cmd=_donations&business=8UK2BZP2K8NSS) 6 | [![Telegram](https://img.shields.io/badge/-telegram-a?color=white&logo=telegram)](https://t.me/+Wy1DPTLyFGg0OWE0) 7 | 8 | ![logo](https://user-images.githubusercontent.com/14991778/117669306-0a931c00-b1a7-11eb-8db8-babc14e767d1.png) 9 | 10 | 11 | GomorraSQL is an easy and straightforward interpreted SQL dialect that allows you to write simpler and more understandable queries in [Neapolitan Language](https://en.wikipedia.org/wiki/Neapolitan_language). 12 | 13 | ## Set up 14 | GomorraSQL can be used either as a Java library or as a standalone SQL database client. 15 | 16 | ### Java library 17 | To use it as a Java library, download the latest jar in the release section and import it into your project, along with the driver of the database you want to use. Then, you can use the methods exposed by the class ```co.aurasphere.gomorrasql.GomorraSqlInterpreter``` which allow you to either execute a GomorraSQL query against your database (to which you will provide a connection) or just translate it into plain old SQL. 18 | GomorraSQL throws a ```CaggiaFaException``` with useful debugging information whenever an error occurs. 19 | 20 | ### Database client 21 | To use GomorraSQL as a database client, download the latest jar in the release section and execute it with the command: 22 | 23 | java -cp gomorra-sql-1.0.0.jar; co.aurasphere.gomorrasql.GomorraSqlShell 24 | 25 | The client will ask for a JDBC string representing the database to connect to (including credentials). After the connection is established, you can start running commands. 26 | 27 | ## Language basics 28 | Before delving into the specific commands, it's important to consider some general rules to avoid "cuoppo" mistakes: 29 | 30 | - parenthesis are not valid characters in GomorraSQL queries. In the following examples, they are just used to distinguish between (mandatory parameters) and [optional parameters] 31 | - GomorraSQL doesn't allow multi-line queries. Therefore, there's no end-of-query character (like ; in SQL) 32 | - spacing is very important when using operators! A query using a condition ```a = 5``` will work but the same query with ```a= 5``` or ```a =5``` or ```a=5``` will not 33 | 34 | ## Data Manipulation Language 35 | Likewise standard SQL, GomorraSQL allows performing data manipulation. Here's a list of supported operations: 36 | 37 | ### Retrieving data 38 | To retrieve data, you can use the ```ripigliammo``` command. Here's the command syntax: 39 | 40 | ripigliammo ( || tutto chillo ch'era 'o nuostro) mmiez 'a [pesc e pesc ...] [arò ] 41 | 42 | The first argument for the ```ripigliammo``` command is the columns to retrieve. They can be specified either as a list of comma-separated values or with the ```tutto chillo ch'era 'o nuostro``` which will return all the columns. 43 | 44 | After the columns, the following parameter is the name of the table where to fetch the data with ```mmiez 'a```. Data can be fetched from multiple tables by using the optional join operator ```pesc e pesc``` followed by another table name. Currently, there's no limit on the number of ```pesc e pesc``` that can be applied to a single ```ripigliammo```. The join condition is specified in the ```arò``` clause, along with the row filtering. 45 | 46 | Finally, you can filter the rows using the optional ```arò``` clause, followed by one or more conditions. The conditions work exactly like in SQL, with a slightly different syntax for some operators (check the [Language Reference](#language-reference) section). 47 | 48 | Here are some sample queries: 49 | 50 | ripigliammo tutto chillo ch'era 'o nuostro mmiez 'a user # retrieves all users' data 51 | ripigliammo email mmiez 'a user arò id = 6 o name è nisciun # retrieves all the emails of the users with id 6 or null name 52 | ripigliammo email mmiez 'a user pesc e pesc city pesc e pesc account arò user.id = 6 e user.birth_city = city.id e user.account_id = account.id # retrieves the data of the user with id 6 along joined with his birth city and his account data 53 | 54 | ### Deleting data 55 | Data deletion can be performed using the ```facimm na' strage``` command which supports a subset of options from the ```ripigliammo``` command. Here's the syntax: 56 | 57 | facimm na' strage mmiez 'a [arò ] 58 | 59 | Here are some sample queries: 60 | 61 | facimm na' strage mmiez 'a user # deletes all users' data 62 | facimm na' strage mmiez 'a user arò name nun è nisciun o deleted è true # deletes the users with name not null or with deleted = true 63 | 64 | ### Updating data 65 | The command ```rifacimm``` is used to update data in a table. The syntax is: 66 | 67 | rifacimm accunza accussì , accussì , ... [arò ] 68 | 69 | The ```accunza``` operator marks the begin of a list of column/values assignments using the assignment operator ```accussì```. 70 | 71 | Here are some sample queries: 72 | 73 | rifacimm user accunza name accussì "Pippo" # sets the name "Pippo" for all the users 74 | rifacimm user accunza name accussì "Pinco", surname accussì "Pallo" arò name è nisciun # sets the name to "Pinco" and surname to "Pallo" for all users with null name 75 | 76 | ### Inserting data 77 | Data insertion can be performed using the ```nzipp 'ngoppa``` operator as following: 78 | 79 | nzipp 'ngoppa (, ...) chist , ... 80 | 81 | After the table name, you can specify a list of columns whose data are being inserted. If not present, GomorraSQL will default to all columns. The ```chist``` keyword marks the beginning of a comma-separated list of values to insert. Each insert statement can only add one row. 82 | 83 | Here are some sample queries: 84 | 85 | nzipp 'ngoppa user chist 1, "Pinco", "Pallo" # inserts a new user with all his data 86 | nzipp 'ngoppa user name chist "Pinco" # inserts a new user with only his name set 87 | 88 | 89 | ## Transaction support 90 | Being a fully ACID compliant language, GomorraSQL offers basic transaction management. To begin a transaction, you can issue the command ```ua uagliò```. You can then commit the transaction with the command ```iamme bello ia'``` or perform rollback with the command ```sfaccimm```. 91 | 92 | ## Language reference 93 | Follows a table that roughly maps GomorraSQL language to standard SQL: 94 | 95 | | GomorraSQL keyword | SQL equivalent | Valid in... | 96 | |--------------------------------|----------------|------------------------| 97 | | ripigliammo | SELECT | SELECT | 98 | | rifacimm | UPDATE | UPDATE | 99 | | nzipp | INSERT | INSERT | 100 | | 'ngoppa | INTO | INSERT | 101 | | facimm na' strage | DELETE | DELETE | 102 | | pesc e pesc | INNER JOIN | SELECT | 103 | | mmiez 'a | FROM | SELECT, DELETE | 104 | | tutto chillo ch'era 'o nuostro | * | SELECT | 105 | | arò | WHERE | SELECT, UPDATE, DELETE | 106 | | e | AND | ANY WHERE CLAUSE | 107 | | o | OR | ANY WHERE CLAUSE | 108 | | nisciun | NULL | ANY WHERE CLAUSE | 109 | | è | IS | ANY WHERE CLAUSE | 110 | | nun è | IS NOT | ANY WHERE CLAUSE | 111 | | chist | VALUES | INSERT | 112 | | accunza | SET | UPDATE | 113 | | accussì | = (assignment) | UPDATE | 114 | | > | > | ANY WHERE CLAUSE | 115 | | < | < | ANY WHERE CLAUSE | 116 | | = (comparison) | = (comparison) | ANY WHERE CLAUSE | 117 | | != | != | ANY WHERE CLAUSE | 118 | | <> | <> | ANY WHERE CLAUSE | 119 | | <= | <= | ANY WHERE CLAUSE | 120 | | >= | >= | ANY WHERE CLAUSE | 121 | | sfaccimm | ROLLBACK | TRANSACTION | 122 | | iamme bello ia' | COMMIT | TRANSACTION | 123 | | ua uagliò | BEGIN TRANSACTION | TRANSACTION | 124 | 125 | ## Supported Database 126 | GomorraSQL has been extensively tested with MySQL and H2. Other databases may not work properly. 127 | 128 | ## Training 129 | Video lessons on GomorraSQL syntax and philosophy [are available here](https://www.nowtv.it/watch/home/asset/gomorra-la-serie/skyatlantic_7bb8b3e11d19439fb71c68349b2cfab3). If you are also interested in corporate training, feel free to contact me for pricing. 130 | 131 | ## Certifications 132 | If you have an issued certification, you can check it out at the following URL by replacing the "user" parameter with your name (replace spaces with hyphens): https://aurasphere.co/gomorra-sql/certificate.html?user=donato-rimenti. 133 | 134 | ## Project status 135 | This project is considered completed and won't be developed further. 136 | 137 | ## Contacts 138 | You can contact me using my account e-mail or by joining the Telegram group (link on the badge on top of this document). I'll try to reply ASAP. 139 | 140 | ## Acknowledgments 141 | Thanks to [Federica Lisci](https://www.linkedin.com/in/federica-lisci-377220162/) for the logo! 142 | 143 | ## License 144 | The project is released under the MIT license, which lets you reuse the code for any purpose you want (even commercial) with the only requirement being copying this project license on your project. 145 | 146 | Copyright (c) 2021 Donato Rimenti 147 | --------------------------------------------------------------------------------