├── .gitignore ├── .travis.yml ├── BUGS ├── CONTRIBUTING.md ├── Changes ├── DOCKER.md ├── Dockerfile ├── INSTALL.md ├── LICENSE ├── Makefile.docker ├── Makefile.test ├── README.md ├── bin └── mysqldiff ├── compose-mysqldiff-tests.env ├── dist.ini ├── docker-aliases.sh ├── docker-compose.yml ├── lib └── MySQL │ ├── Diff.pm │ └── Diff │ ├── Database.pm │ ├── Table.pm │ └── Utils.pm ├── t ├── 01use.t ├── all.t ├── regression-rt-77002.t └── regression-rt-79976.t └── xt ├── 90podtest.t ├── 91podcover.t ├── 94metatest.t ├── 95kwalitee.t └── 96perl_minimum_version.t /.gitignore: -------------------------------------------------------------------------------- 1 | MySQL-Diff-* 2 | mysqldiff-*.tar.gz 3 | 4 | blib* 5 | Makefile 6 | Makefile.old 7 | Build 8 | _build* 9 | pm_to_blib* 10 | *.tar.gz 11 | .lwpcookies 12 | cover_db 13 | Build.bat 14 | *.tmp 15 | 16 | META.yml 17 | MYMETA.yml 18 | MYMETA.json 19 | Debian_CPANTS.txt 20 | debug.log 21 | TAGS 22 | t/logs/ 23 | 24 | *.old 25 | *.swp 26 | 27 | .build -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | branches: 2 | except: 3 | - gh-pages 4 | language: perl 5 | perl: 6 | - "5.14" 7 | - "5.16" 8 | - "5.18" 9 | - "5.20" 10 | - "5.22" 11 | - "5.24" 12 | 13 | install: 14 | - cpanm Dist::Zilla 15 | - dzil authordeps --missing | cpanm 16 | - dzil listdeps --missing | cpanm 17 | - dzil build 18 | 19 | script: "dzil test" 20 | -------------------------------------------------------------------------------- /BUGS: -------------------------------------------------------------------------------- 1 | Please see: https://rt.cpan.org/Public/Dist/Display.html?Name=MySQL-Diff 2 | which supercedes this file. 3 | 4 | Reported by users but unconfirmed: 5 | ================================== 6 | 7 | * If '-P1 3337' is one of the arguments it doesn't seem to get 8 | passed to the arguments for mysqldump (according to the 9 | debugging). [Darrell Taylor] 10 | 11 | Others 12 | ====== 13 | 14 | * You can't specify which database to connect to for creating 15 | temporary tables. 16 | 17 | * Things probably break if you use --password or 18 | -p without a parameter. 19 | 20 | * The remote authentication code is barely tested, and hence 21 | probably broken. 22 | 23 | All easy to fix but I'm so short on time! Patches welcome ... 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Dockerfile 2 | 3 | The Dockerfile provided is intended for development and running testings locally. See 4 | [`DOCKER.md`](DOCKER.md) 5 | 6 | # Good reading 7 | 8 | - https://www.igvita.com/2011/12/19/dont-push-your-pull-requests/ 9 | - http://blog.adamspiers.org/2012/11/10/7-principles-for-contributing-patches-to-software-projects/ 10 | 11 | # NOTE: All changes must add/update tests (NO EXCEPTIONS) 12 | 13 | All new features should include new unit or integration tests to exercise them thoroughly. 14 | 15 | If fixing a bug, please add a regression test. 16 | 17 | # How to contribute 18 | 19 | Contributing is easy. 20 | 21 | First check your issue hasn't already been reported on 22 | [CPAN](https://rt.cpan.org/Public/Dist/Display.html?Name=MySQL-Diff) 23 | or [GitHub](https://github.com/aspiers/mysqldiff/issues). Then 24 | proceed appropriately: 25 | 26 | 1. [File a new issue](https://github.com/aspiers/mysqldiff/issues/new) 27 | 2. Fork the main repo 28 | 3. Create an issue branch, e.g., "Issue-XX-blah-foo-derp" 29 | 4. Make commits of logical units (see below). 30 | 5. Check for unnecessary whitespace with `git diff --check` before committing. 31 | 6. Make sure your commit messages summarize your changes well enough. 32 | 7. Make sure you have added the necessary tests for your changes. 33 | 8. Issue a proper pull request. 34 | 35 | # Commits, pull request, and commit message format 36 | 37 | See [this page](https://wiki.openstack.org/wiki/GitCommitMessages#Structural_split_of_changes) 38 | for some excellent advice on structuring commits correctly. 39 | 40 | 1. Please squash commits which relate to the same thing. 41 | 2. Do not mix new features together, or with bug fixes. 42 | 3. Use a structured commit messsage. 43 | 44 | For example, 45 | 46 | Fixed the foobar bug with the flim-flam. 47 | 48 | Issue-XX: Made changes to the flux memristor so 49 | that the space time continuum would remain consisitent 50 | for the key constraint mechanism. Added a few unit tests 51 | to ensure the inversion of time remained consistent in 52 | all past and future versions of this utility. 53 | 54 | # Using Dist::Zilla 55 | 56 | This module uses Dist::Zilla to manage releases. Please see `./dist.ini`; 57 | 58 | To roll a build; 59 | 60 | 1. Bump version number in dist.ini 61 | 2. Bump $VERSION in all .pm files 62 | 3. Run `dzil clean && dzil test && dzil build` 63 | 4. To push a release to CPAN, `dzil release` (but please ask a committer first). 64 | 65 | # Questions 66 | 67 | When in doubt, issue a pull request. Feel free to email B. Estrade . 68 | -------------------------------------------------------------------------------- /Changes: -------------------------------------------------------------------------------- 1 | # Changes log for Test::CPAN::Meta 2 | Unreleased 3 | - added "--keep-old-columns" option that disables emitting DROP COLUMN 4 | commands 5 | 6 | 0.50 21 July 2016 7 | - fix bug with 0.49's new feature (SilentScope) 8 | - added integration test (SilentScope) 9 | 10 | 0.49 15 July 2016 11 | - added "--single-transaction" option for passing to mysqldump (SilentScope) 12 | 13 | 0.48 27 June 2016 14 | - added support for AUTO_INCREMENT fields (mikeraynham) 15 | - fixed Build.PL conflict making smoker tests fail 16 | 17 | 0.46 21th June 2016 18 | - Bug fixes 19 | - optimized the --table-re option to exclude 20 | tables before comparing them as this would 21 | cause significant slowdowns in databases 22 | with lots of tables. (bdraco) 23 | 24 | 0.43 6th October 2011 25 | - fix missing fields in CPAN meta-data 26 | 27 | 0.43 6th October 2011 28 | - depend on Perl 5.6 29 | - improve docs and CPAN meta-data 30 | 31 | 0.41 5th October 2011 32 | - tidy up POD 33 | 34 | 0.40 5th October 2011 35 | - fix issue with hyphens in database names 36 | - made --tolerant ignore COLLATE and AUTO_INCREMENT 37 | - fixed 'Duplicate specification' options from Getopt::Long 38 | - made CLI options case-sensitive 39 | - fixed some coding style inconsistencies 40 | - remove .cvsignore 41 | - merged changes by Barbie 42 | - removed use of unmaintained Class::MakeMethods 43 | - repackaged distribution with additional package files 44 | - restructured modules to use namespace MySQL::Diff::* 45 | - restructured modules to use better OO style inferface 46 | - Utils.pm now only contains debug handling 47 | - added support for more recent MySQL dumps 48 | - added more documentation 49 | - added more tests 50 | - merged changes by Alexandr Ciornii 51 | - depend on Perl 5.5.3 52 | - remove lib/MySQL/.cvsignore 53 | - fix .gitignore 54 | - upgrade Makefile.PL and Build.PL 55 | 56 | 0.33 8th May 2003 57 | - see ChangeLog.OLD for previous changes. 58 | -------------------------------------------------------------------------------- /DOCKER.md: -------------------------------------------------------------------------------- 1 | The `Dockerfile` is provided as the basis for development and running 2 | a thorough set of tests. 3 | 4 | The general workflow is to use a combination of the `Dockerfile` and the 5 | `docker-compose.yml` file. 6 | 7 | Base Image Information 8 | ---------------------- 9 | 10 | The current image is builting using Alpine and associated `perl` packages 11 | and other development packages. 12 | 13 | The database engine is the one most recently installed by Alpine's package 14 | manager `apk` under the name `mariadb`. 15 | 16 | All Perl modules required by `MySQL::Diff` are installed, but `MySQL::Diff` 17 | is not. Instead, the building of the Docker image is expected to be executed 18 | and run from inside of the top level of the upstream git repository. 19 | 20 | Given this assumption, the current working directory on the host computer is 21 | mounted as `/home/test/git/mysqldiff`. 22 | 23 | NOTE: This container is not intended to roll releases using `Dist::Zilla` - the 24 | build would take a very long time and the image size would not worth it. 25 | 26 | Building the Docker Image 27 | ------------------------- 28 | 29 | The `Makefile.docker` contains that actual `docker build` command, but to 30 | run it: 31 | 32 | $ make -f ./Makefile.docker 33 | 34 | This will run a while. 35 | 36 | Starting the Container 37 | ---------------------- 38 | 39 | Using `docker-compose`, launch the container in the background. To see what's 40 | happing, inspect the `docker-compose.yml` file: 41 | 42 | $ docker-compose up -d 43 | 44 | See the container running (will be named `mysqldiff`): 45 | 46 | $ docker ps 47 | 48 | Running Tests 49 | ------------- 50 | 51 | The following command will enter a running container named `mysqldiff` as the 52 | `test` user and run the test suite: 53 | 54 | $ make -f ./Makefile.docker test 55 | 56 | Interactive Container Access 57 | ---------------------------- 58 | 59 | $ make -f ./Makefile.docker shell 60 | 61 | 62 | Building a Release Container 63 | ---------------------------- 64 | 65 | The only thing one must do is install `Dist::Zilla` while on the running container 66 | as root: 67 | 68 | $ docker exec -it mysqldiff sh 69 | (as root on container)$ cpanm Dist::Zilla 70 | 71 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM yobasystems/alpine-mariadb 2 | 3 | # from https://github.com/scottw/alpine-perl/blob/master/Dockerfile 4 | RUN apk update && apk upgrade && apk add alpine-sdk curl tar make gcc build-base wget gnupg vim 5 | RUN apk add perl perl-utils perl-dev mariadb-dev 6 | RUN curl -LO https://raw.githubusercontent.com/miyagawa/cpanminus/master/cpanm && \ 7 | chmod +x cpanm && \ 8 | ./cpanm App::cpanminus && \ 9 | rm -fr ./cpanm /root/.cpanm /usr/src/perl 10 | 11 | ## from tianon/perl 12 | ENV PERL_CPANM_OPT --verbose --mirror https://cpan.metacpan.org --mirror-only 13 | RUN cpanm Digest::SHA Module::Signature && rm -rf ~/.cpanm 14 | ENV PERL_CPANM_OPT $PERL_CPANM_OPT --verify 15 | 16 | # mysqdiff prereq 17 | RUN cpanm String::ShellQuote File::Slurp 18 | 19 | # adding this because someone will ask 20 | RUN cpanm --noverify Mock::Config 21 | RUN cpanm DBD::Mock Mock::Config DBD::mysql DBI DBI::Shell 22 | 23 | # add test user 24 | RUN adduser \ 25 | --disabled-password \ 26 | --gecos "" \ 27 | --home /home/test \ 28 | test 29 | 30 | # switch to 'test' user, create director for later mounting 31 | USER test 32 | RUN mkdir -p /home/test/git/mysqldiff 33 | WORKDIR /home/test 34 | 35 | # switch to root so entrypoint can run as root 36 | USER root 37 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | #Installation instructions 2 | 3 | First please consult the [README](README.md) to check that you have a new enough 4 | version of Perl. 5 | 6 | (N.B. the rest of this document looks a great deal more complicated 7 | than it actually is, mainly because I'm trying to encourage people to 8 | do the Right Things by using CPANPLUS instead of CPAN, and 9 | Module::Build instead of ExtUtils::MakeMaker.) 10 | 11 | 12 | "Automatic" installation via CPANPLUS.pm or CPAN.pm 13 | ========================================================================= 14 | 15 | Installation from either of the recommended installers can be performed at the 16 | command line, with either of the two following commands: 17 | 18 | $ perl -MCPANPLUS -e 'install MySQL::Diff' 19 | 20 | $ perl -MCPAN -e 'install MySQL::Diff' 21 | 22 | Although CPAN.pm is the default installer for many, with the release of Perl 23 | 5.10, CPANPLUS.pm is now also available in core. However, if you use an earlier 24 | version of Perl, you can install CPANPLUS from the CPAN with the following 25 | command: 26 | 27 | $ perl -MCPAN -e 'install CPANPLUS' 28 | 29 | 30 | "Manual" installation 31 | ========================================================================= 32 | 33 | First ensure you have `File::Slurp` installed. Install also 34 | `Dist::Zilla` via 35 | 36 | cpanm install Dist::Zilla 37 | 38 | Install dependencies with 39 | 40 | dzil authordeps --missing | cpanm 41 | 42 | And then 43 | 44 | dzil listdeps --missing | cpanm 45 | 46 | Build and test 47 | 48 | dzil build 49 | dzil test 50 | 51 | Please bear in mind that this module needs a working Mysql 52 | installation; those tests needing it will be skipped if it is not 53 | present. 54 | 55 | And if everything is OK, 56 | 57 | dzil install 58 | 59 | 60 | 61 | And finally ... 62 | ========================================================================= 63 | 64 | Note that the test suite will not run properly unless you have 65 | a MySQL server which it can connect to. 66 | 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The Clarified Artistic License 2 | 3 | Preamble 4 | 5 | The intent of this document is to state the conditions under which a 6 | Package may be copied, such that the Copyright Holder maintains some 7 | semblance of artistic control over the development of the package, 8 | while giving the users of the package the right to use and distribute 9 | the Package in a more-or-less customary fashion, plus the right to make 10 | reasonable modifications. 11 | 12 | Definitions: 13 | 14 | "Package" refers to the collection of files distributed by the 15 | Copyright Holder, and derivatives of that collection of files 16 | created through textual modification. 17 | 18 | "Standard Version" refers to such a Package if it has not been 19 | modified, or has been modified in accordance with the wishes 20 | of the Copyright Holder as specified below. 21 | 22 | "Copyright Holder" is whoever is named in the copyright or 23 | copyrights for the package. 24 | 25 | "You" is you, if you're thinking about copying or distributing 26 | this Package. 27 | 28 | "Distribution fee" is a fee you charge for providing a copy 29 | of this Package to another party. 30 | 31 | "Freely Available" means that no fee is charged for the right to 32 | use the item, though there may be fees involved in handling the 33 | item. It also means that recipients of the item may redistribute 34 | it under the same conditions they received it. 35 | 36 | 1. You may make and give away verbatim copies of the source form of the 37 | Standard Version of this Package without restriction, provided that you 38 | duplicate all of the original copyright notices and associated disclaimers. 39 | 40 | 2. You may apply bug fixes, portability fixes and other modifications 41 | derived from the Public Domain, or those made Freely Available, or from 42 | the Copyright Holder. A Package modified in such a way shall still be 43 | considered the Standard Version. 44 | 45 | 3. You may otherwise modify your copy of this Package in any way, provided 46 | that you insert a prominent notice in each changed file stating how and 47 | when you changed that file, and provided that you do at least ONE of the 48 | following: 49 | 50 | a) place your modifications in the Public Domain or otherwise make them 51 | Freely Available, such as by posting said modifications to Usenet or an 52 | equivalent medium, or placing the modifications on a major network 53 | archive site allowing unrestricted access to them, or by allowing the 54 | Copyright Holder to include your modifications in the Standard Version 55 | of the Package. 56 | 57 | b) use the modified Package only within your corporation or organization. 58 | 59 | c) rename any non-standard executables so the names do not conflict 60 | with standard executables, which must also be provided, and provide 61 | a separate manual page for each non-standard executable that clearly 62 | documents how it differs from the Standard Version. 63 | 64 | d) make other distribution arrangements with the Copyright Holder. 65 | 66 | e) permit and encourge anyone who receives a copy of the modified Package 67 | permission to make your modifications Freely Available 68 | in some specific way. 69 | 70 | 71 | 4. You may distribute the programs of this Package in object code or 72 | executable form, provided that you do at least ONE of the following: 73 | 74 | a) distribute a Standard Version of the executables and library files, 75 | together with instructions (in the manual page or equivalent) on where 76 | to get the Standard Version. 77 | 78 | b) accompany the distribution with the machine-readable source of 79 | the Package with your modifications. 80 | 81 | c) give non-standard executables non-standard names, and clearly 82 | document the differences in manual pages (or equivalent), together 83 | with instructions on where to get the Standard Version. 84 | 85 | d) make other distribution arrangements with the Copyright Holder. 86 | 87 | e) offer the machine-readable source of the Package, with your 88 | modifications, by mail order. 89 | 90 | 5. You may charge a distribution fee for any distribution of this Package. 91 | If you offer support for this Package, you may charge any fee you choose 92 | for that support. You may not charge a license fee for the right to use 93 | this Package itself. You may distribute this Package in aggregate with 94 | other (possibly commercial and possibly nonfree) programs as part of a 95 | larger (possibly commercial and possibly nonfree) software distribution, 96 | and charge license fees for other parts of that software distribution, 97 | provided that you do not advertise this Package as a product of your own. 98 | If the Package includes an interpreter, You may embed this Package's 99 | interpreter within an executable of yours (by linking); this shall be 100 | construed as a mere form of aggregation, provided that the complete 101 | Standard Version of the interpreter is so embedded. 102 | 103 | 6. The scripts and library files supplied as input to or produced as 104 | output from the programs of this Package do not automatically fall 105 | under the copyright of this Package, but belong to whoever generated 106 | them, and may be sold commercially, and may be aggregated with this 107 | Package. If such scripts or library files are aggregated with this 108 | Package via the so-called "undump" or "unexec" methods of producing a 109 | binary executable image, then distribution of such an image shall 110 | neither be construed as a distribution of this Package nor shall it 111 | fall under the restrictions of Paragraphs 3 and 4, provided that you do 112 | not represent such an executable image as a Standard Version of this 113 | Package. 114 | 115 | 7. C subroutines (or comparably compiled subroutines in other 116 | languages) supplied by you and linked into this Package in order to 117 | emulate subroutines and variables of the language defined by this 118 | Package shall not be considered part of this Package, but are the 119 | equivalent of input as in Paragraph 6, provided these subroutines do 120 | not change the language in any way that would cause it to fail the 121 | regression tests for the language. 122 | 123 | 8. Aggregation of the Standard Version of the Package with a commercial 124 | distribution is always permitted provided that the use of this Package 125 | is embedded; that is, when no overt attempt is made to make this Package's 126 | interfaces visible to the end user of the commercial distribution. 127 | Such use shall not be construed as a distribution of this Package. 128 | 129 | 9. The name of the Copyright Holder may not be used to endorse or promote 130 | products derived from this software without specific prior written permission. 131 | 132 | 10. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 133 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 134 | WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. 135 | 136 | The End 137 | -------------------------------------------------------------------------------- /Makefile.docker: -------------------------------------------------------------------------------- 1 | all: mysqldiff 2 | 3 | mysqldiff: 4 | docker build -t mysqldiff . 5 | 6 | shell: 7 | docker exec -it --user test mysqldiff sh -c 'cd git/mysqldiff && pwd && export PS1="mysqldiff-docker>>> " && clear && pwd && sh' 8 | 9 | test: 10 | docker exec -it --user test mysqldiff sh -c "cd git/mysqldiff && PERL5LIB=./lib prove ./t" 11 | 12 | -------------------------------------------------------------------------------- /Makefile.test: -------------------------------------------------------------------------------- 1 | test: 2 | PERL5LIB=./lib prove ./t 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MySQL-Diff 2 | ========== 3 | 4 | MySQL-Diff is a suite of Perl modules and accompanying CLI script 5 | `mysqldiff` for comparing the schemas of two MySQL/MariaDB databases. 6 | 7 | Prerequisites 8 | ------------- 9 | 10 | This suite requires Perl 5.14 or higher and a MySQL compatible suite 11 | of client utilities (mysql and mysqldump). You need at least Perl 5.14 12 | to be able to install the current version of `Dist::Zilla`; however, 13 | the module proper will probably make do with a lesser version. 14 | 15 | Availability 16 | ------------ 17 | 18 | This GitHub repo contains the latest code. The latest released 19 | version of MySQL-Diff used to be available from 20 | 21 | - http://adamspiers.org/computing/mysqldiff/ 22 | 23 | and from the Comprehensive Perl Archive Network (CPAN). Visit 24 | to find a CPAN site near you. 25 | 26 | Installation 27 | ------------ 28 | 29 | See the [`INSTALL.md`](INSTALL.md) file. 30 | 31 | Documentation 32 | ------------- 33 | 34 | - Homepage: http://adamspiers.org/computing/mysqldiff/ 35 | - Documentation at CPAN: http://search.cpan.org/dist/MySQL-Diff/ 36 | 37 | Support 38 | ------- 39 | 40 | Patches should be sent as pull requests to http://github.com/aspiers/mysqldiff 41 | 42 | Please see the [`CONTRIBUTING.md`](CONTRIBUTING.md) file for more 43 | information. 44 | 45 | New bug reports and feature requests 46 | ------------------------------------ 47 | 48 | https://github.com/aspiers/mysqldiff/issues 49 | 50 | Other known bugs 51 | ---------------- 52 | 53 | See https://rt.cpan.org/Public/Dist/Display.html?Name=MySQL-Diff 54 | 55 | License 56 | ------- 57 | 58 | This program is free software; you can redistribute it and/or modify 59 | it under the same terms as Perl itself. 60 | 61 | Copyright 62 | --------- 63 | 64 | (c) 2000-2016 Adam Spiers and other contributors (as shown 65 | by the git history); all rights reserved. 66 | -------------------------------------------------------------------------------- /bin/mysqldiff: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | =head1 NAME 4 | 5 | mysqldiff - compare MySQL database schemas 6 | 7 | =head1 SYNOPSIS 8 | 9 | mysqldiff [B] B B 10 | 11 | mysqldiff --help 12 | 13 | =head1 DESCRIPTION 14 | 15 | F is a Perl script front-end to the 16 | L module 17 | L which 18 | compares the data structures (i.e. schema / table definitions) of two 19 | L databases, and returns the differences 20 | as a sequence of MySQL commands suitable for piping into F 21 | which will transform the structure of the first database to be 22 | identical to that of the second (I F and F). 23 | 24 | Database structures can be compared whether they are files containing 25 | table definitions or existing databases, local or remote. 26 | 27 | B The program makes I attempt to compare any of the data 28 | which may be stored in the databases. It is purely for comparing the 29 | table definitions. I have no plans to implement data comparison; it 30 | is a complex problem and I have no need of such functionality anyway. 31 | However there is another program 32 | L 33 | which does this, and is based on an older program called 34 | F which seems to have vanished off the 'net. 35 | 36 | For PostgreSQL there are similar tools such as 37 | L and 38 | L. 39 | 40 | =head1 EXAMPLES 41 | 42 | # compare table definitions in two files 43 | mysqldiff db1.mysql db2.mysql 44 | 45 | # compare table definitions in a file 'db1.mysql' with a database 'db2' 46 | mysqldiff db1.mysql db2 47 | 48 | # interactively upgrade schema of database 'db1' to be like the 49 | # schema described in the file 'db2.mysql' 50 | mysqldiff -A db1 db2.mysql 51 | 52 | # compare table definitions in two databases on a remote machine 53 | mysqldiff --host=remote.host.com --user=myaccount db1 db2 54 | 55 | # compare table definitions in a local database 'foo' with a 56 | # database 'bar' on a remote machine, when a file foo already 57 | # exists in the current directory 58 | mysqldiff --host2=remote.host.com --password=secret db:foo bar 59 | 60 | =head1 OPTIONS 61 | 62 | =over 4 63 | 64 | =item C<-?, --help> 65 | 66 | show usage 67 | 68 | =item C<-A, --apply> 69 | 70 | interactively patch database1 to match database2 71 | 72 | =item C<-B, --batch-apply> 73 | 74 | non-interactively patch database1 to match database2 75 | 76 | =item C<-d, --debug[=N]> 77 | 78 | enable debugging [level N, default 1] 79 | 80 | =item C<-l, --list-tables> 81 | 82 | output the list off all used tables 83 | 84 | =item C<-o, --only-both> 85 | 86 | only output changes for tables in both databases 87 | 88 | =item C<-k, --keep-old-tables> 89 | 90 | don't output DROP TABLE commands 91 | 92 | =item C<-c, --keep-old-columns> 93 | 94 | don't output DROP COLUMN commands 95 | 96 | =item C<-n, --no-old-defs> 97 | 98 | suppress comments describing old definitions 99 | 100 | =item C<-t, --table-re=REGEXP> 101 | 102 | restrict comparisons to tables matching REGEXP 103 | 104 | =item C<-i, --tolerant> 105 | 106 | ignore DEFAULT, AUTO_INCREMENT, COLLATE, and formatting changes 107 | 108 | =item C<-S, --single-transaction> 109 | 110 | perform DB dump in transaction 111 | For more info see: http://dev.mysql.com/doc/refman/en/mysqldump.html#option_mysqldump_single-transaction 112 | 113 | =item C<-h, --host=[hostname]> 114 | 115 | connect to host 116 | 117 | =item C<-P, --port=[port]> 118 | 119 | use this port for connection 120 | 121 | =item C<-u, --user=[user]> 122 | 123 | user for login if not current user 124 | 125 | =item C<-p, --password[=password]> 126 | 127 | password to use when connecting to server 128 | 129 | =item C<-s, --socket=...> 130 | 131 | socket to use when connecting to server 132 | 133 | =back 134 | 135 | =head2 For only, where N == 1 or 2 136 | 137 | =over 4 138 | 139 | =item C<--hostN=[hostN]> 140 | 141 | connect to host 142 | 143 | =item C<--portN=[portN]> 144 | 145 | use this port for connection 146 | 147 | =item C<--userN=[userN]> 148 | 149 | user for login if not current user 150 | 151 | =item C<--passwordN[=passwordN]> 152 | 153 | password to use when connecting to server 154 | 155 | =item C<--socketN=[socketN]> 156 | 157 | socket to use when connecting to server 158 | 159 | =back 160 | 161 | =head1 INTERNALS 162 | 163 | For both of the database structures being compared, the following 164 | happens: 165 | 166 | =over 4 167 | 168 | =item 169 | 170 | If the argument is a valid filename, the file is used to create a 171 | temporary database which C is run on to obtain the table 172 | definitions in canonicalised form. The temporary database is then 173 | dropped. (The temporary database is named 174 | C because default MySQL permissions 175 | allow anyone to create databases beginning with the prefix C.) 176 | 177 | =item 178 | 179 | If the argument is a database, C is run directly on it. 180 | 181 | =item 182 | 183 | Where authentication is required, the hostname, username, and password 184 | given by the corresponding options are used (type C 185 | for more information). 186 | 187 | =item 188 | 189 | Each set of table definitions is now parsed into tables, and fields 190 | and index keys within those tables; these are compared, and the 191 | differences outputted in the form of MySQL statements. 192 | 193 | =back 194 | 195 | =cut 196 | 197 | use strict; 198 | 199 | use 5.006; # due to 'our' and qr// 200 | 201 | use FindBin qw($RealBin $Script); 202 | use lib $RealBin; 203 | use Getopt::Long qw(:config no_ignore_case); 204 | use IO::File; 205 | use String::ShellQuote qw(shell_quote); 206 | use MySQL::Diff; 207 | 208 | my %opts = (); 209 | GetOptions(\%opts, "help|?", "debug|d:i", "apply|A", "batch-apply|B", 210 | "keep-old-tables|k", "keep-old-columns|c", "no-old-defs|n", 211 | "only-both|o", "table-re|t=s", 212 | "host|h=s", "port|P=s", "socket|s=s", "user|u=s", "password|p:s", 213 | "host1=s", "port1=s", "socket1=s", "user1=s", "password1:s", 214 | "host2=s", "port2=s", "socket2=s", "user2=s", "password2:s", 215 | "tolerant|i", "single-transaction|S", "list-tables|l" 216 | ) or usage(); 217 | 218 | usage() if (@ARGV != 2 or $opts{help}); 219 | 220 | $opts{debug} ||= 0; 221 | 222 | my $md = MySQL::Diff->new(%opts); 223 | 224 | for my $num (1, 2) { 225 | my $new_db = $md->register_db($ARGV[$num-1], $num); 226 | usage($new_db) unless ref $new_db; 227 | } 228 | 229 | $| = 1; 230 | my $diffs = $md->diff(); 231 | print $diffs; 232 | apply($md, $diffs) if $opts{apply} || $opts{'batch-apply'}; 233 | 234 | exit 0; 235 | 236 | ############################################################################## 237 | 238 | sub usage { 239 | print STDERR @_, "\n" if @_; 240 | die < 242 | 243 | Options: 244 | -?, --help show this help 245 | -A, --apply interactively patch database1 to match database2 246 | -B, --batch-apply non-interactively patch database1 to match database2 247 | -d, --debug[=N] enable debugging [level N, default 1] 248 | -l, --list-tables output the list off all used tables 249 | -o, --only-both only output changes for tables in both databases 250 | -k, --keep-old-tables don't output DROP TABLE commands 251 | -c, --keep-old-columns don't output DROP COLUMN commands 252 | -n, --no-old-defs suppress comments describing old definitions 253 | -t, --table-re=REGEXP restrict comparisons to tables matching REGEXP 254 | -i, --tolerant ignore DEFAULT, AUTO_INCREMENT, COLLATE, and formatting changes 255 | -S, --single-transaction perform DB dump in transaction 256 | 257 | -h, --host=... connect to host 258 | -P, --port=... use this port for connection 259 | -u, --user=... user for login if not current user 260 | -p, --password[=...] password to use when connecting to server 261 | -s, --socket=... socket to use when connecting to server 262 | 263 | for only, where N == 1 or 2, 264 | --hostN=... connect to host 265 | --portN=... use this port for connection 266 | --userN=... user for login if not current user 267 | --passwordN[=...] password to use when connecting to server 268 | --socketN=... socket to use when connecting to server 269 | 270 | Databases can be either files or database names. 271 | If there is an ambiguity, the file will be preferred; 272 | to prevent this prefix the database argument with `db:'. 273 | EOF 274 | } 275 | 276 | sub apply { 277 | my ($md, $diffs) = @_; 278 | 279 | if (! $diffs) { 280 | print "No differences to apply.\n"; 281 | return; 282 | } 283 | 284 | my $db1 = $md->db1; 285 | my $db0 = $db1->name; 286 | if ($db1->source_type ne 'db') { 287 | die "$db0 is not a database; cannot apply changes.\n"; 288 | } 289 | 290 | unless ($opts{'batch-apply'}) { 291 | print "\nApply above changes to $db0 [y/N] ? "; 292 | print "\n(CAUTION! Changes contain DROP TABLE commands.) " 293 | if $diffs =~ /\bDROP TABLE\b/i; 294 | my $reply = ; 295 | return unless $reply =~ /^y(es)?$/i; 296 | } 297 | 298 | print "Applying changes ... "; 299 | my $args_ref = $db1->auth_args; 300 | unshift @$args_ref, q{mysql}; 301 | push @$args_ref, $db0; 302 | my $pipe = shell_quote @$args_ref; 303 | my $fh = IO::File->new("|$pipe") or die "Couldn't open pipe to '$pipe': $!\n"; 304 | print $fh $diffs; 305 | $fh->close or die "Couldn't close pipe: $!\n"; 306 | print "done.\n"; 307 | } 308 | 309 | =head1 BUGS, DEVELOPMENT, CONTRIBUTING 310 | 311 | See L. 312 | 313 | =head1 COPYRIGHT AND LICENSE 314 | 315 | Copyright (c) 2000-2016 Adam Spiers. All rights reserved. This 316 | program is free software; you can redistribute it and/or modify it 317 | under the same terms as Perl itself. 318 | 319 | =head1 SEE ALSO 320 | 321 | L, L, L, L, 322 | L, L, L 323 | 324 | =head1 AUTHOR 325 | 326 | Adam Spiers 327 | 328 | =cut 329 | -------------------------------------------------------------------------------- /compose-mysqldiff-tests.env: -------------------------------------------------------------------------------- 1 | # intentionally empty 2 | -------------------------------------------------------------------------------- /dist.ini: -------------------------------------------------------------------------------- 1 | name = MySQL-Diff 2 | version = 0.60 3 | license = Perl_5 4 | copyright_holder = Adam Spiers 5 | homepage = http://aspiers.github.io/mysqldiff/ 6 | 7 | [Bugtracker] 8 | web = https://github.com/aspiers/mysqldiff/issues 9 | mailto = 10 | 11 | [Repository] 12 | repository = https://github.com/aspiers/mysqldiff 13 | type = git 14 | 15 | ;; install bin/mysqldiff 16 | [ExecDir] 17 | [AutoPrereqs] 18 | [PruneCruft] 19 | [GatherDir] 20 | [MetaYAML] 21 | [MetaJSON] 22 | [MakeMaker] 23 | [Manifest] 24 | [TestRelease] 25 | [ConfirmRelease] 26 | [UploadToCPAN] 27 | 28 | -------------------------------------------------------------------------------- /docker-aliases.sh: -------------------------------------------------------------------------------- 1 | # helpful shortcuts for working with docker-compose and friends 2 | 3 | alias dc='docker-compose' 4 | dsh() { 5 | IMAGE=${1}; 6 | FLAGS=${2}; 7 | docker exec -it ${FLAGS} ${IMAGE} sh 8 | } 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | version: '3.7' 4 | 5 | services: 6 | mysqldiff: 7 | image: mysqldiff 8 | container_name: mysqldiff 9 | environment: 10 | MYSQL_ROOT_PASSWORD: ~ 11 | MYSQL_DATABASE: test 12 | MYSQL_USER: test 13 | MYSQL_PASSWORD: ~ 14 | expose: 15 | - "3306" 16 | volumes: 17 | - "${PWD}:/home/test/git/mysqldiff" 18 | env_file: 19 | - "compose-mysqldiff-tests.env" 20 | -------------------------------------------------------------------------------- /lib/MySQL/Diff.pm: -------------------------------------------------------------------------------- 1 | package MySQL::Diff; 2 | 3 | =head1 NAME 4 | 5 | MySQL::Diff - Generates a database upgrade instruction set 6 | 7 | =head1 SYNOPSIS 8 | 9 | use MySQL::Diff; 10 | 11 | my $md = MySQL::Diff->new( %options ); 12 | my $db1 = $md->register_db($ARGV[0], 1); 13 | my $db2 = $md->register_db($ARGV[1], 2); 14 | my $diffs = $md->diff(); 15 | 16 | =head1 DESCRIPTION 17 | 18 | Generates the SQL instructions required to upgrade the first database to match 19 | the second. 20 | 21 | =cut 22 | 23 | use warnings; 24 | use strict; 25 | 26 | our $VERSION = '0.60'; 27 | 28 | # ------------------------------------------------------------------------------ 29 | # Libraries 30 | 31 | use MySQL::Diff::Database; 32 | use MySQL::Diff::Utils qw(debug debug_level debug_file); 33 | 34 | use Data::Dumper; 35 | 36 | # ------------------------------------------------------------------------------ 37 | 38 | =head1 METHODS 39 | 40 | =head2 Constructor 41 | 42 | =over 4 43 | 44 | =item new( %options ) 45 | 46 | Instantiate the objects, providing the command line options for database 47 | access and process requirements. 48 | 49 | =back 50 | 51 | =cut 52 | 53 | sub new { 54 | my $class = shift; 55 | my %hash = @_; 56 | my $self = {}; 57 | bless $self, ref $class || $class; 58 | 59 | $self->{opts} = \%hash; 60 | 61 | if($hash{debug}) { debug_level($hash{debug}) ; delete $hash{debug}; } 62 | if($hash{debug_file}) { debug_file($hash{debug_file}) ; delete $hash{debug_file}; } 63 | 64 | debug(3,"\nconstructing new MySQL::Diff"); 65 | 66 | return $self; 67 | } 68 | 69 | =head2 Public Methods 70 | 71 | Fuller documentation will appear here in time :) 72 | 73 | =over 4 74 | 75 | =item * register_db($name,$inx) 76 | 77 | Reference the database, and setup a connection. The name can be an already 78 | existing 'MySQL::Diff::Database' database object. The index can be '1' or '2', 79 | and refers both to the order of the diff, and to the host, port, username and 80 | password arguments that have been supplied. 81 | 82 | =cut 83 | 84 | sub register_db { 85 | my ($self, $name, $inx) = @_; 86 | return unless $inx == 1 || $inx == 2; 87 | 88 | my $db = ref $name eq 'MySQL::Diff::Database' ? $name : $self->_load_database($name,$inx); 89 | $self->{databases}[$inx-1] = $db; 90 | return $db; 91 | } 92 | 93 | =item * db1() 94 | 95 | =item * db2() 96 | 97 | Return the first and second databases registered via C. 98 | 99 | =cut 100 | 101 | sub db1 { shift->{databases}->[0] } 102 | sub db2 { shift->{databases}->[1] } 103 | 104 | =item * diff() 105 | 106 | Performs the diff, returning a string containing the commands needed to change 107 | the schema of the first database into that of the second. 108 | 109 | =back 110 | 111 | =cut 112 | 113 | sub diff { 114 | my $self = shift; 115 | my @changes; 116 | my %used_tables = (); 117 | 118 | debug(1, "\ncomparing databases"); 119 | 120 | for my $table1 ($self->db1->tables()) { 121 | my $name = $table1->name(); 122 | $used_tables{'-- '. $name} = 1; 123 | debug(4, "table 1 $name = ".Dumper($table1)); 124 | debug(2,"looking at tables called '$name'"); 125 | if (my $table2 = $self->db2->table_by_name($name)) { 126 | debug(3,"comparing tables called '$name'"); 127 | push @changes, $self->_diff_tables($table1, $table2); 128 | } else { 129 | debug(3,"table '$name' dropped"); 130 | push @changes, "DROP TABLE $name;\n\n" 131 | unless $self->{opts}{'only-both'} || $self->{opts}{'keep-old-tables'}; 132 | } 133 | } 134 | 135 | for my $table2 ($self->db2->tables()) { 136 | my $name = $table2->name(); 137 | $used_tables{'-- '. $name} = 1; 138 | debug(4, "table 2 $name = ".Dumper($table2)); 139 | if (! $self->db1->table_by_name($name)) { 140 | debug(3,"table '$name' added"); 141 | debug(4,"table '$name' added '".$table2->def()."'"); 142 | push @changes, $table2->def() . "\n" 143 | unless $self->{opts}{'only-both'}; 144 | } 145 | } 146 | 147 | debug(4,join '', @changes); 148 | 149 | my $out = ''; 150 | if (@changes) { 151 | if (!$self->{opts}{'list-tables'}) { 152 | $out .= $self->_diff_banner(); 153 | } 154 | else { 155 | $out .= "-- TABLES LIST \n"; 156 | $out .= join "\n", keys %used_tables; 157 | $out .= "\n-- END OF TABLES LIST \n"; 158 | } 159 | $out .= join '', @changes; 160 | } 161 | return $out; 162 | } 163 | 164 | # ------------------------------------------------------------------------------ 165 | # Private Methods 166 | 167 | sub _diff_banner { 168 | my ($self) = @_; 169 | 170 | my $summary1 = $self->db1->summary(); 171 | my $summary2 = $self->db2->summary(); 172 | 173 | my $opt_text = 174 | join ', ', 175 | map { $self->{opts}{$_} eq '1' ? $_ : "$_=$self->{opts}{$_}" } 176 | keys %{$self->{opts}}; 177 | $opt_text = "## Options: $opt_text\n" if $opt_text; 178 | 179 | my $now = scalar localtime(); 180 | return <_diff_foreign_key_drop(@_), 195 | $self->_diff_fields(@_), 196 | $self->_diff_indices(@_), 197 | $self->_diff_primary_key(@_), 198 | $self->_diff_foreign_key_add(@_), 199 | $self->_diff_options(@_) 200 | ); 201 | 202 | $changes[-1] =~ s/\n*$/\n/ if (@changes); 203 | return @changes; 204 | } 205 | 206 | sub _diff_fields { 207 | my ($self, $table1, $table2) = @_; 208 | 209 | my $name1 = $table1->name(); 210 | 211 | my $fields1 = $table1->fields; 212 | my $fields2 = $table2->fields; 213 | 214 | return () unless $fields1 || $fields2; 215 | 216 | my @changes; 217 | 218 | if($fields1) { 219 | for my $field (keys %$fields1) { 220 | debug(3,"table1 had field '$field'"); 221 | my $f1 = $fields1->{$field}; 222 | my $f2 = $fields2->{$field}; 223 | if ($fields2 && $f2) { 224 | if ($self->{opts}{tolerant}) { 225 | for ($f1, $f2) { 226 | s/ COLLATE [\w_]+//gi; 227 | } 228 | } 229 | if ($f1 ne $f2) { 230 | if (not $self->{opts}{tolerant} or 231 | (($f1 !~ m/$f2\(\d+,\d+\)/) and 232 | ($f1 ne "$f2 DEFAULT '' NOT NULL") and 233 | ($f1 ne "$f2 NOT NULL") )) 234 | { 235 | debug(3,"field '$field' changed"); 236 | 237 | my $change = "ALTER TABLE $name1 CHANGE COLUMN $field $field $f2;"; 238 | $change .= " # was $f1" unless $self->{opts}{'no-old-defs'}; 239 | $change .= "\n"; 240 | push @changes, $change; 241 | } 242 | } 243 | } elsif (!$self->{opts}{'keep-old-columns'}) { 244 | debug(3,"field '$field' removed"); 245 | my $change = "ALTER TABLE $name1 DROP COLUMN $field;"; 246 | $change .= " # was $fields1->{$field}" unless $self->{opts}{'no-old-defs'}; 247 | $change .= "\n"; 248 | push @changes, $change; 249 | } 250 | } 251 | } 252 | 253 | if($fields2) { 254 | for my $field (keys %$fields2) { 255 | unless($fields1 && $fields1->{$field}) { 256 | debug(3,"field '$field' added"); 257 | my $changes = "ALTER TABLE $name1 ADD COLUMN $field $fields2->{$field}"; 258 | if ($table2->is_auto_inc($field)) { 259 | if ($table2->isa_primary($field)) { 260 | $changes .= ' PRIMARY KEY'; 261 | } elsif ($table2->is_unique($field)) { 262 | $changes .= ' UNIQUE KEY'; 263 | } 264 | } 265 | push @changes, "$changes;\n"; 266 | } 267 | } 268 | } 269 | 270 | return @changes; 271 | } 272 | 273 | sub _diff_indices { 274 | my ($self, $table1, $table2) = @_; 275 | 276 | my $name1 = $table1->name(); 277 | 278 | my $indices1 = $table1->indices(); 279 | my $indices2 = $table2->indices(); 280 | 281 | return () unless $indices1 || $indices2; 282 | 283 | my @changes; 284 | 285 | if($indices1) { 286 | for my $index (keys %$indices1) { 287 | debug(3,"table1 had index '$index'"); 288 | my $old_type = $table1->is_unique($index) ? 'UNIQUE' : 289 | $table1->is_fulltext($index) ? 'FULLTEXT INDEX' : 'INDEX'; 290 | 291 | if ($indices2 && $indices2->{$index}) { 292 | if( ($indices1->{$index} ne $indices2->{$index}) or 293 | ($table1->is_unique($index) xor $table2->is_unique($index)) or 294 | ($table1->is_fulltext($index) xor $table2->is_fulltext($index)) ) 295 | { 296 | debug(3,"index '$index' changed"); 297 | my $new_type = $table2->is_unique($index) ? 'UNIQUE' : 298 | $table2->is_fulltext($index) ? 'FULLTEXT INDEX' : 'INDEX'; 299 | 300 | my $changes = "ALTER TABLE $name1 DROP INDEX $index;"; 301 | $changes .= " # was $old_type ($indices1->{$index})" 302 | unless $self->{opts}{'no-old-defs'}; 303 | $changes .= "\nALTER TABLE $name1 ADD $new_type $index ($indices2->{$index});\n"; 304 | push @changes, $changes; 305 | } 306 | } else { 307 | debug(3,"index '$index' removed"); 308 | my $auto = _check_for_auto_col($table2, $indices1->{$index}, 1) || ''; 309 | my $changes = $auto ? _index_auto_col($table1, $indices1->{$index}) : ''; 310 | $changes .= "ALTER TABLE $name1 DROP INDEX $index;"; 311 | $changes .= " # was $old_type ($indices1->{$index})" 312 | unless $self->{opts}{'no-old-defs'}; 313 | $changes .= "\n"; 314 | push @changes, $changes; 315 | } 316 | } 317 | } 318 | 319 | if($indices2) { 320 | for my $index (keys %$indices2) { 321 | next if($indices1 && $indices1->{$index}); 322 | next if( 323 | !$table2->isa_primary($index) && 324 | $table2->is_unique($index) && 325 | _key_covers_auto_col($table2, $index) 326 | ); 327 | debug(3,"index '$index' added"); 328 | my $new_type = $table2->is_unique($index) ? 'UNIQUE' : 'INDEX'; 329 | push @changes, "ALTER TABLE $name1 ADD $new_type $index ($indices2->{$index});\n"; 330 | } 331 | } 332 | 333 | return @changes; 334 | } 335 | 336 | sub _diff_primary_key { 337 | my ($self, $table1, $table2) = @_; 338 | 339 | my $name1 = $table1->name(); 340 | 341 | my $primary1 = $table1->primary_key(); 342 | my $primary2 = $table2->primary_key(); 343 | 344 | return () unless $primary1 || $primary2; 345 | 346 | my @changes; 347 | 348 | if ($primary1 && ! $primary2) { 349 | debug(3,"primary key '$primary1' dropped"); 350 | my $changes = _index_auto_col($table2, $primary1); 351 | $changes .= "ALTER TABLE $name1 DROP PRIMARY KEY;"; 352 | $changes .= " # was $primary1" unless $self->{opts}{'no-old-defs'}; 353 | return ( "$changes\n" ); 354 | } 355 | 356 | if (! $primary1 && $primary2) { 357 | debug(3,"primary key '$primary2' added"); 358 | return () if _key_covers_auto_col($table2, $primary2); 359 | return ("ALTER TABLE $name1 ADD PRIMARY KEY $primary2;\n"); 360 | } 361 | 362 | if ($primary1 ne $primary2) { 363 | debug(3,"primary key changed"); 364 | my $auto = _check_for_auto_col($table2, $primary1) || ''; 365 | my $changes = $auto ? _index_auto_col($table2, $auto) : ''; 366 | $changes .= "ALTER TABLE $name1 DROP PRIMARY KEY;"; 367 | $changes .= " # was $primary1" unless $self->{opts}{'no-old-defs'}; 368 | $changes .= "\nALTER TABLE $name1 ADD PRIMARY KEY $primary2;\n"; 369 | $changes .= "ALTER TABLE $name1 DROP INDEX $auto;\n" if($auto); 370 | push @changes, $changes; 371 | } 372 | 373 | return @changes; 374 | } 375 | 376 | sub _diff_foreign_key_drop { 377 | my ($self, $table1, $table2) = @_; 378 | 379 | my $name1 = $table1->name(); 380 | 381 | my $fks1 = $table1->foreign_key(); 382 | my $fks2 = $table2->foreign_key(); 383 | 384 | return () unless $fks1 || $fks2; 385 | 386 | my @changes; 387 | 388 | if($fks1) { 389 | for my $fk (keys %$fks1) { 390 | debug(1,"$name1 has fk '$fk'"); 391 | 392 | if ($fks2 && $fks2->{$fk}) { 393 | if($fks1->{$fk} ne $fks2->{$fk}) 394 | { 395 | debug(1,"foreign key '$fk' changed"); 396 | my $changes = "ALTER TABLE $name1 DROP FOREIGN KEY $fk;"; 397 | $changes .= " # was CONSTRAINT $fk $fks1->{$fk}" 398 | unless $self->{opts}{'no-old-defs'}; 399 | push @changes, $changes; 400 | } 401 | } else { 402 | debug(1,"foreign key '$fk' removed"); 403 | my $changes .= "ALTER TABLE $name1 DROP FOREIGN KEY $fk;"; 404 | $changes .= " # was CONSTRAINT $fk $fks1->{$fk}" 405 | unless $self->{opts}{'no-old-defs'}; 406 | $changes .= "\n"; 407 | push @changes, $changes; 408 | } 409 | } 410 | } 411 | 412 | return @changes; 413 | } 414 | 415 | sub _diff_foreign_key_add { 416 | my ($self, $table1, $table2) = @_; 417 | 418 | my $name1 = $table1->name(); 419 | 420 | my $fks1 = $table1->foreign_key(); 421 | my $fks2 = $table2->foreign_key(); 422 | 423 | return () unless $fks1 || $fks2; 424 | 425 | my @changes; 426 | 427 | if($fks1) { 428 | for my $fk (keys %$fks1) { 429 | debug(1,"$name1 has fk '$fk'"); 430 | 431 | if ($fks2 && $fks2->{$fk}) { 432 | if($fks1->{$fk} ne $fks2->{$fk}) 433 | { 434 | debug(1,"foreign key '$fk' changed"); 435 | my $changes = "\nALTER TABLE $name1 ADD CONSTRAINT $fk FOREIGN KEY $fks2->{$fk};\n"; 436 | push @changes, $changes; 437 | } 438 | } 439 | } 440 | } 441 | 442 | if($fks2) { 443 | for my $fk (keys %$fks2) { 444 | next if($fks1 && $fks1->{$fk}); 445 | debug(1, "foreign key '$fk' added"); 446 | push @changes, "ALTER TABLE $name1 ADD CONSTRAINT $fk FOREIGN KEY $fks2->{$fk};\n"; 447 | } 448 | } 449 | 450 | return @changes; 451 | } 452 | 453 | # If we're about to drop a composite (multi-column) index, we need to 454 | # check whether any of the columns in the composite index are 455 | # auto_increment; if so, we have to add an index for that 456 | # auto_increment column *before* dropping the composite index, since 457 | # auto_increment columns must always be indexed. 458 | sub _check_for_auto_col { 459 | my ($table, $fields, $primary) = @_; 460 | 461 | my @fields = _fields_from_key($fields); 462 | 463 | for my $field (@fields) { 464 | next if($table->field($field) !~ /auto_increment/i); 465 | next if($table->isa_index($field)); 466 | next if($primary && $table->isa_primary($field)); 467 | 468 | return $field; 469 | } 470 | 471 | return; 472 | } 473 | 474 | sub _fields_from_key { 475 | my $key = shift; 476 | $key =~ s/^\s*\((.*)\)\s*$/$1/g; # strip brackets if any 477 | split /\s*,\s*/, $key; 478 | } 479 | 480 | sub _key_covers_auto_col { 481 | my ($table, $key) = @_; 482 | my @fields = _fields_from_key($key); 483 | for my $field (@fields) { 484 | return 1 if $table->is_auto_inc($field); 485 | } 486 | return; 487 | } 488 | 489 | sub _index_auto_col { 490 | my ($table, $field) = @_; 491 | my $name = $table->name; 492 | return "ALTER TABLE $name ADD INDEX ($field); # auto columns must always be indexed\n"; 493 | } 494 | 495 | sub _diff_options { 496 | my ($self, $table1, $table2) = @_; 497 | 498 | my $name = $table1->name(); 499 | my $options1 = $table1->options(); 500 | my $options2 = $table2->options(); 501 | 502 | return () unless $options1 || $options2; 503 | 504 | my @changes; 505 | 506 | if ($self->{opts}{tolerant}) { 507 | for ($options1, $options2) { 508 | s/ AUTO_INCREMENT=\d+//gi; 509 | s/ COLLATE=[\w_]+//gi; 510 | } 511 | } 512 | 513 | if ($options1 ne $options2) { 514 | my $change = "ALTER TABLE $name $options2;"; 515 | $change .= " # was " . ($options1 || 'blank') unless $self->{opts}{'no-old-defs'}; 516 | $change .= "\n"; 517 | push @changes, $change; 518 | } 519 | 520 | return @changes; 521 | } 522 | 523 | sub _load_database { 524 | my ($self, $arg, $authnum) = @_; 525 | 526 | debug(2, "parsing arg $authnum: '$arg'\n"); 527 | 528 | my %auth; 529 | for my $auth (qw/dbh host port user password socket/) { 530 | $auth{$auth} = $self->{opts}{"$auth$authnum"} || $self->{opts}{$auth}; 531 | delete $auth{$auth} unless $auth{$auth}; 532 | } 533 | 534 | if ($arg =~ /^db:(.*)/) { 535 | return MySQL::Diff::Database->new(db => $1, auth => \%auth, 'single-transaction' => $self->{opts}{'single-transaction'}, 'table-re' => $self->{opts}{'table-re'}); 536 | } 537 | 538 | if ($self->{opts}{"dbh"} || 539 | $self->{opts}{"host$authnum"} || 540 | $self->{opts}{"port$authnum"} || 541 | $self->{opts}{"user$authnum"} || 542 | $self->{opts}{"password$authnum"} || 543 | $self->{opts}{"socket$authnum"}) { 544 | return MySQL::Diff::Database->new(db => $arg, auth => \%auth, 'single-transaction' => $self->{opts}{'single-transaction'}, 'table-re' => $self->{opts}{'table-re'}); 545 | } 546 | 547 | if (-f $arg) { 548 | return MySQL::Diff::Database->new(file => $arg, auth => \%auth, 'single-transaction' => $self->{opts}{'single-transaction'}, 'table-re' => $self->{opts}{'table-re'}); 549 | } 550 | 551 | my %dbs = MySQL::Diff::Database::available_dbs(%auth); 552 | debug(2, " available databases: ", (join ', ', keys %dbs), "\n"); 553 | 554 | if ($dbs{$arg}) { 555 | return MySQL::Diff::Database->new(db => $arg, auth => \%auth, 'single-transaction' => $self->{opts}{'single-transaction'}, 'table-re' => $self->{opts}{'table-re'}); 556 | } 557 | 558 | warn "'$arg' is not a valid file or database.\n"; 559 | return; 560 | } 561 | 562 | sub _debug_level { 563 | my ($self,$level) = @_; 564 | debug_level($level); 565 | } 566 | 567 | 1; 568 | 569 | __END__ 570 | 571 | =head1 COPYRIGHT AND LICENSE 572 | 573 | Copyright (c) 2000-2016 Adam Spiers. All rights reserved. This 574 | program is free software; you can redistribute it and/or modify it 575 | under the same terms as Perl itself. 576 | 577 | =head1 SEE ALSO 578 | 579 | L, L, L, L 580 | 581 | =head1 AUTHOR 582 | 583 | Adam Spiers 584 | 585 | =cut 586 | -------------------------------------------------------------------------------- /lib/MySQL/Diff/Database.pm: -------------------------------------------------------------------------------- 1 | package MySQL::Diff::Database; 2 | 3 | =head1 NAME 4 | 5 | MySQL::Diff::Database - Database Definition Class 6 | 7 | =head1 SYNOPSIS 8 | 9 | use MySQL::Diff::Database; 10 | 11 | my $db = MySQL::Diff::Database->new(%options); 12 | my $source = $db->source_type(); 13 | my $summary = $db->summary(); 14 | my $name = $db->name(); 15 | my @tables = $db->tables(); 16 | my $table_def = $db->table_by_name($table); 17 | 18 | my @dbs = MySQL::Diff::Database::available_dbs(); 19 | 20 | =head1 DESCRIPTION 21 | 22 | Parses a database definition into component parts. 23 | 24 | =cut 25 | 26 | use warnings; 27 | use strict; 28 | use String::ShellQuote qw(shell_quote); 29 | 30 | our $VERSION = '0.60'; 31 | 32 | # ------------------------------------------------------------------------------ 33 | # Libraries 34 | 35 | use Carp qw(:DEFAULT); 36 | use File::Slurp; 37 | use IO::File; 38 | 39 | use MySQL::Diff::Utils qw(debug); 40 | use MySQL::Diff::Table; 41 | 42 | # ------------------------------------------------------------------------------ 43 | 44 | =head1 METHODS 45 | 46 | =head2 Constructor 47 | 48 | =over 4 49 | 50 | =item new( %options ) 51 | 52 | Instantiate the objects, providing the command line options for database 53 | access and process requirements. 54 | 55 | =back 56 | 57 | =cut 58 | 59 | sub new { 60 | my $class = shift; 61 | my %p = @_; 62 | my $self = {}; 63 | bless $self, ref $class || $class; 64 | 65 | debug(3,"\nconstructing new MySQL::Diff::Database"); 66 | 67 | my $auth_ref = _auth_args_string(%{$p{auth}}); 68 | my $string = shell_quote @$auth_ref; 69 | debug(3,"auth args: $string"); 70 | $self->{_source}{auth} = $string; 71 | $self->{_source}{dbh} = $p{dbh} if $p{dbh}; 72 | $self->{'single-transaction'} = $p{'single-transaction'}; 73 | $self->{'table-re'} = $p{'table-re'}; 74 | 75 | if ($p{file}) { 76 | $self->_canonicalise_file($p{file}); 77 | } elsif ($p{db}) { 78 | $self->_read_db($p{db}); 79 | } else { 80 | confess "MySQL::Diff::Database::new called without db or file params"; 81 | } 82 | 83 | $self->_parse_defs(); 84 | return $self; 85 | } 86 | 87 | =head2 Public Methods 88 | 89 | =over 4 90 | 91 | =item * source_type() 92 | 93 | Returns 'file' if the data source is a text file, and 'db' if connected 94 | directly to a database. 95 | 96 | =cut 97 | 98 | sub source_type { 99 | my $self = shift; 100 | return 'file' if $self->{_source}{file}; 101 | return 'db' if $self->{_source}{db}; 102 | } 103 | 104 | =item * summary() 105 | 106 | Provides a summary of the database. 107 | 108 | =cut 109 | 110 | sub summary { 111 | my $self = shift; 112 | 113 | if ($self->{_source}{file}) { 114 | return "file: " . $self->{_source}{file}; 115 | } elsif ($self->{_source}{db}) { 116 | my $args = $self->{_source}{auth}; 117 | $args =~ tr/-//d; 118 | $args =~ s/\bpassword=\S+//; 119 | $args =~ s/^\s*(.*?)\s*$/$1/; 120 | my $summary = " db: " . $self->{_source}{db}; 121 | $summary .= " ($args)" if $args; 122 | return $summary; 123 | } else { 124 | return 'unknown'; 125 | } 126 | } 127 | 128 | =item * name() 129 | 130 | Returns the name of the database. 131 | 132 | =cut 133 | 134 | sub name { 135 | my $self = shift; 136 | return $self->{_source}{file} || $self->{_source}{db}; 137 | } 138 | 139 | =item * tables() 140 | 141 | Returns a list of tables for the current database. 142 | 143 | =cut 144 | 145 | sub tables { 146 | my $self = shift; 147 | return @{$self->{_tables}}; 148 | } 149 | 150 | =item * table_by_name( $name ) 151 | 152 | Returns the table definition (see L) for the given table. 153 | 154 | =cut 155 | 156 | sub table_by_name { 157 | my ($self,$name) = @_; 158 | return $self->{_by_name}{$name}; 159 | } 160 | 161 | =back 162 | 163 | =head1 FUNCTIONS 164 | 165 | =head2 Public Functions 166 | 167 | =over 4 168 | 169 | =item * available_dbs() 170 | 171 | Returns a list of the available databases. 172 | 173 | Note that is used as a function call, not a method call. 174 | 175 | =cut 176 | 177 | sub available_dbs { 178 | my %auth = @_; 179 | my $args_ref = _auth_args_string(%auth); 180 | unshift @$args_ref, q{mysqlshow}; 181 | 182 | # evil but we don't use DBI because I don't want to implement -p properly 183 | # not that this works with -p anyway ... 184 | my $command = shell_quote @$args_ref; 185 | my $fh = IO::File->new("$command |") or die "Couldn't execute '$command': $!\n"; 186 | my $dbs_ref = _parse_mysqlshow_from_fh_into_arrayref($fh); 187 | $fh->close() or die "$command failed: $!"; 188 | 189 | return map { $_ => 1 } @{$dbs_ref}; 190 | } 191 | 192 | =back 193 | 194 | =cut 195 | 196 | # ------------------------------------------------------------------------------ 197 | # Private Methods 198 | 199 | sub auth_args { 200 | return _auth_args_string(); 201 | } 202 | 203 | sub _canonicalise_file { 204 | my ($self, $file) = @_; 205 | 206 | $self->{_source}{file} = $file; 207 | debug(2,"fetching table defs from file $file"); 208 | 209 | # FIXME: option to avoid create-and-dump bit 210 | # create a temporary database using defs from file ... 211 | # hopefully the temp db is unique! 212 | my $temp_db = sprintf "test_mysqldiff-temp-%d_%d_%d", time(), $$, rand(); 213 | debug(3,"creating temporary database $temp_db"); 214 | 215 | my $defs = read_file($file); 216 | die "$file contains dangerous command '$1'; aborting.\n" 217 | if $defs =~ /;\s*(use|((drop|create)\s+database))\b/i; 218 | 219 | my $args = $self->{_source}{auth}; 220 | my $fh = IO::File->new("| mysql $args") or die "Couldn't execute 'mysql$args': $!\n"; 221 | print $fh "\nCREATE DATABASE \`$temp_db\`;\nUSE \`$temp_db\`;\n"; 222 | print $fh $defs; 223 | $fh->close; 224 | 225 | # ... and then retrieve defs from mysqldump. Hence we've used 226 | # MySQL to massage the defs file into canonical form. 227 | $self->_get_defs($temp_db); 228 | 229 | debug(3,"dropping temporary database $temp_db"); 230 | $fh = IO::File->new("| mysql $args") or die "Couldn't execute 'mysql$args': $!\n"; 231 | print $fh "DROP DATABASE \`$temp_db\`;\n"; 232 | $fh->close; 233 | } 234 | 235 | sub _read_db { 236 | my ($self, $db) = @_; 237 | $self->{_source}{db} = $db; 238 | debug(3, "fetching table defs from db $db"); 239 | $self->_get_defs($db); 240 | } 241 | 242 | sub _get_tables_to_dump { 243 | my ( $self, $db ) = @_; 244 | 245 | my $tables_ref = $self->_get_tables_in_db($db); 246 | 247 | my $compiled_table_re = qr/$self->{'table-re'}/; 248 | 249 | my @matching_tables = grep { $_ =~ $compiled_table_re } @{$tables_ref}; 250 | 251 | return join( ' ', @matching_tables ); 252 | } 253 | 254 | sub _get_tables_in_db { 255 | my ( $self, $db ) = @_; 256 | 257 | my $args = $self->{_source}{auth}; 258 | 259 | # evil but we don't use DBI because I don't want to implement -p properly 260 | # not that this works with -p anyway ... 261 | my $fh = IO::File->new("mysqlshow $args $db|") 262 | or die "Couldn't execute 'mysqlshow $args $db': $!\n"; 263 | my $tables_ref = _parse_mysqlshow_from_fh_into_arrayref($fh); 264 | $fh->close() or die "mysqlshow $args $db failed: $!"; 265 | 266 | return $tables_ref; 267 | } 268 | 269 | # Note that is used as a function call, not a method call. 270 | sub _parse_mysqlshow_from_fh_into_arrayref { 271 | my ($fh) = @_; 272 | 273 | my @items; 274 | while (<$fh>) { 275 | next unless /^\| ([\w-]+)/; 276 | push @items, $1; 277 | } 278 | 279 | return \@items; 280 | } 281 | 282 | sub _get_defs { 283 | my ( $self, $db ) = @_; 284 | 285 | my $args = $self->{_source}{auth}; 286 | my $single_transaction = $self->{'single-transaction'} ? "--single-transaction" : ""; 287 | my $tables = ''; #dump all tables by default 288 | if ( my $table_re = $self->{'table-re'} ) { 289 | $tables = $self->_get_tables_to_dump($db); 290 | if ( !length $tables ) { # No tables to dump 291 | $self->{_defs} = []; 292 | return; 293 | } 294 | } 295 | 296 | my $fh = IO::File->new("mysqldump -d $single_transaction $args $db $tables 2>&1 |") 297 | or die "Couldn't read ${db}'s table defs via mysqldump: $!\n"; 298 | 299 | debug( 3, "running mysqldump -d $single_transaction $args $db $tables" ); 300 | my $defs = $self->{_defs} = [<$fh>]; 301 | $fh->close; 302 | my $exit_status = $? >> 8; 303 | 304 | if ( grep /mysqldump: Got error: .*: Unknown database/, @$defs ) { 305 | die <{_tables}; 323 | 324 | debug(2, "parsing table defs"); 325 | my $defs = join '', grep ! /^\s*(\#|--|SET|\/\*)/, @{$self->{_defs}}; 326 | $defs =~ s/`//sg; 327 | my @tables = split /(?=^\s*(?:create|alter|drop)\s+table\s+)/im, $defs; 328 | $self->{_tables} = []; 329 | for my $table (@tables) { 330 | debug(4, " table def [$table]"); 331 | if($table =~ /create\s+table/i) { 332 | my $obj = MySQL::Diff::Table->new(source => $self->{_source}, def => $table); 333 | push @{$self->{_tables}}, $obj; 334 | $self->{_by_name}{$obj->name()} = $obj; 335 | } 336 | } 337 | } 338 | 339 | sub _auth_args_string { 340 | my %auth = @_; 341 | my $args = []; 342 | for my $arg (qw/host port user password socket/) { 343 | push @$args, qq/--$arg=$auth{$arg}/ if $auth{$arg}; 344 | } 345 | return $args; 346 | } 347 | 348 | 1; 349 | 350 | __END__ 351 | 352 | =head1 COPYRIGHT AND LICENSE 353 | 354 | Copyright (c) 2000-2016 Adam Spiers. All rights reserved. This 355 | program is free software; you can redistribute it and/or modify it 356 | under the same terms as Perl itself. 357 | 358 | =head1 SEE ALSO 359 | 360 | L, L, L, L 361 | 362 | =head1 AUTHOR 363 | 364 | Adam Spiers 365 | 366 | =cut 367 | -------------------------------------------------------------------------------- /lib/MySQL/Diff/Table.pm: -------------------------------------------------------------------------------- 1 | package MySQL::Diff::Table; 2 | 3 | =head1 NAME 4 | 5 | MySQL::Diff::Table - Table Definition Class 6 | 7 | =head1 SYNOPSIS 8 | 9 | use MySQL::Diff::Table 10 | 11 | my $db = MySQL::Diff::Database->new(%options); 12 | my $def = $db->def(); 13 | my $name = $db->name(); 14 | my $field = $db->field(); 15 | my $fields = $db->fields(); # %$fields 16 | my $primary_key = $db->primary_key(); 17 | my $indices = $db->indices(); # %$indices 18 | my $options = $db->options(); 19 | 20 | my $isfield = $db->isa_field($field); 21 | my $isprimary = $db->isa_primary($field); 22 | my $isindex = $db->isa_index($field); 23 | my $isunique = $db->is_unique($field); 24 | my $isfulltext = $db->is_fulltext($field); 25 | 26 | =head1 DESCRIPTION 27 | 28 | Parses a table definition into component parts. 29 | 30 | =cut 31 | 32 | use warnings; 33 | use strict; 34 | 35 | our $VERSION = '0.60'; 36 | 37 | # ------------------------------------------------------------------------------ 38 | # Libraries 39 | 40 | use Carp qw(:DEFAULT); 41 | use MySQL::Diff::Utils qw(debug); 42 | 43 | # ------------------------------------------------------------------------------ 44 | 45 | =head1 METHODS 46 | 47 | =head2 Constructor 48 | 49 | =over 4 50 | 51 | =item new( %options ) 52 | 53 | Instantiate the objects, providing the command line options for database 54 | access and process requirements. 55 | 56 | =cut 57 | 58 | sub new { 59 | my $class = shift; 60 | my %hash = @_; 61 | my $self = {}; 62 | bless $self, ref $class || $class; 63 | 64 | $self->{$_} = $hash{$_} for(keys %hash); 65 | 66 | debug(3,"\nconstructing new MySQL::Diff::Table"); 67 | croak "MySQL::Diff::Table::new called without def params" unless $self->{def}; 68 | $self->_parse; 69 | return $self; 70 | } 71 | 72 | =back 73 | 74 | =head2 Public Methods 75 | 76 | Fuller documentation will appear here in time :) 77 | 78 | =over 4 79 | 80 | =item * def 81 | 82 | Returns the table definition as a string. 83 | 84 | =item * name 85 | 86 | Returns the name of the current table. 87 | 88 | =item * field 89 | 90 | Returns the current field definition of the given field. 91 | 92 | =item * fields 93 | 94 | Returns an array reference to a list of fields. 95 | 96 | =item * primary_key 97 | 98 | Returns a hash reference to fields used as primary key fields. 99 | 100 | =item * indices 101 | 102 | Returns a hash reference to fields used as index fields. 103 | 104 | =item * options 105 | 106 | Returns the additional options added to the table definition. 107 | 108 | =item * isa_field 109 | 110 | Returns 1 if given field is used in the current table definition, otherwise 111 | returns 0. 112 | 113 | =item * isa_primary 114 | 115 | Returns 1 if given field is defined as a primary key, otherwise returns 0. 116 | 117 | =item * isa_index 118 | 119 | Returns 1 if given field is used as an index field, otherwise returns 0. 120 | 121 | =item * is_unique 122 | 123 | Returns 1 if given field is used as unique index field, otherwise returns 0. 124 | 125 | =item * is_fulltext 126 | 127 | Returns 1 if given field is used as fulltext index field, otherwise returns 0. 128 | 129 | =item * is_auto_inc 130 | 131 | Returns 1 if given field is defined as an auto increment field, otherwise returns 0. 132 | 133 | =back 134 | 135 | =cut 136 | 137 | sub def { my $self = shift; return $self->{def}; } 138 | sub name { my $self = shift; return $self->{name}; } 139 | sub field { my $self = shift; return $self->{fields}{$_[0]}; } 140 | sub fields { my $self = shift; return $self->{fields}; } 141 | sub primary_key { my $self = shift; return $self->{primary_key}; } 142 | sub indices { my $self = shift; return $self->{indices}; } 143 | sub options { my $self = shift; return $self->{options}; } 144 | sub foreign_key { my $self = shift; return $self->{foreign_key}; } 145 | 146 | sub isa_field { my $self = shift; return $_[0] && $self->{fields}{$_[0]} ? 1 : 0; } 147 | sub isa_primary { my $self = shift; return $_[0] && $self->{primary}{$_[0]} ? 1 : 0; } 148 | sub isa_index { my $self = shift; return $_[0] && $self->{indices}{$_[0]} ? 1 : 0; } 149 | sub is_unique { my $self = shift; return $_[0] && $self->{unique}{$_[0]} ? 1 : 0; } 150 | sub is_fulltext { my $self = shift; return $_[0] && $self->{fulltext}{$_[0]} ? 1 : 0; } 151 | sub is_auto_inc { my $self = shift; return $_[0] && $self->{auto_inc}{$_[0]} ? 1 : 0; } 152 | 153 | # ------------------------------------------------------------------------------ 154 | # Private Methods 155 | 156 | sub _parse { 157 | my $self = shift; 158 | 159 | $self->{def} =~ s/`([^`]+)`/$1/gs; # later versions quote names 160 | $self->{def} =~ s/\n+/\n/; 161 | $self->{lines} = [ grep ! /^\s*$/, split /(?=^)/m, $self->{def} ]; 162 | my @lines = @{$self->{lines}}; 163 | debug(4,"parsing table def '$self->{def}'"); 164 | 165 | my $name; 166 | if ($lines[0] =~ /^\s*create\s+table\s+(\S+)\s+\(\s*$/i) { 167 | $self->{name} = $1; 168 | debug(3,"got table name '$self->{name}'"); 169 | shift @lines; 170 | } else { 171 | croak "couldn't figure out table name"; 172 | } 173 | 174 | while (@lines) { 175 | $_ = shift @lines; 176 | s/^\s*(.*?),?\s*$/$1/; # trim whitespace and trailing commas 177 | debug(4,"line: [$_]"); 178 | if (/^PRIMARY\s+KEY\s+(.+)$/) { 179 | my $primary = $1; 180 | croak "two primary keys in table '$self->{name}': '$primary', '$self->{primary_key}'\n" 181 | if $self->{primary_key}; 182 | debug(4,"got primary key $primary"); 183 | $self->{primary_key} = $primary; 184 | $primary =~ s/\((.*?)\)/$1/; 185 | $self->{primary}{$_} = 1 for(split(/,/, $primary)); 186 | 187 | next; 188 | } 189 | 190 | if (/^(?:CONSTRAINT\s+(.*)?)?\s+FOREIGN\s+KEY\s+(.*)$/) { 191 | my ($key, $val) = ($1, $2); 192 | croak "foreign key '$key' duplicated in table '$name'\n" 193 | if $self->{foreign_key}{$key}; 194 | debug(1,"got foreign key $key"); 195 | $self->{foreign_key}{$key} = $val; 196 | next; 197 | } 198 | 199 | if (/^(KEY|UNIQUE(?: KEY)?)\s+(\S+?)(?:\s+USING\s+(?:BTREE|HASH|RTREE))?\s*\((.*)\)(?:\s+USING\s+(?:BTREE|HASH|RTREE))?$/) { 200 | my ($type, $key, $val) = ($1, $2, $3); 201 | croak "index '$key' duplicated in table '$self->{name}'\n" 202 | if $self->{indices}{$key}; 203 | $self->{indices}{$key} = $val; 204 | $self->{unique}{$key} = 1 if($type =~ /unique/i); 205 | debug(4, "got ", defined $self->{unique}{$key} ? 'unique ' : '', "index key '$key': ($val)"); 206 | next; 207 | } 208 | 209 | if (/^(FULLTEXT(?:\s+KEY|INDEX)?)\s+(\S+?)\s*\((.*)\)$/) { 210 | my ($type, $key, $val) = ($1, $2, $3); 211 | croak "FULLTEXT index '$key' duplicated in table '$self->{name}'\n" 212 | if $self->{fulltext}{$key}; 213 | $self->{indices}{$key} = $val; 214 | $self->{fulltext}{$key} = 1; 215 | debug(4,"got FULLTEXT index '$key': ($val)"); 216 | next; 217 | } 218 | 219 | if (/^\)\s*(.*?);$/) { # end of table definition 220 | $self->{options} = $1; 221 | debug(4,"got table options '$self->{options}'"); 222 | last; 223 | } 224 | 225 | if (/^(\S+)\s*(.*)/) { 226 | my ($field, $fdef) = ($1, $2); 227 | croak "definition for field '$field' duplicated in table '$self->{name}'\n" 228 | if $self->{fields}{$field}; 229 | $self->{fields}{$field} = $fdef; 230 | debug(4,"got field def '$field': $fdef"); 231 | next unless $fdef =~ /\s+AUTO_INCREMENT\b/; 232 | $self->{auto_inc}{$field} = 1; 233 | debug(4,"got AUTO_INCREMENT field '$field'"); 234 | next; 235 | } 236 | 237 | croak "unparsable line in definition for table '$self->{name}':\n$_"; 238 | } 239 | 240 | warn "table '$self->{name}' didn't have terminator\n" 241 | unless defined $self->{options}; 242 | 243 | @lines = grep ! m{^/\*!40\d{3} .*? \*/;}, @lines; 244 | @lines = grep ! m{^(SET |DROP TABLE)}, @lines; 245 | 246 | warn "table '$self->{name}' had trailing garbage:\n", join '', @lines 247 | if @lines; 248 | } 249 | 250 | 1; 251 | 252 | __END__ 253 | 254 | =head1 COPYRIGHT AND LICENSE 255 | 256 | Copyright (c) 2000-2016 Adam Spiers. All rights reserved. This 257 | program is free software; you can redistribute it and/or modify it 258 | under the same terms as Perl itself. 259 | 260 | =head1 SEE ALSO 261 | 262 | L, L, L, L 263 | 264 | =head1 AUTHOR 265 | 266 | Adam Spiers 267 | 268 | =cut 269 | -------------------------------------------------------------------------------- /lib/MySQL/Diff/Utils.pm: -------------------------------------------------------------------------------- 1 | package MySQL::Diff::Utils; 2 | 3 | =head1 NAME 4 | 5 | MySQL::Diff::Utils - Supporting functions for MySQL:Diff 6 | 7 | =head1 SYNOPSIS 8 | 9 | use MySQL::Diff::Utils qw(debug_level debug); 10 | 11 | =head1 DESCRIPTION 12 | 13 | Currently contains the debug message handling routines. 14 | 15 | =cut 16 | 17 | use warnings; 18 | use strict; 19 | 20 | our $VERSION = '0.60'; 21 | 22 | # ------------------------------------------------------------------------------ 23 | # Libraries 24 | 25 | use IO::File; 26 | 27 | # ------------------------------------------------------------------------------ 28 | # Export Components 29 | 30 | use base qw(Exporter); 31 | our @EXPORT_OK = qw(debug_file debug_level debug); 32 | 33 | # ------------------------------------------------------------------------------ 34 | 35 | =head1 FUNCTIONS 36 | 37 | =head2 Public Functions 38 | 39 | Fuller documentation will appear here in time :) 40 | 41 | =over 4 42 | 43 | =item * debug_file( $file ) 44 | 45 | Accessor to set/get the current debug log file. 46 | 47 | =item * debug_level( $level ) 48 | 49 | Accessor to set/get the current debug level for messages. 50 | 51 | Current levels range from 1 to 4, with 1 being very brief processing messages, 52 | 2 providing high level process flow messages, 3 providing low level process 53 | flow messages and 4 providing data dumps, etc where appropriate. 54 | 55 | =item * debug 56 | 57 | Writes to debug log file (if specified) and STDERR the given message, provided 58 | is equal to or lower than the current debug level. 59 | 60 | =back 61 | 62 | =cut 63 | 64 | { 65 | my $debug_file; 66 | my $debug_level = 0; 67 | 68 | sub debug_file { 69 | my ($new_debug_file) = @_; 70 | $debug_file = $new_debug_file if defined $new_debug_file; 71 | return $debug_file; 72 | } 73 | 74 | sub debug_level { 75 | my ($new_debug_level) = @_; 76 | $debug_level = $new_debug_level if defined $new_debug_level; 77 | return $debug_level; 78 | } 79 | 80 | sub debug { 81 | my $level = shift; 82 | return unless($debug_level >= $level && @_); 83 | 84 | if($debug_file) { 85 | if(my $fh = IO::File->new($debug_file, 'a+')) { 86 | print $fh @_,"\n"; 87 | $fh->close; 88 | return; 89 | } 90 | } 91 | 92 | print STDERR @_,"\n"; 93 | } 94 | 95 | } 96 | 97 | 1; 98 | 99 | __END__ 100 | 101 | =head1 COPYRIGHT AND LICENSE 102 | 103 | Copyright (c) 2000-2016 Adam Spiers. All rights reserved. This 104 | program is free software; you can redistribute it and/or modify it 105 | under the same terms as Perl itself. 106 | 107 | =head1 SEE ALSO 108 | 109 | L, L, L, L 110 | 111 | =head1 AUTHOR 112 | 113 | Adam Spiers 114 | 115 | =cut 116 | -------------------------------------------------------------------------------- /t/01use.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | use strict; 3 | 4 | use Test::More tests => 4; 5 | 6 | BEGIN { 7 | use_ok( 'MySQL::Diff' ); 8 | use_ok( 'MySQL::Diff::Database' ); 9 | use_ok( 'MySQL::Diff::Table' ); 10 | use_ok( 'MySQL::Diff::Utils' ); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /t/all.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | 3 | use strict; 4 | 5 | use Test::More; 6 | use MySQL::Diff; 7 | use MySQL::Diff::Database; 8 | 9 | my $TEST_USER = 'test'; 10 | my @VALID_ENGINES = qw(MyISAM InnoDB); 11 | my $VALID_ENGINES = join '|', @VALID_ENGINES; 12 | 13 | my %tables = ( 14 | foo1 => ' 15 | CREATE TABLE foo ( 16 | id INT(11) NOT NULL auto_increment, 17 | foreign_id INT(11) NOT NULL, 18 | PRIMARY KEY (id) 19 | ) DEFAULT CHARACTER SET utf8; 20 | ', 21 | 22 | foo2 => ' 23 | # here be a comment 24 | 25 | CREATE TABLE foo ( 26 | id INT(11) NOT NULL auto_increment, 27 | foreign_id INT(11) NOT NULL, # another random comment 28 | field BLOB, 29 | PRIMARY KEY (id) 30 | ) DEFAULT CHARACTER SET utf8; 31 | ', 32 | 33 | foo3 => ' 34 | CREATE TABLE foo ( 35 | id INT(11) NOT NULL auto_increment, 36 | foreign_id INT(11) NOT NULL, 37 | field TINYBLOB, 38 | PRIMARY KEY (id) 39 | ) DEFAULT CHARACTER SET utf8; 40 | ', 41 | 42 | foo4 => ' 43 | CREATE TABLE foo ( 44 | id INT(11) NOT NULL auto_increment, 45 | foreign_id INT(11) NOT NULL, 46 | field TINYBLOB, 47 | PRIMARY KEY (id, foreign_id) 48 | ) DEFAULT CHARACTER SET utf8; 49 | ', 50 | 51 | bar1 => ' 52 | CREATE TABLE bar ( 53 | id INT AUTO_INCREMENT NOT NULL PRIMARY KEY, 54 | ctime DATETIME, 55 | utime DATETIME, 56 | name CHAR(16), 57 | age INT 58 | ) DEFAULT CHARACTER SET utf8; 59 | ', 60 | 61 | bar2 => ' 62 | CREATE TABLE bar ( 63 | id INT AUTO_INCREMENT NOT NULL PRIMARY KEY, 64 | ctime DATETIME, 65 | utime DATETIME, # FOO! 66 | name CHAR(16), 67 | age INT, 68 | UNIQUE (name, age) 69 | ) DEFAULT CHARACTER SET utf8; 70 | ', 71 | 72 | bar3 => ' 73 | CREATE TABLE bar ( 74 | id INT AUTO_INCREMENT NOT NULL PRIMARY KEY, 75 | ctime DATETIME, 76 | utime DATETIME, 77 | name CHAR(16), 78 | age INT, 79 | UNIQUE (id, name, age) 80 | ) DEFAULT CHARACTER SET utf8; 81 | ', 82 | 83 | baz1 => ' 84 | CREATE TABLE baz ( 85 | firstname CHAR(16), 86 | surname CHAR(16) 87 | ) DEFAULT CHARACTER SET utf8; 88 | ', 89 | 90 | baz2 => ' 91 | CREATE TABLE baz ( 92 | firstname CHAR(16), 93 | surname CHAR(16), 94 | UNIQUE (firstname, surname) 95 | ) DEFAULT CHARACTER SET utf8; 96 | ', 97 | 98 | baz3 => ' 99 | CREATE TABLE baz ( 100 | firstname CHAR(16), 101 | surname CHAR(16), 102 | KEY (firstname, surname) 103 | ) DEFAULT CHARACTER SET utf8; 104 | ', 105 | 106 | qux1 => ' 107 | CREATE TABLE qux ( 108 | age INT 109 | ) DEFAULT CHARACTER SET utf8; 110 | ', 111 | 112 | qux2 => ' 113 | CREATE TABLE qux ( 114 | id INT NOT NULL AUTO_INCREMENT, 115 | age INT, 116 | PRIMARY KEY (id) 117 | ) DEFAULT CHARACTER SET utf8; 118 | ', 119 | 120 | qux3 => ' 121 | CREATE TABLE qux ( 122 | id INT NOT NULL AUTO_INCREMENT, 123 | age INT, 124 | UNIQUE KEY (id) 125 | ) DEFAULT CHARACTER SET utf8; 126 | ', 127 | 128 | ); 129 | 130 | my %tests = ( 131 | 'add column' => 132 | [ 133 | {}, 134 | @tables{qw/foo1 foo2/}, 135 | '## mysqldiff 136 | ## 137 | ## Run on 138 | ## 139 | ## --- file: tmp.db1 140 | ## +++ file: tmp.db2 141 | 142 | ALTER TABLE foo ADD COLUMN field blob; 143 | ', 144 | ], 145 | 146 | 'drop column' => 147 | [ 148 | {}, 149 | @tables{qw/foo2 foo1/}, 150 | '## mysqldiff 151 | ## 152 | ## Run on 153 | ## 154 | ## --- file: tmp.db1 155 | ## +++ file: tmp.db2 156 | 157 | ALTER TABLE foo DROP COLUMN field; # was blob 158 | ', 159 | ], 160 | 161 | 'change column' => 162 | [ 163 | {}, 164 | @tables{qw/foo2 foo3/}, 165 | '## mysqldiff 166 | ## 167 | ## Run on 168 | ## 169 | ## --- file: tmp.db1 170 | ## +++ file: tmp.db2 171 | 172 | ALTER TABLE foo CHANGE COLUMN field field tinyblob; # was blob 173 | ' 174 | ], 175 | 176 | 'no-old-defs' => 177 | [ 178 | { 'no-old-defs' => 1 }, 179 | @tables{qw/foo2 foo1/}, 180 | '## mysqldiff 181 | ## 182 | ## Run on 183 | ## Options: no-old-defs 184 | ## 185 | ## --- file: tmp.db1 186 | ## +++ file: tmp.db2 187 | 188 | ALTER TABLE foo DROP COLUMN field; 189 | ', 190 | ], 191 | 192 | 'add table' => 193 | [ 194 | { }, 195 | $tables{foo1}, $tables{foo2} . $tables{bar1}, 196 | '## mysqldiff 197 | ## 198 | ## Run on 199 | ## 200 | ## --- file: tmp.db1 201 | ## +++ file: tmp.db2 202 | 203 | ALTER TABLE foo ADD COLUMN field blob; 204 | CREATE TABLE bar ( 205 | id int(11) NOT NULL auto_increment, 206 | ctime datetime default NULL, 207 | utime datetime default NULL, 208 | name char(16) default NULL, 209 | age int(11) default NULL, 210 | PRIMARY KEY (id) 211 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 212 | 213 | ', 214 | ], 215 | 216 | 'drop table' => 217 | [ 218 | { }, 219 | $tables{foo1} . $tables{bar1}, $tables{foo2}, 220 | '## mysqldiff 221 | ## 222 | ## Run on 223 | ## 224 | ## --- file: tmp.db1 225 | ## +++ file: tmp.db2 226 | 227 | DROP TABLE bar; 228 | 229 | ALTER TABLE foo ADD COLUMN field blob; 230 | ', 231 | ], 232 | 233 | 'only-both' => 234 | [ 235 | { 'only-both' => 1 }, 236 | $tables{foo1} . $tables{bar1}, $tables{foo2}, 237 | '## mysqldiff 238 | ## 239 | ## Run on 240 | ## Options: only-both 241 | ## 242 | ## --- file: tmp.db1 243 | ## +++ file: tmp.db2 244 | 245 | ALTER TABLE foo ADD COLUMN field blob; 246 | ', 247 | ], 248 | 249 | 'keep-old-tables' => 250 | [ 251 | { 'keep-old-tables' => 1 }, 252 | $tables{foo1} . $tables{bar1}, $tables{foo2}, 253 | '## mysqldiff 254 | ## 255 | ## Run on 256 | ## Options: keep-old-tables 257 | ## 258 | ## --- file: tmp.db1 259 | ## +++ file: tmp.db2 260 | 261 | ALTER TABLE foo ADD COLUMN field blob; 262 | ', 263 | ], 264 | 265 | 'keep-old-columns' => 266 | [ 267 | { 'keep-old-columns' => 1 }, 268 | $tables{foo2} . $tables{bar1}, $tables{foo1}, 269 | '## mysqldiff 270 | ## 271 | ## Run on 272 | ## Options: keep-old-columns 273 | ## 274 | ## --- file: tmp.db1 275 | ## +++ file: tmp.db2 276 | 277 | DROP TABLE bar; 278 | 279 | ', 280 | ], 281 | 282 | 'table-re' => 283 | [ 284 | { 'table-re' => 'ba' }, 285 | $tables{foo1} . $tables{bar1} . $tables{baz1}, 286 | $tables{foo2} . $tables{bar2} . $tables{baz2}, 287 | '## mysqldiff 288 | ## 289 | ## Run on 290 | ## Options: table-re=ba 291 | ## 292 | ## --- file: tmp.db1 293 | ## +++ file: tmp.db2 294 | 295 | ALTER TABLE bar ADD UNIQUE name (name,age); 296 | ALTER TABLE baz ADD UNIQUE firstname (firstname,surname); 297 | ', 298 | ], 299 | 300 | 'single-transaction' => 301 | [ 302 | { 'single-transaction' => 'ba' }, 303 | $tables{foo1} . $tables{bar1} . $tables{baz1}, 304 | $tables{foo2} . $tables{bar2} . $tables{baz2}, 305 | '## mysqldiff 306 | ## 307 | ## Run on 308 | ## Options: single-transaction=ba 309 | ## 310 | ## --- file: tmp.db1 311 | ## +++ file: tmp.db2 312 | 313 | ALTER TABLE bar ADD UNIQUE name (name,age); 314 | ALTER TABLE baz ADD UNIQUE firstname (firstname,surname); 315 | ALTER TABLE foo ADD COLUMN field blob; 316 | ', 317 | ], 318 | 319 | 'drop primary key with auto weirdness' => 320 | [ 321 | {}, 322 | $tables{foo3}, 323 | $tables{foo4}, 324 | '## mysqldiff 325 | ## 326 | ## Run on 327 | ## 328 | ## --- file: tmp.db1 329 | ## +++ file: tmp.db2 330 | 331 | ALTER TABLE foo ADD INDEX (id); # auto columns must always be indexed 332 | ALTER TABLE foo DROP PRIMARY KEY; # was (id) 333 | ALTER TABLE foo ADD PRIMARY KEY (id,foreign_id); 334 | ALTER TABLE foo DROP INDEX id; 335 | ', 336 | ], 337 | 338 | 'drop additional primary key' => 339 | [ 340 | {}, 341 | $tables{foo4}, 342 | $tables{foo3}, 343 | '## mysqldiff 344 | ## 345 | ## Run on 346 | ## 347 | ## --- file: tmp.db1 348 | ## +++ file: tmp.db2 349 | 350 | ALTER TABLE foo ADD INDEX (id); # auto columns must always be indexed 351 | ALTER TABLE foo DROP PRIMARY KEY; # was (id,foreign_id) 352 | ALTER TABLE foo ADD PRIMARY KEY (id); 353 | ALTER TABLE foo DROP INDEX id; 354 | ', 355 | ], 356 | 357 | 'unique changes' => 358 | [ 359 | {}, 360 | $tables{bar1}, 361 | $tables{bar2}, 362 | '## mysqldiff 363 | ## 364 | ## Run on 365 | ## 366 | ## --- file: tmp.db1 367 | ## +++ file: tmp.db2 368 | 369 | ALTER TABLE bar ADD UNIQUE name (name,age); 370 | ', 371 | ], 372 | 373 | 'drop index' => 374 | [ 375 | {}, 376 | $tables{bar2}, 377 | $tables{bar1}, 378 | '## mysqldiff 379 | ## 380 | ## Run on 381 | ## 382 | ## --- file: tmp.db1 383 | ## +++ file: tmp.db2 384 | 385 | ALTER TABLE bar DROP INDEX name; # was UNIQUE (name,age) 386 | ', 387 | ], 388 | 389 | 'alter indices' => 390 | [ 391 | {}, 392 | $tables{bar2}, 393 | $tables{bar3}, 394 | '## mysqldiff 395 | ## 396 | ## Run on 397 | ## 398 | ## --- file: tmp.db1 399 | ## +++ file: tmp.db2 400 | 401 | ALTER TABLE bar DROP INDEX name; # was UNIQUE (name,age) 402 | ALTER TABLE bar ADD UNIQUE id (id,name,age); 403 | ', 404 | ], 405 | 406 | 'alter indices 2' => 407 | [ 408 | {}, 409 | $tables{bar3}, 410 | $tables{bar2}, 411 | '## mysqldiff 412 | ## 413 | ## Run on 414 | ## 415 | ## --- file: tmp.db1 416 | ## +++ file: tmp.db2 417 | 418 | ALTER TABLE bar DROP INDEX id; # was UNIQUE (id,name,age) 419 | ALTER TABLE bar ADD UNIQUE name (name,age); 420 | ', 421 | ], 422 | 423 | 'add unique index' => 424 | [ 425 | {}, 426 | $tables{bar1}, 427 | $tables{bar3}, 428 | '## mysqldiff 429 | ## 430 | ## Run on 431 | ## 432 | ## --- file: tmp.db1 433 | ## +++ file: tmp.db2 434 | 435 | ALTER TABLE bar ADD UNIQUE id (id,name,age); 436 | ', 437 | ], 438 | 439 | 'drop unique index' => 440 | [ 441 | {}, 442 | $tables{bar3}, 443 | $tables{bar1}, 444 | '## mysqldiff 445 | ## 446 | ## Run on 447 | ## 448 | ## --- file: tmp.db1 449 | ## +++ file: tmp.db2 450 | 451 | ALTER TABLE bar DROP INDEX id; # was UNIQUE (id,name,age) 452 | ', 453 | ], 454 | 455 | 'alter unique index' => 456 | [ 457 | {}, 458 | $tables{baz2}, 459 | $tables{baz3}, 460 | '## mysqldiff 461 | ## 462 | ## Run on 463 | ## 464 | ## --- file: tmp.db1 465 | ## +++ file: tmp.db2 466 | 467 | ALTER TABLE baz DROP INDEX firstname; # was UNIQUE (firstname,surname) 468 | ALTER TABLE baz ADD INDEX firstname (firstname,surname); 469 | ', 470 | ], 471 | 472 | 'alter unique index 2' => 473 | [ 474 | {}, 475 | $tables{baz3}, 476 | $tables{baz2}, 477 | '## mysqldiff 478 | ## 479 | ## Run on 480 | ## 481 | ## --- file: tmp.db1 482 | ## +++ file: tmp.db2 483 | 484 | ALTER TABLE baz DROP INDEX firstname; # was INDEX (firstname,surname) 485 | ALTER TABLE baz ADD UNIQUE firstname (firstname,surname); 486 | ', 487 | ], 488 | 489 | 'add auto increment primary key' => 490 | [ 491 | {}, 492 | $tables{qux1}, 493 | $tables{qux2}, 494 | '## mysqldiff 495 | ## 496 | ## Run on 497 | ## 498 | ## --- file: tmp.db1 499 | ## +++ file: tmp.db2 500 | 501 | ALTER TABLE qux ADD COLUMN id int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY; 502 | ', 503 | ], 504 | 505 | 'add auto increment unique key' => 506 | [ 507 | {}, 508 | $tables{qux1}, 509 | $tables{qux3}, 510 | '## mysqldiff 511 | ## 512 | ## Run on 513 | ## 514 | ## --- file: tmp.db1 515 | ## +++ file: tmp.db2 516 | 517 | ALTER TABLE qux ADD COLUMN id int(11) NOT NULL AUTO_INCREMENT UNIQUE KEY; 518 | ', 519 | ], 520 | ); 521 | 522 | my $BAIL = check_setup(); 523 | plan skip_all => $BAIL if($BAIL); 524 | 525 | my $total = scalar(keys %tests) * 5; 526 | plan tests => $total; 527 | 528 | use Data::Dumper; 529 | 530 | my @tests = (keys %tests); #keys %tests 531 | 532 | { 533 | my %debug = ( debug_file => 'debug.log', debug => 9 ); 534 | unlink $debug{debug_file}; 535 | 536 | for my $test (@tests) { 537 | note( "Testing $test\n" ); 538 | 539 | my ($opts, $db1_defs, $db2_defs, $expected) = @{$tests{$test}}; 540 | 541 | note("test=".Dumper($tests{$test})); 542 | 543 | my $diff = MySQL::Diff->new(%$opts, %debug); 544 | isa_ok($diff,'MySQL::Diff'); 545 | 546 | my $db1 = get_db($db1_defs, 1, $opts->{'table-re'}, $opts->{'single_transaction'}); 547 | my $db2 = get_db($db2_defs, 2, $opts->{'table-re'}, $opts->{'single_transaction'}); 548 | 549 | my $d1 = $diff->register_db($db1, 1); 550 | my $d2 = $diff->register_db($db2, 2); 551 | note("d1=" . Dumper($d1)); 552 | note("d2=" . Dumper($d2)); 553 | 554 | isa_ok($d1, 'MySQL::Diff::Database'); 555 | isa_ok($d2, 'MySQL::Diff::Database'); 556 | 557 | my $diffs = $diff->diff(); 558 | $diffs =~ s/^## mysqldiff [\d.]+/## mysqldiff /m; 559 | $diffs =~ s/^## Run on .*/## Run on /m; 560 | $diffs =~ s{/\*!40\d{3} .*? \*/;\n*}{}m; 561 | $diffs =~ s/ *$//gm; 562 | for ($diffs, $expected) { 563 | s/ default\b/ DEFAULT/gi; 564 | s/PRIMARY KEY +\(/PRIMARY KEY (/g; 565 | s/auto_increment/AUTO_INCREMENT/gi; 566 | } 567 | 568 | my $engine = 'InnoDB'; 569 | my $ENGINE_RE = qr/ENGINE=($VALID_ENGINES)/; 570 | if ($diffs =~ $ENGINE_RE) { 571 | $engine = $1; 572 | $expected =~ s/$ENGINE_RE/ENGINE=$engine/g; 573 | } 574 | 575 | note("diffs = " . Dumper($diffs)); 576 | note("expected = " . Dumper($expected)); 577 | 578 | is_deeply($diffs, $expected, ".. expected differences for $test"); 579 | 580 | # Now test that $diffs correctly patches $db1_defs to $db2_defs. 581 | my $patched = get_db($db1_defs . "\n" . $diffs, 1, $opts->{'table-re'}, $opts->{'single-transaction'}); 582 | $diff->register_db($patched, 1); 583 | is_deeply($diff->diff(), '', ".. patched differences for $test"); 584 | } 585 | } 586 | 587 | 588 | sub get_db { 589 | my ($defs, $num, $table_re, $single_transaction) = @_; 590 | 591 | note("defs=$defs"); 592 | 593 | my $file = "tmp.db$num"; 594 | open(TMP, ">$file") or die "open: $!"; 595 | print TMP $defs; 596 | close(TMP); 597 | my $db = MySQL::Diff::Database->new(file => $file, auth => { user => $TEST_USER }, 'table-re' => $table_re, 'single-transaction' => $single_transaction); 598 | unlink $file; 599 | return $db; 600 | } 601 | 602 | sub check_setup { 603 | my $failure_string = "Cannot proceed with tests without "; 604 | _output_matches("mysql --help", qr/--password/) or 605 | return $failure_string . 'a MySQL client'; 606 | _output_matches("mysqldump --help", qr/--password/) or 607 | return $failure_string . 'mysqldump'; 608 | _output_matches("echo status | mysql -u $TEST_USER 2>&1", qr/Connection id:/) or 609 | return $failure_string . 'a valid connection'; 610 | return ''; 611 | } 612 | 613 | sub _output_matches { 614 | my ($cmd, $re) = @_; 615 | my ($exit, $out) = _run($cmd); 616 | 617 | my $issue; 618 | if (defined $exit) { 619 | if ($exit == 0) { 620 | $issue = "Output from '$cmd' didn't match /$re/:\n$out" if $out !~ $re; 621 | } 622 | else { 623 | $issue = "'$cmd' exited with status code $exit"; 624 | } 625 | } 626 | else { 627 | $issue = "Failed to execute '$cmd'"; 628 | } 629 | 630 | if ($issue) { 631 | warn $issue, "\n"; 632 | return 0; 633 | } 634 | return 1; 635 | } 636 | 637 | sub _run { 638 | my ($cmd) = @_; 639 | unless (open(CMD, "$cmd|")) { 640 | return (undef, "Failed to execute '$cmd': $!\n"); 641 | } 642 | my $out = join '', ; 643 | close(CMD); 644 | return ($?, $out); 645 | } 646 | -------------------------------------------------------------------------------- /t/regression-rt-77002.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | use strict; 3 | 4 | use Test::More tests => 4; 5 | 6 | # checks for regression to https://rt.cpan.org/Public/Bug/Display.html?id=77002 7 | 8 | BEGIN { 9 | use_ok('MySQL::Diff::Table'); 10 | } 11 | 12 | my $table_def = < [ def => $table_def ]; 22 | 23 | ok $table->{name} eq 'table_1', 'ensuring table name parsed properly'; 24 | 25 | my $duplicate_field_table_def = <new( def => $duplicate_field_table_def ); 38 | }; 39 | my $expected = qq{definition for field 'id' duplicated in table 'table_1'}; 40 | my @g = split /\n/, $@; 41 | my $got = $g[0]; 42 | 43 | ok $got eq $expected, 44 | 'ensuring table name returned in duplicate field name error'; 45 | 46 | __END__ 47 | -------------------------------------------------------------------------------- /t/regression-rt-79976.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | use strict; 3 | 4 | use Test::More tests => 4; 5 | 6 | # checks for regression to https://rt.cpan.org/Public/Bug/Display.html?id=79976 7 | 8 | use_ok('MySQL::Diff::Database'); 9 | can_ok 'MySQL::Diff::Database', 'auth_args'; 10 | 11 | my $out = `mysqldump test`; 12 | SKIP: { 13 | skip q{`mysqldump test` failed.}, 2 if $? != 0; 14 | my $db = new_ok 'MySQL::Diff::Database' => [ db => 'test' ]; 15 | can_ok $db, 'auth_args'; 16 | } 17 | 18 | __END__ 19 | -------------------------------------------------------------------------------- /xt/90podtest.t: -------------------------------------------------------------------------------- 1 | use Test::More; 2 | 3 | # Skip if doing a regular install 4 | plan skip_all => "Author tests not required for installation" 5 | unless ( $ENV{AUTOMATED_TESTING} ); 6 | 7 | eval "use Test::Pod 1.00"; 8 | plan skip_all => "Test::Pod 1.00 required for testing POD" if $@; 9 | all_pod_files_ok(); 10 | 11 | -------------------------------------------------------------------------------- /xt/91podcover.t: -------------------------------------------------------------------------------- 1 | use Test::More; 2 | 3 | # Skip if doing a regular install 4 | plan skip_all => "Author tests not required for installation" 5 | unless ( $ENV{AUTOMATED_TESTING} ); 6 | 7 | eval "use Test::Pod::Coverage 0.08"; 8 | plan skip_all => "Test::Pod::Coverage 0.08 required for testing POD coverage" if $@; 9 | all_pod_coverage_ok(); 10 | -------------------------------------------------------------------------------- /xt/94metatest.t: -------------------------------------------------------------------------------- 1 | use Test::More; 2 | use MySQL::Diff; 3 | 4 | # Skip if doing a regular install 5 | plan skip_all => "Author tests not required for installation" 6 | unless ( $ENV{AUTOMATED_TESTING} ); 7 | 8 | eval "use Test::CPAN::Meta 0.16"; 9 | plan skip_all => "Test::CPAN::Meta 0.16 required for testing META.yml" if $@; 10 | 11 | plan no_plan; 12 | 13 | my $yaml = meta_spec_ok(undef,undef,@_); 14 | 15 | is($yaml->{version},$MySQL::Diff::VERSION, 16 | 'META.yml distribution version matches'); 17 | 18 | if($yaml->{provides}) { 19 | for my $mod (keys %{$yaml->{provides}}) { 20 | is($yaml->{provides}{$mod}{version},$MySQL::Diff::VERSION, 21 | "META.yml entry [$mod] version matches"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /xt/95kwalitee.t: -------------------------------------------------------------------------------- 1 | # -*- perl -*- 2 | use strict; 3 | use warnings; 4 | 5 | use Test::More; 6 | use Config; 7 | 8 | plan skip_all => 'This test is only run for the module author' 9 | unless -d '.git' || $ENV{AUTOMATED_TESTING}; 10 | plan skip_all => 'Test::Kwalitee fails with clang -faddress-sanitizer' 11 | if $Config{ccflags} =~ /(-fsanitize=address|-faddress-sanitizer)/; 12 | 13 | use File::Copy 'cp'; 14 | cp('MYMETA.yml','META.yml') if -e 'MYMETA.yml' and !-e 'META.yml'; 15 | 16 | eval { 17 | require Test::Kwalitee; Test::Kwalitee->import; 18 | }; 19 | plan skip_all => "Test::Kwalitee needed for testing kwalitee" 20 | if $@; 21 | -------------------------------------------------------------------------------- /xt/96perl_minimum_version.t: -------------------------------------------------------------------------------- 1 | # -*- perl -*- 2 | 3 | # Test that our declared minimum Perl version matches our syntax 4 | use strict; 5 | BEGIN { 6 | $| = 1; 7 | $^W = 1; 8 | } 9 | 10 | my @MODULES = ( 11 | 'Perl::MinimumVersion 1.20', 12 | 'Test::MinimumVersion 0.008', 13 | ); 14 | 15 | # Don't run tests during end-user installs 16 | use Test::More; 17 | unless (-d '.git' || $ENV{AUTOMATED_TESTING}) { 18 | plan( skip_all => "Author tests not required for installation" ); 19 | } 20 | 21 | # Load the testing modules 22 | foreach my $MODULE ( @MODULES ) { 23 | eval "use $MODULE"; 24 | if ( $@ ) { 25 | plan( skip_all => "$MODULE not available for testing" ); 26 | die "Failed to load required release-testing module $MODULE" 27 | if -d '.git' || $ENV{AUTOMATED_TESTING}; 28 | } 29 | } 30 | 31 | # false positive use_base_exporter works ok with 5.6.2 32 | all_minimum_version_ok("5.006", {skip => ['lib/MySQL/Diff/Utils.pm']}); 33 | 34 | 1; 35 | --------------------------------------------------------------------------------