org.normalsql:normalsql

Generate Java source code from SQL DML statements.


Keywords
antlr, code-generator, database, java, jdbc, sql, sql-parser
License
Apache-2.0

Documentation

NormalSQL

NormalSQL parses normal SQL statements (SELECT, INSERT, etc), finds the parameters and columns, and generates convenient wrappers.

NormalSQL defines the "SQL-first" workflow. Use normal SQL to generate application source code. There are no mappings, templates, or DSLs.

Most Simple Example

Start with the file SelectPeopleOlderThan.sql containing this query:

SELECT name FROM people WHERE age > 18

NormalSQL generates SelectPeopleOlderThan.java and SelectPeopleOlderThanResultSet.java.

The condition age > 18 is converted into a prepared statement parameter.

// pseudo-code
class SelectPeopleOlderThan 
{
    // Literal has been replaced with a parameter
    String _sql = "SELECT name FROM people WHERE age > ?";
	
    // Original literal is used as the default value
    int _age = 18;
	
    // 'Greater Than' (GT) operand is used as suffix
    void setAgeGT( int age ) { _age = age; }
    
    GetPeopleOlderThanResultSet execute() { ... }
}

The column name is converted into an accessor:

// pseudo-code
class SelectPeopleOlderThanResultSet implements Iterable<Row>
{
    // inner-class
    class Row
    {
        String getName();
    }
}

Your application will look something like this:

Connection conn = DriverManager.getConnection( ... );

GetPeopleOlderThan select = new GetPeopleOlderThan( conn );

select.setAgeGT( 21 );

GetPeopleOlderThanResultSet rs = select.execute();

for( GetPeopleOlderThanResultSet.Row row : rs )
{
    System.out.println( row.getName() );
}

rs.close();
select.close();

What could be easier?

SQL Grammar Support

NormalSQL supports SQL-92 thru SQL-2016. This includes CTEs, JOINs, UNIONs, functions, and so forth.

NormalSQL has initial support for multiple dialects of SQL, including Postgress, MySQL, SQL Server, SQLite.

The initial test harness started quite modest, but is growing rapidly. Like all third party SQL parsers, support will improve as NormalSQL matures. Please submit any potential bugs and errors.

Benefits

Feature Detail
SQL-first workflow Use normal SQL to generate app source code. Instead of using mappings, DSLs, or templates to generate SQL.
Plaintext SQL files • Use your favorite SQL client (eg DBeaver, DataGrip).
• Devs & DBAs can now work independently.
Reduce errors • Compile time processing.
• SQL and app always in sync.
• All prepared statement parameters have a default value.
• No SQL injection attacks.
No runtime dependencies There is no NormalSQL runtime. Use the generated app source code as-is.
Easier debugging • Statement.toString() returns currrent SQL.
• ResultSet.toString() returns current row's fields.

Quick Example

Starting with the simple SELECT SelectForSale.sql:

-- Find cars by style and mileage 
SELECT id, make, model, year 
  FROM automobiles 
 WHERE style = 'coupe' 
   AND odometer < 100000;

NormalSQL generates this prepared statement:

-- Find cars by style and mileage 
SELECT id, make, model, year 
  FROM automobiles 
 WHERE style = ? 
   AND odometer < ?;

The literals 'coupe' and 100000 were automatically replaced with a parameter ?.

The prepared statement is then wrapped in generated class SelectForSale.java:

// pseudo-code
class SelectForSale 
{
    String _style = "coupe";
    void setStyle( String style ) { _style = style; }
    
    Integer _odometer = 100000;
    void setOdometer( Integer odometer ) { _odometer = odometer; }

    SelectForSaleResultSet execute();
}

Next, NormalSQL finds the columns id, make, model, and year. They are added to to the inner class Row. SelectForSaleResultSet.java is the generated source:

// pseudo-code
class SelectForSaleResultSet implements Iterable<Row>
{
    // inner-class
    class Row
    {
        Integer getID()
        String  getMake()
        String  getModel()
        Integer getYear()
    }
}

Your application will use those wrappers something like this:

Connection conn = DriverManager.getConnection( ... );

SelectForSale select = new SelectForSale( conn );
select.setStyle( "sedan" );
select.setOdometer( 90000 );

SelectForSaleResultSet rs = select.execute();

for( SelectForSaleResultSet.Row row : rs )
{
    System.out.println( row.toJSON() );
}

rs.close();
select.close();

Example Project

Usage

Maven Plugin

TODO

Command Line

NormalSQL uses metadata to infer data types of columns and predicates. It requires a live running instance of the target database(s) during processing.

Command Line

Optionally specify .properties. Optionally specify initial source directory.

Properties

Use normalsql.properties to configure processing. Place in the working directory, in the source directory, or specify via command line.

description = MySQL driver for localhost
driver = com.mysql.cj.jdbc.Driver
url = jdbc:mysql://localhost/automobiles?useServerPrepStmts=true
username = root
password =
source = .
target = .
package = .

Future: config templates. Future: config strategy for naming accessors.

Source directory. NormalSQL will recurse thru subdirectories.

Target directory. Like the Java compiler does with packages, will mirror the directory structure of the source directory.

extensions. file extensions to process. default is all.

Theory of Operation

NormalSQL is very simple:

  • Parse SQL statement
  • Find predicates and columns
  • Infer data types
  • Generate source code

Parse

NormalSQL supports SQL:2016 DML statements (SELECT, INSERT, UPDATE, etc), including JOINs, Common Table Expressions, and such. It accommodates the syntax of popular dialects.

All query statements are transformed into prepared statements (see example above). The target database then does its own parsing, validation, etc.

Find Predicates

All predicates are found. Not just those within the WHERE clause.

Since the default values are copied from the source SQL, your application can pick and choose which parameters to override.

TODO is_nullable

Comparison Predicate

NormalSQL can only match predicates when just the column name is used, either on the left or right side. Matched abc < 10. Not matched abc - 10 < 0.

Future: accessor method includes mneumonic of comparison operator

BETWEEN Predicate

Matched: column BETWEEN 0 AND 10 and 5 BETWEEN low AND high. Not matched: every other combination, for example 5 BETWEEN 0 AND high.

And the generated accessors will be:

IN Predicate

Every simple literal value is turned into a parameter, one parameter for each literal.

IN ( 1, 2, 3 )

Becomes

IN ( ?, ?, ? )

And the generated accessors will be:

ANY Predicate

Accepts an array of values, working like how you wish IN predicates would. Only supported by specific databases and JDBC drivers. (Future: list supported)

ANY ( 1, 2, 3 )

Becomes

ANY ( ? )

And the generated accessors will be:

Future: LIKE, REGEXP, Postgress Matches

Find Columns

Only the outer most result columns are found and become accessors.

Column aliases are supported.

When column names (or aliases) contain unsupported characters (like punctuation), a generic accessor is created, like setColumn1(...). (Future.)

Only the first instance of a duplicate column is used.

Infers data types

ResultSet the JDBC driver supports

Code Generation

NormalSQL generates code you'd write yourself, if only you had more time.

Velocity template engine is currently used. Other engines are being evaluated.

Currently, there are simple wrappers for PreparedStatement and ResultSet.

Future features:

ResultSet wrappers tailored per use case. Like returning a single value. Or a single row.

Support Updateable ResultSets.

Have a wrapper create POJOs.

Support multiple statements and resultsets.

Support other languages. PHP, Python, C#, whatever. With templates, any thing its all feasible. Prioritorize by popularity X ease of port X demand.

Generated PreparedStatement

Generates a PreparedStatement wrapper with typesafe setters for the conditions. Capitalization conventions for JavaBean accessors are used. (Future: configurable.)

The default values are copied from your original SQL. Your application overrides these values as needed. (No more worrying about forgetting to set that nth parameter!)

The .toString() method returns the actual query. Great for debugging.

Generated ResultSet

Generates a ResultSet wrapper with getters for the result columns. Capitalization conventions for JavaBean accessors are used. (Future: configurable.)

Column aliases are supported. The query SELECT apple AS banana will generate the accessor setBanana(...).

The .toString() method returns all the columns of the current row. Great for debugging.

Grammar

NormalSQL.g4 is an ANTLR v4 grammar. It supports all DML statements from SQL-99 to SQL:2016. (Exceptions: No index query hints.)

Its two functions are 1) find columns and predicates and 2) replace predicate literals with parameters. It preserves the entire input stream, including whitespace, comments, and formatting.

Therefore, our grammar is permissive, accepting SQL source which may be rejected by the target database.

The robustness of our grammar will improve as we gather more tests cases and real world examples. If NormalSQL fails to parse SQL which is accepted by the target database, please file a bug report. Thank you in advance.

The error reporting of our grammar will improve over time.

Future Work

Allow custom templates for code generation. Because customer have their own best practices for JDBC, logging, monitoring, and so forth.

The 2.x release will introduce our own document model. NormalSQL will then be a general purpose SQL parsing toolkit, useful for other projects.

We have some ideas for how to support dynamic SQL. This will require including NormalSQL as a runtime dependency.

NormalSQL can work along side other SQL stacks. This allows incremental adoption of Normal's SQL-first workflow. Creating examples and tutorials will be a lot of work.

Support other platforms, such as PHP, Python, C#.

Create a standalone native NormalSQL executable to be used by other platform's build chains. Perhaps even port NormalSQL to other platforms.

Standup a SQL Fiddle like public instance of NormalSQL, allowing people to casually try things out.

--

Use normal SQL statements.

Remember Data Access Objects (DAOs)? The SQL was right there. How everything worked was obvious. No black box, no runtime magic, no complicated rules or incantations.

Unfortunately, manually writing and maintaining DAOs was tedious and error prone. So DAOs rightly fell out of fashion.

But what if that process was automated and error free?

That's exactly what NormalSQL does.

Use normal SQL DML statements as-is to generate app source code.