Code Monkey home page Code Monkey logo

goat-query's Introduction

Goat query builder

This is an SQL query builder built over a PHP to SQL and SQL to PHP type converter.

Working with PDO and ext-pgsql, with officially supported drivers:

  • MySQL 5.7 using PDO,
  • MySQL 8.x using PDO,
  • PostgreSQL >= 9.5 (until latest) using PDO,
  • PostgreSQL >= 9.5 (until latest) using ext-pgsql (recommended driver),
  • SQLite >= 3 using PDO (experimental),
  • With a few hacks, any RDBMS speaking SQL-92 standard using PDO.

Documentation is in the ./docs/ folder, generated using Sphinx.

Quickstart

Install it:

composer require makinacorpus/goat-query

Create a connexion:

$driver = \Goat\Driver\DriverFactory::fromUri('pgsql://username:password@hostname:5432/database?option1=value1&option2=value2');

Or create a connexion the verbose way:

$driver = new \Goat\Driver\ExtPgSQLDriver();
$driver->setConfiguration(
    \Goat\Driver\Configuration::fromString(
        'pgsql://username:password@hostname:5432/database?option1=value1&option2=value2'
    )
);

Please note that options given will be treated differently depending upon driver.

Then use it:

$runner = $driver->getRunner();
$platform = $runner->getPlatform();
$queryBuilder = $runner->getQueryBuilder();

if ($platform->supportsReturning()) {
    $result = $queryBuilder
        ->insertValues('users')
        ->columns(['id', 'name'])
        ->values([1, 'Jean'])
        ->values([1, 'Robert'])
        ->returning('*')
        ->setOption('class', \App\Domain\Model\User::class)
        ->execute()
    ;
} else {
    $queryBuilder
        ->insertValues('users')
        ->columns(['id', 'name'])
        ->values([1, 'Jean'])
        ->values([2, 'Robert'])
        ->execute()
    ;

    $result = $queryBuilder
        ->select('users')
        ->where('id', [1, 2])
        ->setOption('class', \App\Domain\Model\User::class)
        ->execute()
}

foreach ($result as $user) {
   \assert($user instanceof \App\Domain\Model\User);

    echo "Hello, ", $user->getName(), " !\n";
}

For advanced documentation, please see the ./docs/ folder.

Roadmap

  • 2.0 - bumps requirement to PHP 7.4,
  • 2.1 - includes MERGE query support, functional testing, driver and platform segregation, as well as many fixes, and deprecated some 1.x methods,
  • 3.0 - is a major overhaul of sql writer, converter context, and query builder,
  • 3.0 - brings an experimental version of schema introspector and console tool,
  • 3.1 - will be a features with many shortcuts and sugar candy additions,
  • 4.0 - will stabilize schema introspector and console tool.

Driver organisation

Driver instance is responsible of (in order):

  • connecting to the database,
  • send configuration,
  • inspect backend variant and version to build platform.

It gets connexion option and configures it, then creates the platform.

Platform contains SQL version-specific code, such as query formatter, schema introspector, and other things the user cannot configure, and which may vary depending upon the SQL server version. It handles everything the user cannot have hands onto, but SQL server has.

Runner is the only runtime object the user needs:

  • public facade for executing SQL queries,
  • holds the converter (which can be injected and may contain user code),
  • creates and holds the query builder,
  • manages transactions.

It contains user configuration and runtime. The runner knows nothing about SQL itself, it just holds a connexion, send requests, and handles iterators and transactions.

In other words:

  • drivers connects,
  • platform handles SQL dialect,
  • runner executes,
  • a single runner implementation can use different plaform implementations, real reason why both implementations are actually separate.

Framework integration

Upgrade

Upgrade from 2.x to 3.x

  • 3.x deprecated all \Goat\Query\Expression* classes. Their backward compatible equivalent still exists, in order to make your code resilient, please use their new implementations in \Goat\Query\Expression\*Expression.

  • 3.x ships a complete \Goat\Driver\Query\SqlWriter interface and implementations rewrite. New code is faster, easier to read and has much less dependencies, driver developers or users using it directly must adapt their code.

  • 3.x removes the \Goat\Query\ArgumentBag, \Goat\Query\ArgumentList, \Goat\Query\Value, \Goat\Query\ValueRepresentation classes and interfaces, people using those must adapt their code.

  • 3.x changes the \Goat\Converter\ValueConverterInterface contracts slightly, you need to adapt your existing custom value converters,

  • 3.x completely changes date handling, for most people, it should go unnoticed and fix many bugs,

  • Generally speaking, this will be the last version providing backward compatible deprecated code, following deprecation notices and the @deprecated PHP documentation annotaton to fix your existing code.

  • For most users, upgrade will be transparent and will not cause any trouble.

Upgrade from 1.x to 2.x

  • 2.x introduced a single user facing change: the Symfony bundle was originally provided by the makinacorpus/goat package, it is now bundled as the standalone makinacorpus/goat-query-bundle package.

  • 2.x changed internal runners implementation and introduces a new \Goat\Driver\ namespace, which focuses on low-level driver implementations, driver developpers will need to convert their code to the new API.

This also introduce a dependency conflict between makinacorpus/goat version prior to 3.0.0 version, if you were using it, you need to upgrade.

Query builder, database runner and result iterator end-user API did not change.

History

Originating and extracted from https://github.com/pounard/goat

goat-query's People

Contributors

pounard avatar

Stargazers

 avatar  avatar

Watchers

 avatar

goat-query's Issues

Improve connect error handling in ext-pgsql driver

If the server refuses the connexion, we get a notice, but execution continue, and subsequent calls will attempt to use a bool (returned by pg_connect()) as a resource.

We should also force a default timeout if none provided.

Allow callables instead of all expressions

Normalize expression processing, allow them to be callables, everywhere, examples:

$select->with('top_sales', function (SelectQuery $select) {
    // $select is a new SELECT query to be used for the WITH statement
    // ... write your own $select query
});

Or:

$select->condition(function (Where $where) {
    // ... do something with $where
});

Or:

$select->getWhere()->exists(function (SelectQuery $query) {
    // $select is a new SELECT query to be used for the WITH statement
    // ... write your own $select query
});

Or more generally, all expressions to be:

$select->expression(function (...$contextualParameters) {
    // ... return some string or Expression instance
});

Get rid of infamous HydratorMap

Need to get rid of HydratorMap which has nothing to do here.

  • write a custom hydrator interface,
  • write generated-hydrator direct usage implementation,
  • write generated-hydrator-bundle direct usage implementation,
  • write a stupid runtime fallback,
  • inject it into runners.

Method renaming suggestions

Here are my propositions:

  • rename condition() to where() to be closer to the SQL terms;
  • rename expression() to whereExpression() to be consistent with columnExpression() and havingExpression() methods (I firstly thought expression() was the method to use to do what columnExpression() does).

As well, I wonder if it would not be better to rename the Where class to Condition for a more generic term completely dissociated from the place the condition will finally have in the query.

Write a independent Symfony bundle

Write an independent Symfony:

  • version bundle following goat-query itself,
  • easy installation without makinacorpus/goat which is a monolith containing way too much stuff,
  • make makinacorpus/goat more resilient to makinacorpus/goat-query evolution and changes.

Allow escaping column names in complex expression

For use cases such as:

$select->expression($columnName . '::date BETWEEN ?::date AND ?::date', [$from, $to]);

We need to be able to properly escape the column name yet keep the pgsql cast. It could be written using a proper standard SQL cast as well:

$select->expression('CAST(' . $columnName . ' AS date) BETWEEN ...', [$from, $to]);

I'm not sure how to solve this:

A solution could be using a placeholder for column names and handle it in the ExpressionRaw class such as:

$select->expression(ExpressionRaw::create('CAST([column] AS date) BETWEEN ...', [$from, $to], ['column' => $columnName]);

But it think it would create yet another non explicit language within the language to learn.

May be a ExpressionColumnRaw object, that would take the column name as an argument and use a placeholder to replace it, like ExpressionLike expression ?

Optimize AbstractSqlWriter::rewriteQueryAndParameters()

This method is historically the oldest piece of this whole component. It has been rewrote or improved many times, but today, we cannot really make it faster anymore as it exists.

In some benchmarks, this method execution time takes up to 1/3rd of query formatting, prepare, execute, which is huge.

Some leads we can follow:

  • make this a component which can be replaced,
  • write default implementation to be the current one,
  • write an optional parser not using preg and benchmark, whereas this could be a nice improvement for PHP8 + JIT (this is a purely arbitrary assumption) it may be much much slower than preg,
  • write an external optional implementation using c, rust, I don't care the language, based upon a parsing framework such as nom (rust), or any other, and plug it using FFI,
  • any other ideas?

A few SelectQuery addtions

  • add a ->count($expression = null, $alias = null) as a shorcut to SELECT COUNT(EXPR) FROM, where $expression per default will be '*'

Can't provide a new Where object to the Where::condition() method

I built a query looking like this:

$count = $this
    ->getRunner()
    ->getQueryBuilder()
    ->select('...')
    // ...
    ->condition((new Where(Where::OR)) // <- Not accepted.
        ->isNull('...')
        ->isNull('...')
        ->isNull('...')
    )
    ->getCountQuery()
    ->execute()
    ->fetchField()
;

Giving a new Where object to the condition method results in a QueryError:

Goat\Query\QueryError: column reference must be a string or an instance of Goat\Query\ExpressionColumn

The problem comes from ExpressionFactory::column() called at line 119 of Where::condition(). It only accepts string or Expression as argument.

Sugar: determine columns from query select aliases in InsertQuery

If the user writes an INSERT such as:

$builder
    ->insertQuery('foo')
    ->columns(['a', 'b'])
    ->query(
        $builder
            ->select('bar')
            ->columns(['c', 'd'])
            /// ...
    )
    ->perform()
;

Allow it to be written as such:

$builder
    ->insertQuery('foo')
    ->query(
        $builder
            ->select('bar')
            ->columns(['a' => 'c', 'b' => 'd'])
            /// ...
    )
    ->perform()
;

Where SELECT column aliases become the INSERT column list.

Of course, this can only work if:

  • every SELECT column as a name (i.e. no expression),
  • every SELECT column as an alias (i.e. expressions are accepted).

Benchmark, profile, etc...

And improve overall performances whenever it's possible

Some ideas:

  • formatter is slow, remove sprintf() and temporary arrays usage,
  • ensure bag merge to be faster,
  • less temporary variables and additional function calls whenever possible (but keep maximum code readiblity),
  • if everything seems fine, do nothing.

Backport MERGE tests and fixes from 2.x to 1.x

It generates wrong SQL for PgSQL, and it will be almost free to port the MySQL feature as well. Forget about MySQL, wait for 2.x, as of now supported MySQL version status is ambigous and porting no so obvious, whereas 2.x removes the ambiguities.

DELETE .. JOIN converted to DELETE .. USING with pgsql is broken

It converts the first JOINed table with USING, but it keeps all others as JOINs which seem to not be working.

  • write tests
  • fix it
  • if there's no way to fix it, remove feature and document that nested SELECT or WITH statements are much better for the job

Documentation (and tests):

  • exemple of DELETE .. WHERE foo IN (SELECT ...)
  • exemple of WHERE foo AS (SELECT ..) DELETE FROM bar USING foo WHERE ...

Specialize exception

At least for transactions, we need to be able to do transaction retries easily.

pgsql create and execute stored procedure tutorial

Add a "tutorial" section in documentation. Divide in sub-sections for specific backends or RDBMS.

Start with documentation on how to create a volatile stored procedure (for session lifetime) then how to execute it (it took me a while to figure it out, even though the answer was simple).

Provide a few columnExpression() with aggregates

Most commonly used aggregate (or not) expressions are the following as of now:

  • ->columnExpression('true') and ->columnExpression('false')
  • ->columnExpression('1')
  • ->columnExpression("count(some_column)") and ->columnExpression("count(*)")
  • ->columnExpression(ExpressionRaw::create('array_agg(some_column)'))
  • ->columnExpression(ExpressionRaw::create('coalesce(col1, col2, ...)'), 'alias')

Get rid of all intermediate unnecessary arrays

  • whenever possible, use iterators or generators,
  • for query builder and data temporary storage, use objects with explicit type,
  • in query formatter, it might not always be possible to get rid of temporary arrays.

Transactions handling

  • per default allow nested transactions
  • default behaviour on savepoints if the backend allows it
  • optionnally provide a method to start a transaction and fail if already within one

Implement driver versionning

Before today, this was not necessary because we had only SQL writer implementation for each RDMS vendor, but now, we have 2 for MySQL, thanks to MERGE implemention. So here is what to be implemented:

  • one PHPUnit test runner per version,
  • read RDMS version on connection init, or force it by configuration (parameter such as &version=57 for example),
  • spawn services depending upon version.

Asynchronous perform

Actual ->perform() does not fetch any results and ask the server for none when possible, yet it remains synchronous.

Add an asynchronous variant of perform, which can name the query, that would return an object able to query for running query state.

This seems possible (and easy) with ext-pgsql, not sure it will be possible with PDO.

Documentation : QueryBuilder - insertValues VS insertQuery

It's not clear the difference between insertValues and insertQuery in QueryuBuilder class. They have the same description, maybe it could be nice to have an example in function annotations.

    /**
     * Create an insert query builder
     *
     * @param string|ExpressionRelation $relation
     *   SQL from statement relation name
     */
    public function insertValues($relation): InsertValuesQuery;

    /**
     * Create an insert with query builder
     *
     * @param string|ExpressionRelation $relation
     *   SQL from statement relation name
     */
    public function insertQuery($relation): InsertQueryQuery;

Stored procedure execution

RIght now, nothing was planned for this, but we need to be able to execute stored procedure. It seems that using ext-pgsql, calling ->perform() or ->execute() doesn't work, so I'd go for adding an ->executeProcedure() method on the runner.

Create an Expression factory

People have to know every variation, using a factory it would be much simpler for users with a good IDE providing auto-completion to work with it.

Add a RawQuery object

Some method signatures requires that you provide a Query object, using a RawQuery which contains arbitrary SQL from the API user could do the trick.

Merge values and query pattern using a constant table expression

Both MERGE and INSERT queries have two variants: one for using VALUES (...) and the other to use a nested SELECT query or arbitrary expression.

This could be solved by materialising a constant table query implementation, which would be a table expression, and be used in place of any other expression.

This way we could systematically have both ->query(Query) and ->values() methods on those queries, values accepting either a callable which gives you a ExpressionConstantTable or such parameter, which yields a ->values() method, or uses an ExpressionConstantTable internally to populate using given array.

We won't have to handle two different implementations anymore, and it would solve #11.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.