├── CNAME ├── tests ├── Tasks │ ├── class │ │ └── Class.php │ ├── test │ │ └── None.php │ ├── anon │ │ └── anon.php │ ├── circular │ │ ├── Circular1.php │ │ └── Circular2.php │ ├── deps │ │ ├── Dep1.php │ │ └── Dep2.php │ ├── seq │ │ └── Seq.php │ ├── conn │ │ └── Conn.php │ ├── delete │ │ └── Delete.php │ ├── DepsTest.php │ ├── AnonTest.php │ ├── ClassTest.php │ ├── IfaceTest.php │ ├── CircularTest.php │ ├── SeqTest.php │ ├── ConnTest.php │ ├── iface │ │ └── NoIface.php │ ├── insert │ │ └── Insert.php │ ├── rename │ │ └── Rename.php │ ├── name │ │ └── Name.php │ ├── UpTest.php │ ├── CrudTest.php │ ├── NameTest.php │ ├── create │ │ └── Create.php │ └── update │ │ └── Update.php ├── config.php └── Upscheme │ ├── Task │ └── BaseTest.php │ ├── Schema │ ├── SequenceTest.php │ ├── ForeignTest.php │ ├── ColumnTest.php │ ├── DBTest.php │ └── TableTest.php │ └── UpTest.php ├── _config.yml ├── assets ├── upscheme-logo.png ├── upscheme-social.png ├── octocat.svg ├── js │ └── script.js ├── css │ └── style.scss └── upscheme-logo.svg ├── .gitignore ├── stubs ├── view.stub ├── sequence.stub └── table.stub ├── composer.json ├── phpunit.xml ├── src ├── Task │ ├── Iface.php │ └── Base.php ├── Schema │ ├── Sequence.php │ ├── Foreign.php │ ├── Column.php │ └── Table.php ├── Generate.php └── Up.php ├── _layouts └── default.html ├── LICENSE └── .circleci └── config.yml /CNAME: -------------------------------------------------------------------------------- 1 | upscheme.org -------------------------------------------------------------------------------- /tests/Tasks/class/Class.php: -------------------------------------------------------------------------------- 1 | 'pdo_pgsql', 5 | 'path' => 'sqlite.test', 6 | 'host' => '127.0.0.1', 7 | // 'port' => 5433, 8 | 'dbname' => 'upscheme', 9 | 'user' => 'aimeos', 10 | 'password' => 'aimeos' 11 | ]; 12 | -------------------------------------------------------------------------------- /tests/Tasks/anon/anon.php: -------------------------------------------------------------------------------- 1 | info('Create view "{{NAME}}"'); 11 | 12 | $this->db('{{DB}}')->view('{{NAME}}', '{{SQL}}'); 13 | } 14 | }; -------------------------------------------------------------------------------- /tests/Tasks/circular/Circular1.php: -------------------------------------------------------------------------------- 1 | info('Create sequence "{{NAME}}"'); 13 | 14 | $this->db('{{DB}}')->sequence('{{NAME}}', function(Sequence $s) { 15 | 16 | {{SEQUENCE}} 17 | }); 18 | } 19 | }; -------------------------------------------------------------------------------- /tests/Tasks/seq/Seq.php: -------------------------------------------------------------------------------- 1 | info( 'Testing sequences' ); 14 | 15 | $this->sequence( 'testseq', function( Sequence $seq ) { 16 | 17 | $seq->start( 100 )->step( 50 )->cache( 10 ); 18 | 19 | } ); 20 | 21 | $this->dropSequence( 'testseq' ); 22 | } 23 | } -------------------------------------------------------------------------------- /tests/Tasks/conn/Conn.php: -------------------------------------------------------------------------------- 1 | info( 'Testing connections' ); 14 | 15 | $this->db( 'test' )->table( 'testconn' )->int( 'id' )->up(); 16 | 17 | for( $i = 0; $i < 10; $i++ ) { 18 | $this->db( 'test', true )->delete( 'testconn' )->close(); 19 | } 20 | 21 | $this->db( 'test' )->dropTable( 'testconn' )->up(); 22 | } 23 | } -------------------------------------------------------------------------------- /tests/Tasks/delete/Delete.php: -------------------------------------------------------------------------------- 1 | info( 'Removing test tables' ); 14 | 15 | $this->dropView( 'testview' )->delete( 'test2' ) 16 | ->dropTable( 'testref2' )->dropTable( 'test2' ) 17 | ->dropSequence( 'seq_test2' ) 18 | ->dropSequence( 'test_SEQ' )->dropSequence( 'testref_SEQ' ); // workaround for Oracle 19 | } 20 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aimeos/upscheme", 3 | "description": "Database schema upgrades made easy", 4 | "license": "LGPL-3.0-or-later", 5 | "type": "library", 6 | "require": { 7 | "php": "~7.4||~8.0", 8 | "aimeos/macro": "^1.0", 9 | "doctrine/dbal": "~3.9||~4.0" 10 | }, 11 | "require-dev": { 12 | "phpunit/phpunit": "~9.0||~10.0", 13 | "php-coveralls/php-coveralls": "~2.0" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "Aimeos\\Upscheme\\": "src" 18 | }, 19 | "classmap": [ 20 | "src" 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests/Upscheme 6 | 7 | 8 | tests/Tasks 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /stubs/table.stub: -------------------------------------------------------------------------------- 1 | info('Create table "{{NAME}}"'); 19 | 20 | $this->db('{{DB}}')->table('{{NAME}}', function(Table $t) { 21 | 22 | {{COLUMN}} 23 | 24 | {{FOREIGN}} 25 | 26 | {{INDEX}} 27 | }); 28 | } 29 | }; -------------------------------------------------------------------------------- /tests/Tasks/DepsTest.php: -------------------------------------------------------------------------------- 1 | config = include dirname( __DIR__ ) . DIRECTORY_SEPARATOR . 'config.php'; 16 | } 17 | 18 | 19 | public function testDeps() 20 | { 21 | $this->expectOutputString( 'dep2dep1' ); 22 | \Aimeos\Upscheme\Up::use( $this->config, __DIR__ . '/deps' )->up(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Tasks/AnonTest.php: -------------------------------------------------------------------------------- 1 | config = include dirname( __DIR__ ) . DIRECTORY_SEPARATOR . 'config.php'; 16 | } 17 | 18 | 19 | public function testDeps() 20 | { 21 | $this->expectOutputString( 'anonymous class' ); 22 | \Aimeos\Upscheme\Up::use( $this->config, __DIR__ . '/anon' )->up(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Tasks/ClassTest.php: -------------------------------------------------------------------------------- 1 | config = include dirname( __DIR__ ) . DIRECTORY_SEPARATOR . 'config.php'; 16 | } 17 | 18 | 19 | public function testClass() 20 | { 21 | $this->expectException( '\RuntimeException' ); 22 | \Aimeos\Upscheme\Up::use( $this->config, __DIR__ . '/class' )->up(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Tasks/IfaceTest.php: -------------------------------------------------------------------------------- 1 | config = include dirname( __DIR__ ) . DIRECTORY_SEPARATOR . 'config.php'; 16 | } 17 | 18 | 19 | public function testIface() 20 | { 21 | $this->expectException( '\RuntimeException' ); 22 | \Aimeos\Upscheme\Up::use( $this->config, __DIR__ . '/iface' )->up(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Tasks/CircularTest.php: -------------------------------------------------------------------------------- 1 | config = include dirname( __DIR__ ) . DIRECTORY_SEPARATOR . 'config.php'; 16 | } 17 | 18 | 19 | public function testCircular() 20 | { 21 | $this->expectException( '\RuntimeException' ); 22 | \Aimeos\Upscheme\Up::use( $this->config, __DIR__ . '/circular' )->up(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Tasks/SeqTest.php: -------------------------------------------------------------------------------- 1 | config = include dirname( __DIR__ ) . DIRECTORY_SEPARATOR . 'config.php'; 16 | } 17 | 18 | 19 | public function testSeq() 20 | { 21 | $result = \Aimeos\Upscheme\Up::use( $this->config, __DIR__ . '/seq' )->up(); 22 | $this->assertInstanceOf( \Aimeos\Upscheme\Up::class, $result ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Tasks/ConnTest.php: -------------------------------------------------------------------------------- 1 | config = include dirname( __DIR__ ) . DIRECTORY_SEPARATOR . 'config.php'; 16 | } 17 | 18 | 19 | public function testConn() 20 | { 21 | $result = \Aimeos\Upscheme\Up::use( $this->config, __DIR__ . '/conn' )->up(); 22 | $this->assertInstanceOf( \Aimeos\Upscheme\Up::class, $result ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Tasks/iface/NoIface.php: -------------------------------------------------------------------------------- 1 | List of task names 21 | */ 22 | public function after() : array; 23 | 24 | /** 25 | * Returns the list of task names which this task depends on 26 | * 27 | * @return array List of task names 28 | */ 29 | public function before() : array; 30 | 31 | /** 32 | * Executes the tasks to update the database 33 | */ 34 | public function up(); 35 | } -------------------------------------------------------------------------------- /tests/Tasks/insert/Insert.php: -------------------------------------------------------------------------------- 1 | info( 'Insert data' ); 14 | 15 | $db = $this->db( 'test' ); 16 | 17 | $db->insert( 'test', [ 18 | // 'hex' => '0xff', 'image' => 'svg+xml:', 19 | 'status' => true, 'birthday' => '2000-01-01', 20 | 'ctime' => '2000-01-01 00:00:00', 'mtime' => '2000-01-01 00:00:00', 'price' => '100.00', 21 | 'scale' => 0.1, 'pos' => 1, 'test' => 1234, 'config' => '{}', 'type' => 123, 'code' => 'test', 22 | 'content' => 'some text', 'uuid' => '7e57d004-2b97-0e7a-b45f-5387367791cd' 23 | ] ); 24 | 25 | $db->insert( 'testref', ['parentid' => $db->lastId(), 'label' => 'test ref'] ); 26 | } 27 | } -------------------------------------------------------------------------------- /tests/Tasks/rename/Rename.php: -------------------------------------------------------------------------------- 1 | info( 'Renames test tables/columns/indexes' ); 14 | 15 | $this->renameTable( ['test' => 'test2', 'testref' => 'testref2'] ); 16 | 17 | if( !$this->hasTable( ['test2', 'testref2'] ) ) { 18 | throw new \Exception( 'Renaming tables failed' ); 19 | } 20 | 21 | $this->renameColumn( 'test2', ['uuid' => 'guid'] ); 22 | 23 | if( !$this->hasColumn( 'test2', 'guid' ) ) { 24 | throw new \Exception( 'Renaming column failed' ); 25 | } 26 | 27 | $this->renameIndex( 'test2', ['unq_code' => 'unq_code2'] ); 28 | 29 | if( !$this->hasIndex( 'test2', 'unq_code2' ) ) { 30 | throw new \Exception( 'Renaming index failed' ); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /assets/octocat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/Tasks/name/Name.php: -------------------------------------------------------------------------------- 1 | info( 'Testing Names' ); 14 | 15 | $this->table( 'testname', function( $table ) { 16 | 17 | $table->string( 'id' ); 18 | $table->string( 'siteid', 32 ); 19 | $table->string( 'product_id', 32 ); 20 | $table->string( 'order_base_product_id', 32 ); 21 | $table->string( 'product_name', 32 ); 22 | $table->string( 'order_base_product_name' ); 23 | $table->string( 'order_base_product_type', 32 ); 24 | $table->string( 'order_base_product_value' ); 25 | 26 | $table->primary( 'id' ); 27 | $table->unique( ['siteid', 'product_id'] ); 28 | $table->index( ['siteid', 'product_id', 'order_base_product_id'] ); 29 | $table->index( ['siteid', 'product_name', 'order_base_product_name', 'order_base_product_type', 'order_base_product_value'] ); 30 | 31 | } ); 32 | } 33 | } -------------------------------------------------------------------------------- /assets/js/script.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | const nav = document.querySelectorAll('main nav'); 3 | const methods = document.querySelector('.sidebar .methods'); 4 | 5 | if(nav && methods) { 6 | nav.forEach(function(el) { 7 | methods.appendChild(el.cloneNode(true)); 8 | }); 9 | } 10 | 11 | const open = document.querySelector('.open'); 12 | const close = document.querySelector('.close'); 13 | const sidebar = document.querySelector('.sidebar'); 14 | const content = document.querySelector('.main-content'); 15 | 16 | open.addEventListener('click', function() { 17 | open.classList.toggle('show'); 18 | close.classList.toggle('show'); 19 | sidebar.classList.toggle('show'); 20 | }); 21 | 22 | close.addEventListener('click', function() { 23 | sidebar.classList.toggle('show'); 24 | close.classList.toggle('show'); 25 | open.classList.toggle('show'); 26 | }); 27 | 28 | const fcn = function() { 29 | sidebar.classList.remove('show'); 30 | content.classList.remove('show'); 31 | close.classList.remove('show'); 32 | open.classList.add('show'); 33 | }; 34 | 35 | methods.addEventListener('click', fcn); 36 | content.addEventListener('click', fcn); 37 | 38 | 39 | const search = document.querySelector('.sidebar .search'); 40 | 41 | search.addEventListener('input', function(ev) { 42 | let regex = new RegExp( ev.target.value, 'i'); 43 | 44 | methods.querySelectorAll('a').forEach(function(item) { 45 | if(regex.test(item.textContent)) { 46 | item.parentNode.classList.remove('hide'); 47 | } else { 48 | item.parentNode.classList.add('hide'); 49 | } 50 | }); 51 | }); 52 | })(); 53 | -------------------------------------------------------------------------------- /tests/Tasks/UpTest.php: -------------------------------------------------------------------------------- 1 | opt( 'engine', 'InnoDB' ); 19 | $this->string( 'editor' )->null( true ); 20 | } ); 21 | 22 | $this->config = include dirname( __DIR__ ) . DIRECTORY_SEPARATOR . 'config.php'; 23 | $this->dir = dirname( __DIR__ ) . '/tmp'; 24 | } 25 | 26 | 27 | public function testCreate() 28 | { 29 | file_exists( $this->dir ) ?: mkdir( $this->dir ); 30 | 31 | $object = \Aimeos\Upscheme\Up::use( $this->config, dirname( __DIR__ ) . '/Tasks/create' )->up(); 32 | 33 | $this->assertInstanceOf( \Aimeos\Upscheme\Up::class, $object ); 34 | } 35 | 36 | 37 | public function testExecute() 38 | { 39 | $object = \Aimeos\Upscheme\Up::use( $this->config, $this->dir )->create(); 40 | $this->assertInstanceOf( \Aimeos\Upscheme\Up::class, $object ); 41 | 42 | \Aimeos\Upscheme\Up::use( $this->config, $this->dir )->up(); 43 | } 44 | 45 | 46 | public function testCleanup() 47 | { 48 | $object = \Aimeos\Upscheme\Up::use( $this->config, dirname( __DIR__ ) . '/Tasks/rename' )->up(); 49 | $object2 = \Aimeos\Upscheme\Up::use( $this->config, dirname( __DIR__ ) . '/Tasks/delete' )->up(); 50 | 51 | $this->assertInstanceOf( \Aimeos\Upscheme\Up::class, $object ); 52 | $this->assertInstanceOf( \Aimeos\Upscheme\Up::class, $object2 ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Tasks/CrudTest.php: -------------------------------------------------------------------------------- 1 | opt( 'engine', 'InnoDB' ); 17 | $this->string( 'editor' )->null( true ); 18 | } ); 19 | 20 | $this->config = include dirname( __DIR__ ) . DIRECTORY_SEPARATOR . 'config.php'; 21 | } 22 | 23 | 24 | public function testCreate() 25 | { 26 | $result = \Aimeos\Upscheme\Up::use( $this->config, __DIR__ . '/create' )->up(); 27 | $this->assertInstanceOf( \Aimeos\Upscheme\Up::class, $result ); 28 | 29 | // test if schema is unchanged 30 | $result = \Aimeos\Upscheme\Up::use( $this->config, __DIR__ . '/create' )->up(); 31 | $this->assertInstanceOf( \Aimeos\Upscheme\Up::class, $result ); 32 | } 33 | 34 | 35 | public function testInsert() 36 | { 37 | $result = \Aimeos\Upscheme\Up::use( $this->config, __DIR__ . '/insert' )->up(); 38 | $this->assertInstanceOf( \Aimeos\Upscheme\Up::class, $result ); 39 | } 40 | 41 | 42 | public function testUpdate() 43 | { 44 | $result = \Aimeos\Upscheme\Up::use( $this->config, __DIR__ . '/update' )->up(); 45 | $this->assertInstanceOf( \Aimeos\Upscheme\Up::class, $result ); 46 | } 47 | 48 | 49 | public function testRename() 50 | { 51 | $result = \Aimeos\Upscheme\Up::use( $this->config, __DIR__ . '/rename' )->up(); 52 | $this->assertInstanceOf( \Aimeos\Upscheme\Up::class, $result ); 53 | } 54 | 55 | 56 | public function testDelete() 57 | { 58 | $result = \Aimeos\Upscheme\Up::use( $this->config, __DIR__ . '/delete' )->up(); 59 | $this->assertInstanceOf( \Aimeos\Upscheme\Up::class, $result ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Tasks/NameTest.php: -------------------------------------------------------------------------------- 1 | config = include dirname( __DIR__ ) . DIRECTORY_SEPARATOR . 'config.php'; 16 | 17 | 18 | Aimeos\Upscheme\Schema\Table::macro( 'nameIndex', function( string $table, array $columns, string $type ) { 19 | 20 | $parts = explode( '_', $table ); 21 | 22 | $table = substr( (string) array_shift( $parts ), 0, 2 ); 23 | $table .= substr( (string) array_shift( $parts ), 0, 3 ); 24 | 25 | foreach( $parts as $part ) { 26 | $table .= substr( $part, 0, 2 ); 27 | } 28 | 29 | $max = 30 - strlen( $table ) - strlen( $type ) - count( $columns ) - 1; 30 | $count = count( $columns ); 31 | 32 | foreach( $columns as $idx => $name ) 33 | { 34 | $num = (int) floor( $max / $count-- ); 35 | $parts = explode( '_', $name ); 36 | $name = array_pop( $parts ); 37 | 38 | if( !substr_compare( $name, 'id', -2, 2 ) && strlen( $name ) > 2 ) { 39 | $name = $name[0] . 'id'; 40 | } 41 | 42 | foreach( array_reverse( $parts ) as $part ) { 43 | $name = $part[0] . $name; 44 | } 45 | 46 | $columns[$idx] = substr( $name, 0, $num ); 47 | $max -= strlen( $columns[$idx] ); 48 | } 49 | 50 | return $type . '_' . $table . '_' . join( '_', $columns ); 51 | } ); 52 | } 53 | 54 | 55 | public function testName() 56 | { 57 | $up = \Aimeos\Upscheme\Up::use( $this->config, __DIR__ . '/name' )->up(); 58 | 59 | $this->assertInstanceOf( \Aimeos\Upscheme\Up::class, $up ); 60 | $this->assertTrue( $up->db()->hasIndex( 'testname', 'unq_te_sid_pid' ) ); 61 | $this->assertTrue( $up->db()->hasIndex( 'testname', 'idx_te_sid_pid_obpid' ) ); 62 | $this->assertTrue( $up->db()->hasIndex( 'testname', 'idx_te_sid_pnam_obpn_obpt_obpv' ) ); 63 | 64 | $up->db()->dropTable( 'testname' ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/Tasks/create/Create.php: -------------------------------------------------------------------------------- 1 | info( 'Create tables' ); 14 | 15 | $db = $this->db( 'test' ); 16 | 17 | $db->sequence( 'testseq' )->start( 1000 ); 18 | 19 | $db->table( 'test', function( Table $t ) { 20 | 21 | $this->info( 'Create test table', 'v', 1 ); 22 | 23 | $t->bigint( 'id' )->seq( true )->primary(); 24 | // $t->binary( 'hex' ); // PostgreSQL insert problem 25 | // $t->blob( 'image' ); // PostgreSQL insert problem 26 | $t->bool( 'status' ); 27 | $t->date( 'birthday' ); 28 | $t->datetime( 'ctime' ); 29 | $t->datetimetz( 'mtime' ); 30 | $t->decimal( 'price', 10 ); 31 | $t->float( 'scale' ); 32 | $t->int( 'pos' ); 33 | $t->int( 'test' ); 34 | $t->json( 'config' ); 35 | $t->smallint( 'type' ); 36 | $t->string( 'code' ); 37 | $t->text( 'content' ); 38 | // $t->time( 'time' ); // not supported by Oracle 39 | $t->guid( 'uuid' ); 40 | $t->default(); 41 | $t->uuid( 'uid2' )->null( true )->custom( 'UUID DEFAULT gen_random_uuid()', 'postgresql' ); 42 | 43 | $t->unique( 'code', 'unq_code' ); 44 | $t->index( ['status', 'pos'], 'idx_status_type' ); 45 | $t->index( 'uuid' ); 46 | 47 | } )->up(); 48 | 49 | if( !$db->hasTable( 'test' ) ) { 50 | throw new \RuntimeException( 'Table not created' ); 51 | } 52 | 53 | 54 | $db->table( 'testref', function( Table $t ) { 55 | 56 | $this->info( 'Create testref table', 'v', 1 ); 57 | 58 | $t->id(); 59 | $t->foreign( 'parentid', 'test' ); 60 | $t->string( 'label' ); 61 | 62 | } )->up(); 63 | 64 | if( !$db->hasTable( 'testref' ) ) { 65 | throw new \RuntimeException( 'Table not created' ); 66 | } 67 | 68 | 69 | $db->view( 'testview', 'SELECT ' . $db->qi( 'id' ) . ', ' . $db->qi( 'config' ) . ' FROM ' . $db->qi( 'test' ) ); 70 | 71 | if( !$db->hasView( 'testview' ) ) { 72 | throw new \RuntimeException( 'View not created' ); 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /tests/Tasks/update/Update.php: -------------------------------------------------------------------------------- 1 | info( 'Change test table', 'v', 1 ); 14 | 15 | $this->db( 'test' )->dropIndex( 'test', 'idx_status_type' ); 16 | $this->db( 'test' )->dropIndex( 'test', 'unq_code' ); // workaround for SQL Server 17 | 18 | $this->db( 'test' )->table( 'test', function( Table $t ) { 19 | 20 | $t->bool( 'status' )->comment( 'some status' ); 21 | $t->text( 'content' )->length( 255 ); 22 | $t->bool( 'status' )->default( true ); 23 | $t->date( 'birthday' )->null( true ); 24 | 25 | $t->string( 'code', 5 )->fixed( true ); 26 | 27 | if( $this->type() !== 'oracle' ) 28 | { 29 | $t->decimal( 'price', 8 )->scale( 3 ); // Oracle can't change NUMBER columns with data 30 | $t->int( 'pos' )->type( 'smallint' ); // Oracle can't change NUMBER columns with data 31 | $t->smallint( 'type' )->unsigned( true ); // Oracle can't change NUMBER columns with data 32 | } 33 | 34 | $t->unique( 'code', 'unq_code' ); 35 | $t->index( ['status', 'pos'], 'idx_status_pos' ); 36 | $t->index( 'uuid' ); 37 | 38 | } )->dropColumn( 'test' )->up(); 39 | 40 | if( ( $row = current( $this->db( 'test' )->select( 'test', ['id' => 1, 'pos' => 1] ) ) ) === false ) { 41 | throw new \RuntimeException( 'No row available' ); 42 | } 43 | 44 | $expected = [ 45 | 'birthday' => ['2000-01-01'], 46 | 'code' => ['test', 'test '], // MySQL/SQLite, PostgreSQL 47 | 'config' => ['{}'], 48 | 'content' => ['some text'], 49 | 'ctime' => ['2000-01-01 00:00:00', '2000-01-01 00:00:00.000000'], // MySQL5/PostgreSQL/SQLite, SQLServer 50 | // 'hex' => ['0xff'], 51 | 'id' => [1], 52 | // 'image' => ['svg+xml:'], 53 | // 'mtime' => ['2000-01-01 00:00:00', '2000-01-01 00:00:00+xx'], // MySQL5/SQLite, PostgreSQL 54 | 'pos' => [1], 55 | 'price' => [100], 56 | 'scale' => [0.1], 57 | 'status' => [1], 58 | 'type' => [123], 59 | 'uuid' => ['7e57d004-2b97-0e7a-b45f-5387367791cd', '7E57D004-2B97-0E7A-B45F-5387367791CD'], // MySQL5/PostgreSQL/SQLite, SQLServer 60 | 'editor' => [null] 61 | ]; 62 | 63 | foreach( $expected as $key => $values ) 64 | { 65 | if( !in_array( $row[$key], $values ) ) 66 | { 67 | $d1 = var_export( $values, true ); 68 | $d2 = var_export( $row[$key], true ); 69 | throw new \RuntimeException( "Data mismatch for '" . $key . "', expected: " . $d1 . ", actual: " . $d2 ); 70 | } 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /tests/Upscheme/Task/BaseTest.php: -------------------------------------------------------------------------------- 1 | dbmock = $this->getMockBuilder( '\Aimeos\Upscheme\Schema\DB' ) 21 | ->disableOriginalConstructor() 22 | ->getMock(); 23 | 24 | $this->upmock = $this->getMockBuilder( '\Aimeos\Upscheme\Up' ) 25 | ->disableOriginalConstructor() 26 | ->getMock(); 27 | 28 | $this->object = $this->getMockBuilder( '\Aimeos\Upscheme\Task\TestBase' ) 29 | ->setConstructorArgs( [$this->upmock] ) 30 | ->onlyMethods( [] ) 31 | ->getMock(); 32 | } 33 | 34 | 35 | protected function tearDown() : void 36 | { 37 | unset( $this->object, $this->dbmock, $this->upmock ); 38 | } 39 | 40 | 41 | public function testCall() 42 | { 43 | $this->upmock->expects( $this->once() )->method( 'db' )->willReturn( $this->dbmock ); 44 | $this->dbmock->expects( $this->once() )->method( 'dropTable' ); 45 | 46 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $this->object->dropTable( 'unittest' ) ); 47 | } 48 | 49 | 50 | public function testCallMacro() 51 | { 52 | \Aimeos\Upscheme\Task\Base::macro( 'unittest', function() { return 'yes'; } ); 53 | 54 | $this->assertEquals( 'yes', $this->object->unittest() ); 55 | } 56 | 57 | 58 | public function testAfter() 59 | { 60 | $this->assertEquals( [], $this->object->after() ); 61 | } 62 | 63 | 64 | public function testBefore() 65 | { 66 | $this->assertEquals( [], $this->object->before() ); 67 | } 68 | 69 | 70 | public function testDb() 71 | { 72 | $this->upmock->expects( $this->once() )->method( 'db' )->willReturn( $this->dbmock ); 73 | 74 | $result = $this->access( 'db' )->invokeArgs( $this->object, ['unittest', true] ); 75 | 76 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $result ); 77 | } 78 | 79 | 80 | public function testInfo() 81 | { 82 | $this->upmock->expects( $this->once() )->method( 'info' ); 83 | 84 | $result = $this->access( 'info' )->invokeArgs( $this->object, ['unittest', 2] ); 85 | 86 | $this->assertInstanceOf( \Aimeos\Upscheme\Task\Iface::class, $result ); 87 | } 88 | 89 | 90 | public function testPaths() 91 | { 92 | $this->upmock->expects( $this->once() )->method( 'paths' )->willReturn( [dirname( __DIR__, 2 ) . '/Tasks'] ); 93 | 94 | $result = $this->access( 'paths' )->invokeArgs( $this->object, ['test'] ); 95 | 96 | $this->assertEquals( '/test', substr( current( $result ), -5 ) ); 97 | } 98 | 99 | 100 | protected function access( $name ) 101 | { 102 | $class = new \ReflectionClass( \Aimeos\Upscheme\Task\Base::class ); 103 | $method = $class->getMethod( $name ); 104 | $method->setAccessible( true ); 105 | 106 | return $method; 107 | } 108 | } 109 | 110 | 111 | class TestBase extends \Aimeos\Upscheme\Task\Base 112 | { 113 | public function up() 114 | { 115 | } 116 | } -------------------------------------------------------------------------------- /src/Task/Base.php: -------------------------------------------------------------------------------- 1 | up = $up; 40 | } 41 | 42 | 43 | /** 44 | * Passes unknown method calls to the database scheme object 45 | * 46 | * @param string $method Method name 47 | * @param array $args Method arguments 48 | * @return mixed Result or database schema object 49 | */ 50 | public function __call( string $method, array $args ) 51 | { 52 | if( self::macro( $method ) ) { 53 | return $this->call( $method, $args ); 54 | } 55 | 56 | return $this->db()->{$method}( ...$args ); 57 | } 58 | 59 | 60 | /** 61 | * This task will run after the returned list of task names 62 | * 63 | * @return array List of task names 64 | */ 65 | public function after() : array 66 | { 67 | return []; 68 | } 69 | 70 | 71 | /** 72 | * This task will run before the returned list of task names 73 | * 74 | * @return array List of task names 75 | */ 76 | public function before() : array 77 | { 78 | return []; 79 | } 80 | 81 | 82 | /** 83 | * Returns the database schema for the given connection name 84 | * 85 | * @param string $name Name of the connection from the configuration or empty string for first one 86 | * @param bool $new If a new connection should be created instead of reusing an existing one 87 | * @return \Aimeos\Upscheme\Schema\DB DB schema object 88 | */ 89 | protected function db( string $name = '', bool $new = false ) : \Aimeos\Upscheme\Schema\DB 90 | { 91 | return $this->up->db( $name, $new ); 92 | } 93 | 94 | 95 | /** 96 | * Outputs the message depending on the verbosity 97 | * 98 | * @param string $msg Message to display 99 | * @param string $verbosity Verbosity level ("v": standard, "vv": more info, "vvv": debug) 100 | * @param int $level Level for indenting the message 101 | * @return self Same object for fluid method calls 102 | */ 103 | protected function info( string $msg, string $verbosity = 'v', int $level = 0 ) : self 104 | { 105 | $this->up->info( str_repeat( ' ', $level * 2 ) . $msg, $verbosity ); 106 | return $this; 107 | } 108 | 109 | 110 | /** 111 | * Returns the paths for the setup tasks including the given relative paths 112 | * 113 | * @param string $relpath Relative path to add to the base paths 114 | * @return array List of paths which really exist 115 | */ 116 | protected function paths( string $relpath = '' ) : array 117 | { 118 | $list = []; 119 | $relpath = DIRECTORY_SEPARATOR . trim( $relpath, DIRECTORY_SEPARATOR ); 120 | 121 | foreach( $this->up->paths() as $path ) 122 | { 123 | $abspath = $path . $relpath; 124 | 125 | if( file_exists( $abspath ) ) { 126 | $list[] = $abspath; 127 | } 128 | } 129 | 130 | return $list; 131 | } 132 | } -------------------------------------------------------------------------------- /_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Upscheme - Database migrations and schema updates made easy 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 53 | 54 |
55 | 60 |
61 | {{ content }} 62 |
63 |
64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /tests/Upscheme/Schema/SequenceTest.php: -------------------------------------------------------------------------------- 1 | dbmock = $this->getMockBuilder( '\Aimeos\Upscheme\Schema\DB' ) 21 | ->disableOriginalConstructor() 22 | ->getMock(); 23 | 24 | $this->seqmock = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Sequence' ) 25 | ->disableOriginalConstructor() 26 | ->getMock(); 27 | 28 | $this->object = new \Aimeos\Upscheme\Schema\Sequence( $this->dbmock, $this->seqmock ); 29 | } 30 | 31 | 32 | protected function tearDown() : void 33 | { 34 | unset( $this->object, $this->seqmock, $this->dbmock ); 35 | } 36 | 37 | 38 | public function testCall() 39 | { 40 | $this->seqmock->expects( $this->once() )->method( 'getAllocationSize' ); 41 | 42 | $this->object->getAllocationSize(); 43 | } 44 | 45 | 46 | public function testCallMacro() 47 | { 48 | \Aimeos\Upscheme\Schema\Sequence::macro( 'unittest', function() { return 'yes'; } ); 49 | 50 | $this->assertEquals( 'yes', $this->object->unittest() ); 51 | } 52 | 53 | 54 | public function testGetMagic() 55 | { 56 | $object = $this->getMockBuilder( '\Aimeos\Upscheme\Schema\Sequence' ) 57 | ->disableOriginalConstructor() 58 | ->onlyMethods( ['cache'] ) 59 | ->getMock(); 60 | 61 | $object->expects( $this->once() )->method( 'cache' ) 62 | ->willReturn( 1000 ); 63 | 64 | $this->assertEquals( 1000, $object->cache ); 65 | } 66 | 67 | 68 | public function testSetMagic() 69 | { 70 | $object = $this->getMockBuilder( '\Aimeos\Upscheme\Schema\Sequence' ) 71 | ->disableOriginalConstructor() 72 | ->onlyMethods( ['cache'] ) 73 | ->getMock(); 74 | 75 | $object->expects( $this->once() )->method( 'cache' ); 76 | 77 | $object->cache = 100; 78 | } 79 | 80 | 81 | public function testName() 82 | { 83 | $this->seqmock->expects( $this->once() )->method( 'getName' )->willReturn( 'seq_name' ); 84 | 85 | $this->assertEquals( 'seq_name', $this->object->name() ); 86 | } 87 | 88 | 89 | public function testCacheGet() 90 | { 91 | $this->assertEquals( false, $this->object->cache() ); 92 | } 93 | 94 | 95 | public function testCacheSet() 96 | { 97 | $this->seqmock->expects( $this->once() )->method( 'setCache' ); 98 | 99 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Sequence::class, $this->object->cache( true ) ); 100 | } 101 | 102 | 103 | public function testStartGet() 104 | { 105 | $this->seqmock->expects( $this->once() )->method( 'getInitialValue' )->willReturn( 1 ); 106 | 107 | $this->assertEquals( 1, $this->object->start() ); 108 | } 109 | 110 | 111 | public function testStartSet() 112 | { 113 | $this->seqmock->expects( $this->once() )->method( 'setInitialValue' ); 114 | 115 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Sequence::class, $this->object->start( 10 ) ); 116 | } 117 | 118 | 119 | public function testStepGet() 120 | { 121 | $this->seqmock->expects( $this->once() )->method( 'getAllocationSize' )->willReturn( 1 ); 122 | 123 | $this->assertEquals( 1, $this->object->step() ); 124 | } 125 | 126 | 127 | public function testStepSet() 128 | { 129 | $this->seqmock->expects( $this->once() )->method( 'setAllocationSize' ); 130 | 131 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Sequence::class, $this->object->step( 2 ) ); 132 | } 133 | 134 | 135 | public function testUp() 136 | { 137 | $this->dbmock->expects( $this->once() )->method( 'up' ); 138 | 139 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Sequence::class, $this->object->up() ); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /tests/Upscheme/Schema/ForeignTest.php: -------------------------------------------------------------------------------- 1 | dbmock = $this->getMockBuilder( '\Aimeos\Upscheme\Schema\DB' ) 22 | ->disableOriginalConstructor() 23 | ->getMock(); 24 | 25 | $this->tablemock = $this->getMockBuilder( '\Aimeos\Upscheme\Schema\Table' ) 26 | ->disableOriginalConstructor() 27 | ->getMock(); 28 | 29 | $this->dbalmock = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Table' ) 30 | ->disableOriginalConstructor() 31 | ->getMock(); 32 | 33 | $this->object = new \Aimeos\Upscheme\Schema\Foreign( 34 | $this->dbmock, $this->tablemock, $this->dbalmock, 35 | ['local'], 'fktable', ['foreign'], 'fk_name' 36 | ); 37 | } 38 | 39 | 40 | protected function tearDown() : void 41 | { 42 | unset( $this->object, $this->dbalmock, $this->tablemock ); 43 | } 44 | 45 | 46 | public function testCallMacro() 47 | { 48 | \Aimeos\Upscheme\Schema\Foreign::macro( 'unittest', function() { return 'yes'; } ); 49 | 50 | $this->assertEquals( 'yes', $this->object->unittest() ); 51 | } 52 | 53 | 54 | public function testCall() 55 | { 56 | $this->expectException( '\BadMethodCallException' ); 57 | $this->object->unittest2(); 58 | } 59 | 60 | 61 | public function testGetMagic() 62 | { 63 | $object = $this->getMockBuilder( '\Aimeos\Upscheme\Schema\Foreign' ) 64 | ->disableOriginalConstructor() 65 | ->onlyMethods( ['onDelete'] ) 66 | ->getMock(); 67 | 68 | $object->expects( $this->once() )->method( 'onDelete' ) 69 | ->willReturn( 'CASCADE' ); 70 | 71 | $this->assertEquals( 'CASCADE', $object->onDelete ); 72 | } 73 | 74 | 75 | public function testSetMagic() 76 | { 77 | $object = $this->getMockBuilder( '\Aimeos\Upscheme\Schema\Foreign' ) 78 | ->disableOriginalConstructor() 79 | ->onlyMethods( ['onDelete'] ) 80 | ->getMock(); 81 | 82 | $object->expects( $this->once() )->method( 'onDelete' ); 83 | 84 | $object->onDelete = 'RESTRICT'; 85 | } 86 | 87 | 88 | public function testDo() 89 | { 90 | $this->dbalmock->expects( $this->once() )->method( 'removeForeignKey' ); 91 | $this->dbalmock->expects( $this->once() )->method( 'addForeignKeyConstraint' ); 92 | 93 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Foreign::class, $this->object->do( 'SET NULL' ) ); 94 | $this->assertEquals( 'SET NULL', $this->object->onDelete ); 95 | $this->assertEquals( 'SET NULL', $this->object->onUpdate ); 96 | } 97 | 98 | 99 | public function testName() 100 | { 101 | $this->assertEquals( 'fk_name', $this->object->name() ); 102 | } 103 | 104 | 105 | public function testOnDeleteGet() 106 | { 107 | $this->assertEquals( 'CASCADE', $this->object->onDelete() ); 108 | } 109 | 110 | 111 | public function testOnDeleteSet() 112 | { 113 | $this->dbalmock->expects( $this->once() )->method( 'removeForeignKey' ); 114 | $this->dbalmock->expects( $this->once() )->method( 'addForeignKeyConstraint' ); 115 | 116 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Foreign::class, $this->object->onDelete( 'RESTRICT' ) ); 117 | } 118 | 119 | 120 | public function testOnUpdateGet() 121 | { 122 | $this->assertEquals( 'CASCADE', $this->object->onUpdate() ); 123 | } 124 | 125 | 126 | public function testOnUpdateSet() 127 | { 128 | $this->dbalmock->expects( $this->once() )->method( 'removeForeignKey' ); 129 | $this->dbalmock->expects( $this->once() )->method( 'addForeignKeyConstraint' ); 130 | 131 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Foreign::class, $this->object->onUpdate( 'RESTRICT' ) ); 132 | } 133 | 134 | 135 | public function testUp() 136 | { 137 | $this->tablemock->expects( $this->once() )->method( 'up' ); 138 | 139 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Foreign::class, $this->object->up() ); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Schema/Sequence.php: -------------------------------------------------------------------------------- 1 | db = $db; 43 | $this->sequence = $sequence; 44 | } 45 | 46 | 47 | /** 48 | * Calls custom methods or passes unknown method calls to the Doctrine table object 49 | * 50 | * @param string $method Name of the method 51 | * @param array $args Method parameters 52 | * @return mixed Return value of the called method 53 | */ 54 | public function __call( string $method, array $args ) 55 | { 56 | if( self::macro( $method ) ) { 57 | return $this->call( $method, ...$args ); 58 | } 59 | 60 | return $this->sequence->{$method}( ...$args ); 61 | } 62 | 63 | 64 | /** 65 | * Returns the value for the given sequence option 66 | * 67 | * @param string $name Sequence option name 68 | * @return mixed Sequence option value 69 | */ 70 | public function __get( string $name ) 71 | { 72 | return $this->{$name}(); 73 | } 74 | 75 | 76 | /** 77 | * Sets the new value for the given sequence option 78 | * 79 | * @param string $name Sequence option name 80 | * @param mixed $value Sequence option value 81 | */ 82 | public function __set( string $name, $value ) 83 | { 84 | $this->{$name}( $value ); 85 | } 86 | 87 | 88 | /** 89 | * Sets the cached size of the sequence or returns the current value 90 | * 91 | * @param int $value New number of sequence IDs cached by the client or NULL to return current value 92 | * @return self|int Same object for setting value, current value without parameter 93 | */ 94 | public function cache( ?int $value = null ) 95 | { 96 | if( $value === null ) { 97 | return $this->sequence->getCache(); 98 | } 99 | 100 | $this->sequence->setCache( $value ); 101 | return $this; 102 | } 103 | 104 | 105 | /** 106 | * Returns the name of the sequence 107 | * 108 | * @return string Sequence name 109 | */ 110 | public function name() 111 | { 112 | return $this->sequence->getName(); 113 | } 114 | 115 | 116 | /** 117 | * Sets the new start value of the sequence or returns the current value 118 | * 119 | * @param int $value New start value of the sequence or NULL to return current value 120 | * @return self|int Same object for setting value, current value without parameter 121 | */ 122 | public function start( ?int $value = null ) 123 | { 124 | if( $value === null ) { 125 | return $this->sequence->getInitialValue(); 126 | } 127 | 128 | $this->sequence->setInitialValue( $value ); 129 | return $this; 130 | } 131 | 132 | 133 | /** 134 | * Sets the step size of new sequence values or returns the current value 135 | * 136 | * @param int $value New step size the sequence is incremented or decremented by or NULL to return current value 137 | * @return self|int Same object for setting value, current value without parameter 138 | */ 139 | public function step( ?int $value = null ) 140 | { 141 | if( $value === null ) { 142 | return $this->sequence->getAllocationSize(); 143 | } 144 | 145 | $this->sequence->setAllocationSize( $value ); 146 | return $this; 147 | } 148 | 149 | 150 | /** 151 | * Applies the changes to the database schema 152 | * 153 | * @return self Same object for fluid method calls 154 | */ 155 | public function up() : self 156 | { 157 | $this->db->up(); 158 | return $this; 159 | } 160 | } -------------------------------------------------------------------------------- /assets/css/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "jekyll-theme-cayman"; 5 | 6 | 7 | body { 8 | background-color: #333; 9 | line-height: 1.75; 10 | } 11 | 12 | a { 13 | display: inline-block; 14 | line-height: 2.5; 15 | } 16 | 17 | header.page-header { 18 | background-color: #25a2db; 19 | background-image: linear-gradient(120deg, #16629c, #25a2db); 20 | } 21 | 22 | header h1>a { 23 | color: #fff; 24 | } 25 | 26 | header .icon.github { 27 | display: none; 28 | } 29 | 30 | header .octocat { 31 | width: 3rem; 32 | } 33 | 34 | header svg { 35 | display: none; 36 | padding: 0.5rem; 37 | color: #fff; 38 | } 39 | 40 | .container { 41 | background-color: #fff; 42 | display: flex; 43 | margin: auto; 44 | } 45 | 46 | .sidebar { 47 | position: -webkit-sticky; 48 | position: sticky; 49 | top: 0; 50 | order: 1; 51 | padding: 1rem; 52 | height: 100vh; 53 | overflow-y: auto; 54 | background-color: #f3f6fa; 55 | scrollbar-color: #606c71 #F5F5F5; 56 | scrollbar-width: 5px; 57 | } 58 | 59 | .sidebar::-webkit-scrollbar-thumb { 60 | background-color: #606c71; 61 | } 62 | 63 | .sidebar::-webkit-scrollbar-track { 64 | background-color: #F5F5F5; 65 | } 66 | 67 | .sidebar::-webkit-scrollbar { 68 | width: 5px; 69 | } 70 | 71 | .sidebar .search { 72 | width: calc(100% + 1rem); 73 | margin: 0 -0.5rem; 74 | margin-bottom: 1rem; 75 | padding: 0.25rem; 76 | color: #606c71; 77 | } 78 | 79 | .sidebar .search:focus { 80 | outline: none; 81 | } 82 | 83 | .sidebar .search::placeholder { 84 | color: #aaa; 85 | } 86 | 87 | .sidebar a { 88 | display: block; 89 | } 90 | 91 | .sidebar li.hide { 92 | display: none; 93 | } 94 | 95 | .main-content { 96 | margin: 0!important; 97 | } 98 | 99 | .main-content .badge { 100 | display: inline-block; 101 | padding: 0.45rem 0; 102 | } 103 | 104 | .main-content h1, .main-content h2, .main-content h3, 105 | .main-content h4, .main-content h5, .main-content h6 { 106 | padding-top: 3rem; 107 | margin-top: -1rem; 108 | } 109 | 110 | .main-content nav { 111 | column-count: 3; 112 | column-width: 8rem; 113 | } 114 | 115 | .main-content nav a { 116 | display: block; 117 | padding: 0 1rem; 118 | } 119 | 120 | .main-content .highlight pre, .main-content pre { 121 | line-height: 1.75; 122 | } 123 | 124 | .method-list { 125 | margin-top: 0; 126 | } 127 | 128 | @media screen and (min-width: 64em) { 129 | .page-header { 130 | padding: 2.5rem 6rem; 131 | } 132 | header .btn { 133 | padding: 0.25rem 1rem; 134 | } 135 | header .logo { 136 | max-height: 7.5rem; 137 | } 138 | .container { 139 | max-width: 80rem; 140 | margin: 0 auto; 141 | } 142 | .sidebar { 143 | width: 16rem; 144 | } 145 | .main-content { 146 | width: calc(100% - 16rem); 147 | padding: 4rem!important; 148 | } 149 | } 150 | 151 | @media screen and (max-width: 64em) { 152 | .page-header { 153 | padding: 1.5rem 4rem; 154 | } 155 | header .btn { 156 | padding: 0.125rem 0.9rem; 157 | } 158 | header .logo { 159 | max-height: 6rem; 160 | } 161 | .sidebar { 162 | width: 12rem; 163 | } 164 | .main-content { 165 | width: calc(100% - 12rem); 166 | padding: 2rem!important; 167 | } 168 | } 169 | 170 | @media screen and (max-width: 48em) { 171 | .page-header { 172 | position: fixed; 173 | padding: 0.5rem; 174 | height: 4rem; 175 | width: 100%; 176 | display: flex; 177 | justify-content: space-between; 178 | } 179 | header .logo { 180 | height: 100%; 181 | } 182 | .project-name { 183 | font-size: 1.75rem!important; 184 | } 185 | .project-tagline { 186 | display: none; 187 | } 188 | header .icon.github { 189 | display: inline-block; 190 | } 191 | header .btn.github { 192 | display: none; 193 | } 194 | header .open.show, 195 | header .close.show { 196 | display: inline-block; 197 | } 198 | .sidebar { 199 | position: fixed; 200 | right: -16rem; 201 | top: 4rem; 202 | width: 16rem; 203 | padding: 1rem 2rem; 204 | height: calc(100vh - 4rem); 205 | transition: right .5s; 206 | scrollbar-color: #f0f0f0 #606c71; 207 | background-color: #333; 208 | color: #f0f0f0; 209 | } 210 | .sidebar::-webkit-scrollbar-thumb { 211 | background-color: #f0f0f0; 212 | } 213 | .sidebar::-webkit-scrollbar-track { 214 | background-color: #606c71; 215 | } 216 | .sidebar.show { 217 | right: 0; 218 | } 219 | .sidebar a { 220 | color: #f0f0f0; 221 | } 222 | .main-content { 223 | margin-top: 4rem!important; 224 | width: 100%; 225 | } 226 | } 227 | 228 | /* Better contrast of code examples */ 229 | 230 | .highlight .bp, .highlight .c1, .highlight .cd, .highlight .cm, .highlight .cp, .highlight .cs, .highlight .gh { color: #707070 } 231 | .highlight .nb { color: #007aa0 } 232 | .highlight .nv { color: #007b7b } 233 | .highlight .no, .highlight .nv { color: #007b7b } 234 | .highlight .mi, .highlight .mf { color: #007e7e } 235 | .highlight .err { color: inherit; background-color: inherit } 236 | -------------------------------------------------------------------------------- /assets/upscheme-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 12 | 15 | 16 | 19 | 22 | 25 | 28 | 31 | 34 | 37 | 40 | 43 | 44 | 51 | 52 | -------------------------------------------------------------------------------- /tests/Upscheme/UpTest.php: -------------------------------------------------------------------------------- 1 | object = $this->getMockBuilder( '\Aimeos\Upscheme\Up' ) 19 | ->setConstructorArgs( [['driver' => 'sqlite'], 'test'] ) 20 | ->onlyMethods( [] ) 21 | ->getMock(); 22 | } 23 | 24 | 25 | protected function tearDown() : void 26 | { 27 | unset( $this->object ); 28 | } 29 | 30 | 31 | public function testConstruct() 32 | { 33 | $object = new \Aimeos\Upscheme\Up( ['driver' => ''], 'test' ); 34 | $this->assertInstanceOf( \Aimeos\Upscheme\Up::class, $object ); 35 | } 36 | 37 | 38 | public function testConstructConfig() 39 | { 40 | $this->expectException( '\RuntimeException' ); 41 | new \Aimeos\Upscheme\Up( [], 'test' ); 42 | } 43 | 44 | 45 | public function testConstructPath() 46 | { 47 | $this->expectException( '\RuntimeException' ); 48 | new \Aimeos\Upscheme\Up( ['driver' => ''], [] ); 49 | } 50 | 51 | 52 | public function testAutoload() 53 | { 54 | $object = new \Aimeos\Upscheme\Up( ['driver' => 'pdo_sqlite'], dirname( __DIR__ ) . '/Tasks/test' ); 55 | $this->assertInstanceOf( \Aimeos\Upscheme\Up::class, $object->up() ); 56 | } 57 | 58 | 59 | public function testAutoloadCustom() 60 | { 61 | \Aimeos\Upscheme\Up::macro( 'autoload', function( $class ) { return true; } ); 62 | 63 | $object = new \Aimeos\Upscheme\Up( ['driver' => 'pdo_sqlite'], dirname( __DIR__ ) . '/Tasks/test' ); 64 | $this->assertInstanceOf( \Aimeos\Upscheme\Up::class, $object->up() ); 65 | } 66 | 67 | 68 | public function testDb() 69 | { 70 | $object = new \Aimeos\Upscheme\Up( ['test' => ['driver' => 'pdo_sqlite', 'path' => 'up.test']], 'testpath' ); 71 | 72 | $db = $object->db( 'test' ); 73 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $db ); 74 | 75 | $db2 = $object->db( 'test' ); 76 | $this->assertSame( $db, $db2 ); 77 | } 78 | 79 | 80 | public function testDbNew() 81 | { 82 | $object = new \Aimeos\Upscheme\Up( ['test' => ['driver' => 'pdo_sqlite', 'path' => 'up.test']], 'testpath' ); 83 | 84 | $db = $object->db( 'test' ); 85 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $db ); 86 | 87 | $db2 = $object->db( 'test', true ); 88 | $this->assertNotSame( $db, $db2 ); 89 | } 90 | 91 | 92 | public function testDbFallback() 93 | { 94 | $object = new \Aimeos\Upscheme\Up( ['test' => ['driver' => 'pdo_sqlite', 'path' => 'up.test']], 'testpath' ); 95 | 96 | $db = $object->db( 'test2' ); 97 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $db ); 98 | 99 | $db2 = $object->db(); 100 | $this->assertSame( $db, $db2 ); 101 | } 102 | 103 | 104 | public function testDbSingle() 105 | { 106 | $object = new \Aimeos\Upscheme\Up( ['driver' => 'pdo_sqlite', 'path' => 'up.test'], 'testpath' ); 107 | 108 | $db = $object->db( 'test2' ); 109 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $db ); 110 | 111 | $db2 = $object->db(); 112 | $this->assertSame( $db, $db2 ); 113 | } 114 | 115 | 116 | public function testDbCustom() 117 | { 118 | \Aimeos\Upscheme\Up::macro( 'connect', function( array $cfg ) { 119 | return \Doctrine\DBAL\DriverManager::getConnection( ['driver' => 'pdo_sqlite', 'path' => 'up.test'] ); 120 | } ); 121 | 122 | $result = ( new \Aimeos\Upscheme\Up( ['driver' => 'pdo_mysql'], 'testpath' ) )->db(); 123 | \Aimeos\Upscheme\Up::unmacro( 'connect' ); 124 | 125 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $result ); 126 | } 127 | 128 | 129 | public function testInfo() 130 | { 131 | $this->expectOutputString( 'test' . PHP_EOL ); 132 | $this->object->info( 'test', '' ); 133 | } 134 | 135 | 136 | public function testInfoVerbose() 137 | { 138 | $this->expectOutputString( '' ); 139 | $this->object->info( 'test', 'v' ); 140 | } 141 | 142 | 143 | public function testInfoCustom() 144 | { 145 | \Aimeos\Upscheme\Up::macro( 'info', function( $msg ) { echo 'custom'; } ); 146 | 147 | $this->expectOutputString( 'custom' ); 148 | $this->object->info( 'test', 'v' ); 149 | 150 | \Aimeos\Upscheme\Up::unmacro( 'info' ); 151 | } 152 | 153 | 154 | public function testPaths() 155 | { 156 | $this->assertEquals( ['test'], $this->object->paths() ); 157 | } 158 | 159 | 160 | public function testUse() 161 | { 162 | $this->assertInstanceOf( \Aimeos\Upscheme\Up::class, \Aimeos\Upscheme\Up::use( ['driver' => ''], 'test' ) ); 163 | } 164 | 165 | 166 | public function testVerbose() 167 | { 168 | $this->assertInstanceOf( \Aimeos\Upscheme\Up::class, $this->object->verbose() ); 169 | } 170 | 171 | 172 | public function testVerboseMore() 173 | { 174 | $this->assertInstanceOf( \Aimeos\Upscheme\Up::class, $this->object->verbose( 'vv' ) ); 175 | } 176 | 177 | 178 | public function testVerboseCustom() 179 | { 180 | \Aimeos\Upscheme\Up::macro( 'verbose', function( $level ) { return 3; } ); 181 | $result = $this->object->verbose( 'v' ); 182 | \Aimeos\Upscheme\Up::unmacro( 'info' ); 183 | 184 | $this->assertInstanceOf( \Aimeos\Upscheme\Up::class, $result ); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /src/Schema/Foreign.php: -------------------------------------------------------------------------------- 1 | 42 | */ 43 | private $localcol; 44 | 45 | /** 46 | * @var string 47 | */ 48 | private $fktable; 49 | 50 | /** 51 | * @var array 52 | */ 53 | private $fkcol; 54 | 55 | /** 56 | * @var string|null 57 | */ 58 | private $name; 59 | 60 | /** 61 | * @var array 62 | */ 63 | private $opts; 64 | 65 | 66 | /** 67 | * Initializes the foreign key object 68 | * 69 | * @param \Aimeos\Upscheme\Schema\DB $db DB schema object 70 | * @param \Aimeos\Upscheme\Schema\Table $table Table schema object 71 | * @param \Doctrine\DBAL\Schema\Table $dbaltable Doctrine table object 72 | * @param array $localcol List of columns from the current table spawning the foreign key constraint 73 | * @param string $fktable Name of the referenced table 74 | * @param array $fkcol List of columns from the referenced table spawning the foreign key constraint 75 | * @param string|null $name $name Name of the foreign key constraint and index or NULL for autogenerated name 76 | */ 77 | public function __construct( DB $db, Table $table, DbalTable $dbaltable, array $localcol, string $fktable, array $fkcol, ?string $name = null ) 78 | { 79 | $this->db = $db; 80 | $this->table = $table; 81 | $this->dbaltable = $dbaltable; 82 | $this->localcol = $localcol; 83 | $this->fktable = $fktable; 84 | $this->fkcol = $fkcol; 85 | $this->name = $name; 86 | $this->opts = [ 87 | 'onDelete' => 'CASCADE', 88 | 'onUpdate' => 'CASCADE', 89 | ]; 90 | 91 | if( !$table->hasIndex( (string) $name ) ) { 92 | $table->index( $localcol, $name ); 93 | } 94 | 95 | if( !$table->hasForeign( (string) $name ) ) 96 | { 97 | $lcol = $fcol = []; 98 | 99 | foreach( (array) $localcol as $key => $column ) { 100 | $lcol[$key] = $db->qi( $column ); 101 | } 102 | 103 | foreach( (array) $fkcol as $key => $column ) { 104 | $fcol[$key] = $db->qi( $column ); 105 | } 106 | 107 | $dbaltable->addForeignKeyConstraint( $db->qi( $fktable ), $lcol, $fcol, $this->opts, $name ? $db->qi( $name ) : null ); 108 | } 109 | } 110 | 111 | 112 | /** 113 | * Calls custom methods or passes unknown method calls to the Doctrine table object 114 | * 115 | * @param string $method Name of the method 116 | * @param array $args Method parameters 117 | * @return mixed Return value of the called method 118 | */ 119 | public function __call( string $method, array $args ) 120 | { 121 | if( self::macro( $method ) ) { 122 | return $this->call( $method, ...$args ); 123 | } 124 | 125 | throw new \BadMethodCallException( sprintf( 'Unknown method "%1$s" in %2$s', $method, __CLASS__ ) ); 126 | } 127 | 128 | 129 | /** 130 | * Returns the value for the given sequence option 131 | * 132 | * @param string $name Sequence option name 133 | * @return mixed Sequence option value 134 | */ 135 | public function __get( string $name ) 136 | { 137 | return $this->{$name}(); 138 | } 139 | 140 | 141 | /** 142 | * Sets the new value for the given sequence option 143 | * 144 | * @param string $name Sequence option name 145 | * @param mixed $value Sequence option value 146 | */ 147 | public function __set( string $name, $value ) 148 | { 149 | $this->{$name}( $value ); 150 | } 151 | 152 | 153 | /** 154 | * Sets the action if referenced rows are deleted or updated 155 | * 156 | * Available actions are: 157 | * - CASCADE : Delete or update referenced value 158 | * - NO ACTION : No change in referenced value 159 | * - RESTRICT : Forbid changing values 160 | * - SET DEFAULT : Set referenced value to the default value 161 | * - SET NULL : Set referenced value to NULL 162 | * 163 | * @param string $action Performed action 164 | * @return self Same object for fluid method calls 165 | */ 166 | public function do( string $action ) : self 167 | { 168 | if( $this->opts['onDelete'] !== $action || $this->opts['onUpdate'] !== $action ) 169 | { 170 | $this->opts['onDelete'] = $action; 171 | $this->opts['onUpdate'] = $action; 172 | 173 | return $this->replace(); 174 | } 175 | 176 | return $this; 177 | } 178 | 179 | 180 | /** 181 | * Returns the current name of the foreign key constraint 182 | * 183 | * @return string|null Name of the constraint or NULL if no name is available 184 | */ 185 | public function name() 186 | { 187 | return $this->name; 188 | } 189 | 190 | 191 | /** 192 | * Sets the action if the referenced row is deleted or returns the current value 193 | * 194 | * Available actions are: 195 | * - CASCADE : Delete referenced value 196 | * - NO ACTION : No change in referenced value 197 | * - RESTRICT : Forbid changing values 198 | * - SET DEFAULT : Set referenced value to the default value 199 | * - SET NULL : Set referenced value to NULL 200 | * 201 | * @param string|null $value Performed action or NULL to return current value 202 | * @return self|string Same object for setting the value, current value without parameter 203 | */ 204 | public function onDelete( ?string $value = null ) 205 | { 206 | if( $value === null ) { 207 | return $this->opts['onDelete']; 208 | } 209 | 210 | if( $this->opts['onDelete'] !== $value ) 211 | { 212 | $this->opts['onDelete'] = $value; 213 | return $this->replace(); 214 | } 215 | 216 | return $this; 217 | } 218 | 219 | 220 | /** 221 | * Sets the action if the referenced row is updated or returns the current value 222 | * 223 | * Available actions are: 224 | * - CASCADE : Update referenced value 225 | * - NO ACTION : No change in referenced value 226 | * - RESTRICT : Forbid changing values 227 | * - SET DEFAULT : Set referenced value to the default value 228 | * - SET NULL : Set referenced value to NULL 229 | * 230 | * @param string|null $value Performed action or NULL to return current value 231 | * @return self|string Same object for setting the value, current value without parameter 232 | */ 233 | public function onUpdate( ?string $value = null ) 234 | { 235 | if( $value === null ) { 236 | return $this->opts['onUpdate']; 237 | } 238 | 239 | if( $this->opts['onUpdate'] !== $value ) 240 | { 241 | $this->opts['onUpdate'] = $value; 242 | return $this->replace(); 243 | } 244 | 245 | return $this; 246 | } 247 | 248 | 249 | /** 250 | * Applies the changes to the database schema 251 | * 252 | * @return self Same object for fluid method calls 253 | */ 254 | public function up() : self 255 | { 256 | $this->table->up(); 257 | return $this; 258 | } 259 | 260 | 261 | /** 262 | * Deletes the current constraint and creates a new one 263 | * 264 | * @param string|null $newname Name of the new constraint or same name if NULL 265 | * @return self Same object for fluid method calls 266 | */ 267 | protected function replace( ?string $newname = null ) : self 268 | { 269 | $newname = $newname ?: $this->name; 270 | 271 | if( $this->name ) 272 | { 273 | $this->dbaltable->removeForeignKey( $this->db->qi( $this->name ) ); 274 | $this->table->dropIndex( $this->name ); 275 | } 276 | 277 | $lcol = $fcol = []; 278 | 279 | foreach( $this->localcol as $key => $column ) { 280 | $lcol[$key] = $this->db->qi( $column ); 281 | } 282 | 283 | foreach( $this->fkcol as $key => $column ) { 284 | $fcol[$key] = $this->db->qi( $column ); 285 | } 286 | 287 | $this->table->index( $this->localcol, $newname )->up(); 288 | $this->dbaltable->addForeignKeyConstraint( 289 | $this->db->qi( $this->fktable ), 290 | $lcol, $fcol, $this->opts, 291 | $newname ? $this->db->qi( $newname ) : null 292 | ); 293 | 294 | $this->name = $newname; 295 | return $this; 296 | } 297 | } -------------------------------------------------------------------------------- /src/Generate.php: -------------------------------------------------------------------------------- 1 | seqtpl = $this->read( $dir . $ds . 'stubs' . $ds . 'sequence.stub' ); 34 | $this->tabletpl = $this->read( $dir . $ds . 'stubs' . $ds . 'table.stub' ); 35 | $this->viewtpl = $this->read( $dir . $ds . 'stubs' . $ds . 'view.stub' ); 36 | 37 | $this->path = $path; 38 | } 39 | 40 | 41 | /** 42 | * Generates the migration files 43 | * 44 | * @param array $schema Associative list of schema definitions 45 | * @param string $dbname Name of the database 46 | * @throws \RuntimeException If a file can't be created 47 | */ 48 | public function __invoke( array $schema, string $dbname = '' ) : void 49 | { 50 | $ds = DIRECTORY_SEPARATOR; 51 | $prefix = $dbname ? preg_replace( '/[^A-Za-z0-9]/', '', $dbname ) . '_' : ''; 52 | 53 | $seqtpl = str_replace( '{{DB}}', $dbname, $this->seqtpl ); 54 | $tabletpl = str_replace( '{{DB}}', $dbname, $this->tabletpl ); 55 | $viewtpl = str_replace( '{{DB}}', $dbname, $this->viewtpl ); 56 | 57 | foreach( $schema['sequence'] ?? [] as $name => $def ) { 58 | $this->write( $this->path . $ds . $prefix . 'seq_' . $name . '.php', $this->sequence( $def, $seqtpl ) ); 59 | } 60 | 61 | foreach( $schema['table'] ?? [] as $name => $def ) { 62 | $this->write( $this->path . $ds . $prefix . 'table_' . $name . '.php', $this->table( $def, $tabletpl ) ); 63 | } 64 | 65 | foreach( $schema['view'] ?? [] as $name => $def ) { 66 | $this->write( $this->path . $ds . $prefix . 'view_' . $name . '.php', $this->view( $def, $viewtpl ) ); 67 | } 68 | } 69 | 70 | 71 | /** 72 | * Returns the PHP code for an array 73 | * 74 | * @param array $a Associative list of array values 75 | * @return string PHP code for the array 76 | */ 77 | protected function array( array $a ) : string 78 | { 79 | return str_replace( ['array (', ')', "\n", ' ', ',]'], ['[', ']', '', '', ']'], var_export( $a, true ) ); 80 | } 81 | 82 | 83 | /** 84 | * Returns the PHP code for a column definition 85 | * 86 | * @param array $def Associative list of column definitions 87 | * @return string PHP code for the column definitions 88 | */ 89 | protected function col( array $def ) : string 90 | { 91 | $lines = []; 92 | $types = [ 93 | 'bigint', 'binary', 'blob', 'boolean', 94 | 'date', 'datetime', 'datetimetz', 95 | 'float', 'guid', 'integer', 'json', 96 | 'smallint', 'string', 'text', 'time' 97 | ]; 98 | 99 | foreach( $def as $name => $e ) 100 | { 101 | if( in_array( $e['type'], $types ) ) { 102 | $string = '$t->' . $e['type'] . '(\'' . $name . '\')'; 103 | } else { 104 | $string = '$t->col(\'' . $name . '\', \'' . $e['type'] . '\')'; 105 | } 106 | 107 | foreach( ['seq', 'fixed', 'unsigned', 'null'] as $key ) 108 | { 109 | if( $e[$key] ?? false ) { 110 | $string .= '->' . $key . '(true)'; 111 | } 112 | } 113 | 114 | foreach( ['length', 'precision', 'scale'] as $key ) 115 | { 116 | if( $e[$key] ?? false ) { 117 | $string .= '->' . $key . '(' . $e[$key] . ')'; 118 | } 119 | } 120 | 121 | foreach( ['default', 'comment'] as $key ) 122 | { 123 | if( $e[$key] ?? false ) { 124 | $string .= '->' . $key . '(\'' . $e[$key] . '\')'; 125 | } 126 | } 127 | 128 | foreach( $e['opt'] ?? [] as $key => $value ) { 129 | $string .= '->opt(\'' . $key . '\', \'' . $value . '\')'; 130 | } 131 | 132 | $lines[] = $string . ';'; 133 | } 134 | 135 | return join( "\n\t\t\t", $lines ); 136 | } 137 | 138 | 139 | /** 140 | * Returns the PHP code for a foreign key definition 141 | * 142 | * @param array $def Associative list of foreign key definitions 143 | * @return string PHP code for the foreign key definitions 144 | */ 145 | protected function foreign( array $def ) : string 146 | { 147 | $lines = []; 148 | 149 | foreach( $def as $name => $e ) 150 | { 151 | $string = '$t->foreign(' . json_encode( $e['localcol'] ) . ', \'' . ( $e['fktable'] ?? '' ) . '\', ' . json_encode( $e['fkcol'] ) . ', ' . ( $e['name'] ? '\'' . $e['name'] . '\'' : 'null' ) . ')'; 152 | 153 | foreach( ['onDelete', 'onUpdate'] as $key ) 154 | { 155 | if( $e[$key] ?? false ) { 156 | $string .= '->' . $key . '(\'' . $e[$key] . '\')'; 157 | } 158 | } 159 | 160 | $lines[] = $string . ';'; 161 | } 162 | 163 | return join( "\n\t\t\t", $lines ); 164 | } 165 | 166 | 167 | /** 168 | * Returns the PHP code for an index definition 169 | * 170 | * @param array $def Associative list of index definitions 171 | * @return string PHP code for the index definitions 172 | */ 173 | protected function index( array $def ) : string 174 | { 175 | $lines = []; 176 | 177 | foreach( $def as $name => $e ) 178 | { 179 | if( $e['primary'] ?? false ) { 180 | $lines[] = '$t->primary(' . json_encode( $e['columns'] ) . ', ' . ( $e['name'] ? '\'' . $e['name'] . '\'' : 'null' ) . ');'; 181 | } elseif( $e['unique'] ?? false ) { 182 | $lines[] = '$t->unique(' . json_encode( $e['columns'] ) . ', ' . ( $e['name'] ? '\'' . $e['name'] . '\'' : 'null' ) . ');'; 183 | } else { 184 | $lines[] = '$t->index(' . json_encode( $e['columns'] ?? [] ) . ', ' . ( $e['name'] ? '\'' . $e['name'] . '\'' : 'null' ) . ', ' . $this->array( $e['flags'] ?? [] ) . ', ' . $this->array( $e['options'] ?? [] ) . ');'; 185 | } 186 | } 187 | 188 | return join( "\n\t\t\t", $lines ); 189 | } 190 | 191 | 192 | /** 193 | * Reads the content of a file 194 | * 195 | * @param string $filename Name of the file 196 | * @return string Content of the file 197 | * @throws \RuntimeException If the file can't be read 198 | */ 199 | protected function read( string $filename ) : string 200 | { 201 | if( ( $content = file_get_contents( $filename ) ) === false ) { 202 | throw new \RuntimeException( 'Unable to read from file "' . $filename . '"' ); 203 | } 204 | 205 | return $content; 206 | } 207 | 208 | 209 | /** 210 | * Returns the PHP code for a sequence definition 211 | * 212 | * @param array $def Associative list of sequence definitions 213 | * @param string $template Template for the sequence definition 214 | * @return string PHP code for the sequence definitions 215 | */ 216 | protected function sequence( array $def, string $template ) : string 217 | { 218 | $string = ''; 219 | 220 | foreach( ['cache', 'start', 'step'] as $key ) 221 | { 222 | if( $def[$key] ?? false ) { 223 | $string .= '->' . $key . '(' . $def[$key] . ')'; 224 | } 225 | } 226 | 227 | if( $string ) { 228 | $string = '$s' . $string . ';'; 229 | } 230 | 231 | return str_replace( ['{{NAME}}', '{{SEQUENCE}}'], [$def['name'] ?? '', $string], $template ); 232 | } 233 | 234 | 235 | /** 236 | * Returns the PHP code for a table definition 237 | * 238 | * @param array $def Associative list of table definitions 239 | * @param string $template Template for the table definition 240 | * @return string PHP code for the table definitions 241 | */ 242 | protected function table( array $def, string $template ) : string 243 | { 244 | $fktables = []; 245 | 246 | foreach( $def['foreign'] ?? [] as $e ) { 247 | $fktables[] = 'table_' . $e['fktable'] ?? ''; 248 | } 249 | 250 | return str_replace( [ 251 | '{{NAME}}', 252 | '{{COLUMN}}', 253 | '{{INDEX}}', 254 | '{{FOREIGN}}', 255 | '{{AFTER}}' 256 | ], [ 257 | $def['name'] ?? '', 258 | $this->col( $def['col'] ?? [] ), 259 | $this->index( $def['index'] ?? [] ), 260 | $this->foreign( $def['foreign'] ?? [] ), 261 | $fktables ? "'" . join( "', '", $fktables ) . "'" : '' 262 | ], $template ); 263 | } 264 | 265 | 266 | /** 267 | * Returns the PHP code for a view definition 268 | * 269 | * @param array $def Associative list of view definitions 270 | * @param string $template Template for the view definition 271 | * @return string PHP code for the view definitions 272 | */ 273 | protected function view( array $def, string $template ) : string 274 | { 275 | return str_replace( ['{{NAME}}', '{{SQL}}'], [$def['name'] ?? '', $def['sql'] ?? ''], $template ); 276 | } 277 | 278 | 279 | /** 280 | * Writes the content to the file 281 | * 282 | * @param string $filename Name of the file 283 | * @param string $content Content to write 284 | * @throws \RuntimeException If the file can't be written 285 | */ 286 | protected function write( string $filename, string $content ) : void 287 | { 288 | if( file_put_contents( $filename, $content ) === false ) { 289 | throw new \RuntimeException( 'Unable to write to file "' . $filename . '"' ); 290 | } 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # PHP CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-php/ for more details 4 | # 5 | version: 2 6 | 7 | jobs: 8 | "php-7.4 / mysql-5.7": 9 | docker: 10 | - image: aimeos/ci-php:7.4 11 | - image: cimg/mysql:5.7 12 | environment: 13 | MYSQL_ROOT_PASSWORD: rootpw 14 | MYSQL_DATABASE: upscheme 15 | MYSQL_USER: upscheme 16 | MYSQL_PASSWORD: upscheme 17 | steps: 18 | - checkout 19 | - restore_cache: 20 | keys: 21 | - php74-{{ checksum "composer.json" }} 22 | - run: composer update -n --prefer-dist 23 | - save_cache: 24 | key: php74-{{ checksum "composer.json" }} 25 | paths: [./vendor] 26 | - run: for i in `seq 1 10`; do nc -z 127.0.0.1 3306 && echo OK && exit 0; echo -n .; sleep 1; done 27 | - run: echo " 'pdo_mysql', 'host' => '127.0.0.1', 'dbname' => 'upscheme', 'user' => 'upscheme', 'password' => 'upscheme'];" > tests/config.php 28 | - run: ./vendor/bin/phpunit 29 | 30 | "php-7.4 / mariadb-10": 31 | docker: 32 | - image: aimeos/ci-php:7.4 33 | - image: cimg/mariadb:10.4 34 | environment: 35 | MYSQL_ROOT_PASSWORD: rootpw 36 | MYSQL_DATABASE: upscheme 37 | MYSQL_USER: upscheme 38 | MYSQL_PASSWORD: upscheme 39 | steps: 40 | - checkout 41 | - restore_cache: 42 | keys: 43 | - php74-{{ checksum "composer.json" }} 44 | - run: composer update -n --prefer-dist 45 | - save_cache: 46 | key: php74-{{ checksum "composer.json" }} 47 | paths: [./vendor] 48 | - run: for i in `seq 1 10`; do nc -z 127.0.0.1 3306 && echo OK && exit 0; echo -n .; sleep 1; done 49 | - run: echo " 'pdo_mysql', 'host' => '127.0.0.1', 'dbname' => 'upscheme', 'user' => 'upscheme', 'password' => 'upscheme'];" > tests/config.php 50 | - run: ./vendor/bin/phpunit 51 | 52 | "php-7.4 / pgsql-13": 53 | docker: 54 | - image: aimeos/ci-php:7.4 55 | - image: cimg/postgres:13.16 56 | environment: 57 | POSTGRES_PASSWORD: upscheme 58 | POSTGRES_USER: upscheme 59 | POSTGRES_DB: upscheme 60 | steps: 61 | - checkout 62 | - restore_cache: 63 | keys: 64 | - php74-{{ checksum "composer.json" }} 65 | - run: composer update -n --prefer-dist 66 | - save_cache: 67 | key: php74-{{ checksum "composer.json" }} 68 | paths: [./vendor] 69 | - run: for i in `seq 1 10`; do nc -z 127.0.0.1 5432 && echo OK && exit 0; echo -n .; sleep 1; done 70 | - run: echo " 'pdo_pgsql', 'host' => '127.0.0.1', 'dbname' => 'upscheme', 'user' => 'upscheme', 'password' => 'upscheme'];" > tests/config.php 71 | - run: ./vendor/bin/phpunit 72 | 73 | "php-7.4 / mysql-8": 74 | docker: 75 | - image: aimeos/ci-php:7.4 76 | - image: cimg/mysql:8.0 77 | environment: 78 | MYSQL_ROOT_PASSWORD: rootpw 79 | MYSQL_DATABASE: upscheme 80 | MYSQL_USER: upscheme 81 | MYSQL_PASSWORD: upscheme 82 | steps: 83 | - checkout 84 | - restore_cache: 85 | keys: 86 | - php74-{{ checksum "composer.json" }} 87 | - run: composer update -n --prefer-dist 88 | - save_cache: 89 | key: php74-{{ checksum "composer.json" }} 90 | paths: [./vendor] 91 | - run: for i in `seq 1 10`; do nc -z 127.0.0.1 3306 && echo OK && exit 0; echo -n .; sleep 1; done 92 | - run: echo " 'pdo_mysql', 'host' => '127.0.0.1', 'dbname' => 'upscheme', 'user' => 'upscheme', 'password' => 'upscheme'];" > tests/config.php 93 | - run: ./vendor/bin/phpunit 94 | 95 | "php-8.1 / oracle-18": 96 | docker: 97 | - image: aimeos/ci-php:8.1 98 | - image: gvenzl/oracle-xe:18-slim 99 | environment: 100 | ORACLE_PASSWORD: oracle 101 | steps: 102 | - checkout 103 | - restore_cache: 104 | keys: 105 | - php81-{{ checksum "composer.json" }} 106 | - run: composer update -n --prefer-dist 107 | - save_cache: 108 | key: php81-{{ checksum "composer.json" }} 109 | paths: [./vendor] 110 | - run: echo " 'pdo_oci', 'host' => '127.0.0.1', 'dbname' => 'XE', 'user' => 'upscheme', 'password' => 'upscheme'];" > tests/config.php 111 | - run: echo "CREATE USER upscheme IDENTIFIED BY upscheme;" > init.sql && echo "GRANT DBA TO upscheme;" >> init.sql && echo "quit" >> init.sql 112 | - run: for i in `seq 1 5`; do sqlplus -L system/oracle@127.0.0.1 @init.sql && exit 0; echo -n .; sleep 5; done 113 | - run: ./vendor/bin/phpunit || true 114 | 115 | "php-8.1 / mssql-2019": 116 | docker: 117 | - image: aimeos/ci-php:8.1 118 | - image: mcr.microsoft.com/mssql/server:2019-latest 119 | environment: 120 | SA_PASSWORD: S3c0r3P4ss 121 | ACCEPT_EULA: Y 122 | steps: 123 | - checkout 124 | - restore_cache: 125 | keys: 126 | - php81-{{ checksum "composer.json" }} 127 | - run: composer update -n --prefer-dist 128 | - save_cache: 129 | key: php81-{{ checksum "composer.json" }} 130 | paths: [./vendor] 131 | - run: echo "CREATE DATABASE upscheme;" > sqlserver.sql; for i in `seq 1 5`; do /opt/mssql-tools/bin/sqlcmd -S 127.0.0.1 -U SA -P 'S3c0r3P4ss' -i sqlserver.sql && exit 0; sleep 3; done 132 | - run: echo " 'pdo_sqlsrv', 'host' => '127.0.0.1', 'dbname' => 'upscheme', 'user' => 'SA', 'password' => 'S3c0r3P4ss'];" > tests/config.php 133 | - run: ./vendor/bin/phpunit 134 | 135 | "php-8.2 / sqlite": 136 | docker: 137 | - image: aimeos/ci-php:8.2 138 | steps: 139 | - checkout 140 | - restore_cache: 141 | keys: 142 | - php82-{{ checksum "composer.json" }} 143 | - run: composer update -n --prefer-dist 144 | - save_cache: 145 | key: php82-{{ checksum "composer.json" }} 146 | paths: [./vendor] 147 | - run: echo " 'pdo_sqlite', 'path' => 'sqlite.test'];" > tests/config.php 148 | - run: ./vendor/bin/phpunit --coverage-clover coverage.xml --coverage-filter ./tests/ 149 | - run: ./vendor/bin/php-coveralls -vvv -o coverage.json -x coverage.xml 150 | 151 | "php-8.3 / mariadb-11": 152 | docker: 153 | - image: aimeos/ci-php:8.3 154 | - image: cimg/mariadb:11.0 155 | environment: 156 | MYSQL_ROOT_PASSWORD: rootpw 157 | MYSQL_DATABASE: upscheme 158 | MYSQL_USER: upscheme 159 | MYSQL_PASSWORD: upscheme 160 | steps: 161 | - checkout 162 | - restore_cache: 163 | keys: 164 | - php83-{{ checksum "composer.json" }} 165 | - run: composer update -n --prefer-dist 166 | - save_cache: 167 | key: php83-{{ checksum "composer.json" }} 168 | paths: [./vendor] 169 | - run: for i in `seq 1 10`; do nc -z 127.0.0.1 3306 && echo OK && exit 0; echo -n .; sleep 1; done 170 | - run: echo " 'pdo_mysql', 'host' => '127.0.0.1', 'dbname' => 'upscheme', 'user' => 'upscheme', 'password' => 'upscheme'];" > tests/config.php 171 | - run: ./vendor/bin/phpunit 172 | 173 | "php-8.4 / pgsql-18": 174 | docker: 175 | - image: aimeos/ci-php:8.4 176 | - image: cimg/postgres:18.0 177 | environment: 178 | POSTGRES_PASSWORD: upscheme 179 | POSTGRES_USER: upscheme 180 | POSTGRES_DB: upscheme 181 | steps: 182 | - checkout 183 | - restore_cache: 184 | keys: 185 | - php84-{{ checksum "composer.json" }} 186 | - run: composer update -n --prefer-dist 187 | - save_cache: 188 | key: php84-{{ checksum "composer.json" }} 189 | paths: [./vendor] 190 | - run: for i in `seq 1 10`; do nc -z 127.0.0.1 5432 && echo OK && exit 0; echo -n .; sleep 1; done 191 | - run: echo " 'pdo_pgsql', 'host' => '127.0.0.1', 'dbname' => 'upscheme', 'user' => 'upscheme', 'password' => 'upscheme'];" > tests/config.php 192 | - run: ./vendor/bin/phpunit 193 | 194 | "php-8.5 / mysql-9": 195 | docker: 196 | - image: aimeos/ci-php:8.5 197 | - image: cimg/mysql:9.5 198 | environment: 199 | MYSQL_ROOT_PASSWORD: rootpw 200 | MYSQL_DATABASE: upscheme 201 | MYSQL_USER: upscheme 202 | MYSQL_PASSWORD: upscheme 203 | steps: 204 | - checkout 205 | - restore_cache: 206 | keys: 207 | - php85-{{ checksum "composer.json" }} 208 | - run: composer update -n --prefer-dist 209 | - save_cache: 210 | key: php85-{{ checksum "composer.json" }} 211 | paths: [./vendor] 212 | - run: for i in `seq 1 10`; do nc -z 127.0.0.1 3306 && echo OK && exit 0; echo -n .; sleep 1; done 213 | - run: echo " 'pdo_mysql', 'host' => '127.0.0.1', 'dbname' => 'upscheme', 'user' => 'upscheme', 'password' => 'upscheme'];" > tests/config.php 214 | - run: ./vendor/bin/phpunit 215 | 216 | workflows: 217 | version: 2 218 | unittest: 219 | jobs: 220 | - "php-7.4 / mysql-5.7" 221 | - "php-7.4 / mariadb-10" 222 | - "php-7.4 / pgsql-13" 223 | - "php-7.4 / mysql-8" 224 | - "php-8.1 / oracle-18" 225 | - "php-8.1 / mssql-2019" 226 | - "php-8.2 / sqlite" 227 | - "php-8.3 / mariadb-11" 228 | - "php-8.4 / pgsql-18" 229 | - "php-8.5 / mysql-9" 230 | -------------------------------------------------------------------------------- /src/Up.php: -------------------------------------------------------------------------------- 1 | > 22 | */ 23 | private $config; 24 | 25 | /** 26 | * @var array 27 | */ 28 | private $tasks; 29 | 30 | /** 31 | * @var array 32 | */ 33 | private $tasksDone; 34 | 35 | /** 36 | * @var array> 37 | */ 38 | private $dependencies; 39 | 40 | /** 41 | * @var int 42 | */ 43 | private $verbose = 0; 44 | 45 | /** 46 | * @var array 47 | */ 48 | private $paths = []; 49 | 50 | /** 51 | * @var array 52 | */ 53 | private $db = []; 54 | 55 | 56 | /** 57 | * Initializes the new object 58 | * 59 | * @param array> $config One or more database configuration parameters 60 | * @param array|string $paths One or more paths to the tasks which updates the database 61 | */ 62 | public function __construct( array $config, $paths ) 63 | { 64 | if( empty( $config ) ) { 65 | throw new \RuntimeException( 'No database configuration passed' ); 66 | } 67 | 68 | if( empty( $paths ) ) { 69 | throw new \RuntimeException( 'No path for the tasks passed' ); 70 | } 71 | 72 | if( spl_autoload_register( static::macro( 'autoload' ) ?: [$this, 'autoload'] ) === false ) { 73 | throw new \RuntimeException( 'Unable to register autoloader' ); 74 | } 75 | 76 | $this->config = $config; 77 | $this->paths = (array) $paths; 78 | 79 | $this->call( 'macros' ); 80 | } 81 | 82 | 83 | /** 84 | * Creates a new Upscheme object initialized with the given configuration and paths 85 | * 86 | * @param array> $config One or more database configuration parameters 87 | * @param array|string $paths One or more paths to the tasks which updates the database 88 | * @return \Aimeos\Upscheme\Up Upscheme object 89 | */ 90 | public static function use( array $config, $paths ) : self 91 | { 92 | return new self( $config, $paths ); 93 | } 94 | 95 | 96 | /** 97 | * Creates the database migration files for the given connection name 98 | * 99 | * @param string|array $name Name of the connection from the configuration or empty string for first one 100 | * @return self Same object for fluid method calls 101 | */ 102 | public function create( $name = '' ) : self 103 | { 104 | $path = current( $this->paths ); 105 | $generator = ( $fcn = static::macro( 'generate' ) ) ? $fcn( $path ) : new Generate( $path ); 106 | 107 | foreach( (array) $name as $dbname ) { 108 | $generator( $this->db( $dbname )->toArray(), $dbname ); 109 | } 110 | 111 | return $this; 112 | } 113 | 114 | 115 | /** 116 | * Returns the DB schema for the passed connection name 117 | * 118 | * @param string $name Name of the connection from the configuration or empty string for first one 119 | * @param bool $new If a new connection should be created instead of reusing an existing one 120 | * @return \Aimeos\Upscheme\Schema\DB DB schema object 121 | */ 122 | public function db( string $name = '', bool $new = false ) : \Aimeos\Upscheme\Schema\DB 123 | { 124 | if( !isset( $this->config[$name] ) ) { 125 | $cfg = is_array( $first = reset( $this->config ) ) ? $first : $this->config; $name = ''; 126 | } else { 127 | $cfg = $this->config[$name]; 128 | } 129 | 130 | if( !isset( $this->db[$name] ) ) { 131 | $this->db[$name] = new \Aimeos\Upscheme\Schema\DB( $this, $this->connect( $cfg ) ); 132 | } 133 | 134 | return $new ? clone $this->db[$name] : $this->db[$name]; 135 | } 136 | 137 | 138 | /** 139 | * Outputs the message depending on the passed verbosity level 140 | * 141 | * @param string $msg Message to display 142 | * @param mixed $level Verbosity level (empty: always, v: notice: vv: info, vvv: debug) 143 | * @return self Same object for fluid method calls 144 | */ 145 | public function info( string $msg, $level = 'v' ) : self 146 | { 147 | if( $fcn = static::macro( 'info' ) ) { 148 | $fcn( $msg, $level ); 149 | } elseif( strlen( (string) $level ) <= $this->verbose ) { 150 | echo $msg . PHP_EOL; 151 | } 152 | 153 | return $this; 154 | } 155 | 156 | 157 | /** 158 | * Returns the paths for the setup tasks 159 | * 160 | * @return array List of paths 161 | */ 162 | public function paths() : array 163 | { 164 | return $this->paths; 165 | } 166 | 167 | 168 | /** 169 | * Executes the tasks to update the database 170 | * 171 | * @return self Same object for fluid method calls 172 | */ 173 | public function up() : self 174 | { 175 | $this->tasksDone = []; 176 | $this->dependencies = []; 177 | $this->tasks = $this->createTasks( $this->paths() ); 178 | 179 | foreach( $this->tasks as $name => $task ) 180 | { 181 | foreach( (array) $task->before() as $taskname ) { 182 | $this->dependencies[$taskname][] = $name; 183 | } 184 | 185 | foreach( (array) $task->after() as $taskname ) { 186 | $this->dependencies[$name][] = $taskname; 187 | } 188 | } 189 | 190 | foreach( $this->tasks as $taskname => $task ) { 191 | $this->runTasks( [$taskname] ); 192 | } 193 | 194 | return $this; 195 | } 196 | 197 | 198 | /** 199 | * Sets the verbosity level 200 | * 201 | * @param mixed $level Verbosity level (empty: none, v: notice: vv: info, vvv: debug) 202 | * @return self Same object for fluid method calls 203 | */ 204 | public function verbose( $level = 'v' ) : self 205 | { 206 | $this->verbose = ( $fcn = static::macro( 'verbose' ) ) ? $fcn( $level ) : strlen( (string) $level ); 207 | return $this; 208 | } 209 | 210 | 211 | /** 212 | * Autoloader for setup tasks. 213 | * 214 | * @param string $classname Name of the class to load 215 | * @return bool True if class was found, false if not 216 | */ 217 | protected function autoload( string $classname ) : bool 218 | { 219 | if( !strncmp( $classname, 'Aimeos\Upscheme\Task\\', 21 ) ) 220 | { 221 | $fileName = substr( $classname, 21 ) . '.php'; 222 | 223 | foreach( $this->paths() as $path ) 224 | { 225 | $file = $path . '/' . $fileName; 226 | 227 | if( file_exists( $file ) === true && ( include_once $file ) !== false ) { 228 | return true; 229 | } 230 | } 231 | } 232 | 233 | return false; 234 | } 235 | 236 | 237 | /** 238 | * Creates a new database connection from the given configuration 239 | * 240 | * @param array $cfg Database configuration 241 | * @return \Doctrine\DBAL\Connection New DBAL database connection 242 | */ 243 | protected function connect( array $cfg ) : \Doctrine\DBAL\Connection 244 | { 245 | $cfg['driverOptions'][\PDO::ATTR_CASE] = \PDO::CASE_NATURAL; 246 | $cfg['driverOptions'][\PDO::ATTR_ERRMODE] = \PDO::ERRMODE_EXCEPTION; 247 | $cfg['driverOptions'][\PDO::ATTR_ORACLE_NULLS] = \PDO::NULL_NATURAL; 248 | $cfg['driverOptions'][\PDO::ATTR_STRINGIFY_FETCHES] = false; 249 | 250 | if( $fcn = static::macro( 'connect' ) ) { 251 | return $fcn( $cfg ); 252 | } 253 | 254 | $dbalconf = new \Doctrine\DBAL\Configuration(); 255 | $dbalconf->setSchemaManagerFactory( new \Doctrine\DBAL\Schema\DefaultSchemaManagerFactory() ); 256 | $conn = \Doctrine\DBAL\DriverManager::getConnection( $cfg, $dbalconf ); 257 | 258 | if( in_array( $cfg['driver'], ['oci8', 'pdo_oci'] ) ) { 259 | $conn->executeStatement( "ALTER SESSION SET NLS_TIME_FORMAT='HH24:MI:SS' NLS_DATE_FORMAT='YYYY-MM-DD' NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS' NLS_TIMESTAMP_TZ_FORMAT='YYYY-MM-DD HH24:MI:SS TZH:TZM' NLS_NUMERIC_CHARACTERS='.,'" ); 260 | } 261 | 262 | return $conn; 263 | } 264 | 265 | 266 | /** 267 | * Creates the tasks from the given directories 268 | * 269 | * @param array $paths List of paths containing task classes 270 | * @return array<\Aimeos\Upscheme\Task\Iface> List of task objects 271 | */ 272 | protected function createTasks( array $paths ) : array 273 | { 274 | $tasks = []; 275 | 276 | foreach( $paths as $path ) 277 | { 278 | foreach( new \DirectoryIterator( $path ) as $item ) 279 | { 280 | if( $item->isDir() === true || substr( $item->getFilename(), -4 ) != '.php' ) { continue; } 281 | 282 | $interface = \Aimeos\Upscheme\Task\Iface::class; 283 | $taskname = substr( $item->getFilename(), 0, -4 ); 284 | 285 | if( !is_object( $task = include_once $item->getPathName() ) ) 286 | { 287 | $classname = '\Aimeos\Upscheme\Task\\' . $taskname; 288 | 289 | if( class_exists( $classname ) === false ) { 290 | throw new \RuntimeException( sprintf( 'Class "%1$s" not found', $classname ) ); 291 | } 292 | 293 | $task = ( $fcn = static::macro( 'createTask' ) ) ? $fcn( $classname ) : new $classname( $this ); 294 | } 295 | 296 | if( ( $task instanceof $interface ) === false ) { 297 | throw new \RuntimeException( sprintf( 'Class "%1$s" doesn\'t implement "%2$s"', $classname, $interface ) ); 298 | } 299 | 300 | $task->_filename = $item->getPathName(); 301 | $tasks[$taskname] = $task; 302 | } 303 | } 304 | 305 | ksort( $tasks ); 306 | return $tasks; 307 | } 308 | 309 | 310 | /** 311 | * Adds default macros which can be overwritten 312 | */ 313 | protected function macros() 314 | { 315 | \Aimeos\Upscheme\Schema\Table::macro( 'id', function( ?string $name = null ) : Schema\Column { 316 | return $this->integer( $name ?: 'id' )->seq( true )->primary(); 317 | } ); 318 | 319 | \Aimeos\Upscheme\Schema\Table::macro( 'bigid', function( ?string $name = null ) : Schema\Column { 320 | return $this->bigint( $name ?: 'id' )->seq( true )->primary(); 321 | } ); 322 | } 323 | 324 | 325 | /** 326 | * Executes each task depending of the task dependencies 327 | * 328 | * @param array $tasknames List of task names 329 | * @param array $stack List of task names that are scheduled after this task 330 | */ 331 | protected function runTasks( array $tasknames, array $stack = [] ) : void 332 | { 333 | $dir = getcwd(); 334 | $dirlen = strlen( $dir ); 335 | 336 | foreach( $tasknames as $taskname ) 337 | { 338 | if( in_array( $taskname, $this->tasksDone ) ) { 339 | continue; 340 | } 341 | 342 | if( in_array( $taskname, $stack ) ) 343 | { 344 | $msg = 'Circular dependency for "%1$s" detected. Task stack: %2$s'; 345 | throw new \RuntimeException( sprintf( $msg, $taskname, join( ', ', $stack ) ) ); 346 | } 347 | 348 | $stack[] = $taskname; 349 | 350 | if( isset( $this->dependencies[$taskname] ) ) { 351 | $this->runTasks( (array) $this->dependencies[$taskname], $stack ); 352 | } 353 | 354 | if( isset( $this->tasks[$taskname] ) ) 355 | { 356 | $start = microtime( true ); 357 | $file = $this->tasks[$taskname]->_filename; 358 | 359 | if( !strncmp( $file, $dir, $dirlen ) ) { 360 | $file = ltrim( substr( $file, $dirlen ), '/' ); 361 | } 362 | 363 | $this->info( 'Migrating: ' . $file, 'v' ); 364 | $this->tasks[$taskname]->up(); 365 | 366 | foreach( $this->db as $db ) { 367 | $db->up(); 368 | } 369 | 370 | $this->info( 'Migrated: ' . $file . ' (' . round( ( microtime( true ) - $start ) * 1000, 2 ) . 'ms)', 'v' ); 371 | } 372 | else 373 | { 374 | $this->info( 'Missing: ' . $taskname, 'v' ); 375 | } 376 | 377 | $this->tasksDone[] = $taskname; 378 | } 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /src/Schema/Column.php: -------------------------------------------------------------------------------- 1 | db = $db; 48 | $this->table = $table; 49 | $this->column = $column; 50 | } 51 | 52 | 53 | /** 54 | * Calls custom methods or passes unknown method calls to the Doctrine column object 55 | * 56 | * @param string $method Name of the method 57 | * @param array $args Method parameters 58 | * @return mixed Return value of the called method 59 | */ 60 | public function __call( string $method, array $args ) 61 | { 62 | if( self::macro( $method ) ) { 63 | return $this->call( $method, ...$args ); 64 | } 65 | 66 | return $this->column->{$method}( ...$args ); 67 | } 68 | 69 | 70 | /** 71 | * Returns the value for the given column option 72 | * 73 | * @param string $name Column option name 74 | * @return mixed Column option value 75 | */ 76 | public function __get( string $name ) 77 | { 78 | return $this->opt( $name ); 79 | } 80 | 81 | 82 | /** 83 | * Sets the new value for the given column option 84 | * 85 | * @param string $name Column option name 86 | * @param mixed $value Column option value 87 | */ 88 | public function __set( string $name, $value ) 89 | { 90 | $this->opt( $name, $value ); 91 | } 92 | 93 | 94 | /** 95 | * Sets the column option value or returns the current value 96 | * 97 | * @param string $option Column option name 98 | * @param mixed $value New column option value or NULL to return current value 99 | * @param array|string|null $for Database type this option should be used for ("mysql", "postgresql", "sqlite", "mssql", "oracle", "db2") 100 | * @return self|mixed Same object for setting the value, current value without parameter 101 | */ 102 | public function opt( string $option, $value = null, $for = null ) 103 | { 104 | if( $value === null ) { 105 | return $this->column->getPlatformOption( $option ); 106 | } 107 | 108 | if( $for === null || in_array( $this->db->type(), (array) $for ) ) { 109 | $this->column->setPlatformOption( $option, $value ); 110 | } 111 | 112 | return $this; 113 | } 114 | 115 | 116 | /** 117 | * Sets the column as autoincrement or returns the current value 118 | * 119 | * This method is an alias for seq(). 120 | * 121 | * @param bool|null $value New autoincrement flag or NULL to return current value 122 | * @return self|bool Same object for setting the value, current value without parameter 123 | */ 124 | public function autoincrement( ?bool $value = null ) 125 | { 126 | return $this->seq( $value ); 127 | } 128 | 129 | 130 | /** 131 | * Sets the column charset or returns the current value 132 | * 133 | * @param string|null $value New column charset or NULL to return current value 134 | * @return self|string Same object for setting the value, current value without parameter 135 | */ 136 | public function charset( ?string $value = null ) 137 | { 138 | return $this->opt( 'charset', $value ); 139 | } 140 | 141 | 142 | /** 143 | * Sets the column collation or returns the current value 144 | * 145 | * @param string|null $value New column collation or NULL to return current value 146 | * @return self|string Same object for setting the value, current value without parameter 147 | */ 148 | public function collation( ?string $value = null ) 149 | { 150 | return $this->opt( 'collation', $value ); 151 | } 152 | 153 | 154 | /** 155 | * Sets the column comment or returns the current value 156 | * 157 | * @param string|null $value New column comment or NULL to return current value 158 | * @return self|string Same object for setting the value, current value without parameter 159 | */ 160 | public function comment( ?string $value = null ) 161 | { 162 | if( $value === null ) { 163 | return $this->column->getComment(); 164 | } 165 | 166 | $this->column->setComment( $value ); 167 | return $this; 168 | } 169 | 170 | 171 | /** 172 | * Sets the custom column definition or returns the current value 173 | * 174 | * @param string $value Custom column definition 175 | * @param array|string|null $for Database type this option should be used for ("mysql", "postgresql", "sqlite", "mssql", "oracle", "db2") 176 | * @return \Aimeos\Upscheme\Schema\Column Column object 177 | */ 178 | public function custom( ?string $value = null, $for = null ) 179 | { 180 | if( $value === null ) { 181 | return $this->column->getColumnDefinition(); 182 | } 183 | 184 | if( $for === null || in_array( $this->db->type(), (array) $for ) ) { 185 | $this->column->setColumnDefinition( $value ); 186 | } 187 | 188 | return $this; 189 | } 190 | 191 | 192 | /** 193 | * Sets the column default value or returns the current value 194 | * 195 | * @param mixed $value New column default value or NULL to return current value 196 | * @return self|mixed Same object for setting the value, current value without parameter 197 | */ 198 | public function default( $value = null ) 199 | { 200 | if( $value === null ) { 201 | return $this->column->getDefault(); 202 | } 203 | 204 | $this->column->setDefault( $value ); 205 | return $this; 206 | } 207 | 208 | 209 | /** 210 | * Sets the column fixed flag or returns the current value 211 | * 212 | * @param bool|null $value New column fixed flag or NULL to return current value 213 | * @return self|bool Same object for setting the value, current value without parameter 214 | */ 215 | public function fixed( ?bool $value = null ) 216 | { 217 | if( $value === null ) { 218 | return $this->column->getFixed(); 219 | } 220 | 221 | $this->column->setFixed( $value ); 222 | return $this; 223 | } 224 | 225 | 226 | /** 227 | * Sets the column length or returns the current value 228 | * 229 | * @param int|null $value New column length or NULL to return current value 230 | * @return self|int Same object for setting the value, current value without parameter 231 | */ 232 | public function length( ?int $value = null ) 233 | { 234 | if( $value === null ) { 235 | return $this->column->getLength(); 236 | } 237 | 238 | $this->column->setLength( $value ); 239 | return $this; 240 | } 241 | 242 | 243 | /** 244 | * Returns the name of the column 245 | * 246 | * @return string Column name 247 | */ 248 | public function name() : string 249 | { 250 | return $this->column->getName(); 251 | } 252 | 253 | 254 | /** 255 | * Sets the column null flag or returns the current value 256 | * 257 | * @param bool|null $value New column null flag or NULL to return current value 258 | * @return self|bool Same object for setting the value, current value without parameter 259 | */ 260 | public function null( ?bool $value = null ) 261 | { 262 | if( $value === null ) { 263 | return !$this->column->getNotnull(); 264 | } 265 | 266 | $this->column->setNotnull( !$value ); 267 | return $this; 268 | } 269 | 270 | 271 | /** 272 | * Sets the column precision or returns the current value 273 | * 274 | * @param int|null $value New column precision value or NULL to return current value 275 | * @return self|int Same object for setting the value, current value without parameter 276 | */ 277 | public function precision( ?int $value = null ) 278 | { 279 | if( $value === null ) { 280 | return $this->column->getPrecision(); 281 | } 282 | 283 | $this->column->setPrecision( $value ); 284 | return $this; 285 | } 286 | 287 | 288 | /** 289 | * Sets the column scale or returns the current value 290 | * 291 | * @param int|null $value New column scale value or NULL to return current value 292 | * @return self|int Same object for setting the value, current value without parameter 293 | */ 294 | public function scale( ?int $value = null ) 295 | { 296 | if( $value === null ) { 297 | return $this->column->getScale(); 298 | } 299 | 300 | $this->column->setScale( $value ); 301 | return $this; 302 | } 303 | 304 | 305 | /** 306 | * Sets the column as autoincrement or returns the current value 307 | * 308 | * @param bool|null $value New autoincrement flag or NULL to return current value 309 | * @return self|bool Same object for setting the value, current value without parameter 310 | */ 311 | public function seq( ?bool $value = null ) 312 | { 313 | if( $value === null ) { 314 | return $this->column->getAutoincrement(); 315 | } 316 | 317 | $this->column->setAutoincrement( $value ); 318 | return $this; 319 | } 320 | 321 | 322 | /** 323 | * Sets the column type or returns the current value 324 | * 325 | * @param string|null $value New column type or NULL to return current value 326 | * @return self|string Same object for setting the value, current value without parameter 327 | */ 328 | public function type( ?string $value = null ) 329 | { 330 | if( $value === null ) 331 | { 332 | $type = $this->column->getType(); 333 | return \Doctrine\DBAL\Types\Type::lookupName( $type ); 334 | } 335 | 336 | $this->column->setType( \Doctrine\DBAL\Types\Type::getType( $value ) ); 337 | return $this; 338 | } 339 | 340 | 341 | /** 342 | * Sets the column unsigned flag or returns the current value 343 | * 344 | * @param bool|null $value New column unsigned flag or NULL to return current value 345 | * @return self|bool Same object for setting the value, current value without parameter 346 | */ 347 | public function unsigned( ?bool $value = null ) 348 | { 349 | if( $value === null ) { 350 | return $this->column->getUnsigned(); 351 | } 352 | 353 | $this->column->setUnsigned( $value ); 354 | return $this; 355 | } 356 | 357 | 358 | /** 359 | * Creates a regular index for the column 360 | * 361 | * @param string|null $name Name of the index or NULL to generate automatically 362 | * @return self Same object for fluid method calls 363 | */ 364 | public function index( ?string $name = null ) : self 365 | { 366 | $this->table->index( [$this->name()], $name ); 367 | return $this; 368 | } 369 | 370 | 371 | /** 372 | * Creates a primary index for the column 373 | * 374 | * @param string|null $name Name of the index or NULL to generate automatically 375 | * @return self Same object for fluid method calls 376 | */ 377 | public function primary( ?string $name = null ) : self 378 | { 379 | $this->table->primary( [$this->name()], $name ); 380 | return $this; 381 | } 382 | 383 | 384 | /** 385 | * Creates a spatial index for the column 386 | * 387 | * @param string|null $name Name of the index or NULL to generate automatically 388 | * @return self Same object for fluid method calls 389 | */ 390 | public function spatial( ?string $name = null ) : self 391 | { 392 | $this->table->spatial( [$this->name()], $name ); 393 | return $this; 394 | } 395 | 396 | 397 | /** 398 | * Creates an unique index for the column 399 | * 400 | * @param string|null $name Name of the index or NULL to generate automatically 401 | * @return self Same object for fluid method calls 402 | */ 403 | public function unique( ?string $name = null ) : self 404 | { 405 | $this->table->unique( $this->name(), $name ); 406 | return $this; 407 | } 408 | 409 | 410 | /** 411 | * Applies the changes to the database schema 412 | * 413 | * @return self Same object for fluid method calls 414 | */ 415 | public function up() : self 416 | { 417 | $this->table->up(); 418 | return $this; 419 | } 420 | } -------------------------------------------------------------------------------- /tests/Upscheme/Schema/ColumnTest.php: -------------------------------------------------------------------------------- 1 | dbmock = $this->getMockBuilder( '\Aimeos\Upscheme\Schema\DB' ) 22 | ->disableOriginalConstructor() 23 | ->getMock(); 24 | 25 | $this->tablemock = $this->getMockBuilder( '\Aimeos\Upscheme\Schema\Table' ) 26 | ->disableOriginalConstructor() 27 | ->getMock(); 28 | 29 | $this->colmock = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Column' ) 30 | ->disableOriginalConstructor() 31 | ->getMock(); 32 | 33 | $this->object = new \Aimeos\Upscheme\Schema\Column( $this->dbmock, $this->tablemock, $this->colmock ); 34 | } 35 | 36 | 37 | protected function tearDown() : void 38 | { 39 | unset( $this->object, $this->colmock, $this->tablemock, $this->dbmock ); 40 | } 41 | 42 | 43 | public function testCall() 44 | { 45 | $this->colmock->expects( $this->once() )->method( 'getComment' ); 46 | 47 | $this->object->getComment(); 48 | } 49 | 50 | 51 | public function testCallMacro() 52 | { 53 | \Aimeos\Upscheme\Schema\Column::macro( 'unittest', function() { return 'yes'; } ); 54 | 55 | $this->assertEquals( 'yes', $this->object->unittest() ); 56 | } 57 | 58 | 59 | public function testGetMagic() 60 | { 61 | $this->colmock->expects( $this->once() )->method( 'getPlatformOption' ) 62 | ->willReturn( 'yes' ); 63 | 64 | $this->assertEquals( 'yes', $this->object->unittest ); 65 | } 66 | 67 | 68 | public function testSetMagic() 69 | { 70 | $this->colmock->expects( $this->once() )->method( 'setPlatformOption' ); 71 | 72 | $this->object->unittest = 'yes'; 73 | } 74 | 75 | 76 | public function testOptGet() 77 | { 78 | $this->colmock->expects( $this->once() )->method( 'getPlatformOption' ) 79 | ->willReturn( 'yes' ); 80 | 81 | $this->assertEquals( 'yes', $this->object->opt( 'unittest' ) ); 82 | } 83 | 84 | 85 | public function testOptSet() 86 | { 87 | $this->colmock->expects( $this->once() )->method( 'setPlatformOption' ); 88 | 89 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $this->object->opt( 'unittest', 'yes' ) ); 90 | } 91 | 92 | 93 | public function testOptSetType() 94 | { 95 | $this->dbmock->expects( $this->once() )->method( 'type' )->willReturn( 'mydb' ); 96 | $this->colmock->expects( $this->once() )->method( 'setPlatformOption' ); 97 | 98 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $this->object->opt( 'unittest', 'yes', 'mydb' ) ); 99 | } 100 | 101 | 102 | public function testOptSetTypeNot() 103 | { 104 | $this->dbmock->expects( $this->once() )->method( 'type' )->willReturn( 'mydb' ); 105 | $this->colmock->expects( $this->never() )->method( 'setPlatformOption' ); 106 | 107 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $this->object->opt( 'unittest', 'yes', 'yourdb' ) ); 108 | } 109 | 110 | 111 | public function testAutoincrementGet() 112 | { 113 | $this->colmock->expects( $this->once() )->method( 'getAutoincrement' ) 114 | ->willReturn( true ); 115 | 116 | $this->assertEquals( true, $this->object->autoincrement() ); 117 | } 118 | 119 | 120 | public function testAutoincrementSet() 121 | { 122 | $this->colmock->expects( $this->once() )->method( 'setAutoincrement' ); 123 | 124 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $this->object->autoincrement( true ) ); 125 | } 126 | 127 | 128 | public function testCharsetGet() 129 | { 130 | $this->colmock->expects( $this->once() )->method( 'getPlatformOption' ) 131 | ->willReturn( 'utf8' ); 132 | 133 | $this->assertEquals( 'utf8', $this->object->charset() ); 134 | } 135 | 136 | 137 | public function testCharsetSet() 138 | { 139 | $this->colmock->expects( $this->once() )->method( 'setPlatformOption' ); 140 | 141 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $this->object->charset( 'utf8' ) ); 142 | } 143 | 144 | 145 | public function testCollationGet() 146 | { 147 | $this->colmock->expects( $this->once() )->method( 'getPlatformOption' ) 148 | ->willReturn( 'binary' ); 149 | 150 | $this->assertEquals( 'binary', $this->object->collation() ); 151 | } 152 | 153 | 154 | public function testCollationSet() 155 | { 156 | $this->colmock->expects( $this->once() )->method( 'setPlatformOption' ); 157 | 158 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $this->object->collation( 'binary' ) ); 159 | } 160 | 161 | 162 | public function testCommentGet() 163 | { 164 | $this->colmock->expects( $this->once() )->method( 'getComment' ) 165 | ->willReturn( 'yes' ); 166 | 167 | $this->assertEquals( 'yes', $this->object->comment() ); 168 | } 169 | 170 | 171 | public function testCommentSet() 172 | { 173 | $this->colmock->expects( $this->once() )->method( 'setComment' ); 174 | 175 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $this->object->comment( 'yes' ) ); 176 | } 177 | 178 | 179 | public function testCustomGet() 180 | { 181 | $this->colmock->expects( $this->once() )->method( 'getColumnDefinition' ) 182 | ->willReturn( 'INT NOT NULL' ); 183 | 184 | $this->assertEquals( 'INT NOT NULL', $this->object->custom() ); 185 | } 186 | 187 | 188 | public function testCustomSet() 189 | { 190 | $this->colmock->expects( $this->once() )->method( 'setColumnDefinition' ); 191 | 192 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $this->object->custom( 'INT NOT NULL' ) ); 193 | } 194 | 195 | 196 | public function testCustomSetType() 197 | { 198 | $this->dbmock->expects( $this->once() )->method( 'type' )->willReturn( 'mydb' ); 199 | $this->colmock->expects( $this->once() )->method( 'setColumnDefinition' ); 200 | 201 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $this->object->custom( 'INT NOT NULL', 'mydb' ) ); 202 | } 203 | 204 | 205 | public function testCustomSetTypeNot() 206 | { 207 | $this->dbmock->expects( $this->once() )->method( 'type' )->willReturn( 'mydb' ); 208 | $this->colmock->expects( $this->never() )->method( 'setColumnDefinition' ); 209 | 210 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $this->object->custom( 'INT NOT NULL', 'yourdb' ) ); 211 | } 212 | 213 | 214 | public function testDefaultGet() 215 | { 216 | $this->colmock->expects( $this->once() )->method( 'getDefault' ) 217 | ->willReturn( 'yes' ); 218 | 219 | $this->assertEquals( 'yes', $this->object->default() ); 220 | } 221 | 222 | 223 | public function testDefaultSet() 224 | { 225 | $this->colmock->expects( $this->once() )->method( 'setDefault' ); 226 | 227 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $this->object->default( 'yes' ) ); 228 | } 229 | 230 | 231 | public function testFixedGet() 232 | { 233 | $this->colmock->expects( $this->once() )->method( 'getFixed' ) 234 | ->willReturn( true ); 235 | 236 | $this->assertEquals( true, $this->object->fixed() ); 237 | } 238 | 239 | 240 | public function testFixedSet() 241 | { 242 | $this->colmock->expects( $this->once() )->method( 'setFixed' ); 243 | 244 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $this->object->fixed( true ) ); 245 | } 246 | 247 | 248 | public function testLengthGet() 249 | { 250 | $this->colmock->expects( $this->once() )->method( 'getLength' ) 251 | ->willReturn( 10 ); 252 | 253 | $this->assertEquals( 10, $this->object->length() ); 254 | } 255 | 256 | 257 | public function testLengthSet() 258 | { 259 | $this->colmock->expects( $this->once() )->method( 'setLength' ); 260 | 261 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $this->object->length( 10 ) ); 262 | } 263 | 264 | 265 | public function testName() 266 | { 267 | $this->colmock->expects( $this->once() )->method( 'getName' ) 268 | ->willReturn( 'unittest' ); 269 | 270 | $this->assertEquals( 'unittest', $this->object->name() ); 271 | } 272 | 273 | 274 | public function testNullGet() 275 | { 276 | $this->colmock->expects( $this->once() )->method( 'getNotnull' ) 277 | ->willReturn( true ); 278 | 279 | $this->assertEquals( false, $this->object->null() ); 280 | } 281 | 282 | 283 | public function testNullSet() 284 | { 285 | $this->colmock->expects( $this->once() )->method( 'setNotnull' ); 286 | 287 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $this->object->null( true ) ); 288 | } 289 | 290 | 291 | public function testPrecisionGet() 292 | { 293 | $this->colmock->expects( $this->once() )->method( 'getPrecision' ) 294 | ->willReturn( 10 ); 295 | 296 | $this->assertEquals( 10, $this->object->precision() ); 297 | } 298 | 299 | 300 | public function testPrecisionSet() 301 | { 302 | $this->colmock->expects( $this->once() )->method( 'setPrecision' ); 303 | 304 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $this->object->precision( 10 ) ); 305 | } 306 | 307 | 308 | public function testScaleGet() 309 | { 310 | $this->colmock->expects( $this->once() )->method( 'getScale' ) 311 | ->willReturn( 10 ); 312 | 313 | $this->assertEquals( 10, $this->object->scale() ); 314 | } 315 | 316 | 317 | public function testScaleSet() 318 | { 319 | $this->colmock->expects( $this->once() )->method( 'setScale' ); 320 | 321 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $this->object->scale( 10 ) ); 322 | } 323 | 324 | 325 | public function testSeqGet() 326 | { 327 | $this->colmock->expects( $this->once() )->method( 'getAutoincrement' ) 328 | ->willReturn( true ); 329 | 330 | $this->assertEquals( true, $this->object->seq() ); 331 | } 332 | 333 | 334 | public function testSeqSet() 335 | { 336 | $this->colmock->expects( $this->once() )->method( 'setAutoincrement' ); 337 | 338 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $this->object->seq( true ) ); 339 | } 340 | 341 | 342 | public function testTypeGet() 343 | { 344 | $this->colmock->expects( $this->once() )->method( 'getType' ) 345 | ->willReturn( \Doctrine\DBAL\Types\Type::getType( 'string' ) ); 346 | 347 | $this->assertEquals( 'string', $this->object->type() ); 348 | } 349 | 350 | 351 | public function testTypeSet() 352 | { 353 | $this->colmock->expects( $this->once() )->method( 'setType' ); 354 | 355 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $this->object->type( 'string' ) ); 356 | } 357 | 358 | 359 | public function testUnsignedGet() 360 | { 361 | $this->colmock->expects( $this->once() )->method( 'getUnsigned' ) 362 | ->willReturn( true ); 363 | 364 | $this->assertEquals( true, $this->object->unsigned() ); 365 | } 366 | 367 | 368 | public function testUnsignedSet() 369 | { 370 | $this->colmock->expects( $this->once() )->method( 'setUnsigned' ); 371 | 372 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $this->object->unsigned( true ) ); 373 | } 374 | 375 | 376 | public function testIndex() 377 | { 378 | $this->colmock->expects( $this->once() )->method( 'getName' )->willReturn( 'unitcol' ); 379 | $this->tablemock->expects( $this->once() )->method( 'index' ); 380 | 381 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $this->object->index( 'unittest', 'idx_utst' ) ); 382 | } 383 | 384 | 385 | public function testPrimary() 386 | { 387 | $this->colmock->expects( $this->once() )->method( 'getName' )->willReturn( 'unitcol' ); 388 | $this->tablemock->expects( $this->once() )->method( 'primary' ); 389 | 390 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $this->object->primary( 'unittest', 'pk_utst' ) ); 391 | } 392 | 393 | 394 | public function testSpatial() 395 | { 396 | $this->colmock->expects( $this->once() )->method( 'getName' )->willReturn( 'unitcol' ); 397 | $this->tablemock->expects( $this->once() )->method( 'spatial' ); 398 | 399 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $this->object->spatial( 'unittest', 'sp_utst' ) ); 400 | } 401 | 402 | 403 | public function testUnique() 404 | { 405 | $this->colmock->expects( $this->once() )->method( 'getName' )->willReturn( 'unitcol' ); 406 | $this->tablemock->expects( $this->once() )->method( 'unique' ); 407 | 408 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $this->object->unique( 'unittest', 'unq_utst' ) ); 409 | } 410 | 411 | 412 | public function testUp() 413 | { 414 | $this->tablemock->expects( $this->once() )->method( 'up' ); 415 | 416 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $this->object->up() ); 417 | } 418 | } 419 | -------------------------------------------------------------------------------- /tests/Upscheme/Schema/DBTest.php: -------------------------------------------------------------------------------- 1 | upmock = $this->getMockBuilder( '\Aimeos\Upscheme\Up' ) 25 | ->disableOriginalConstructor() 26 | ->getMock(); 27 | 28 | $this->connmock = $this->getMockBuilder( '\Doctrine\DBAL\Connection' ) 29 | ->disableOriginalConstructor() 30 | ->getMock(); 31 | 32 | $this->pfmock = $this->getMockBuilder( '\Doctrine\DBAL\Platforms\MySQLPlatform' ) 33 | ->disableOriginalConstructor() 34 | ->getMock(); 35 | 36 | $this->smmock = $this->getMockBuilder( '\Doctrine\DBAL\Schema\AbstractSchemaManager' ) 37 | ->disableOriginalConstructor() 38 | ->getMock(); 39 | 40 | $this->schemamock = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Schema' ) 41 | ->disableOriginalConstructor() 42 | ->disableOriginalClone() 43 | ->getMock(); 44 | 45 | $this->tablemock = $this->getMockBuilder( '\Aimeos\Upscheme\Schema\Table' ) 46 | ->disableOriginalConstructor() 47 | ->getMock(); 48 | 49 | 50 | $this->connmock->expects( $this->any() )->method( 'createSchemaManager' ) 51 | ->willReturn( $this->smmock ); 52 | 53 | $this->connmock->expects( $this->any() )->method( 'quoteIdentifier' ) 54 | ->willReturnCallback( function( $value ) { 55 | return '"' . $value . '"'; 56 | } ); 57 | 58 | $this->connmock->expects( $this->any() )->method( 'getDatabasePlatform' ) 59 | ->willReturn( $this->pfmock ); 60 | 61 | $this->smmock->expects( $this->any() )->method( 'introspectSchema' ) 62 | ->willReturn( $this->schemamock ); 63 | 64 | 65 | $this->object = $this->getMockBuilder( '\Aimeos\Upscheme\Schema\DB' ) 66 | ->setConstructorArgs( [$this->upmock, $this->connmock] ) 67 | ->onlyMethods( ['table', 'up', 'getColumnSQL', 'getViews'] ) 68 | ->getMock(); 69 | 70 | $this->object->expects( $this->any() )->method( 'table' )->willReturn( $this->tablemock ); 71 | } 72 | 73 | 74 | protected function tearDown() : void 75 | { 76 | unset( $this->object, $this->tablemock, $this->schemamock, $this->smmock, $this->connmock, $this->upmock ); 77 | } 78 | 79 | 80 | public function testCall() 81 | { 82 | $this->schemamock->expects( $this->once() )->method( 'hasNamespace' ); 83 | $this->object->hasNamespace( 'test' ); 84 | } 85 | 86 | 87 | public function testCallMacro() 88 | { 89 | \Aimeos\Upscheme\Schema\DB::macro( 'unittest', function() { return 'yes'; } ); 90 | $this->assertEquals( 'yes', $this->object->unittest() ); 91 | } 92 | 93 | 94 | public function testClone() 95 | { 96 | $this->connmock->expects( $this->once() )->method( 'close' ); 97 | $this->object->expects( $this->once() )->method( 'up' ); 98 | 99 | $obj = clone $this->object; 100 | } 101 | 102 | 103 | public function testClose() 104 | { 105 | $this->connmock->expects( $this->once() )->method( 'close' ); 106 | $this->object->close(); 107 | } 108 | 109 | 110 | public function testDelete() 111 | { 112 | $this->connmock->expects( $this->once() )->method( 'delete' ); 113 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $this->object->delete( 'unittest' ) ); 114 | } 115 | 116 | 117 | public function testDropColumn() 118 | { 119 | $this->schemamock->expects( $this->once() )->method( 'hasTable' )->willReturn( true ); 120 | $this->tablemock->expects( $this->once() )->method( 'hasColumn' )->willReturn( true ); 121 | $this->tablemock->expects( $this->once() )->method( 'dropColumn' ); 122 | 123 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $this->object->dropColumn( 'unit', 'test' ) ); 124 | } 125 | 126 | 127 | public function testDropColumnMultiple() 128 | { 129 | $this->schemamock->expects( $this->exactly( 2 ) )->method( 'hasTable' )->willReturn( true ); 130 | $this->tablemock->expects( $this->exactly( 2 ) )->method( 'hasColumn' )->willReturn( true ); 131 | $this->tablemock->expects( $this->exactly( 2 ) )->method( 'dropColumn' ); 132 | 133 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $this->object->dropColumn( 'unit', ['test', 'test2'] ) ); 134 | } 135 | 136 | 137 | public function testDropForeign() 138 | { 139 | $this->schemamock->expects( $this->once() )->method( 'hasTable' )->willReturn( true ); 140 | $this->tablemock->expects( $this->once() )->method( 'hasForeign' )->willReturn( true ); 141 | $this->tablemock->expects( $this->once() )->method( 'dropForeign' ); 142 | 143 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $this->object->dropForeign( 'unit', 'test' ) ); 144 | } 145 | 146 | 147 | public function testDropForeignMultiple() 148 | { 149 | $this->schemamock->expects( $this->exactly( 2 ) )->method( 'hasTable' )->willReturn( true ); 150 | $this->tablemock->expects( $this->exactly( 2 ) )->method( 'hasForeign' )->willReturn( true ); 151 | $this->tablemock->expects( $this->exactly( 2 ) )->method( 'dropForeign' ); 152 | 153 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $this->object->dropForeign( 'unit', ['test', 'test2'] ) ); 154 | } 155 | 156 | 157 | public function testDropIndex() 158 | { 159 | $this->schemamock->expects( $this->once() )->method( 'hasTable' )->willReturn( true ); 160 | $this->tablemock->expects( $this->once() )->method( 'hasIndex' )->willReturn( true ); 161 | $this->tablemock->expects( $this->once() )->method( 'dropIndex' ); 162 | 163 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $this->object->dropIndex( 'unit', 'test' ) ); 164 | } 165 | 166 | 167 | public function testDropIndexMultiple() 168 | { 169 | $this->schemamock->expects( $this->exactly( 2 ) )->method( 'hasTable' )->willReturn( true ); 170 | $this->tablemock->expects( $this->exactly( 2 ) )->method( 'hasIndex' )->willReturn( true ); 171 | $this->tablemock->expects( $this->exactly( 2 ) )->method( 'dropIndex' ); 172 | 173 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $this->object->dropIndex( 'unit', ['test', 'test2'] ) ); 174 | } 175 | 176 | 177 | public function testDropSequence() 178 | { 179 | $this->schemamock->expects( $this->once() )->method( 'hasSequence' )->willReturn( true ); 180 | $this->schemamock->expects( $this->once() )->method( 'dropSequence' ); 181 | 182 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $this->object->dropSequence( 'unit', 'test' ) ); 183 | } 184 | 185 | 186 | public function testDropSequenceMultiple() 187 | { 188 | $this->schemamock->expects( $this->exactly( 2 ) )->method( 'hasSequence' )->willReturn( true ); 189 | $this->schemamock->expects( $this->exactly( 2 ) )->method( 'dropSequence' ); 190 | 191 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $this->object->dropSequence( ['test', 'test2'] ) ); 192 | } 193 | 194 | 195 | public function testDropTable() 196 | { 197 | $this->schemamock->expects( $this->once() )->method( 'hasTable' )->willReturn( true ); 198 | 199 | if( $this->object->type() !== 'oracle' ) { 200 | $this->schemamock->expects( $this->once() )->method( 'dropTable' ); 201 | } else { 202 | $this->smmock->expects( $this->once() )->method( 'dropTable' ); 203 | } 204 | 205 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $this->object->dropTable( 'unit', 'test' ) ); 206 | } 207 | 208 | 209 | public function testDropTableMultiple() 210 | { 211 | $this->schemamock->expects( $this->exactly( 2 ) )->method( 'hasTable' )->willReturn( true ); 212 | 213 | if( $this->object->type() !== 'oracle' ) { 214 | $this->schemamock->expects( $this->exactly( 2 ) )->method( 'dropTable' ); 215 | } else { 216 | $this->smmock->expects( $this->exactly( 2 ) )->method( 'dropTable' ); 217 | } 218 | 219 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $this->object->dropTable( ['test', 'test2'] ) ); 220 | } 221 | 222 | 223 | public function testDropView() 224 | { 225 | $this->object->expects( $this->once() )->method( 'getViews' )->willReturn( ['test' => new \stdClass] ); 226 | $this->smmock->expects( $this->once() )->method( 'dropView' ); 227 | 228 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $this->object->dropView( 'test' ) ); 229 | } 230 | 231 | 232 | public function testDropViewMultiple() 233 | { 234 | $this->object->expects( $this->exactly( 2 ) )->method( 'getViews' )->willReturn( ['test' => new \stdClass, 'test2' => new \stdClass] ); 235 | $this->smmock->expects( $this->exactly( 2 ) )->method( 'dropView' ); 236 | 237 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $this->object->dropView( ['test', 'test2'] ) ); 238 | } 239 | 240 | 241 | public function testExec() 242 | { 243 | $this->connmock->expects( $this->once() )->method( 'executeStatement' )->willReturn( 123 ); 244 | 245 | $this->assertEquals( 123, $this->object->exec( 'test' ) ); 246 | } 247 | 248 | 249 | public function testFor() 250 | { 251 | $this->connmock->expects( $this->once() )->method( 'executeStatement' )->willReturn( 123 ); 252 | 253 | $this->assertEquals( 123, $this->object->exec( 'test' ) ); 254 | } 255 | 256 | 257 | public function testForMultiple() 258 | { 259 | $this->connmock->expects( $this->exactly( 2 ) )->method( 'executeStatement' ); 260 | 261 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $this->object->for( 'mysql', ['test', 'test2'] ) ); 262 | } 263 | 264 | 265 | public function testForMismatch() 266 | { 267 | $this->connmock->expects( $this->never() )->method( 'executeStatement' ); 268 | 269 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $this->object->for( 'postgresql', 'test' ) ); 270 | } 271 | 272 | 273 | public function testHasColumn() 274 | { 275 | $this->schemamock->expects( $this->once() )->method( 'hasTable' )->willReturn( true ); 276 | $this->tablemock->expects( $this->once() )->method( 'hasColumn' )->willReturn( true ); 277 | 278 | $this->assertTrue( $this->object->hasColumn( 'unit', 'test' ) ); 279 | } 280 | 281 | 282 | public function testHasColumnNot() 283 | { 284 | $this->schemamock->expects( $this->once() )->method( 'hasTable' )->willReturn( false ); 285 | $this->assertFalse( $this->object->hasColumn( 'unit', 'test' ) ); 286 | } 287 | 288 | 289 | public function testHasForeign() 290 | { 291 | $this->schemamock->expects( $this->once() )->method( 'hasTable' )->willReturn( true ); 292 | $this->tablemock->expects( $this->once() )->method( 'hasForeign' )->willReturn( true ); 293 | 294 | $this->assertTrue( $this->object->hasForeign( 'unit', 'test' ) ); 295 | } 296 | 297 | 298 | public function testHasForeignNot() 299 | { 300 | $this->schemamock->expects( $this->once() )->method( 'hasTable' )->willReturn( false ); 301 | $this->assertFalse( $this->object->hasForeign( 'unit', 'test' ) ); 302 | } 303 | 304 | 305 | public function testHasIndex() 306 | { 307 | $this->schemamock->expects( $this->once() )->method( 'hasTable' )->willReturn( true ); 308 | $this->tablemock->expects( $this->once() )->method( 'hasIndex' )->willReturn( true ); 309 | 310 | $this->assertTrue( $this->object->hasIndex( 'unit', 'test' ) ); 311 | } 312 | 313 | 314 | public function testHasIndexNot() 315 | { 316 | $this->schemamock->expects( $this->once() )->method( 'hasTable' )->willReturn( false ); 317 | $this->assertFalse( $this->object->hasIndex( 'unit', 'test' ) ); 318 | } 319 | 320 | 321 | public function testHasSequence() 322 | { 323 | $this->schemamock->expects( $this->once() )->method( 'hasSequence' )->willReturn( true ); 324 | $this->assertTrue( $this->object->hasSequence( 'unit', 'test' ) ); 325 | } 326 | 327 | 328 | public function testHasSequenceNot() 329 | { 330 | $this->schemamock->expects( $this->once() )->method( 'hasSequence' )->willReturn( false ); 331 | $this->assertFalse( $this->object->hasSequence( 'unit', 'test' ) ); 332 | } 333 | 334 | 335 | public function testHasTable() 336 | { 337 | $this->schemamock->expects( $this->once() )->method( 'hasTable' )->willReturn( true ); 338 | $this->assertTrue( $this->object->hasTable( 'unit', 'test' ) ); 339 | } 340 | 341 | 342 | public function testHasTableNot() 343 | { 344 | $this->schemamock->expects( $this->once() )->method( 'hasTable' )->willReturn( false ); 345 | $this->assertFalse( $this->object->hasTable( 'unit', 'test' ) ); 346 | } 347 | 348 | 349 | public function testHasView() 350 | { 351 | $this->object->expects( $this->once() )->method( 'getViews' )->willReturn( ['test' => new \stdClass] ); 352 | $this->assertTrue( $this->object->hasView( 'test' ) ); 353 | } 354 | 355 | 356 | public function testHasViewNot() 357 | { 358 | $this->object->expects( $this->once() )->method( 'getViews' )->willReturn( [] ); 359 | $this->assertFalse( $this->object->hasView( 'test' ) ); 360 | } 361 | 362 | 363 | public function testInsert() 364 | { 365 | $this->connmock->expects( $this->once() )->method( 'insert' ); 366 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $this->object->insert( 'unittest', [] ) ); 367 | } 368 | 369 | 370 | public function testLastId() 371 | { 372 | $this->connmock->expects( $this->once() )->method( 'lastInsertId' )->willReturn( '123' ); 373 | $this->assertEquals( '123', $this->object->lastId() ); 374 | } 375 | 376 | 377 | public function testName() 378 | { 379 | $this->schemamock->expects( $this->once() )->method( 'getName' )->willReturn( 'testdb' ); 380 | $this->assertEquals( 'testdb', $this->object->name() ); 381 | } 382 | 383 | 384 | public function testQ() 385 | { 386 | $this->connmock->expects( $this->once() )->method( 'quote' )->willReturn( '123' ); 387 | $this->assertEquals( '123', $this->object->q( 123 ) ); 388 | } 389 | 390 | 391 | public function testQi() 392 | { 393 | $this->connmock->expects( $this->once() )->method( 'quoteIdentifier' )->willReturn( '"key"' ); 394 | $this->assertEquals( '"key"', $this->object->qi( 'key' ) ); 395 | } 396 | 397 | 398 | public function testQuery() 399 | { 400 | $mock = $this->getMockBuilder( '\Doctrine\DBAL\Result' ) 401 | ->disableOriginalConstructor() 402 | ->getMock(); 403 | 404 | $this->connmock->expects( $this->once() )->method( 'executeQuery' )->willReturn( $mock ); 405 | 406 | $this->assertInstanceOf( \Doctrine\DBAL\Result::class, $this->object->query( 'test' ) ); 407 | } 408 | 409 | 410 | public function testRenameColumn() 411 | { 412 | $this->schemamock->expects( $this->once() )->method( 'hasTable' )->willReturn( true ); 413 | $this->tablemock->expects( $this->once() )->method( 'hasColumn' )->willReturn( true ); 414 | $this->connmock->expects( $this->any() )->method( 'quoteIdentifier' )->willReturnArgument( 0 ); 415 | $this->object->expects( $this->any() )->method( 'getColumnSQL' )->willReturn( 'test INTEGER' ); 416 | $this->connmock->expects( $this->once() )->method( 'executeStatement' ); 417 | 418 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $this->object->renameColumn( 'table', 'unit', 'test' ) ); 419 | } 420 | 421 | 422 | public function testRenameColumnMultiple() 423 | { 424 | $this->schemamock->expects( $this->exactly( 2 ) )->method( 'hasTable' )->willReturn( true ); 425 | $this->tablemock->expects( $this->exactly( 2 ) )->method( 'hasColumn' )->willReturn( true ); 426 | $this->connmock->expects( $this->any() )->method( 'quoteIdentifier' )->willReturnArgument( 0 ); 427 | $this->object->expects( $this->any() )->method( 'getColumnSQL' )->willReturn( 'test INTEGER' ); 428 | $this->connmock->expects( $this->exactly( 2 ) )->method( 'executeStatement' ); 429 | 430 | $cols = ['unit' => 'test', 'unit2' => 'test2']; 431 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $this->object->renameColumn( 'table', $cols ) ); 432 | } 433 | 434 | 435 | public function testRenameColumnException() 436 | { 437 | $this->schemamock->expects( $this->once() )->method( 'hasTable' )->willReturn( true ); 438 | $this->tablemock->expects( $this->once() )->method( 'hasColumn' )->willReturn( true ); 439 | 440 | $this->expectException( \RuntimeException::class ); 441 | $this->object->renameColumn( 'table', 'unit' ); 442 | } 443 | 444 | 445 | public function testRenameIndex() 446 | { 447 | $this->schemamock->expects( $this->once() )->method( 'hasTable' )->willReturn( true ); 448 | $this->object->expects( $this->once() )->method( 'table' )->willReturn( $this->tablemock ); 449 | $this->tablemock->expects( $this->once() )->method( 'renameIndex' ); 450 | 451 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $this->object->renameIndex( 'table', 'unit', 'test' ) ); 452 | } 453 | 454 | 455 | public function testRenameTable() 456 | { 457 | $this->schemamock->expects( $this->once() )->method( 'hasTable' )->willReturn( true ); 458 | $this->smmock->expects( $this->once() )->method( 'renameTable' ); 459 | 460 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $this->object->renameTable( 'unit', 'test' ) ); 461 | } 462 | 463 | 464 | public function testRenameTableMultiple() 465 | { 466 | $this->schemamock->expects( $this->exactly( 2 ) )->method( 'hasTable' )->willReturn( true ); 467 | $this->smmock->expects( $this->exactly( 2 ) )->method( 'renameTable' ); 468 | 469 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $this->object->renameTable( ['test', 'test2'] ) ); 470 | } 471 | 472 | 473 | public function testRenameTableException() 474 | { 475 | $this->schemamock->expects( $this->once() )->method( 'hasTable' )->willReturn( true ); 476 | 477 | $this->expectException( \RuntimeException::class ); 478 | $this->object->renameTable( 'unit' ); 479 | } 480 | 481 | 482 | public function testReset() 483 | { 484 | $this->smmock->expects( $this->once() )->method( 'introspectSchema' ) 485 | ->willReturn( $this->schemamock ); 486 | 487 | $this->object->reset(); 488 | } 489 | 490 | 491 | public function testSequence() 492 | { 493 | $seqmock = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Sequence' ) 494 | ->disableOriginalConstructor() 495 | ->getMock(); 496 | 497 | $this->schemamock->expects( $this->once() )->method( 'hasSequence' )->willReturn( true ); 498 | $this->schemamock->expects( $this->once() )->method( 'getSequence' )->willReturn( $seqmock ); 499 | 500 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Sequence::class, $this->object->sequence( 'unittest' ) ); 501 | } 502 | 503 | 504 | public function testSequenceNew() 505 | { 506 | $seqmock = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Sequence' ) 507 | ->disableOriginalConstructor() 508 | ->getMock(); 509 | 510 | $this->schemamock->expects( $this->once() )->method( 'hasSequence' )->willReturn( false ); 511 | $this->schemamock->expects( $this->once() )->method( 'createSequence' )->willReturn( $seqmock ); 512 | 513 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Sequence::class, $this->object->sequence( 'unittest' ) ); 514 | } 515 | 516 | 517 | public function testSequenceClosure() 518 | { 519 | $seqmock = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Sequence' ) 520 | ->disableOriginalConstructor() 521 | ->getMock(); 522 | 523 | $this->schemamock->expects( $this->once() )->method( 'hasSequence' )->willReturn( false ); 524 | $this->schemamock->expects( $this->once() )->method( 'createSequence' )->willReturn( $seqmock ); 525 | 526 | $fcn = function( $seq ) {}; 527 | 528 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Sequence::class, $this->object->sequence( 'unittest', $fcn ) ); 529 | } 530 | 531 | 532 | public function testStmt() 533 | { 534 | $qbmock = $this->getMockBuilder( '\Doctrine\DBAL\Query\QueryBuilder' ) 535 | ->disableOriginalConstructor() 536 | ->getMock(); 537 | 538 | $this->connmock->expects( $this->once() )->method( 'createQueryBuilder' )->willReturn( $qbmock ); 539 | 540 | $this->assertInstanceOf( \Doctrine\DBAL\Query\QueryBuilder::class, $this->object->stmt() ); 541 | } 542 | 543 | 544 | public function testTable() 545 | { 546 | $tablemock = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Table' ) 547 | ->disableOriginalConstructor() 548 | ->getMock(); 549 | 550 | $object = new \Aimeos\Upscheme\Schema\DB( $this->upmock, $this->connmock ); 551 | 552 | $this->schemamock->expects( $this->once() )->method( 'hasTable' )->willReturn( true ); 553 | $this->schemamock->expects( $this->once() )->method( 'getTable' )->willReturn( $tablemock ); 554 | 555 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $object->table( 'unittest' ) ); 556 | } 557 | 558 | 559 | public function testTableNew() 560 | { 561 | $tablemock = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Table' ) 562 | ->disableOriginalConstructor() 563 | ->getMock(); 564 | 565 | $object = new \Aimeos\Upscheme\Schema\DB( $this->upmock, $this->connmock ); 566 | 567 | $this->schemamock->expects( $this->once() )->method( 'hasTable' )->willReturn( false ); 568 | $this->schemamock->expects( $this->once() )->method( 'createTable' )->willReturn( $tablemock ); 569 | 570 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $object->table( 'unittest' ) ); 571 | } 572 | 573 | 574 | public function testTableClosure() 575 | { 576 | $tablemock = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Table' ) 577 | ->disableOriginalConstructor() 578 | ->getMock(); 579 | 580 | $object = $this->getMockBuilder( '\Aimeos\Upscheme\Schema\DB' ) 581 | ->setConstructorArgs( [$this->upmock, $this->connmock] ) 582 | ->onlyMethods( ['up'] ) 583 | ->getMock(); 584 | 585 | $this->schemamock->expects( $this->once() )->method( 'hasTable' )->willReturn( false ); 586 | $this->schemamock->expects( $this->once() )->method( 'createTable' )->willReturn( $tablemock ); 587 | 588 | $fcn = function( $seq ) {}; 589 | 590 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $object->table( 'unittest', $fcn ) ); 591 | } 592 | 593 | 594 | public function testTransaction() 595 | { 596 | $object = $this->getMockBuilder( '\Aimeos\Upscheme\Schema\DB' ) 597 | ->setConstructorArgs( [$this->upmock, $this->connmock] ) 598 | ->onlyMethods( ['up'] ) 599 | ->getMock(); 600 | 601 | $fcn = function( $db ) {}; 602 | 603 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $object->transaction( $fcn ) ); 604 | } 605 | 606 | 607 | public function testTransactionException() 608 | { 609 | $object = $this->getMockBuilder( '\Aimeos\Upscheme\Schema\DB' ) 610 | ->setConstructorArgs( [$this->upmock, $this->connmock] ) 611 | ->onlyMethods( ['up'] ) 612 | ->getMock(); 613 | 614 | $fcn = function( $db ) { 615 | throw new \Exception(); 616 | }; 617 | 618 | $this->expectException( \Exception::class ); 619 | $object->transaction( $fcn ); 620 | } 621 | 622 | 623 | public function testType() 624 | { 625 | $this->assertEquals( 'mysql', $this->object->type() ); 626 | } 627 | 628 | 629 | public function testUpdate() 630 | { 631 | $this->connmock->expects( $this->once() )->method( 'update' ); 632 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $this->object->update( 'unittest', [] ) ); 633 | } 634 | 635 | 636 | public function testView() 637 | { 638 | $view = new class { 639 | public function getNamespaceName() { return ''; } 640 | public function getShortestName() { return 'unittest'; } 641 | }; 642 | 643 | $object = new \Aimeos\Upscheme\Schema\DB( $this->upmock, $this->connmock ); 644 | 645 | $this->smmock->expects( $this->once() )->method( 'listViews' )->willReturn( [$view] ); 646 | 647 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $object->view( 'unittest', 'CREATE VIEW' ) ); 648 | } 649 | 650 | 651 | public function testViewNew() 652 | { 653 | $object = new \Aimeos\Upscheme\Schema\DB( $this->upmock, $this->connmock ); 654 | 655 | $this->smmock->expects( $this->once() )->method( 'listViews' )->willReturn( [] ); 656 | $this->smmock->expects( $this->once() )->method( 'createView' ); 657 | 658 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\DB::class, $object->view( 'unittest', 'CREATE VIEW' ) ); 659 | } 660 | } 661 | -------------------------------------------------------------------------------- /src/Schema/Table.php: -------------------------------------------------------------------------------- 1 | db = $db; 44 | $this->table = $table; 45 | } 46 | 47 | 48 | /** 49 | * Calls custom methods or passes unknown method calls to the Doctrine table object 50 | * 51 | * @param string $method Name of the method 52 | * @param array $args Method parameters 53 | * @return mixed Return value of the called method 54 | */ 55 | public function __call( string $method, array $args ) 56 | { 57 | if( self::macro( $method ) ) { 58 | return $this->call( $method, ...$args ); 59 | } 60 | 61 | return $this->table->{$method}( ...$args ); 62 | } 63 | 64 | 65 | /** 66 | * Returns the value for the given table option 67 | * 68 | * @param string $name Table option name 69 | * @return mixed Table option value 70 | */ 71 | public function __get( string $name ) 72 | { 73 | return $this->opt( $name ); 74 | } 75 | 76 | 77 | /** 78 | * Sets the new value for the given table option 79 | * 80 | * @param string $name Table option name 81 | * @param mixed $value Table option value 82 | */ 83 | public function __set( string $name, $value ) 84 | { 85 | $this->opt( $name, $value ); 86 | } 87 | 88 | 89 | /** 90 | * Creates a new column of type "bigint" or returns the existing one 91 | * 92 | * @param string $name Name of the column 93 | * @return \Aimeos\Upscheme\Schema\Column Column object 94 | */ 95 | public function bigint( string $name ) : Column 96 | { 97 | return $this->col( $name, 'bigint' ); 98 | } 99 | 100 | 101 | /** 102 | * Creates a new column of type "binary" or returns the existing one 103 | * 104 | * @param string $name Name of the column 105 | * @param int $length Length of the column in bytes 106 | * @return \Aimeos\Upscheme\Schema\Column Column object 107 | */ 108 | public function binary( string $name, int $length = 255 ) : Column 109 | { 110 | return $this->col( $name, 'binary' )->length( $length ); 111 | } 112 | 113 | 114 | /** 115 | * Creates a new column of type "blob" or returns the existing one 116 | * 117 | * The maximum length of a "blob" column is 2GB. 118 | * 119 | * @param string $name Name of the column 120 | * @param int $length Length of the column in bytes 121 | * @return \Aimeos\Upscheme\Schema\Column Column object 122 | */ 123 | public function blob( string $name, int $length = 0x7fff ) : Column 124 | { 125 | return $this->col( $name, 'blob' )->length( $length ); 126 | } 127 | 128 | 129 | /** 130 | * Creates a new column of type "boolean" or returns the existing one 131 | * 132 | * This method is an alias for boolean() 133 | * 134 | * @param string $name Name of the column 135 | * @return \Aimeos\Upscheme\Schema\Column Column object 136 | */ 137 | public function bool( string $name ) : Column 138 | { 139 | return $this->boolean( $name ); 140 | } 141 | 142 | 143 | /** 144 | * Creates a new column of type "boolean" or returns the existing one 145 | * 146 | * @param string $name Name of the column 147 | * @return \Aimeos\Upscheme\Schema\Column Column object 148 | */ 149 | public function boolean( string $name ) : Column 150 | { 151 | return $this->col( $name, 'boolean' )->default( false ); 152 | } 153 | 154 | 155 | /** 156 | * Creates a new column of type "char" with a fixed length or returns the existing one 157 | * 158 | * @param string $name Name of the column 159 | * @param int $length Length of the column in characters 160 | * @return \Aimeos\Upscheme\Schema\Column Column object 161 | */ 162 | public function char( string $name, int $length ) : Column 163 | { 164 | return $this->col( $name, 'string' )->length( $length )->fixed( true ); 165 | } 166 | 167 | 168 | /** 169 | * Creates a new column or returns the existing one 170 | * 171 | * If the column doesn't exist yet, it will be created. 172 | * 173 | * @param string $name Name of the column 174 | * @param string $type|null Type of the column 175 | * @return \Aimeos\Upscheme\Schema\Column Column object 176 | */ 177 | public function col( string $name, ?string $type = null ) : Column 178 | { 179 | if( $this->table->hasColumn( $name ) ) { 180 | $col = $this->table->getColumn( $name ); 181 | } else { 182 | $col = $this->table->addColumn( $this->db->qi( $name ), $type ?: 'string' ); 183 | } 184 | 185 | if( $type ) { 186 | $col->setType( \Doctrine\DBAL\Types\Type::getType( $type ) ); 187 | } 188 | 189 | return new Column( $this->db, $this, $col ); 190 | } 191 | 192 | 193 | /** 194 | * Creates a new column of type "date" or returns the existing one 195 | * 196 | * @param string $name Name of the column 197 | * @return \Aimeos\Upscheme\Schema\Column Column object 198 | */ 199 | public function date( string $name ) : Column 200 | { 201 | return $this->col( $name, 'date' ); 202 | } 203 | 204 | 205 | /** 206 | * Creates a new column of type "datetime" or returns the existing one 207 | * 208 | * @param string $name Name of the column 209 | * @return \Aimeos\Upscheme\Schema\Column Column object 210 | */ 211 | public function datetime( string $name ) : Column 212 | { 213 | return $this->col( $name, 'datetime' ); 214 | } 215 | 216 | 217 | /** 218 | * Creates a new column of type "datetimetz" or returns the existing one 219 | * 220 | * @param string $name Name of the column 221 | * @return \Aimeos\Upscheme\Schema\Column Column object 222 | */ 223 | public function datetimetz( string $name ) : Column 224 | { 225 | return $this->col( $name, 'datetimetz' ); 226 | } 227 | 228 | 229 | /** 230 | * Creates a new column of type "decimal" or returns the existing one 231 | * 232 | * @param string $name Name of the column 233 | * @param int $digits Total number of decimal digits including decimals 234 | * @param int $decimals Number of digits after the decimal point 235 | * @return \Aimeos\Upscheme\Schema\Column Column object 236 | */ 237 | public function decimal( string $name, int $digits, int $decimals = 2 ) : Column 238 | { 239 | return $this->col( $name, 'decimal' )->precision( $digits )->scale( $decimals )->default( 0 ); 240 | } 241 | 242 | 243 | /** 244 | * Creates a new column of type "float" or returns the existing one 245 | * 246 | * @param string $name Name of the column 247 | * @return \Aimeos\Upscheme\Schema\Column Column object 248 | */ 249 | public function float( string $name ) : Column 250 | { 251 | return $this->col( $name, 'float' )->default( 0 ); 252 | } 253 | 254 | 255 | /** 256 | * Creates a new column of type "guid" or returns the existing one 257 | * 258 | * @param string $name Name of the column 259 | * @return \Aimeos\Upscheme\Schema\Column Column object 260 | */ 261 | public function guid( string $name ) : Column 262 | { 263 | return $this->col( $name, 'guid' ); 264 | } 265 | 266 | 267 | /** 268 | * Creates a new column of type "integer" or returns the existing one 269 | * 270 | * This method is an alias for integer() 271 | * 272 | * @param string $name Name of the column 273 | * @return \Aimeos\Upscheme\Schema\Column Column object 274 | */ 275 | public function int( string $name ) : Column 276 | { 277 | return $this->integer( $name ); 278 | } 279 | 280 | 281 | /** 282 | * Creates a new column of type "integer" or returns the existing one 283 | * 284 | * @param string $name Name of the column 285 | * @return \Aimeos\Upscheme\Schema\Column Column object 286 | */ 287 | public function integer( string $name ) : Column 288 | { 289 | return $this->col( $name, 'integer' ); 290 | } 291 | 292 | 293 | /** 294 | * Creates a new column of type "json" or returns the existing one 295 | * 296 | * @param string $name Name of the column 297 | * @return \Aimeos\Upscheme\Schema\Column Column object 298 | */ 299 | public function json( string $name ) : Column 300 | { 301 | return $this->col( $name, 'json' ); 302 | } 303 | 304 | 305 | /** 306 | * Creates a new column of type "smallint" or returns the existing one 307 | * 308 | * @param string $name Name of the column 309 | * @return \Aimeos\Upscheme\Schema\Column Column object 310 | */ 311 | public function smallint( string $name ) : Column 312 | { 313 | return $this->col( $name, 'smallint' )->default( 0 ); 314 | } 315 | 316 | 317 | /** 318 | * Creates a new column of type "string" or returns the existing one 319 | * 320 | * This type should be used for up to 255 characters. For more characters, 321 | * use the "text" type. 322 | * 323 | * @param string $name Name of the column 324 | * @param int $length Length of the column in characters 325 | * @return \Aimeos\Upscheme\Schema\Column Column object 326 | */ 327 | public function string( string $name, int $length = 255 ) : Column 328 | { 329 | return $this->col( $name, 'string' )->length( $length ); 330 | } 331 | 332 | 333 | /** 334 | * Creates a new column of type "text" or returns the existing one 335 | * 336 | * The maximum length of a "text" column is 2GB. 337 | * 338 | * @param string $name Name of the column 339 | * @param int $length Length of the column in characters 340 | * @return \Aimeos\Upscheme\Schema\Column Column object 341 | */ 342 | public function text( string $name, int $length = 0xffff ) : Column 343 | { 344 | return $this->col( $name, 'text' )->length( $length ); 345 | } 346 | 347 | 348 | /** 349 | * Creates a new column of type "time" or returns the existing one 350 | * 351 | * This datatype is not available when using Oracle databases 352 | * 353 | * @param string $name Name of the column 354 | * @return \Aimeos\Upscheme\Schema\Column Column object 355 | */ 356 | public function time( string $name ) : Column 357 | { 358 | return $this->col( $name, 'time' ); 359 | } 360 | 361 | 362 | /** 363 | * Creates a new column of type "guid" or returns the existing one 364 | * 365 | * This method is an alias for guid() 366 | * 367 | * @param string $name Name of the column 368 | * @return \Aimeos\Upscheme\Schema\Column Column object 369 | */ 370 | public function uuid( string $name ) : Column 371 | { 372 | return $this->guid( $name ); 373 | } 374 | 375 | 376 | /** 377 | * Drops the column given by its name if it exists 378 | * 379 | * @param array|string $name Name of the column or columns 380 | * @return self Same object for fluid method calls 381 | */ 382 | public function dropColumn( $name ) : self 383 | { 384 | foreach( (array) $name as $entry ) 385 | { 386 | if( $this->table->hasColumn( $entry ) ) { 387 | $this->table->dropColumn( $entry ); 388 | } 389 | } 390 | 391 | return $this; 392 | } 393 | 394 | 395 | /** 396 | * Drops the index given by its name if it exists 397 | * 398 | * @param array|string $name Name of the index or indexes 399 | * @return self Same object for fluid method calls 400 | */ 401 | public function dropIndex( $name ) : self 402 | { 403 | foreach( (array) $name as $entry ) 404 | { 405 | if( $this->table->hasIndex( $entry ) ) { 406 | $this->table->dropIndex( $entry ); 407 | } 408 | } 409 | 410 | return $this; 411 | } 412 | 413 | 414 | /** 415 | * Drops the foreign key constraint given by its name if it exists 416 | * 417 | * @param array|string $name Name of the foreign key constraint or constraints 418 | * @return self Same object for fluid method calls 419 | */ 420 | public function dropForeign( $name ) : self 421 | { 422 | foreach( (array) $name as $entry ) 423 | { 424 | if( $this->table->hasForeignKey( $entry ) ) { 425 | $this->table->removeForeignKey( $entry ); 426 | } 427 | } 428 | 429 | return $this; 430 | } 431 | 432 | 433 | /** 434 | * Drops the primary key if it exists 435 | * 436 | * @return self Same object for fluid method calls 437 | */ 438 | public function dropPrimary() : self 439 | { 440 | if( $this->table->getPrimaryKey() ) { 441 | $this->table->dropPrimaryKey(); 442 | } 443 | 444 | return $this; 445 | } 446 | 447 | 448 | /** 449 | * Checks if the column exists 450 | * 451 | * @param array|string $name Name of the column or columns 452 | * @return bool TRUE if the columns exists, FALSE if not 453 | */ 454 | public function hasColumn( $name ) : bool 455 | { 456 | foreach( (array) $name as $entry ) 457 | { 458 | if( !$this->table->hasColumn( $entry ) ) { 459 | return false; 460 | } 461 | } 462 | 463 | return true; 464 | } 465 | 466 | 467 | /** 468 | * Checks if the index exists 469 | * 470 | * @param array|string $name Name of the index or indexes 471 | * @return bool TRUE if the indexes exists, FALSE if not 472 | */ 473 | public function hasIndex( $name ) : bool 474 | { 475 | foreach( (array) $name as $entry ) 476 | { 477 | if( !$this->table->hasIndex( $entry ) ) { 478 | return false; 479 | } 480 | } 481 | 482 | return true; 483 | } 484 | 485 | 486 | /** 487 | * Checks if the foreign key constraint exists 488 | * 489 | * @param array|string $name Name of the foreign key constraint or constraints 490 | * @return bool TRUE if the foreign key constraints exists, FALSE if not 491 | */ 492 | public function hasForeign( $name ) : bool 493 | { 494 | foreach( (array) $name as $entry ) 495 | { 496 | if( !$this->table->hasForeignKey( $entry ) ) { 497 | return false; 498 | } 499 | } 500 | 501 | return true; 502 | } 503 | 504 | 505 | /** 506 | * Creates a new foreign key or returns the existing one 507 | * 508 | * @param array|string $localcolumn Name of the local column or columns 509 | * @param string $foreigntable Name of the referenced table 510 | * @param array|string $foreigncolumn Name of the referenced column or columns 511 | * @param string|null $name Name of the foreign key constraint and foreign key index or NULL for autogenerated name 512 | * @return \Aimeos\Upscheme\Schema\Foreign Foreign key constraint object 513 | */ 514 | public function foreign( $localcolumn, string $foreigntable, $foreigncolumn = 'id', ?string $name = null ) : Foreign 515 | { 516 | $localcolumn = (array) $localcolumn; 517 | $foreigncolumn = (array) $foreigncolumn; 518 | 519 | if( !$this->db->hasTable( $foreigntable ) ) { 520 | throw new \RuntimeException( sprintf( 'Table "%1$s" is missing', $foreigntable ) ); 521 | } 522 | 523 | $table = $this->db->table( $foreigntable ); 524 | 525 | foreach( $foreigncolumn as $idx => $col ) 526 | { 527 | if( !$table->hasColumn( $col ) ) { 528 | throw new \RuntimeException( sprintf( 'Column "%1$s" in table "%2$s" is missing', $col, $table->name() ) ); 529 | } 530 | 531 | if( !isset( $localcolumn[$idx] ) ) { 532 | throw new \LogicException( sprintf( 'No matching local column for foreign column "%1$s" in table "%2$s"', $col, $foreigntable ) ); 533 | } 534 | 535 | $this->copyColumn( $table->getColumn( $col ), $localcolumn[$idx] ); 536 | } 537 | 538 | $name = $name ?: $this->nameIndex( $this->name(), $localcolumn, 'fk' ); 539 | return new Foreign( $this->db, $this, $this->table, $localcolumn, $foreigntable, $foreigncolumn, $name ); 540 | } 541 | 542 | 543 | /** 544 | * Creates a new index or replaces an existing one 545 | * 546 | * @param array|string $columns Name of the columns or columns spawning the index 547 | * @param string|null $name Index name or NULL for autogenerated name 548 | * @param array $flags Name of DB-specific index flags 549 | * @param array $options Associative key/value pairs of DB-specific index options 550 | * @return self Same object for fluid method calls 551 | */ 552 | public function index( $columns, ?string $name = null, array $flags = [], array $options = [] ) : self 553 | { 554 | $columns = (array) $columns; 555 | $name = $name ?: $this->nameIndex( $this->name(), $columns, 'idx' ); 556 | 557 | if( $name && $this->table->hasIndex( $name ) ) 558 | { 559 | if( $this->table->getIndex( $name )->spansColumns( $columns ) ) { 560 | return $this; 561 | } 562 | 563 | $this->table->dropIndex( $name ); 564 | } 565 | 566 | if( $name === null ) 567 | { 568 | foreach( $this->table->getIndexes() as $index ) 569 | { 570 | if( $index->spansColumns( $columns ) ) { 571 | return $this; 572 | } 573 | } 574 | } 575 | 576 | foreach( $columns as $key => $column ) { 577 | $columns[$key] = $this->db->qi( $column ); 578 | } 579 | 580 | $this->table->addIndex( $columns, $name ? $this->db->qi( $name ) : null, $flags, $options ); 581 | return $this; 582 | } 583 | 584 | 585 | /** 586 | * Returns the name of the table 587 | * 588 | * @return string Table name 589 | */ 590 | public function name() : string 591 | { 592 | return $this->table->getName(); 593 | } 594 | 595 | 596 | /** 597 | * Sets a custom schema option or returns the current value 598 | * 599 | * Available custom schema options are: 600 | * - charset (MySQL) 601 | * - collation (MySQL) 602 | * - engine (MySQL) 603 | * - temporary (MySQL) 604 | * 605 | * @param string $name Name of the table-related custom schema option 606 | * @param mixed $value Value of the custom schema option 607 | * @return self|mixed Same object for setting value, current value without second parameter 608 | */ 609 | public function opt( string $name, $value = null ) 610 | { 611 | if( $value === null ) { 612 | return $this->table->hasOption( $name ) ? $this->table->getOption( $name ) : null; 613 | } 614 | 615 | $this->table->addOption( $name, $value ); 616 | return $this; 617 | } 618 | 619 | 620 | /** 621 | * Creates a new primary index or replaces an existing one 622 | * 623 | * @param array|string $columns Name of the columns or columns spawning the index 624 | * @param string|null $name Index name or NULL for autogenerated name 625 | * @return self Same object for fluid method calls 626 | */ 627 | public function primary( $columns, ?string $name = null ) : self 628 | { 629 | $columns = (array) $columns; 630 | $index = $this->table->getPrimaryKey(); 631 | 632 | if( $index && $index->spansColumns( $columns ) ) { 633 | return $this; 634 | } 635 | 636 | if( $index ) { 637 | $this->table->dropPrimaryKey(); 638 | } 639 | 640 | foreach( $columns as $key => $column ) { 641 | $columns[$key] = $this->db->qi( $column ); 642 | } 643 | 644 | $name = $name ?: $this->nameIndex( $this->name(), $columns, 'pk' ); 645 | $this->table->setPrimaryKey( $columns, $name ? $this->db->qi( $name ) : 'primary' ); 646 | return $this; 647 | } 648 | 649 | 650 | /** 651 | * Renames a column or a list of column 652 | * 653 | * @param array|string $from Column name or array of old/new column names 654 | * @param string|null $to New column name or NULL if first parameter is an array 655 | * @return self Same object for fluid method calls 656 | */ 657 | public function renameColumn( $from, ?string $to = null ) : self 658 | { 659 | if( !$this->hasColumn( $from ) ) { 660 | return $this; 661 | } 662 | 663 | $this->db->renameColumn( $this->name(), $from, $to ); 664 | return $this; 665 | } 666 | 667 | 668 | /** 669 | * Renames an index or a list of indexes 670 | * 671 | * @param array|string $from Index name or array of old/new index names (if new index name is NULL, it will be generated) 672 | * @param string|null $to New index name or NULL for autogenerated name (ignored if first parameter is an array) 673 | * @return self Same object for fluid method calls 674 | */ 675 | public function renameIndex( $from, ?string $to = null ) : self 676 | { 677 | if( !is_array( $from ) ) { 678 | $from = [$from => $to]; 679 | } 680 | 681 | foreach( $from as $name => $to ) 682 | { 683 | if( $this->table->hasIndex( $name ) ) 684 | { 685 | if( !$to ) 686 | { 687 | $index = $this->table->getIndex( $name ); 688 | $type = $index->isPrimary() ? 'pk' : ( $index->isUnique() ? 'unq' : 'idx' ); 689 | $to = $this->nameIndex( $this->name(), $index->getColumns(), $type ); 690 | } 691 | 692 | $this->table->renameIndex( $this->db->qi( $name ), $to ? $this->db->qi( $to ) : null ); 693 | } 694 | } 695 | 696 | return $this->up(); 697 | } 698 | 699 | 700 | /** 701 | * Creates a new spatial index or replaces an existing one 702 | * 703 | * @param array|string $columns Name of the columns or columns spawning the index 704 | * @param string|null $name Index name or NULL for autogenerated name 705 | * @return self Same object for fluid method calls 706 | */ 707 | public function spatial( $columns, ?string $name = null ) : self 708 | { 709 | $columns = (array) $columns; 710 | $name = $name ?: $this->nameIndex( $this->name(), $columns, 'idx' ); 711 | 712 | if( $name && $this->table->hasIndex( $name ) ) 713 | { 714 | $index = $this->table->getIndex( $name ); 715 | 716 | if( $index->hasFlag( 'spatial' ) && $index->spansColumns( $columns ) ) { 717 | return $this; 718 | } 719 | 720 | $this->table->dropIndex( $name ); 721 | } 722 | 723 | foreach( $columns as $key => $column ) { 724 | $columns[$key] = $this->db->qi( $column ); 725 | } 726 | 727 | $this->table->addIndex( $columns, $name, ['spatial' => true] ); 728 | return $this; 729 | } 730 | 731 | 732 | /** 733 | * Creates a new unique index or replaces an existing one 734 | * 735 | * @param array|string $columns Name of the columns or columns spawning the index 736 | * @param string|null $name Index name or NULL for autogenerated name 737 | * @return self Same object for fluid method calls 738 | */ 739 | public function unique( $columns, ?string $name = null ) : self 740 | { 741 | $columns = (array) $columns; 742 | $name = $name ?: $this->nameIndex( $this->name(), $columns, 'unq' ); 743 | 744 | if( $name && $this->table->hasIndex( $name ) ) 745 | { 746 | $index = $this->table->getIndex( $name ); 747 | 748 | if( $index->isUnique() && $index->spansColumns( $columns ) ) { 749 | return $this; 750 | } 751 | 752 | $this->table->dropIndex( $name ); 753 | } 754 | 755 | foreach( $columns as $key => $column ) { 756 | $columns[$key] = $this->db->qi( $column ); 757 | } 758 | 759 | $this->table->addUniqueIndex( $columns, $name ? $this->db->qi( $name ) : null ); 760 | return $this; 761 | } 762 | 763 | 764 | /** 765 | * Applies the changes to the database schema 766 | * 767 | * @return self Same object for fluid method calls 768 | */ 769 | public function up() : self 770 | { 771 | $this->db->up(); 772 | return $this; 773 | } 774 | 775 | 776 | /** 777 | * Adds a new column to the table by copying the given column 778 | * 779 | * If the column already exists, the column specification is changed if necessary 780 | * 781 | * @param \Doctrine\DBAL\Schema\Column $column Doctrine column object 782 | * @param string $name New local column name 783 | */ 784 | protected function copyColumn( \Doctrine\DBAL\Schema\Column $column, $name ) : void 785 | { 786 | $options = $column->toArray(); 787 | $custom = array_intersect_key( $options, ['charset' => null, 'collation' => null, 'check' => null] ); 788 | unset( $options['name'], $options['autoincrement'], $options['charset'], $options['collation'], $options['check'] ); 789 | 790 | if( $this->table->hasColumn( $name ) ) { 791 | $this->table->modifyColumn( $this->db->qi( $name ), $options + ['platformOptions' => $custom] ); 792 | } else { 793 | $types = $column->getType()->getTypeRegistry(); 794 | $this->table->addColumn( $this->db->qi( $name ), $types->lookupName( $column->getType() ), $options + ['platformOptions' => $custom] ); 795 | } 796 | } 797 | 798 | 799 | /** 800 | * Returns the name that should be used for the index 801 | * 802 | * Available types are: 803 | * - idx: Regular and spatial indexes 804 | * - fk: Foreign key index 805 | * - pk: Primary key index 806 | * - unq: Unique index 807 | * 808 | * @param string $table Table name 809 | * @param array $columns Column names 810 | * @param string $type Index type 811 | * @return string|null Name of the index or NULL to use the generated name by Doctrine DBAL 812 | */ 813 | protected function nameIndex( string $table, array $columns, string $type = 'idx' ) : ?string 814 | { 815 | if( $fcn = self::macro( 'nameIndex' ) ) { 816 | return $fcn( $table, $columns, $type ); 817 | } 818 | 819 | return null; 820 | } 821 | } -------------------------------------------------------------------------------- /tests/Upscheme/Schema/TableTest.php: -------------------------------------------------------------------------------- 1 | dbmock = $this->getMockBuilder( '\Aimeos\Upscheme\Schema\DB' ) 21 | ->disableOriginalConstructor() 22 | ->getMock(); 23 | 24 | $this->dbmock->expects( $this->any() )->method( 'qi' ) 25 | ->willReturnCallback( function( $value ) { 26 | return '"' . $value . '"'; 27 | } ); 28 | 29 | $methods = [ 30 | 'addIndex', 'getIndex', 'getIndexes', 'hasIndex', 'renameIndex', 'dropIndex', 31 | 'dropPrimaryKey', 'getPrimaryKey', 'setPrimaryKey', 32 | 'addUniqueIndex', 'hasUniqueConstraint', 'removeUniqueConstraint', 33 | 'getName', 'addOption', 'getOption', 'hasOption', 34 | 'dropColumn', 'hasColumn', 35 | 'hasForeignKey', 'removeForeignKey', 36 | ]; 37 | 38 | $this->tablemock = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Table' ) 39 | ->disableOriginalConstructor() 40 | ->onlyMethods( $methods ) 41 | ->getMock(); 42 | 43 | $this->object = new \Aimeos\Upscheme\Schema\Table( $this->dbmock, $this->tablemock ); 44 | } 45 | 46 | 47 | protected function tearDown() : void 48 | { 49 | unset( $this->object, $this->tablemock, $this->dbmock ); 50 | } 51 | 52 | 53 | public function testCall() 54 | { 55 | $this->tablemock->expects( $this->once() )->method( 'hasForeignKey' ); 56 | 57 | $this->object->hasForeignKey( 'unittest' ); 58 | } 59 | 60 | 61 | public function testCallMacro() 62 | { 63 | \Aimeos\Upscheme\Schema\Table::macro( 'unittest', function() { return 'yes'; } ); 64 | 65 | $this->assertEquals( 'yes', $this->object->unittest() ); 66 | } 67 | 68 | 69 | public function testGetMagic() 70 | { 71 | $this->tablemock->expects( $this->once() )->method( 'hasOption' ) 72 | ->willReturn( true ); 73 | 74 | $this->tablemock->expects( $this->once() )->method( 'getOption' ) 75 | ->willReturn( 'yes' ); 76 | 77 | $this->assertEquals( 'yes', $this->object->unittest ); 78 | } 79 | 80 | 81 | public function testSetMagic() 82 | { 83 | $this->tablemock->expects( $this->once() )->method( 'addOption' ); 84 | 85 | $this->object->unittest = 'yes'; 86 | } 87 | 88 | 89 | public function testOptGet() 90 | { 91 | $this->tablemock->expects( $this->once() )->method( 'hasOption' ) 92 | ->willReturn( true ); 93 | 94 | $this->tablemock->expects( $this->once() )->method( 'getOption' ) 95 | ->willReturn( 'yes' ); 96 | 97 | $this->assertEquals( 'yes', $this->object->opt( 'unittest' ) ); 98 | } 99 | 100 | 101 | public function testOptSet() 102 | { 103 | $this->tablemock->expects( $this->once() )->method( 'addOption' ); 104 | 105 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->opt( 'unittest', 'yes' ) ); 106 | } 107 | 108 | 109 | public function testBigid() 110 | { 111 | \Aimeos\Upscheme\Schema\Table::macro( 'bigid', function( string $name = null ) : Column { 112 | return $this->bigint( $name ?: 'id' )->seq( true )->primary(); 113 | } ); 114 | 115 | $this->tablemock->expects( $this->once() )->method( 'getName' )->willReturn( 'test' ); 116 | 117 | $col = $this->object->bigid(); 118 | 119 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 120 | $this->assertEquals( 'id', $col->name() ); 121 | $this->assertEquals( 'bigint', $col->type() ); 122 | $this->assertTrue( $col->seq() ); 123 | } 124 | 125 | 126 | public function testBigidName() 127 | { 128 | \Aimeos\Upscheme\Schema\Table::macro( 'bigid', function( string $name = null ) : Column { 129 | return $this->bigint( $name ?: 'id' )->seq( true )->primary(); 130 | } ); 131 | 132 | $this->tablemock->expects( $this->once() )->method( 'getName' )->willReturn( 'test' ); 133 | 134 | $col = $this->object->bigid( 'uid' ); 135 | 136 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 137 | $this->assertEquals( 'uid', $col->name() ); 138 | $this->assertEquals( 'bigint', $col->type() ); 139 | $this->assertTrue( $col->seq() ); 140 | } 141 | 142 | 143 | public function testBigint() 144 | { 145 | $col = $this->object->bigint( 'unittest' ); 146 | 147 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 148 | $this->assertEquals( 'unittest', $col->name() ); 149 | $this->assertEquals( 'bigint', $col->type() ); 150 | $this->assertEquals( 0, $col->default() ); 151 | } 152 | 153 | 154 | public function testBinary() 155 | { 156 | $col = $this->object->binary( 'unittest', 255 ); 157 | 158 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 159 | $this->assertEquals( 'unittest', $col->name() ); 160 | $this->assertEquals( 'binary', $col->type() ); 161 | $this->assertEquals( 255, $col->length() ); 162 | $this->assertEquals( '', $col->default() ); 163 | } 164 | 165 | 166 | public function testBlob() 167 | { 168 | $col = $this->object->blob( 'unittest', 0x7fff ); 169 | 170 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 171 | $this->assertEquals( 'unittest', $col->name() ); 172 | $this->assertEquals( 'blob', $col->type() ); 173 | $this->assertEquals( 0x7fff, $col->length() ); 174 | $this->assertEquals( '', $col->default() ); 175 | } 176 | 177 | 178 | public function testBool() 179 | { 180 | $col = $this->object->bool( 'unittest' ); 181 | 182 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 183 | $this->assertEquals( 'unittest', $col->name() ); 184 | $this->assertEquals( 'boolean', $col->type() ); 185 | $this->assertEquals( false, $col->default() ); 186 | } 187 | 188 | 189 | public function testBoolean() 190 | { 191 | $col = $this->object->boolean( 'unittest' ); 192 | 193 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 194 | $this->assertEquals( 'unittest', $col->name() ); 195 | $this->assertEquals( 'boolean', $col->type() ); 196 | $this->assertEquals( false, $col->default() ); 197 | } 198 | 199 | 200 | public function testChar() 201 | { 202 | $col = $this->object->char( 'unittest', 3 ); 203 | 204 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 205 | $this->assertEquals( 'unittest', $col->name() ); 206 | $this->assertEquals( 'string', $col->type() ); 207 | $this->assertEquals( true, $col->fixed() ); 208 | $this->assertEquals( 3, $col->length() ); 209 | } 210 | 211 | 212 | public function testCol() 213 | { 214 | $col = $this->object->col( 'unittest', 'integer' ); 215 | 216 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 217 | $this->assertEquals( 'unittest', $col->name() ); 218 | $this->assertEquals( 'integer', $col->type() ); 219 | 220 | 221 | $this->tablemock->expects( $this->any() )->method( 'hasColumn' )->willReturn( true ); 222 | 223 | $col = $this->object->col( 'unittest' ); 224 | 225 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 226 | $this->assertEquals( 'unittest', $col->name() ); 227 | $this->assertEquals( 'integer', $col->type() ); 228 | 229 | 230 | $this->tablemock->expects( $this->any() )->method( 'hasColumn' )->willReturn( true ); 231 | 232 | $col = $this->object->col( 'unittest', 'bigint' ); 233 | 234 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 235 | $this->assertEquals( 'unittest', $col->name() ); 236 | $this->assertEquals( 'bigint', $col->type() ); 237 | } 238 | 239 | 240 | public function testDate() 241 | { 242 | $col = $this->object->date( 'unittest' ); 243 | 244 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 245 | $this->assertEquals( 'unittest', $col->name() ); 246 | $this->assertEquals( 'date', $col->type() ); 247 | } 248 | 249 | 250 | public function testDatetime() 251 | { 252 | $col = $this->object->datetime( 'unittest' ); 253 | 254 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 255 | $this->assertEquals( 'unittest', $col->name() ); 256 | $this->assertEquals( 'datetime', $col->type() ); 257 | } 258 | 259 | 260 | public function testDatetimetz() 261 | { 262 | $col = $this->object->datetimetz( 'unittest' ); 263 | 264 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 265 | $this->assertEquals( 'unittest', $col->name() ); 266 | $this->assertEquals( 'datetimetz', $col->type() ); 267 | } 268 | 269 | 270 | public function testDecimal() 271 | { 272 | $col = $this->object->decimal( 'unittest', 10, 3 ); 273 | 274 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 275 | $this->assertEquals( 'unittest', $col->name() ); 276 | $this->assertEquals( 'decimal', $col->type() ); 277 | $this->assertEquals( 10, $col->precision() ); 278 | $this->assertEquals( 3, $col->scale() ); 279 | $this->assertEquals( 0, $col->default() ); 280 | } 281 | 282 | 283 | public function testFloat() 284 | { 285 | $col = $this->object->float( 'unittest' ); 286 | 287 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 288 | $this->assertEquals( 'unittest', $col->name() ); 289 | $this->assertEquals( 'float', $col->type() ); 290 | $this->assertEquals( 0, $col->default() ); 291 | } 292 | 293 | 294 | public function testGuid() 295 | { 296 | $col = $this->object->guid( 'unittest' ); 297 | 298 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 299 | $this->assertEquals( 'unittest', $col->name() ); 300 | $this->assertEquals( 'guid', $col->type() ); 301 | } 302 | 303 | 304 | public function testId() 305 | { 306 | \Aimeos\Upscheme\Schema\Table::macro( 'id', function( string $name = null ) : Column { 307 | return $this->integer( $name ?: 'id' )->seq( true )->primary(); 308 | } ); 309 | 310 | $this->tablemock->expects( $this->once() )->method( 'getName' )->willReturn( 'test' ); 311 | 312 | $col = $this->object->id(); 313 | 314 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 315 | $this->assertEquals( 'id', $col->name() ); 316 | $this->assertEquals( 'integer', $col->type() ); 317 | $this->assertTrue( $col->seq() ); 318 | } 319 | 320 | 321 | public function testIdName() 322 | { 323 | \Aimeos\Upscheme\Schema\Table::macro( 'id', function( string $name = null ) : Column { 324 | return $this->integer( $name ?: 'id' )->seq( true )->primary(); 325 | } ); 326 | 327 | $this->tablemock->expects( $this->once() )->method( 'getName' )->willReturn( 'test' ); 328 | 329 | $col = $this->object->id( 'uid' ); 330 | 331 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 332 | $this->assertEquals( 'uid', $col->name() ); 333 | $this->assertEquals( 'integer', $col->type() ); 334 | $this->assertTrue( $col->seq() ); 335 | } 336 | 337 | 338 | public function testInt() 339 | { 340 | $col = $this->object->int( 'unittest' ); 341 | 342 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 343 | $this->assertEquals( 'unittest', $col->name() ); 344 | $this->assertEquals( 'integer', $col->type() ); 345 | $this->assertEquals( 0, $col->default() ); 346 | } 347 | 348 | 349 | public function testInteger() 350 | { 351 | $col = $this->object->int( 'unittest' ); 352 | 353 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 354 | $this->assertEquals( 'unittest', $col->name() ); 355 | $this->assertEquals( 'integer', $col->type() ); 356 | $this->assertEquals( 0, $col->default() ); 357 | } 358 | 359 | 360 | public function testJson() 361 | { 362 | $col = $this->object->json( 'unittest' ); 363 | 364 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 365 | $this->assertEquals( 'unittest', $col->name() ); 366 | $this->assertEquals( 'json', $col->type() ); 367 | } 368 | 369 | 370 | public function testName() 371 | { 372 | $this->tablemock->expects( $this->once() )->method( 'getName' ) 373 | ->willReturn( 'unittest' ); 374 | 375 | $this->assertEquals( 'unittest', $this->object->name() ); 376 | } 377 | 378 | 379 | public function testSmallint() 380 | { 381 | $col = $this->object->smallint( 'unittest' ); 382 | 383 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 384 | $this->assertEquals( 'unittest', $col->name() ); 385 | $this->assertEquals( 'smallint', $col->type() ); 386 | $this->assertEquals( 0, $col->default() ); 387 | } 388 | 389 | 390 | public function testString() 391 | { 392 | $col = $this->object->string( 'unittest', 128 ); 393 | 394 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 395 | $this->assertEquals( 'unittest', $col->name() ); 396 | $this->assertEquals( 'string', $col->type() ); 397 | $this->assertEquals( 128, $col->length() ); 398 | $this->assertEquals( '', $col->default() ); 399 | } 400 | 401 | 402 | public function testText() 403 | { 404 | $col = $this->object->text( 'unittest', 0x7fff ); 405 | 406 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 407 | $this->assertEquals( 'unittest', $col->name() ); 408 | $this->assertEquals( 'text', $col->type() ); 409 | $this->assertEquals( 0x7fff, $col->length() ); 410 | $this->assertEquals( '', $col->default() ); 411 | } 412 | 413 | 414 | public function testTime() 415 | { 416 | $col = $this->object->time( 'unittest' ); 417 | 418 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 419 | $this->assertEquals( 'unittest', $col->name() ); 420 | $this->assertEquals( 'time', $col->type() ); 421 | } 422 | 423 | 424 | public function testUuid() 425 | { 426 | $col = $this->object->uuid( 'unittest' ); 427 | 428 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Column::class, $col ); 429 | $this->assertEquals( 'unittest', $col->name() ); 430 | $this->assertEquals( 'guid', $col->type() ); 431 | } 432 | 433 | 434 | public function testDropColumn() 435 | { 436 | $this->tablemock->expects( $this->once() )->method( 'hasColumn' )->willReturn( true ); 437 | $this->tablemock->expects( $this->once() )->method( 'dropColumn' ); 438 | 439 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->dropColumn( 'unittest' ) ); 440 | } 441 | 442 | 443 | public function testDropIndex() 444 | { 445 | $this->tablemock->expects( $this->once() )->method( 'hasIndex' )->willReturn( true ); 446 | $this->tablemock->expects( $this->once() )->method( 'dropIndex' ); 447 | 448 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->dropIndex( 'unittest' ) ); 449 | } 450 | 451 | 452 | public function testDropForeign() 453 | { 454 | $this->tablemock->expects( $this->once() )->method( 'hasForeignKey' )->willReturn( true ); 455 | $this->tablemock->expects( $this->once() )->method( 'removeForeignKey' ); 456 | 457 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->dropForeign( 'unittest' ) ); 458 | } 459 | 460 | 461 | public function testDropPrimary() 462 | { 463 | $idx = new \Doctrine\DBAL\Schema\Index( 'PRIMARY', ['id'], true, true ); 464 | $this->tablemock->expects( $this->once() )->method( 'getPrimaryKey' )->willReturn( $idx ); 465 | $this->tablemock->expects( $this->once() )->method( 'dropPrimaryKey' ); 466 | 467 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->dropPrimary( 'unittest' ) ); 468 | } 469 | 470 | 471 | public function testHasColumn() 472 | { 473 | $this->tablemock->expects( $this->once() )->method( 'hasColumn' )->willReturn( true ); 474 | $this->assertTrue( $this->object->hasColumn( 'unittest' ) ); 475 | } 476 | 477 | 478 | public function testHasColumnNot() 479 | { 480 | $this->tablemock->expects( $this->once() )->method( 'hasColumn' )->willReturn( false ); 481 | $this->assertFalse( $this->object->hasColumn( 'unittest' ) ); 482 | } 483 | 484 | 485 | public function testHasColumnMultiple() 486 | { 487 | $this->tablemock->expects( $this->exactly( 2 ) )->method( 'hasColumn' )->willReturn( true ); 488 | $this->assertTrue( $this->object->hasColumn( ['unittest', 'testunit'] ) ); 489 | } 490 | 491 | 492 | public function testHasIndex() 493 | { 494 | $this->tablemock->expects( $this->once() )->method( 'hasIndex' )->willReturn( true ); 495 | $this->assertTrue( $this->object->hasIndex( 'unittest' ) ); 496 | } 497 | 498 | 499 | public function testHasIndexNot() 500 | { 501 | $this->tablemock->expects( $this->once() )->method( 'hasIndex' )->willReturn( false ); 502 | $this->assertFalse( $this->object->hasIndex( 'unittest' ) ); 503 | } 504 | 505 | 506 | public function testHasIndexMultiple() 507 | { 508 | $this->tablemock->expects( $this->exactly( 2 ) )->method( 'hasIndex' )->willReturn( true ); 509 | $this->assertTrue( $this->object->hasIndex( ['unittest', 'testunit'] ) ); 510 | } 511 | 512 | 513 | public function testHasForeign() 514 | { 515 | $this->tablemock->expects( $this->once() )->method( 'hasForeignKey' )->willReturn( true ); 516 | $this->assertTrue( $this->object->hasForeign( 'unittest' ) ); 517 | } 518 | 519 | 520 | public function testHasForeignNot() 521 | { 522 | $this->tablemock->expects( $this->once() )->method( 'hasForeignKey' )->willReturn( false ); 523 | $this->assertFalse( $this->object->hasForeign( 'unittest' ) ); 524 | } 525 | 526 | 527 | public function testHasForeignMultiple() 528 | { 529 | $this->tablemock->expects( $this->exactly( 2 ) )->method( 'hasForeignKey' )->willReturn( true ); 530 | $this->assertTrue( $this->object->hasForeign( ['unittest', 'testunit'] ) ); 531 | } 532 | 533 | 534 | public function testForeign() 535 | { 536 | $dbalcol = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Column' ) 537 | ->disableOriginalConstructor() 538 | ->getMock(); 539 | 540 | $dbaltable = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Table' ) 541 | ->disableOriginalConstructor() 542 | ->getMock(); 543 | 544 | $table = $this->getMockBuilder( '\Aimeos\Upscheme\Schema\Table' ) 545 | ->onlyMethods( ['copyColumn', 'hasColumn'] ) 546 | ->setConstructorArgs( [$this->dbmock, $dbaltable] ) 547 | ->getMock(); 548 | 549 | $table->expects( $this->once() )->method( 'copyColumn' ); 550 | $table->expects( $this->once() )->method( 'hasColumn' )->willReturn( true ); 551 | $dbaltable->expects( $this->once() )->method( 'getColumn' )->willReturn( $dbalcol ); 552 | 553 | $this->dbmock->expects( $this->once() )->method( 'hasTable' )->willReturn( true ); 554 | $this->dbmock->expects( $this->once() )->method( 'table' )->willReturn( $table ); 555 | 556 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Foreign::class, $table->foreign( 'pid', 'fktable', 'id', 'fk_pid' ) ); 557 | } 558 | 559 | 560 | public function testForeignTableMissing() 561 | { 562 | $this->expectException( 'RuntimeException' ); 563 | $this->object->foreign( 'pid', 'fktable', 'id', 'fk_pid' ); 564 | } 565 | 566 | 567 | public function testForeignColumnMissing() 568 | { 569 | $this->dbmock->expects( $this->once() )->method( 'hasTable' )->willReturn( true ); 570 | 571 | $this->expectException( 'RuntimeException' ); 572 | $this->object->foreign( 'pid', 'fktable', 'id', 'fk_pid' ); 573 | } 574 | 575 | 576 | public function testForeignLocalcolMissing() 577 | { 578 | $dbalcol = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Column' ) 579 | ->disableOriginalConstructor() 580 | ->getMock(); 581 | 582 | $dbaltable = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Table' ) 583 | ->disableOriginalConstructor() 584 | ->getMock(); 585 | 586 | $table = $this->getMockBuilder( '\Aimeos\Upscheme\Schema\Table' ) 587 | ->onlyMethods( ['copyColumn', 'hasColumn'] ) 588 | ->setConstructorArgs( [$this->dbmock, $dbaltable] ) 589 | ->getMock(); 590 | 591 | $table->expects( $this->once() )->method( 'copyColumn' ); 592 | $table->expects( $this->exactly( 2 ) )->method( 'hasColumn' )->willReturn( true ); 593 | $dbaltable->expects( $this->once() )->method( 'getColumn' )->willReturn( $dbalcol ); 594 | 595 | $this->dbmock->expects( $this->once() )->method( 'hasTable' )->willReturn( true ); 596 | $this->dbmock->expects( $this->once() )->method( 'table' )->willReturn( $table ); 597 | 598 | $this->expectException( 'LogicException' ); 599 | $table->foreign( ['pid'], 'fktable', ['id', 'sid'], 'fk_pid' ); 600 | } 601 | 602 | 603 | public function testIndex() 604 | { 605 | $this->tablemock->expects( $this->once() )->method( 'addIndex' ); 606 | $this->tablemock->expects( $this->once() )->method( 'getName' )->willReturn( 'test' ); 607 | $this->tablemock->expects( $this->once() )->method( 'getIndexes' )->willReturn( [] ); 608 | 609 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->index( 'pid' ) ); 610 | } 611 | 612 | 613 | public function testIndexName() 614 | { 615 | $this->tablemock->expects( $this->once() )->method( 'addIndex' ); 616 | 617 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->index( 'pid', 'idx_pid' ) ); 618 | } 619 | 620 | 621 | public function testIndexExists() 622 | { 623 | $idxmock = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Index' ) 624 | ->onlyMethods( ['getColumns'] ) 625 | ->disableOriginalConstructor() 626 | ->getMock(); 627 | 628 | $idxmock->expects( $this->once() )->method( 'getColumns' )->willReturn( ['pid'] ); 629 | $this->tablemock->expects( $this->once() )->method( 'getName' )->willReturn( 'test' ); 630 | $this->tablemock->expects( $this->once() )->method( 'getIndexes' )->willReturn( [$idxmock] ); 631 | 632 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->index( 'pid' ) ); 633 | } 634 | 635 | 636 | public function testIndexExistsName() 637 | { 638 | $idxmock = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Index' ) 639 | ->onlyMethods( ['spansColumns'] ) 640 | ->disableOriginalConstructor() 641 | ->getMock(); 642 | 643 | $idxmock->expects( $this->once() )->method( 'spansColumns' )->willReturn( true ); 644 | $this->tablemock->expects( $this->once() )->method( 'getIndex' )->willReturn( $idxmock ); 645 | $this->tablemock->expects( $this->once() )->method( 'hasIndex' )->willReturn( true ); 646 | 647 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->index( 'pid', 'idx_pid' ) ); 648 | } 649 | 650 | 651 | public function testIndexChange() 652 | { 653 | $idxmock = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Index' ) 654 | ->onlyMethods( ['spansColumns'] ) 655 | ->disableOriginalConstructor() 656 | ->getMock(); 657 | 658 | $idxmock->expects( $this->once() )->method( 'spansColumns' )->willReturn( false ); 659 | $this->tablemock->expects( $this->once() )->method( 'getIndex' )->willReturn( $idxmock ); 660 | $this->tablemock->expects( $this->once() )->method( 'hasIndex' )->willReturn( true ); 661 | $this->tablemock->expects( $this->once() )->method( 'dropIndex' ); 662 | $this->tablemock->expects( $this->once() )->method( 'addIndex' ); 663 | 664 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->index( 'pid', 'idx_pid' ) ); 665 | } 666 | 667 | 668 | public function testPrimary() 669 | { 670 | $this->tablemock->expects( $this->once() )->method( 'setPrimaryKey' ); 671 | $this->tablemock->expects( $this->once() )->method( 'getName' )->willReturn( 'test' ); 672 | 673 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->primary( 'id' ) ); 674 | } 675 | 676 | 677 | public function testPrimaryName() 678 | { 679 | $this->tablemock->expects( $this->once() )->method( 'setPrimaryKey' ); 680 | 681 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->primary( 'id', 'pk_id' ) ); 682 | } 683 | 684 | 685 | public function testPrimaryExists() 686 | { 687 | $idxmock = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Index' ) 688 | ->onlyMethods( ['spansColumns'] ) 689 | ->disableOriginalConstructor() 690 | ->getMock(); 691 | 692 | $idxmock->expects( $this->once() )->method( 'spansColumns' )->willReturn( true ); 693 | $this->tablemock->expects( $this->once() )->method( 'getPrimaryKey' )->willReturn( $idxmock ); 694 | 695 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->primary( 'id', 'pk_id' ) ); 696 | } 697 | 698 | 699 | public function testPrimaryChange() 700 | { 701 | $idxmock = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Index' ) 702 | ->onlyMethods( ['spansColumns'] ) 703 | ->disableOriginalConstructor() 704 | ->getMock(); 705 | 706 | $idxmock->expects( $this->once() )->method( 'spansColumns' )->willReturn( false ); 707 | $this->tablemock->expects( $this->once() )->method( 'getPrimaryKey' )->willReturn( $idxmock ); 708 | $this->tablemock->expects( $this->once() )->method( 'dropPrimaryKey' ); 709 | $this->tablemock->expects( $this->once() )->method( 'setPrimaryKey' ); 710 | 711 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->primary( 'id', 'pk_id' ) ); 712 | } 713 | 714 | 715 | public function testRenameIndex() 716 | { 717 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->renameIndex( 'idx_test' ) ); 718 | } 719 | 720 | 721 | public function testRenameIndexExists() 722 | { 723 | $this->tablemock->expects( $this->once() )->method( 'hasIndex' )->willReturn( true ); 724 | $this->tablemock->expects( $this->once() )->method( 'renameIndex' ); 725 | 726 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->renameIndex( 'idx_t1', 'idx_t2' ) ); 727 | } 728 | 729 | 730 | public function testRenameIndexMultiple() 731 | { 732 | $this->tablemock->expects( $this->exactly( 2 ) )->method( 'hasIndex' )->willReturn( true ); 733 | $this->tablemock->expects( $this->exactly( 2 ) )->method( 'renameIndex' ); 734 | 735 | $idx = ['idx_t1' => 'idx_t2', 'idx_t3' => 'idx_t4']; 736 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->renameIndex( $idx ) ); 737 | } 738 | 739 | 740 | public function testRenameIndexName() 741 | { 742 | $idxmock = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Index' ) 743 | ->onlyMethods( ['getColumns'] ) 744 | ->disableOriginalConstructor() 745 | ->getMock(); 746 | 747 | $idxmock->expects( $this->once() )->method( 'getColumns' )->willReturn( ['a', 'b'] ); 748 | $this->tablemock->expects( $this->once() )->method( 'getIndex' )->willReturn( $idxmock ); 749 | $this->tablemock->expects( $this->once() )->method( 'hasIndex' )->willReturn( true ); 750 | $this->tablemock->expects( $this->once() )->method( 'getName' )->willReturn( 'test' ); 751 | $this->tablemock->expects( $this->once() )->method( 'renameIndex' ); 752 | 753 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->renameIndex( 'idx_test' ) ); 754 | } 755 | 756 | 757 | public function testRenameColumn() 758 | { 759 | $this->tablemock->expects( $this->once() )->method( 'hasColumn' )->willReturn( true ); 760 | $this->tablemock->expects( $this->once() )->method( 'getName' )->willReturn( 'test' ); 761 | $this->dbmock->expects( $this->once() )->method( 'renameColumn' ); 762 | 763 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->renameColumn( 'test' ) ); 764 | } 765 | 766 | 767 | public function testSpatial() 768 | { 769 | $this->tablemock->expects( $this->once() )->method( 'addIndex' ); 770 | $this->tablemock->expects( $this->once() )->method( 'getName' )->willReturn( 'test' ); 771 | 772 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->spatial( 'pid' ) ); 773 | } 774 | 775 | 776 | public function testSpatialName() 777 | { 778 | $this->tablemock->expects( $this->once() )->method( 'addIndex' ); 779 | 780 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->spatial( 'pid', 'idx_pid' ) ); 781 | } 782 | 783 | 784 | public function testSpatialExists() 785 | { 786 | $idxmock = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Index' ) 787 | ->onlyMethods( ['hasFlag', 'spansColumns'] ) 788 | ->disableOriginalConstructor() 789 | ->getMock(); 790 | 791 | $idxmock->expects( $this->once() )->method( 'hasFlag' )->willReturn( true ); 792 | $idxmock->expects( $this->once() )->method( 'spansColumns' )->willReturn( true ); 793 | $this->tablemock->expects( $this->once() )->method( 'getIndex' )->willReturn( $idxmock ); 794 | $this->tablemock->expects( $this->once() )->method( 'hasIndex' )->willReturn( true ); 795 | 796 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->spatial( 'pid', 'idx_pid' ) ); 797 | } 798 | 799 | 800 | public function testSpatialChange() 801 | { 802 | $idxmock = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Index' ) 803 | ->onlyMethods( ['hasFlag', 'spansColumns'] ) 804 | ->disableOriginalConstructor() 805 | ->getMock(); 806 | 807 | $idxmock->expects( $this->once() )->method( 'hasFlag' )->willReturn( true ); 808 | $idxmock->expects( $this->once() )->method( 'spansColumns' )->willReturn( false ); 809 | $this->tablemock->expects( $this->once() )->method( 'getIndex' )->willReturn( $idxmock ); 810 | $this->tablemock->expects( $this->once() )->method( 'hasIndex' )->willReturn( true ); 811 | $this->tablemock->expects( $this->once() )->method( 'dropIndex' ); 812 | $this->tablemock->expects( $this->once() )->method( 'addIndex' ); 813 | 814 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->spatial( 'pid', 'idx_pid' ) ); 815 | } 816 | 817 | 818 | public function testUnique() 819 | { 820 | $this->tablemock->expects( $this->once() )->method( 'addUniqueIndex' ); 821 | $this->tablemock->expects( $this->once() )->method( 'getName' )->willReturn( 'test' ); 822 | 823 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->unique( 'pid' ) ); 824 | } 825 | 826 | 827 | public function testUniqueName() 828 | { 829 | $this->tablemock->expects( $this->once() )->method( 'addUniqueIndex' ); 830 | 831 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->unique( 'pid', 'unq_pid' ) ); 832 | } 833 | 834 | 835 | public function testUniqueExists() 836 | { 837 | $idxmock = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Index' ) 838 | ->onlyMethods( ['isUnique', 'spansColumns'] ) 839 | ->disableOriginalConstructor() 840 | ->getMock(); 841 | 842 | $idxmock->expects( $this->once() )->method( 'isUnique' )->willReturn( true ); 843 | $idxmock->expects( $this->once() )->method( 'spansColumns' )->willReturn( true ); 844 | $this->tablemock->expects( $this->once() )->method( 'getIndex' )->willReturn( $idxmock ); 845 | $this->tablemock->expects( $this->once() )->method( 'hasIndex' )->willReturn( true ); 846 | 847 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->unique( 'pid', 'unq_pid' ) ); 848 | } 849 | 850 | 851 | public function testUniqueChange() 852 | { 853 | $idxmock = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Index' ) 854 | ->onlyMethods( ['isUnique', 'spansColumns'] ) 855 | ->disableOriginalConstructor() 856 | ->getMock(); 857 | 858 | $idxmock->expects( $this->once() )->method( 'isUnique' )->willReturn( true ); 859 | $idxmock->expects( $this->once() )->method( 'spansColumns' )->willReturn( false ); 860 | $this->tablemock->expects( $this->once() )->method( 'getIndex' )->willReturn( $idxmock ); 861 | $this->tablemock->expects( $this->once() )->method( 'hasIndex' )->willReturn( true ); 862 | $this->tablemock->expects( $this->once() )->method( 'dropIndex' ); 863 | $this->tablemock->expects( $this->once() )->method( 'addUniqueIndex' ); 864 | 865 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->unique( 'pid', 'unq_pid' ) ); 866 | } 867 | 868 | 869 | public function testUp() 870 | { 871 | $this->dbmock->expects( $this->once() )->method( 'up' ); 872 | 873 | $this->assertInstanceOf( \Aimeos\Upscheme\Schema\Table::class, $this->object->up() ); 874 | } 875 | 876 | 877 | public function testCopyColumn() 878 | { 879 | $dbalcol = new \Doctrine\DBAL\Schema\Column( 'unittest2', \Doctrine\DBAL\Types\Type::getType( 'string' ) ); 880 | 881 | $dbaltable = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Table' ) 882 | ->disableOriginalConstructor() 883 | ->onlyMethods( ['addColumn'] ) 884 | ->getMock(); 885 | 886 | $dbaltable->expects( $this->once() )->method( 'addColumn' ); 887 | 888 | $object = new \Aimeos\Upscheme\Schema\Table( $this->dbmock, $dbaltable ); 889 | 890 | $this->access( 'copyColumn' )->invokeArgs( $object, [$dbalcol, 'unittest'] ); 891 | } 892 | 893 | 894 | public function testCopyColumnExisting() 895 | { 896 | $dbalcol = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Column' ) 897 | ->disableOriginalConstructor() 898 | ->onlyMethods( ['toArray'] ) 899 | ->getMock(); 900 | 901 | $dbaltable = $this->getMockBuilder( '\Doctrine\DBAL\Schema\Table' ) 902 | ->onlyMethods( ['modifyColumn', 'hasColumn'] ) 903 | ->disableOriginalConstructor() 904 | ->getMock(); 905 | 906 | $dbalcol->expects( $this->once() )->method( 'toArray' )->willReturn( [] ); 907 | $dbaltable->expects( $this->once() )->method( 'hasColumn' )->willReturn( true ); 908 | $dbaltable->expects( $this->once() )->method( 'modifyColumn' ); 909 | 910 | $object = new \Aimeos\Upscheme\Schema\Table( $this->dbmock, $dbaltable ); 911 | 912 | $this->access( 'copyColumn' )->invokeArgs( $object, [$dbalcol, 'unittest'] ); 913 | } 914 | 915 | 916 | protected function access( $name ) 917 | { 918 | $class = new \ReflectionClass( \Aimeos\Upscheme\Schema\Table::class ); 919 | $method = $class->getMethod( $name ); 920 | $method->setAccessible( true ); 921 | 922 | return $method; 923 | } 924 | } 925 | --------------------------------------------------------------------------------