Giter Club home page Giter Club logo

sqlbuilder's Introduction

Object oriented representation of SQL queries.

This library does not generate any SQL, instead it provides an object oriented representation of SQL queries, allowing the structure of queries to be defined by the developer whilst gaining the benefits of inheritance.

When a PHP application reaches scale, it is often the database that is the performance bottleneck. When queries are generated on behalf of the developer, they are often difficult to optimise because control is lost. With SqlBuilder, the developer is always in control of the raw SQL.


Build status Code quality Code coverage Current version PHP.Gt/SqlBuilder documentation

Example usage: a class that represents a SELECT query

Imagine a typical database application with a student table used to store details of each student. A basic select might look something like this:

select
	id,
	forename,
	surname,
	dateOfBirth
from
	student

The above query will return a list of all students. The problem here is that when you come to need to select from the student table again, this time with some constraints such as an age range or ordered by surname, the whole query will need to be repeated and only a small portion of the original query will need to be changed.

Instead, the following class can be used to represent the above query:

class StudentSelect extends SelectQuery {
	public function select():array {
		return [
			"id",
			"forename",
			"surname",
			"dateOfBirth",
		];
	}
	
	public function from():array {
		return [
			"student",
		];
	}
}

The __toString method of the above class will produce identical SQL to the original query.

Now, to write another query that returns students of a certain age:

class StudentSelectByAge extends StudentSelect {
	public function where():array {
		return [
			"year(now()) - year(dateOfBirth) = :age",
		];
	}
}

Example usage: A fluent class that builds a SELECT query (coming in v2 release)

As you can see in the example above, SqlQuery functions always return an array of expressions. The SqlBuilder classes have the same methods (select, from, where, etc.) but take the expressions as parameters, acting as a fluent interface.

To create the same query as in the example above with fluent syntax:

$selectQuery = new SelectBuilder(
	"id",
	"forename",
	"surname",
	"dateOfBirth"
)->from(
	"student"
)->where(
	"year(now()) - year(dateOfBirth) = :age"
);

This is particularly useful for when there is a base query, say StudentSelect, and your code only requires a single additional condition. Rather than having to create a separate class for this single usage, it can be called inline:

// Start by using a base StudentSelect, then add a single inline condition to it.
$studentSelect = new StudentSelect();

$selectQuery = new SelectBuilder($studentSelect)
->where(
	"year(now()) - year(dateOfBirth) = :age"
);

Conditionals

Expressions within where and having clauses can be connected with logical operators, which are often combined. To avoid logical errors, Condition objects are used to specify the precedence of logic.

For example, to select students with a specific age and gender:

class StudentSelectByAge extends StudentSelect {
	public function where():array {
		return [
			new AndCondition("year(now()) - year(dateOfBirth) = :age"),
			new AndCondition("gender = :gender"),
		];
	}
}

Subqueries

SqlQuery objects have a __toString() function, and SqlBuilder results create SqlQuery instances. Because of this, they can be used in place of any other expression within a Query or Builder.

Limitations of plain SQL

The only tools provided by plain SQL that can be used to write DRY code are views and stored procedures, both of which have their own set of limitations when writing clean and maintainable code.

The solution provided by this library is to break down an SQL query into its different sections, represented by a PHP class which can be extended by other classes, while still retaining the plain SQL that is being represented.

SQL compatibility

This library does not provide any SQL processing capabilities by design. Any driver-specific SQL used will not be compatible with other drivers. This allows the developer to fully utilise their SQL driver of choice, rather than generating generic SQL.

sqlbuilder's People

Contributors

dependabot[bot] avatar g105b avatar

Watchers

 avatar  avatar

Forkers

isabella232

sqlbuilder's Issues

Class representation for all parts that change between SQL drivers

We already have the Condition classes tracked in their own issue, because they have some useful functionality when combining different logical conditions, but this issue covers all the other parts of a query that might be output in a different way between different database drivers.

For example:

  • like comparison uses slightly different syntax in MySQL to SQLite and Postgres (field like 'gr?g' in MySQL vs. field like 'gr_g' in others)

// TODO: More examples to flesh out the syntactical differences between the major vendors.

Enhanced Conditional

For future use of SqlBuilder classes, various Conditional classes need creating:

  • Equals (with null support), GreaterThan, LessThan
  • Between (with date support)
  • MultipleCondition that takes MatchAny and MatchAll

SqlBuilder classes

We've got the Query classes working nicely, but now's the time to move out the functionality to something that can be built automatically by the Database Repository.

Builder classes:

  • SelectBuilder
  • UpdateBuilder
  • DeleteBuilder
  • InsertBuilder

On the back of this, we need to have the Conditional functionality added to.

Rough notes:

class SelectBuilder extends SqlBuilder {
	const DEFAULT_LIMIT = 100;

	private ?string $prefix;
	/** @var string[] */
	private array $columnArray;
	private string $from;

	private Condition $where;
	/** @var string[] */
	private array $groupBy;
	private Condition $having;

	private int $limit;
	private int $offset;

	public function __construct(string $prefix = null) {
		$this->prefix = $prefix;
		$this->limit = self::DEFAULT_LIMIT;
		$this->offset = 0;
	}

	public function __toString():string {
		$sql = "";
		if(isset($this->prefix)) {
			$sql .= $this->prefix . "\n";
		}
		$sql .= "select";
		foreach($this->columnArray as $i => $col) {
			if($i > 0) {
				$sql .= ",";
			}
			$sql .= "\n\t$col";
		}
		$sql .= "\n\nfrom";
		$sql .= "\n\t" . $this->from;

		if(isset($this->where)) {
			$sql .= "\n\nwhere";
			foreach($this->where as $condition) {
				$sql .= "\n\t$condition";
			}
		}

		return $sql;
	}

	public function select(string...$columns):self {
		$this->columnArray = $columns;
		return $this;
	}

	public function from(string $tableName):self {
		$this->from = $tableName;
		return $this;
	}

	public function where(Condition $condition):self {
		$this->where = $condition;
		return $this;
	}

	public function groupBy(string...$groupBy):self {
		$this->groupBy = $groupBy;
		return $this;
	}

	public function having(Condition $condition):self {
		$this->having = $condition;
		return $this;
	}
}

abstract class Condition {}

abstract class MatchMultipleCondition extends Condition {
	/** @var array<Condition> */
	private array $conditionArray;

	public function __construct(Condition...$conditions) {
		$this->conditionArray = $conditions;
	}
}

class MatchAny extends MatchMultipleCondition {}

class MatchAll extends MatchMultipleCondition {}

abstract class SingleCondition extends Condition {
	public function __construct(
		public readonly string $propertyName,
		public readonly mixed $value,
	) {}
}

class Equals extends SingleCondition {}

class GreaterThan extends SingleCondition {}

class LessThan extends SingleCondition {}

Type safe objects for each component

Rather than returning an array for everything, return an object of the appropriate type.

For example:

UpdateQuery::set():AssignmentList

class UpdateStudentName extends UpdateQuery {
  public function set():AssignmentList {
    return new AssignmentList(
      "name",
      new Assignment("updatedAt", new NowDate()),
    );
  }
}

Test subqueries

In theory this should not be a problem, as a Query object can be represented as a string, but it may be necessary to wrap brackets, etc.

Builder::getQuery

In the builder classes, it should be possible to get a reference to the object that represents the query.

SelectBuilder::getQuery(); should return the class the extends the SqlQuery, which is currently only managed within the __toString() function.

Named/indexed parameter collection

Don't return an array from "set" functions, like InsertQuery::set, instead return an implementation of abstract class ParameterCollection.

An array can also be returned, which will require a doc block until PHP 8 is supported with union type array|ParameterCollection.

Create table

Missing functionality - no create table. This is required for full ORM usage.

Joins

Inner, left, right, full outer ... They should all be handled.

SqlBuilder for table queries

This is tracked in its own issue due to table operations coming with their own set of driver-specific complexities.

  • Create
  • Alter
  • Drop

Audit all returns

The main example I want to take a look at is limit(). It can only ever be a single integer, so should it really always return an array?

While we're looking at return types, make sure that string[] is always the case, and clearly indicate where a subquery can be present within a returned array.

Update

  • Low priority
  • Ignore errors
  • Order by
  • Limit
  • Multiple tables
  • Set assignments

Idea for Query Builder

To save on repetitive queries:

class getCustomerList extends SqlQuery {

	public function select () {
		return [
    			'id',
			'name'
			'status'
		];
	}

	public function from {
		return 'customer';
		// or another SqlQuery class:
		// return new getNestedQuery();
	}

	public function order {
		'created desc';
	}

}

class getCustomerListUnprocessed extends getCustomerList {

	public function where () {
		return array_merge(parent::where(), [
			'processed is null'
		]);
	}

}

Indexed/named parameter array syntax helper

Idea:

For indexed parameters (with the ? character):

return [
    "?uuid",
    "?name",
    "createdAt" => "now()",
]

For named parameters (with :-prefixed named):

return [
    ":uuid",
    ":name",
    "createdAt" => "now()",
]

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.