<?php
/**
* @class dbStruct
*
* @package Clearbricks
* @subpackage DBSchema
*
* @copyright Olivier Meunier & Association Dotclear
* @copyright GPL-2.0-only
*/
class dbStruct
{
protected $con;
protected $prefix;
protected $tables = [];
protected $references = [];
public function __construct($con, string $prefix = '')
{
$this->con = &$con;
$this->prefix = $prefix;
}
public function driver(): string
{
return $this->con->driver();
}
public function table(string $name)
{
$this->tables[$name] = new dbStructTable($name);
return $this->tables[$name];
}
public function __get(string $name)
{
if (!isset($this->tables[$name])) {
return $this->table($name);
}
return $this->tables[$name];
}
public function reverse()
{
$schema = dbSchema::init($this->con);
# Get tables
$tables = $schema->getTables();
foreach ($tables as $t_name) {
if ($this->prefix && strpos($t_name, $this->prefix) !== 0) {
continue;
}
$t = $this->table($t_name);
# Get columns
$cols = $schema->getColumns($t_name);
foreach ($cols as $c_name => $col) {
$type = $schema->dbt2udt($col['type'], $col['len'], $col['default']);
$t->field($c_name, $type, $col['len'], $col['null'], $col['default'], true);
}
# Get keys
$keys = $schema->getKeys($t_name);
foreach ($keys as $k) {
$args = $k['cols'];
array_unshift($args, $k['name']);
if ($k['primary']) {
call_user_func_array([$t, 'primary'], $args);
} elseif ($k['unique']) {
call_user_func_array([$t, 'unique'], $args);
}
}
# Get indexes
$idx = $schema->getIndexes($t_name);
foreach ($idx as $i) {
$args = [$i['name'], $i['type']];
$args = array_merge($args, $i['cols']);
call_user_func_array([$t, 'index'], $args);
}
# Get foreign keys
$ref = $schema->getReferences($t_name);
foreach ($ref as $r) {
$t->reference($r['name'], $r['c_cols'], $r['p_table'], $r['p_cols'], $r['update'], $r['delete']);
}
}
}
/**
* Synchronize this schema taken from database with $schema.
*
* @param dbStruct $s Structure to synchronize with
*/
public function synchronize($s)
{
$this->tables = [];
$this->reverse();
if (!($s instanceof self)) {
throw new Exception('Invalid database schema');
}
$tables = $s->getTables();
$table_create = [];
$key_create = [];
$index_create = [];
$reference_create = [];
$field_create = [];
$field_update = [];
$key_update = [];
$index_update = [];
$reference_update = [];
$got_work = false;
$schema = dbSchema::init($this->con);
foreach ($tables as $tname => $t) {
if (!$this->tableExists($tname)) {
# Table does not exist, create table
$table_create[$tname] = $t->getFields();
# Add keys, indexes and references
$keys = $t->getKeys();
$indexes = $t->getIndexes();
$references = $t->getReferences();
foreach ($keys as $k => $v) {
$key_create[$tname][$this->prefix . $k] = $v;
}
foreach ($indexes as $k => $v) {
$index_create[$tname][$this->prefix . $k] = $v;
}
foreach ($references as $k => $v) {
$v['p_table'] = $this->prefix . $v['p_table'];
$reference_create[$tname][$this->prefix . $k] = $v;
}
$got_work = true;
} else { # Table exists
# Check new fields to create
$fields = $t->getFields();
/* @phpstan-ignore-next-line */
$db_fields = $this->tables[$tname]->getFields();
foreach ($fields as $fname => $f) {
/* @phpstan-ignore-next-line */
if (!$this->tables[$tname]->fieldExists($fname)) {
# Field doest not exist, create it
$field_create[$tname][$fname] = $f;
$got_work = true;
} elseif ($this->fieldsDiffer($db_fields[$fname], $f)) {
# Field exists and differs from db version
$field_update[$tname][$fname] = $f;
$got_work = true;
}
}
# Check keys to add or upgrade
$keys = $t->getKeys();
/* @phpstan-ignore-next-line */
$db_keys = $this->tables[$tname]->getKeys();
foreach ($keys as $kname => $k) {
if ($k['type'] == 'primary' && $this->con->syntax() == 'mysql') {
$kname = 'PRIMARY';
} else {
$kname = $this->prefix . $kname;
}
/* @phpstan-ignore-next-line */
$db_kname = $this->tables[$tname]->keyExists($kname, $k['type'], $k['cols']);
if (!$db_kname) {
# Key does not exist, create it
$key_create[$tname][$kname] = $k;
$got_work = true;
} elseif ($this->keysDiffer($db_kname, $db_keys[$db_kname]['cols'], $kname, $k['cols'])) {
# Key exists and differs from db version
$key_update[$tname][$db_kname] = array_merge(['name' => $kname], $k);
$got_work = true;
}
}
# Check index to add or upgrade
$idx = $t->getIndexes();
/* @phpstan-ignore-next-line */
$db_idx = $this->tables[$tname]->getIndexes();
foreach ($idx as $iname => $i) {
$iname = $this->prefix . $iname;
/* @phpstan-ignore-next-line */
$db_iname = $this->tables[$tname]->indexExists($iname, $i['type'], $i['cols']);
if (!$db_iname) {
# Index does not exist, create it
$index_create[$tname][$iname] = $i;
$got_work = true;
} elseif ($this->indexesDiffer($db_iname, $db_idx[$db_iname], $iname, $i)) {
# Index exists and differs from db version
$index_update[$tname][$db_iname] = array_merge(['name' => $iname], $i);
$got_work = true;
}
}
# Check references to add or upgrade
$ref = $t->getReferences();
/* @phpstan-ignore-next-line */
$db_ref = $this->tables[$tname]->getReferences();
foreach ($ref as $rname => $r) {
$rname = $this->prefix . $rname;
$r['p_table'] = $this->prefix . $r['p_table'];
/* @phpstan-ignore-next-line */
$db_rname = $this->tables[$tname]->referenceExists($rname, $r['c_cols'], $r['p_table'], $r['p_cols']);
if (!$db_rname) {
# Reference does not exist, create it
$reference_create[$tname][$rname] = $r;
$got_work = true;
} elseif ($this->referencesDiffer($db_rname, $db_ref[$db_rname], $rname, $r)) {
$reference_update[$tname][$db_rname] = array_merge(['name' => $rname], $r);
$got_work = true;
}
}
}
}
if (!$got_work) {
return;
}
# Create tables
foreach ($table_create as $table => $fields) {
$schema->createTable($table, $fields);
}
# Create new fields
foreach ($field_create as $tname => $fields) {
foreach ($fields as $fname => $f) {
$schema->createField($tname, $fname, $f['type'], $f['len'], $f['null'], $f['default']);
}
}
# Update fields
foreach ($field_update as $tname => $fields) {
foreach ($fields as $fname => $f) {
$schema->alterField($tname, $fname, $f['type'], $f['len'], $f['null'], $f['default']);
}
}
# Create new keys
foreach ($key_create as $tname => $keys) {
foreach ($keys as $kname => $k) {
if ($k['type'] == 'primary') {
$schema->createPrimary($tname, $kname, $k['cols']);
} elseif ($k['type'] == 'unique') {
$schema->createUnique($tname, $kname, $k['cols']);
}
}
}
# Update keys
foreach ($key_update as $tname => $keys) {
foreach ($keys as $kname => $k) {
if ($k['type'] == 'primary') {
$schema->alterPrimary($tname, $kname, $k['name'], $k['cols']);
} elseif ($k['type'] == 'unique') {
$schema->alterUnique($tname, $kname, $k['name'], $k['cols']);
}
}
}
# Create indexes
foreach ($index_create as $tname => $index) {
foreach ($index as $iname => $i) {
$schema->createIndex($tname, $iname, $i['type'], $i['cols']);
}
}
# Update indexes
foreach ($index_update as $tname => $index) {
foreach ($index as $iname => $i) {
$schema->alterIndex($tname, $iname, $i['name'], $i['type'], $i['cols']);
}
}
# Create references
foreach ($reference_create as $tname => $ref) {
foreach ($ref as $rname => $r) {
$schema->createReference($rname, $tname, $r['c_cols'], $r['p_table'], $r['p_cols'], $r['update'], $r['delete']);
}
}
# Update references
foreach ($reference_update as $tname => $ref) {
foreach ($ref as $rname => $r) {
$schema->alterReference($rname, $r['name'], $tname, $r['c_cols'], $r['p_table'], $r['p_cols'], $r['update'], $r['delete']);
}
}
# Flush execution stack
$schema->flushStack();
return
count($table_create) + count($key_create) + count($index_create) + count($reference_create) + count($field_create) + count($field_update) + count($key_update) + count($index_update) + count($reference_update);
}
public function getTables(): array
{
$res = [];
foreach ($this->tables as $t => $v) {
$res[$this->prefix . $t] = $v;
}
return $res;
}
public function tableExists(string $name): bool
{
return isset($this->tables[$name]);
}
private function fieldsDiffer($db_field, $schema_field): bool
{
$d_type = $db_field['type'];
$d_len = (int) $db_field['len'];
$d_default = $db_field['default'];
$d_null = $db_field['null'];
$s_type = $schema_field['type'];
$s_len = (int) $schema_field['len'];
$s_default = $schema_field['default'];
$s_null = $schema_field['null'];
return $d_type != $s_type || $d_len != $s_len || $d_default != $s_default || $d_null != $s_null;
}
private function keysDiffer($d_name, $d_cols, $s_name, $s_cols): bool
{
return $d_name != $s_name || $d_cols != $s_cols;
}
private function indexesDiffer($d_name, $d_i, $s_name, $s_i): bool
{
return $d_name != $s_name || $d_i['cols'] != $s_i['cols'] || $d_i['type'] != $s_i['type'];
}
private function referencesDiffer($d_name, $d_r, $s_name, $s_r): bool
{
return $d_name != $s_name || $d_r['c_cols'] != $s_r['c_cols'] || $d_r['p_table'] != $s_r['p_table'] || $d_r['p_cols'] != $s_r['p_cols'] || $d_r['update'] != $s_r['update'] || $d_r['delete'] != $s_r['delete'];
}
}
/**
* @class dbStructTable
*
* @package Clearbricks
* @subpackage DBSchema
*/
class dbStructTable
{
protected $name;
protected $has_primary = false;
protected $fields = [];
protected $keys = [];
protected $indexes = [];
protected $references = [];
/**
Universal data types supported by dbSchema
SMALLINT : signed 2 bytes integer
INTEGER : signed 4 bytes integer
BIGINT : signed 8 bytes integer
REAL : signed 4 bytes floating point number
FLOAT : signed 8 bytes floating point number
NUMERIC : exact numeric type
DATE : Calendar date (day, month and year)
TIME : Time of day
TIMESTAMP : Date and time
CHAR : A fixed n-length character string
VARCHAR : A variable length character string
TEXT : A variable length of text
*/
protected $allowed_types = [
'smallint', 'integer', 'bigint', 'real', 'float', 'numeric',
'date', 'time', 'timestamp',
'char', 'varchar', 'text',
];
public function __construct(string $name)
{
$this->name = $name;
}
public function getFields(): array
{
return $this->fields;
}
public function getKeys(bool $primary = null): array
{
return $this->keys;
}
public function getIndexes(): array
{
return $this->indexes;
}
public function getReferences(): array
{
return $this->references;
}
public function fieldExists(string $name): bool
{
return isset($this->fields[$name]);
}
public function keyExists(string $name, string $type, $cols)
{
# Look for key with the same name
if (isset($this->keys[$name])) {
return $name;
}
# Look for key with the same columns list and type
foreach ($this->keys as $n => $k) {
if ($k['cols'] == $cols && $k['type'] == $type) {
# Same columns and type, return new name
return $n;
}
}
return false;
}
public function indexExists(string $name, string $type, array $cols)
{
# Look for key with the same name
if (isset($this->indexes[$name])) {
return $name;
}
# Look for index with the same columns list and type
foreach ($this->indexes as $n => $i) {
if ($i['cols'] == $cols && $i['type'] == $type) {
# Same columns and type, return new name
return $n;
}
}
return false;
}
public function referenceExists(string $name, array $c_cols, string $p_table, array $p_cols)
{
if (isset($this->references[$name])) {
return $name;
}
# Look for reference with same chil columns, parent table and columns
foreach ($this->references as $n => $r) {
if ($c_cols == $r['c_cols'] && $p_table == $r['p_table'] && $p_cols == $r['p_cols']) {
# Only name differs, return new name
return $n;
}
}
return false;
}
public function field(string $name, string $type, ?int $len, bool $null = true, $default = false, bool $to_null = false)
{
$type = strtolower($type);
if (!in_array($type, $this->allowed_types)) {
if ($to_null) {
$type = null;
} else {
throw new Exception('Invalid data type ' . $type . ' in schema');
}
}
$this->fields[$name] = [
'type' => $type,
'len' => (int) $len,
'default' => $default,
'null' => (bool) $null,
];
return $this;
}
public function __call(string $name, $args)
{
array_unshift($args, $name);
// was return call_user_func_array([$this, 'field'], $args);
return $this->field(...$args);
}
public function primary(string $name, $col)
{
if ($this->has_primary) {
throw new Exception(sprintf('Table %s already has a primary key', $this->name));
}
$cols = func_get_args();
array_shift($cols);
return $this->newKey('primary', $name, $cols);
}
public function unique(string $name, $col)
{
$cols = func_get_args();
array_shift($cols);
return $this->newKey('unique', $name, $cols);
}
public function index(string $name, string $type, $col)
{
$cols = func_get_args();
array_shift($cols);
array_shift($cols);
$this->checkCols($cols);
$this->indexes[$name] = [
'type' => strtolower($type),
'cols' => $cols,
];
return $this;
}
public function reference(string $name, $c_cols, string $p_table, $p_cols, $update = false, $delete = false)
{
if (!is_array($p_cols)) {
$p_cols = [$p_cols];
}
if (!is_array($c_cols)) {
$c_cols = [$c_cols];
}
$this->checkCols($c_cols);
$this->references[$name] = [
'c_cols' => $c_cols,
'p_table' => $p_table,
'p_cols' => $p_cols,
'update' => $update,
'delete' => $delete,
];
}
protected function newKey(string $type, string $name, array $cols)
{
$this->checkCols($cols);
$this->keys[$name] = [
'type' => $type,
'cols' => $cols,
];
if ($type == 'primary') {
$this->has_primary = true;
}
return $this;
}
protected function checkCols(array $cols)
{
foreach ($cols as $v) {
if (!preg_match('/^\(.*?\)$/', $v) && !isset($this->fields[$v])) {
throw new Exception(sprintf('Field %s does not exist in table %s', $v, $this->name));
}
}
}
}