├── .travis.yml
├── composer.json
├── phpunit.xml
├── readme.md
├── src
├── Console
│ └── SqlCommand.php
├── SqlFormatter.php
├── SqlGeneratorServiceProvider.php
├── migrations
│ └── 2016_12_14_085908_test_sql_generator_table.php
└── sql_generator.php
└── tests
├── SqlGeneratorTest.php
├── TestCase.php
└── sql
└── database.sql
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - 5.6
5 | - 7.0
6 |
7 | sudo: false
8 |
9 | cache:
10 | directories:
11 | - laravel
12 |
13 | services: mysql
14 |
15 | script:
16 | - vendor/bin/phpunit
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "froiden/sql-generator",
3 | "description": "convert Laravel migrations to raw SQL scripts",
4 | "authors": [
5 | {
6 | "name": "Jayant Soni",
7 | "email": "jayant@froiden.com"
8 | }
9 | ],
10 | "autoload": {
11 | "psr-4": {
12 | "Froiden\\SqlGenerator\\": "src",
13 | "Froiden\\SqlGenerator\\Tests\\": "tests"
14 | }
15 | },
16 | "require": {
17 | "laravel/framework": ">= 5.3"
18 | },
19 | "license": "MIT"
20 | }
21 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
tags 125 | public static $use_pre = true; 126 | // This flag tells us if SqlFormatted has been initialized 127 | protected static $init; 128 | // Regular expressions for tokenizing 129 | protected static $regex_boundaries; 130 | protected static $regex_reserved; 131 | protected static $regex_reserved_newline; 132 | protected static $regex_reserved_toplevel; 133 | protected static $regex_function; 134 | // Cache variables 135 | // Only tokens shorter than this size will be cached. Somewhere between 10 and 20 seems to work well for most cases. 136 | public static $max_cachekey_size = 15; 137 | protected static $token_cache = array(); 138 | protected static $cache_hits = 0; 139 | protected static $cache_misses = 0; 140 | /** 141 | * Get stats about the token cache 142 | * @return Array An array containing the keys 'hits', 'misses', 'entries', and 'size' in bytes 143 | */ 144 | public static function getCacheStats() 145 | { 146 | return array( 147 | 'hits'=>self::$cache_hits, 148 | 'misses'=>self::$cache_misses, 149 | 'entries'=>count(self::$token_cache), 150 | 'size'=>strlen(serialize(self::$token_cache)) 151 | ); 152 | } 153 | /** 154 | * Stuff that only needs to be done once. Builds regular expressions and sorts the reserved words. 155 | */ 156 | protected static function init() 157 | { 158 | if (self::$init) return; 159 | // Sort reserved word list from longest word to shortest, 3x faster than usort 160 | $reservedMap = array_combine(self::$reserved, array_map('strlen', self::$reserved)); 161 | arsort($reservedMap); 162 | self::$reserved = array_keys($reservedMap); 163 | // Set up regular expressions 164 | self::$regex_boundaries = '('.implode('|',array_map(array(__CLASS__, 'quote_regex'),self::$boundaries)).')'; 165 | self::$regex_reserved = '('.implode('|',array_map(array(__CLASS__, 'quote_regex'),self::$reserved)).')'; 166 | self::$regex_reserved_toplevel = str_replace(' ','\\s+','('.implode('|',array_map(array(__CLASS__, 'quote_regex'),self::$reserved_toplevel)).')'); 167 | self::$regex_reserved_newline = str_replace(' ','\\s+','('.implode('|',array_map(array(__CLASS__, 'quote_regex'),self::$reserved_newline)).')'); 168 | self::$regex_function = '('.implode('|',array_map(array(__CLASS__, 'quote_regex'),self::$functions)).')'; 169 | self::$init = true; 170 | } 171 | /** 172 | * Return the next token and token type in a SQL string. 173 | * Quoted strings, comments, reserved words, whitespace, and punctuation are all their own tokens. 174 | * 175 | * @param String $string The SQL string 176 | * @param array $previous The result of the previous getNextToken() call 177 | * 178 | * @return Array An associative array containing the type and value of the token. 179 | */ 180 | protected static function getNextToken($string, $previous = null) 181 | { 182 | // Whitespace 183 | if (preg_match('/^\s+/',$string,$matches)) { 184 | return array( 185 | self::TOKEN_VALUE => $matches[0], 186 | self::TOKEN_TYPE=>self::TOKEN_TYPE_WHITESPACE 187 | ); 188 | } 189 | // Comment 190 | if ($string[0] === '#' || (isset($string[1])&&($string[0]==='-'&&$string[1]==='-') || ($string[0]==='/'&&$string[1]==='*'))) { 191 | // Comment until end of line 192 | if ($string[0] === '-' || $string[0] === '#') { 193 | $last = strpos($string, "\n"); 194 | $type = self::TOKEN_TYPE_COMMENT; 195 | } else { // Comment until closing comment tag 196 | $last = strpos($string, "*/", 2) + 2; 197 | $type = self::TOKEN_TYPE_BLOCK_COMMENT; 198 | } 199 | if ($last === false) { 200 | $last = strlen($string); 201 | } 202 | return array( 203 | self::TOKEN_VALUE => substr($string, 0, $last), 204 | self::TOKEN_TYPE => $type 205 | ); 206 | } 207 | // Quoted String 208 | if ($string[0]==='"' || $string[0]==='\'' || $string[0]==='`' || $string[0]==='[') { 209 | $return = array( 210 | self::TOKEN_TYPE => (($string[0]==='`' || $string[0]==='[')? self::TOKEN_TYPE_BACKTICK_QUOTE : self::TOKEN_TYPE_QUOTE), 211 | self::TOKEN_VALUE => self::getQuotedString($string) 212 | ); 213 | return $return; 214 | } 215 | // User-defined Variable 216 | if (($string[0] === '@' || $string[0] === ':') && isset($string[1])) { 217 | $ret = array( 218 | self::TOKEN_VALUE => null, 219 | self::TOKEN_TYPE => self::TOKEN_TYPE_VARIABLE 220 | ); 221 | 222 | // If the variable name is quoted 223 | if ($string[1]==='"' || $string[1]==='\'' || $string[1]==='`') { 224 | $ret[self::TOKEN_VALUE] = $string[0].self::getQuotedString(substr($string,1)); 225 | } 226 | // Non-quoted variable name 227 | else { 228 | preg_match('/^('.$string[0].'[a-zA-Z0-9\._\$]+)/',$string,$matches); 229 | if ($matches) { 230 | $ret[self::TOKEN_VALUE] = $matches[1]; 231 | } 232 | } 233 | 234 | if($ret[self::TOKEN_VALUE] !== null) return $ret; 235 | } 236 | // Number (decimal, binary, or hex) 237 | if (preg_match('/^([0-9]+(\.[0-9]+)?|0x[0-9a-fA-F]+|0b[01]+)($|\s|"\'`|'.self::$regex_boundaries.')/',$string,$matches)) { 238 | return array( 239 | self::TOKEN_VALUE => $matches[1], 240 | self::TOKEN_TYPE=>self::TOKEN_TYPE_NUMBER 241 | ); 242 | } 243 | // Boundary Character (punctuation and symbols) 244 | if (preg_match('/^('.self::$regex_boundaries.')/',$string,$matches)) { 245 | return array( 246 | self::TOKEN_VALUE => $matches[1], 247 | self::TOKEN_TYPE => self::TOKEN_TYPE_BOUNDARY 248 | ); 249 | } 250 | // A reserved word cannot be preceded by a '.' 251 | // this makes it so in "mytable.from", "from" is not considered a reserved word 252 | if (!$previous || !isset($previous[self::TOKEN_VALUE]) || $previous[self::TOKEN_VALUE] !== '.') { 253 | $upper = strtoupper($string); 254 | // Top Level Reserved Word 255 | if (preg_match('/^('.self::$regex_reserved_toplevel.')($|\s|'.self::$regex_boundaries.')/', $upper,$matches)) { 256 | return array( 257 | self::TOKEN_TYPE=>self::TOKEN_TYPE_RESERVED_TOPLEVEL, 258 | self::TOKEN_VALUE=>substr($string,0,strlen($matches[1])) 259 | ); 260 | } 261 | // Newline Reserved Word 262 | if (preg_match('/^('.self::$regex_reserved_newline.')($|\s|'.self::$regex_boundaries.')/', $upper,$matches)) { 263 | return array( 264 | self::TOKEN_TYPE=>self::TOKEN_TYPE_RESERVED_NEWLINE, 265 | self::TOKEN_VALUE=>substr($string,0,strlen($matches[1])) 266 | ); 267 | } 268 | // Other Reserved Word 269 | if (preg_match('/^('.self::$regex_reserved.')($|\s|'.self::$regex_boundaries.')/', $upper,$matches)) { 270 | return array( 271 | self::TOKEN_TYPE=>self::TOKEN_TYPE_RESERVED, 272 | self::TOKEN_VALUE=>substr($string,0,strlen($matches[1])) 273 | ); 274 | } 275 | } 276 | // A function must be suceeded by '(' 277 | // this makes it so "count(" is considered a function, but "count" alone is not 278 | $upper = strtoupper($string); 279 | // function 280 | if (preg_match('/^('.self::$regex_function.'[(]|\s|[)])/', $upper,$matches)) { 281 | return array( 282 | self::TOKEN_TYPE=>self::TOKEN_TYPE_RESERVED, 283 | self::TOKEN_VALUE=>substr($string,0,strlen($matches[1])-1) 284 | ); 285 | } 286 | // Non reserved word 287 | preg_match('/^(.*?)($|\s|["\'`]|'.self::$regex_boundaries.')/',$string,$matches); 288 | return array( 289 | self::TOKEN_VALUE => $matches[1], 290 | self::TOKEN_TYPE => self::TOKEN_TYPE_WORD 291 | ); 292 | } 293 | protected static function getQuotedString($string) 294 | { 295 | $ret = null; 296 | 297 | // This checks for the following patterns: 298 | // 1. backtick quoted string using `` to escape 299 | // 2. square bracket quoted string (SQL Server) using ]] to escape 300 | // 3. double quoted string using "" or \" to escape 301 | // 4. single quoted string using '' or \' to escape 302 | if ( preg_match('/^(((`[^`]*($|`))+)|((\[[^\]]*($|\]))(\][^\]]*($|\]))*)|(("[^"\\\\]*(?:\\\\.[^"\\\\]*)*("|$))+)|((\'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*(\'|$))+))/s', $string, $matches)) { 303 | $ret = $matches[1]; 304 | } 305 | 306 | return $ret; 307 | } 308 | /** 309 | * Takes a SQL string and breaks it into tokens. 310 | * Each token is an associative array with type and value. 311 | * 312 | * @param String $string The SQL string 313 | * 314 | * @return Array An array of tokens. 315 | */ 316 | protected static function tokenize($string) 317 | { 318 | self::init(); 319 | $tokens = array(); 320 | // Used for debugging if there is an error while tokenizing the string 321 | $original_length = strlen($string); 322 | // Used to make sure the string keeps shrinking on each iteration 323 | $old_string_len = strlen($string) + 1; 324 | $token = null; 325 | $current_length = strlen($string); 326 | // Keep processing the string until it is empty 327 | while ($current_length) { 328 | // If the string stopped shrinking, there was a problem 329 | if ($old_string_len <= $current_length) { 330 | $tokens[] = array( 331 | self::TOKEN_VALUE=>$string, 332 | self::TOKEN_TYPE=>self::TOKEN_TYPE_ERROR 333 | ); 334 | return $tokens; 335 | } 336 | $old_string_len = $current_length; 337 | // Determine if we can use caching 338 | if ($current_length >= self::$max_cachekey_size) { 339 | $cacheKey = substr($string,0,self::$max_cachekey_size); 340 | } else { 341 | $cacheKey = false; 342 | } 343 | // See if the token is already cached 344 | if ($cacheKey && isset(self::$token_cache[$cacheKey])) { 345 | // Retrieve from cache 346 | $token = self::$token_cache[$cacheKey]; 347 | $token_length = strlen($token[self::TOKEN_VALUE]); 348 | self::$cache_hits++; 349 | } else { 350 | // Get the next token and the token type 351 | $token = self::getNextToken($string, $token); 352 | $token_length = strlen($token[self::TOKEN_VALUE]); 353 | self::$cache_misses++; 354 | // If the token is shorter than the max length, store it in cache 355 | if ($cacheKey && $token_length < self::$max_cachekey_size) { 356 | self::$token_cache[$cacheKey] = $token; 357 | } 358 | } 359 | $tokens[] = $token; 360 | // Advance the string 361 | $string = substr($string, $token_length); 362 | $current_length -= $token_length; 363 | } 364 | return $tokens; 365 | } 366 | /** 367 | * Format the whitespace in a SQL string to make it easier to read. 368 | * 369 | * @param String $string The SQL string 370 | * @param boolean $highlight If true, syntax highlighting will also be performed 371 | * 372 | * @return String The SQL string with HTML styles and formatting wrapped in atag 373 | */ 374 | public static function format($string, $highlight=true) 375 | { 376 | // This variable will be populated with formatted html 377 | $return = ''; 378 | // Use an actual tab while formatting and then switch out with self::$tab at the end 379 | $tab = "\t"; 380 | $indent_level = 0; 381 | $newline = false; 382 | $inline_parentheses = false; 383 | $increase_special_indent = false; 384 | $increase_block_indent = false; 385 | $indent_types = array(); 386 | $added_newline = false; 387 | $inline_count = 0; 388 | $inline_indented = false; 389 | $clause_limit = false; 390 | // Tokenize String 391 | $original_tokens = self::tokenize($string); 392 | // Remove existing whitespace 393 | $tokens = array(); 394 | foreach ($original_tokens as $i=>$token) { 395 | if ($token[self::TOKEN_TYPE] !== self::TOKEN_TYPE_WHITESPACE) { 396 | $token['i'] = $i; 397 | $tokens[] = $token; 398 | } 399 | } 400 | // Format token by token 401 | foreach ($tokens as $i=>$token) { 402 | // Get highlighted token if doing syntax highlighting 403 | if ($highlight) { 404 | $highlighted = self::highlightToken($token); 405 | } else { // If returning raw text 406 | $highlighted = $token[self::TOKEN_VALUE]; 407 | } 408 | // If we are increasing the special indent level now 409 | if ($increase_special_indent) { 410 | $indent_level++; 411 | $increase_special_indent = false; 412 | array_unshift($indent_types,'special'); 413 | } 414 | // If we are increasing the block indent level now 415 | if ($increase_block_indent) { 416 | $indent_level++; 417 | $increase_block_indent = false; 418 | array_unshift($indent_types,'block'); 419 | } 420 | // If we need a new line before the token 421 | if ($newline) { 422 | $return .= "\n" . str_repeat($tab, $indent_level); 423 | $newline = false; 424 | $added_newline = true; 425 | } else { 426 | $added_newline = false; 427 | } 428 | // Display comments directly where they appear in the source 429 | if ($token[self::TOKEN_TYPE] === self::TOKEN_TYPE_COMMENT || $token[self::TOKEN_TYPE] === self::TOKEN_TYPE_BLOCK_COMMENT) { 430 | if ($token[self::TOKEN_TYPE] === self::TOKEN_TYPE_BLOCK_COMMENT) { 431 | $indent = str_repeat($tab,$indent_level); 432 | $return .= "\n" . $indent; 433 | $highlighted = str_replace("\n","\n".$indent,$highlighted); 434 | } 435 | $return .= $highlighted; 436 | $newline = true; 437 | continue; 438 | } 439 | if ($inline_parentheses) { 440 | // End of inline parentheses 441 | if ($token[self::TOKEN_VALUE] === ')') { 442 | $return = rtrim($return,' '); 443 | if ($inline_indented) { 444 | array_shift($indent_types); 445 | $indent_level --; 446 | $return .= "\n" . str_repeat($tab, $indent_level); 447 | } 448 | $inline_parentheses = false; 449 | $return .= $highlighted . ' '; 450 | continue; 451 | } 452 | if ($token[self::TOKEN_VALUE] === ',') { 453 | if ($inline_count >= 30) { 454 | $inline_count = 0; 455 | $newline = true; 456 | } 457 | } 458 | $inline_count += strlen($token[self::TOKEN_VALUE]); 459 | } 460 | // Opening parentheses increase the block indent level and start a new line 461 | if ($token[self::TOKEN_VALUE] === '(') { 462 | // First check if this should be an inline parentheses block 463 | // Examples are "NOW()", "COUNT(*)", "int(10)", key(`somecolumn`), DECIMAL(7,2) 464 | // Allow up to 3 non-whitespace tokens inside inline parentheses 465 | $length = 0; 466 | for ($j=1;$j<=250;$j++) { 467 | // Reached end of string 468 | if (!isset($tokens[$i+$j])) break; 469 | $next = $tokens[$i+$j]; 470 | // Reached closing parentheses, able to inline it 471 | if ($next[self::TOKEN_VALUE] === ')') { 472 | $inline_parentheses = true; 473 | $inline_count = 0; 474 | $inline_indented = false; 475 | break; 476 | } 477 | // Reached an invalid token for inline parentheses 478 | if ($next[self::TOKEN_VALUE]===';' || $next[self::TOKEN_VALUE]==='(') { 479 | break; 480 | } 481 | // Reached an invalid token type for inline parentheses 482 | if ($next[self::TOKEN_TYPE]===self::TOKEN_TYPE_RESERVED_TOPLEVEL || $next[self::TOKEN_TYPE]===self::TOKEN_TYPE_RESERVED_NEWLINE || $next[self::TOKEN_TYPE]===self::TOKEN_TYPE_COMMENT || $next[self::TOKEN_TYPE]===self::TOKEN_TYPE_BLOCK_COMMENT) { 483 | break; 484 | } 485 | $length += strlen($next[self::TOKEN_VALUE]); 486 | } 487 | if ($inline_parentheses && $length > 30) { 488 | $increase_block_indent = true; 489 | $inline_indented = true; 490 | $newline = true; 491 | } 492 | // Take out the preceding space unless there was whitespace there in the original query 493 | if (isset($original_tokens[$token['i']-1]) && $original_tokens[$token['i']-1][self::TOKEN_TYPE] !== self::TOKEN_TYPE_WHITESPACE) { 494 | $return = rtrim($return,' '); 495 | } 496 | if (!$inline_parentheses) { 497 | $increase_block_indent = true; 498 | // Add a newline after the parentheses 499 | $newline = true; 500 | } 501 | } 502 | // Closing parentheses decrease the block indent level 503 | elseif ($token[self::TOKEN_VALUE] === ')') { 504 | // Remove whitespace before the closing parentheses 505 | $return = rtrim($return,' '); 506 | $indent_level--; 507 | // Reset indent level 508 | while ($j=array_shift($indent_types)) { 509 | if ($j==='special') { 510 | $indent_level--; 511 | } else { 512 | break; 513 | } 514 | } 515 | if ($indent_level < 0) { 516 | // This is an error 517 | $indent_level = 0; 518 | if ($highlight) { 519 | $return .= "\n".self::highlightError($token[self::TOKEN_VALUE]); 520 | continue; 521 | } 522 | } 523 | // Add a newline before the closing parentheses (if not already added) 524 | if (!$added_newline) { 525 | $return .= "\n" . str_repeat($tab, $indent_level); 526 | } 527 | } 528 | // Top level reserved words start a new line and increase the special indent level 529 | elseif ($token[self::TOKEN_TYPE] === self::TOKEN_TYPE_RESERVED_TOPLEVEL) { 530 | $increase_special_indent = true; 531 | // If the last indent type was 'special', decrease the special indent for this round 532 | reset($indent_types); 533 | if (current($indent_types)==='special') { 534 | $indent_level--; 535 | array_shift($indent_types); 536 | } 537 | // Add a newline after the top level reserved word 538 | $newline = true; 539 | // Add a newline before the top level reserved word (if not already added) 540 | if (!$added_newline) { 541 | $return .= "\n" . str_repeat($tab, $indent_level); 542 | } 543 | // If we already added a newline, redo the indentation since it may be different now 544 | else { 545 | $return = rtrim($return,$tab).str_repeat($tab, $indent_level); 546 | } 547 | // If the token may have extra whitespace 548 | if (strpos($token[self::TOKEN_VALUE],' ')!==false || strpos($token[self::TOKEN_VALUE],"\n")!==false || strpos($token[self::TOKEN_VALUE],"\t")!==false) { 549 | $highlighted = preg_replace('/\s+/',' ',$highlighted); 550 | } 551 | //if SQL 'LIMIT' clause, start variable to reset newline 552 | if ($token[self::TOKEN_VALUE] === 'LIMIT' && !$inline_parentheses) { 553 | $clause_limit = true; 554 | } 555 | } 556 | // Checks if we are out of the limit clause 557 | elseif ($clause_limit && $token[self::TOKEN_VALUE] !== "," && $token[self::TOKEN_TYPE] !== self::TOKEN_TYPE_NUMBER && $token[self::TOKEN_TYPE] !== self::TOKEN_TYPE_WHITESPACE) { 558 | $clause_limit = false; 559 | } 560 | // Commas start a new line (unless within inline parentheses or SQL 'LIMIT' clause) 561 | elseif ($token[self::TOKEN_VALUE] === ',' && !$inline_parentheses) { 562 | //If the previous TOKEN_VALUE is 'LIMIT', resets new line 563 | if ($clause_limit === true) { 564 | $newline = false; 565 | $clause_limit = false; 566 | } 567 | // All other cases of commas 568 | else { 569 | $newline = true; 570 | } 571 | } 572 | // Newline reserved words start a new line 573 | elseif ($token[self::TOKEN_TYPE] === self::TOKEN_TYPE_RESERVED_NEWLINE) { 574 | // Add a newline before the reserved word (if not already added) 575 | if (!$added_newline) { 576 | $return .= "\n" . str_repeat($tab, $indent_level); 577 | } 578 | // If the token may have extra whitespace 579 | if (strpos($token[self::TOKEN_VALUE],' ')!==false || strpos($token[self::TOKEN_VALUE],"\n")!==false || strpos($token[self::TOKEN_VALUE],"\t")!==false) { 580 | $highlighted = preg_replace('/\s+/',' ',$highlighted); 581 | } 582 | } 583 | // Multiple boundary characters in a row should not have spaces between them (not including parentheses) 584 | elseif ($token[self::TOKEN_TYPE] === self::TOKEN_TYPE_BOUNDARY) { 585 | if (isset($tokens[$i-1]) && $tokens[$i-1][self::TOKEN_TYPE] === self::TOKEN_TYPE_BOUNDARY) { 586 | if (isset($original_tokens[$token['i']-1]) && $original_tokens[$token['i']-1][self::TOKEN_TYPE] !== self::TOKEN_TYPE_WHITESPACE) { 587 | $return = rtrim($return,' '); 588 | } 589 | } 590 | } 591 | // If the token shouldn't have a space before it 592 | if ($token[self::TOKEN_VALUE] === '.' || $token[self::TOKEN_VALUE] === ',' || $token[self::TOKEN_VALUE] === ';') { 593 | $return = rtrim($return, ' '); 594 | } 595 | $return .= $highlighted.' '; 596 | // If the token shouldn't have a space after it 597 | if ($token[self::TOKEN_VALUE] === '(' || $token[self::TOKEN_VALUE] === '.') { 598 | $return = rtrim($return,' '); 599 | } 600 | 601 | // If this is the "-" of a negative number, it shouldn't have a space after it 602 | if($token[self::TOKEN_VALUE] === '-' && isset($tokens[$i+1]) && $tokens[$i+1][self::TOKEN_TYPE] === self::TOKEN_TYPE_NUMBER && isset($tokens[$i-1])) { 603 | $prev = $tokens[$i-1][self::TOKEN_TYPE]; 604 | if($prev !== self::TOKEN_TYPE_QUOTE && $prev !== self::TOKEN_TYPE_BACKTICK_QUOTE && $prev !== self::TOKEN_TYPE_WORD && $prev !== self::TOKEN_TYPE_NUMBER) { 605 | $return = rtrim($return,' '); 606 | } 607 | } 608 | } 609 | // If there are unmatched parentheses 610 | if ($highlight && array_search('block',$indent_types) !== false) { 611 | $return .= "\n".self::highlightError("WARNING: unclosed parentheses or section"); 612 | } 613 | // Replace tab characters with the configuration tab character 614 | $return = trim(str_replace("\t",self::$tab,$return)); 615 | if ($highlight) { 616 | $return = self::output($return); 617 | } 618 | return $return; 619 | } 620 | /** 621 | * Add syntax highlighting to a SQL string 622 | * 623 | * @param String $string The SQL string 624 | * 625 | * @return String The SQL string with HTML styles applied 626 | */ 627 | public static function highlight($string) 628 | { 629 | $tokens = self::tokenize($string); 630 | $return = ''; 631 | foreach ($tokens as $token) { 632 | $return .= self::highlightToken($token); 633 | } 634 | return self::output($return); 635 | } 636 | /** 637 | * Split a SQL string into multiple queries. 638 | * Uses ";" as a query delimiter. 639 | * 640 | * @param String $string The SQL string 641 | * 642 | * @return Array An array of individual query strings without trailing semicolons 643 | */ 644 | public static function splitQuery($string) 645 | { 646 | $queries = array(); 647 | $current_query = ''; 648 | $empty = true; 649 | $tokens = self::tokenize($string); 650 | foreach ($tokens as $token) { 651 | // If this is a query separator 652 | if ($token[self::TOKEN_VALUE] === ';') { 653 | if (!$empty) { 654 | $queries[] = $current_query.';'; 655 | } 656 | $current_query = ''; 657 | $empty = true; 658 | continue; 659 | } 660 | // If this is a non-empty character 661 | if ($token[self::TOKEN_TYPE] !== self::TOKEN_TYPE_WHITESPACE && $token[self::TOKEN_TYPE] !== self::TOKEN_TYPE_COMMENT && $token[self::TOKEN_TYPE] !== self::TOKEN_TYPE_BLOCK_COMMENT) { 662 | $empty = false; 663 | } 664 | $current_query .= $token[self::TOKEN_VALUE]; 665 | } 666 | if (!$empty) { 667 | $queries[] = trim($current_query); 668 | } 669 | return $queries; 670 | } 671 | /** 672 | * Remove all comments from a SQL string 673 | * 674 | * @param String $string The SQL string 675 | * 676 | * @return String The SQL string without comments 677 | */ 678 | public static function removeComments($string) 679 | { 680 | $result = ''; 681 | $tokens = self::tokenize($string); 682 | foreach ($tokens as $token) { 683 | // Skip comment tokens 684 | if ($token[self::TOKEN_TYPE] === self::TOKEN_TYPE_COMMENT || $token[self::TOKEN_TYPE] === self::TOKEN_TYPE_BLOCK_COMMENT) { 685 | continue; 686 | } 687 | $result .= $token[self::TOKEN_VALUE]; 688 | } 689 | $result = self::format( $result,false); 690 | return $result; 691 | } 692 | /** 693 | * Compress a query by collapsing white space and removing comments 694 | * 695 | * @param String $string The SQL string 696 | * 697 | * @return String The SQL string without comments 698 | */ 699 | public static function compress($string) 700 | { 701 | $result = ''; 702 | $tokens = self::tokenize($string); 703 | $whitespace = true; 704 | foreach ($tokens as $token) { 705 | // Skip comment tokens 706 | if ($token[self::TOKEN_TYPE] === self::TOKEN_TYPE_COMMENT || $token[self::TOKEN_TYPE] === self::TOKEN_TYPE_BLOCK_COMMENT) { 707 | continue; 708 | } 709 | // Remove extra whitespace in reserved words (e.g "OUTER JOIN" becomes "OUTER JOIN") 710 | elseif ($token[self::TOKEN_TYPE] === self::TOKEN_TYPE_RESERVED || $token[self::TOKEN_TYPE] === self::TOKEN_TYPE_RESERVED_NEWLINE || $token[self::TOKEN_TYPE] === self::TOKEN_TYPE_RESERVED_TOPLEVEL) { 711 | $token[self::TOKEN_VALUE] = preg_replace('/\s+/',' ',$token[self::TOKEN_VALUE]); 712 | } 713 | if ($token[self::TOKEN_TYPE] === self::TOKEN_TYPE_WHITESPACE) { 714 | // If the last token was whitespace, don't add another one 715 | if ($whitespace) { 716 | continue; 717 | } else { 718 | $whitespace = true; 719 | // Convert all whitespace to a single space 720 | $token[self::TOKEN_VALUE] = ' '; 721 | } 722 | } else { 723 | $whitespace = false; 724 | } 725 | $result .= $token[self::TOKEN_VALUE]; 726 | } 727 | return rtrim($result); 728 | } 729 | /** 730 | * Highlights a token depending on its type. 731 | * 732 | * @param Array $token An associative array containing type and value. 733 | * 734 | * @return String HTML code of the highlighted token. 735 | */ 736 | protected static function highlightToken($token) 737 | { 738 | $type = $token[self::TOKEN_TYPE]; 739 | if (self::is_cli()) { 740 | $token = $token[self::TOKEN_VALUE]; 741 | } else { 742 | if (defined('ENT_IGNORE')) { 743 | $token = htmlentities($token[self::TOKEN_VALUE],ENT_COMPAT | ENT_IGNORE ,'UTF-8'); 744 | } else { 745 | $token = htmlentities($token[self::TOKEN_VALUE],ENT_COMPAT,'UTF-8'); 746 | } 747 | } 748 | if ($type===self::TOKEN_TYPE_BOUNDARY) { 749 | return self::highlightBoundary($token); 750 | } elseif ($type===self::TOKEN_TYPE_WORD) { 751 | return self::highlightWord($token); 752 | } elseif ($type===self::TOKEN_TYPE_BACKTICK_QUOTE) { 753 | return self::highlightBacktickQuote($token); 754 | } elseif ($type===self::TOKEN_TYPE_QUOTE) { 755 | return self::highlightQuote($token); 756 | } elseif ($type===self::TOKEN_TYPE_RESERVED) { 757 | return self::highlightReservedWord($token); 758 | } elseif ($type===self::TOKEN_TYPE_RESERVED_TOPLEVEL) { 759 | return self::highlightReservedWord($token); 760 | } elseif ($type===self::TOKEN_TYPE_RESERVED_NEWLINE) { 761 | return self::highlightReservedWord($token); 762 | } elseif ($type===self::TOKEN_TYPE_NUMBER) { 763 | return self::highlightNumber($token); 764 | } elseif ($type===self::TOKEN_TYPE_VARIABLE) { 765 | return self::highlightVariable($token); 766 | } elseif ($type===self::TOKEN_TYPE_COMMENT || $type===self::TOKEN_TYPE_BLOCK_COMMENT) { 767 | return self::highlightComment($token); 768 | } 769 | return $token; 770 | } 771 | /** 772 | * Highlights a quoted string 773 | * 774 | * @param String $value The token's value 775 | * 776 | * @return String HTML code of the highlighted token. 777 | */ 778 | protected static function highlightQuote($value) 779 | { 780 | if (self::is_cli()) { 781 | return self::$cli_quote . $value . "\x1b[0m"; 782 | } else { 783 | return '' . $value . ''; 784 | } 785 | } 786 | /** 787 | * Highlights a backtick quoted string 788 | * 789 | * @param String $value The token's value 790 | * 791 | * @return String HTML code of the highlighted token. 792 | */ 793 | protected static function highlightBacktickQuote($value) 794 | { 795 | if (self::is_cli()) { 796 | return self::$cli_backtick_quote . $value . "\x1b[0m"; 797 | } else { 798 | return '' . $value . ''; 799 | } 800 | } 801 | /** 802 | * Highlights a reserved word 803 | * 804 | * @param String $value The token's value 805 | * 806 | * @return String HTML code of the highlighted token. 807 | */ 808 | protected static function highlightReservedWord($value) 809 | { 810 | if (self::is_cli()) { 811 | return self::$cli_reserved . $value . "\x1b[0m"; 812 | } else { 813 | return '' . $value . ''; 814 | } 815 | } 816 | /** 817 | * Highlights a boundary token 818 | * 819 | * @param String $value The token's value 820 | * 821 | * @return String HTML code of the highlighted token. 822 | */ 823 | protected static function highlightBoundary($value) 824 | { 825 | if ($value==='(' || $value===')') return $value; 826 | if (self::is_cli()) { 827 | return self::$cli_boundary . $value . "\x1b[0m"; 828 | } else { 829 | return '' . $value . ''; 830 | } 831 | } 832 | /** 833 | * Highlights a number 834 | * 835 | * @param String $value The token's value 836 | * 837 | * @return String HTML code of the highlighted token. 838 | */ 839 | protected static function highlightNumber($value) 840 | { 841 | if (self::is_cli()) { 842 | return self::$cli_number . $value . "\x1b[0m"; 843 | } else { 844 | return '' . $value . ''; 845 | } 846 | } 847 | /** 848 | * Highlights an error 849 | * 850 | * @param String $value The token's value 851 | * 852 | * @return String HTML code of the highlighted token. 853 | */ 854 | protected static function highlightError($value) 855 | { 856 | if (self::is_cli()) { 857 | return self::$cli_error . $value . "\x1b[0m"; 858 | } else { 859 | return '' . $value . ''; 860 | } 861 | } 862 | /** 863 | * Highlights a comment 864 | * 865 | * @param String $value The token's value 866 | * 867 | * @return String HTML code of the highlighted token. 868 | */ 869 | protected static function highlightComment($value) 870 | { 871 | if (self::is_cli()) { 872 | return self::$cli_comment . $value . "\x1b[0m"; 873 | } else { 874 | return '' . $value . ''; 875 | } 876 | } 877 | /** 878 | * Highlights a word token 879 | * 880 | * @param String $value The token's value 881 | * 882 | * @return String HTML code of the highlighted token. 883 | */ 884 | protected static function highlightWord($value) 885 | { 886 | if (self::is_cli()) { 887 | return self::$cli_word . $value . "\x1b[0m"; 888 | } else { 889 | return '' . $value . ''; 890 | } 891 | } 892 | /** 893 | * Highlights a variable token 894 | * 895 | * @param String $value The token's value 896 | * 897 | * @return String HTML code of the highlighted token. 898 | */ 899 | protected static function highlightVariable($value) 900 | { 901 | if (self::is_cli()) { 902 | return self::$cli_variable . $value . "\x1b[0m"; 903 | } else { 904 | return '' . $value . ''; 905 | } 906 | } 907 | /** 908 | * Helper function for building regular expressions for reserved words and boundary characters 909 | * 910 | * @param String $a The string to be quoted 911 | * 912 | * @return String The quoted string 913 | */ 914 | private static function quote_regex($a) 915 | { 916 | return preg_quote($a,'/'); 917 | } 918 | /** 919 | * Helper function for building string output 920 | * 921 | * @param String $string The string to be quoted 922 | * 923 | * @return String The quoted string 924 | */ 925 | private static function output($string) 926 | { 927 | if (self::is_cli()) { 928 | return $string."\n"; 929 | } else { 930 | $string=trim($string); 931 | if (!self::$use_pre) { 932 | return $string; 933 | } 934 | return '' . $string . ''; 935 | } 936 | } 937 | private static function is_cli() 938 | { 939 | if (isset(self::$cli)) return self::$cli; 940 | else return php_sapi_name() === 'cli'; 941 | } 942 | } -------------------------------------------------------------------------------- /src/SqlGeneratorServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 19 | __DIR__.'/sql_generator.php' => config_path("sql_generator.php"), 20 | ]); 21 | } 22 | 23 | /** 24 | * Register the application services. 25 | * 26 | * @return void 27 | */ 28 | public function register() 29 | { 30 | $this->registerCommands(); 31 | } 32 | 33 | /** 34 | * Register all of sql generator command. 35 | * 36 | * @return void 37 | */ 38 | protected function registerCommands() 39 | { 40 | $this->commands('command.generate.sql'); 41 | 42 | $this->registerInstallCommand(); 43 | } 44 | 45 | /** 46 | * @return void 47 | */ 48 | protected function registerInstallCommand() 49 | { 50 | $this->app->singleton('command.generate.sql', function($app) { 51 | 52 | return new SqlCommand(); 53 | }); 54 | } 55 | 56 | /** 57 | * @return void 58 | */ 59 | public function provides() 60 | { 61 | return [ 62 | 'command.generate.sql' 63 | ]; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/migrations/2016_12_14_085908_test_sql_generator_table.php: -------------------------------------------------------------------------------- 1 | increments('test_id'); 18 | $table->string('test_name'); 19 | $table->string('test_email')->unique(); 20 | $table->enum('test_gender', ['male', 'female']); 21 | $table->string('test_password'); 22 | $table->rememberToken(); 23 | $table->timestamps(); 24 | }); 25 | // 26 | Schema::create('test_test_sql_blogs', function (Blueprint $table) { 27 | $table->increments('test_id'); 28 | $table->integer('test_userId')->unsigned(); 29 | $table->string('test_userName'); 30 | $table->string('test_title')->unique(); 31 | $table->text('test_blogMsg'); 32 | $table->timestamps(); 33 | }); 34 | Schema::table('test_test_sql_blogs', function($table) { 35 | $table->foreign('test_userId')->references('test_id')->on('test_sql_generator_')->onDelete('cascade'); 36 | }); 37 | 38 | } 39 | 40 | /** 41 | * Reverse the migrations. 42 | * 43 | * @return void 44 | */ 45 | public function down() 46 | { 47 | // 48 | Schema::drop('test_sql_generator'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/sql_generator.php: -------------------------------------------------------------------------------- 1 | database_path('sql'), 9 | 10 | ]; -------------------------------------------------------------------------------- /tests/SqlGeneratorTest.php: -------------------------------------------------------------------------------- 1 | copyMigration(); 14 | $this->artisan('sql:generate'); 15 | $this->deleteMigration(); 16 | 17 | $this->assertTrue(true); 18 | $this->assertDirectoryExists($this->directory); 19 | $this->assertFileExists($this->directory.'/database.sql'); 20 | } 21 | 22 | public function testSqlExitOrNot() 23 | { 24 | $content = file_get_contents($this->directory.'/database.sql'); 25 | $queries = \Froiden\SqlGenerator\SqlFormatter::splitQuery($content); 26 | foreach ($queries as $query) { 27 | 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | __DIR__.'/sql']); 26 | $this->directory = __DIR__."/sql"; 27 | 28 | } 29 | 30 | public function copyMigration() 31 | { 32 | $fileName = '/2016_12_14_085908_test_sql_generator_table.php'; 33 | $source = 'src/migrations'.$fileName; 34 | $destination = database_path('migrations'.$fileName); 35 | 36 | if(file_exists($destination)){ 37 | unlink($destination); 38 | } 39 | copy($source,$destination); 40 | } 41 | 42 | public function deleteMigration(){ 43 | $fileName = '/2016_12_14_085908_test_sql_generator_table.php'; 44 | $destination = database_path('migrations'.$fileName); 45 | 46 | if(file_exists($destination)){ 47 | unlink($destination); 48 | } 49 | } 50 | 51 | 52 | /** 53 | * Creates the application. 54 | * 55 | * @return \Illuminate\Foundation\Application 56 | */ 57 | public function createApplication() 58 | { 59 | $app = require __DIR__.'/../../../../bootstrap/app.php'; 60 | $app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); 61 | return $app; 62 | } 63 | 64 | 65 | 66 | } -------------------------------------------------------------------------------- /tests/sql/database.sql: -------------------------------------------------------------------------------- 1 | -- convert Laravel migrations to raw SQL scripts -- 2 | 3 | -- migration:2016_12_14_085908_test_sql_generator_table -- 4 | create table `test_test_sql_generator_users` ( 5 | `test_id` int unsigned not null auto_increment primary key, 6 | `test_name` varchar(255) not null, 7 | `test_email` varchar(255) not null, 8 | `test_gender` enum('male', 'female') not null, 9 | `test_password` varchar(255) not null, 10 | `remember_token` varchar(100) null, 11 | `created_at` timestamp null, 12 | `updated_at` timestamp null 13 | ) default character set utf8 collate utf8_unicode_ci; 14 | alter table 15 | `test_test_sql_generator_users` 16 | add 17 | unique `test_test_sql_generator_users_test_email_unique`(`test_email`); 18 | create table `test_test_sql_blogs` ( 19 | `test_id` int unsigned not null auto_increment primary key, 20 | `test_userId` int unsigned not null, 21 | `test_userName` varchar(255) not null, 22 | `test_title` varchar(255) not null, 23 | `test_blogMsg` text not null, 24 | `created_at` timestamp null, 25 | `updated_at` timestamp null 26 | ) default character set utf8 collate utf8_unicode_ci; 27 | alter table 28 | `test_test_sql_blogs` 29 | add 30 | unique `test_test_sql_blogs_test_title_unique`(`test_title`); 31 | alter table 32 | `test_test_sql_blogs` 33 | add 34 | constraint `test_test_sql_blogs_test_userid_foreign` foreign key (`test_userId`) references `test_sql_generator_` (`test_id`) on delete cascade; 35 | --------------------------------------------------------------------------------