├── .gitignore ├── .pre-commit-hooks.yaml ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── TODO ├── docker ├── Dockerfile └── README.md ├── pep8.sh ├── pgsanity ├── __init__.py ├── ecpg.py ├── pgsanity.py └── sqlprep.py ├── setup.cfg ├── setup.py └── test ├── __init__.py ├── test_ecpg.py ├── test_pgsanity.py └── test_sqlprep.py /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *\.swp 3 | *.pyc 4 | dist 5 | build 6 | MANIFEST 7 | pgsanity.egg-info 8 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | # For use with pre-commit. 2 | # See usage instructions at http://pre-commit.com 3 | 4 | - id: pgsanity_lint 5 | name: pgsanity lint 6 | description: This hook runs pgsanity-lint for linting postgresql SQL files 7 | entry: pgsanity 8 | types: [file, sql] 9 | language: python 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | - "3.5" 6 | sudo: false 7 | addons: 8 | apt: 9 | packages: 10 | - libecpg-dev 11 | script: python setup.py test 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Mark Drago 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## PgSanity 2 | 3 | PgSanity checks the syntax of Postgresql SQL files. 4 | 5 | It does this by leveraging the ecpg command which is traditionally 6 | used for preparing C files with embedded SQL for compilation. 7 | However, as part of that preparation, ecpg checks the embedded SQL 8 | statements for syntax errors using the exact same parser that is 9 | in PostgreSQL. 10 | 11 | So the approach that PgSanity takes is to take a file that has a 12 | list of bare SQL in it, make that file look like a C file with 13 | embedded SQL, run it through ecpg and let ecpg report on the syntax 14 | errors of the SQL. 15 | 16 | [![Build Status](https://travis-ci.org/markdrago/pgsanity.svg?branch=master)](https://travis-ci.org/markdrago/pgsanity) 17 | 18 | ## Installation 19 | ### Dependencies 20 | - Python >= 2.7 21 | - May work with Python 2.6 if you install argparse (`sudo pip install argparse`) 22 | - If you need support for Python < 2.6 let me know 23 | - ecpg 24 | - Ubuntu/Debian: `sudo apt-get install libecpg-dev` 25 | - RHEL/CentOS: `sudo yum install postgresql-devel` 26 | - Arch: `sudo pacman -S postgresql-libs` 27 | 28 | ### Getting PgSanity 29 | PgSanity is available in the Python Package Index, so you can install it with either easy_install or pip. Here's [PgSanity's page on PyPI](http://pypi.python.org/pypi/pgsanity). 30 | - `sudo pip install pgsanity` or `sudo easy_install pgsanity` 31 | - If you don't have pip you can get it on Ubuntu/Debian by running: `sudo apt-get install python-pip` 32 | 33 | It is also available in the [FreeBSD ports](https://www.freebsd.org/ports/index.html) as [`databases/pgsanity`](https://www.freshports.org/databases/pgsanity/). You can install it with one of those commands: 34 | - `pkg install py36-pgsanity` 35 | - `pkg install py27-pgsanity` 36 | - `cd /usr/ports/databases/pgsanity && make install clean` 37 | 38 | ## Usage 39 | PgSanity accepts filenames as parameters and it will report SQL syntax errors which exist in those files. PgSanity will exit with a status code of 0 if the syntax of the SQL looks good and a 1 if any errors were found. 40 | 41 | $ pgsanity file_with_sql.sql 42 | $ echo $? 43 | 0 44 | $ pgsanity good1.sql good2.sql bad.sql 45 | bad.sql: line 1: ERROR: syntax error at or near "bogus_token" 46 | $ echo $? 47 | 1 48 | 49 | Since pgsanity can handle multiple filenames as parameters it is very comfortable to use with find & xargs. 50 | 51 | $ find -name '*.sql' | xargs pgsanity 52 | ./sql/bad1.sql: line 59: ERROR: syntax error at or near ";" 53 | ./sql/bad2.sql: line 41: ERROR: syntax error at or near "insert" 54 | ./sql/bad3.sql: line 57: ERROR: syntax error at or near "update" 55 | 56 | Additionally PgSanity will read SQL from stdin if it is not given any parameters. This way it can be used interactively or by piping SQL through it. 57 | 58 | $ pgsanity 59 | select column1 alias2 asdf from table3 60 | line 1: ERROR: syntax error at or near "asdf" 61 | $ echo $? 62 | 1 63 | $ echo "select mycol from mytable;" | pgsanity 64 | $ echo $? 65 | 0 66 | 67 | ## Interpreting The Results 68 | The error messages pretty much come directly from ecpg. Something I have noticed while using pgsanity is that an error message on line X is probably more indicative of the statement right above X. For example: 69 | 70 | $ echo "select a from b\ninsert into mytable values (1, 2, 3);" | pgsanity 71 | line 2: ERROR: syntax error at or near "into" 72 | 73 | The real problem in that SQL is that there is no semicolon after the 'b' in the select statement. However, the SQL can not be determined to be invalid until the word "into" is encountered in the insert statement. When in doubt, look up to the previous statement. 74 | 75 | Another common error message that can be a little weird to interpret is illustrated here: 76 | 77 | echo "select a from b" | pgsanity 78 | line 2: ERROR: syntax error at or near "" 79 | 80 | The 'at or near ""' bit is trying to say that we got to the end of the file and no semicolon was found. 81 | 82 | ## pre-commit 83 | 84 | This repository is a [pre-commit]() hook. 85 | 86 | Usage: 87 | 88 | ``` 89 | - repo: https://github.com/markdrago/pgsanity 90 | rev: v0.2.9 91 | hooks: 92 | - id: pgsanity_lint 93 | ``` 94 | 95 | ## Reporting Problems 96 | If you encounter any problems with PgSanity, especially any issues where it incorrectly states that invalid SQL is valid or vice versa, please report the issue on [PgSanity's github page](http://github.com/markdrago/pgsanity). Thanks! 97 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | - verbose mode which prints name of file and OK 2 | - properly handle files that start with a unicode BOM -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.5-alpine 2 | 3 | RUN apk add --no-cache postgresql-dev 4 | RUN pip install pgsanity 5 | 6 | ENTRYPOINT ["/usr/local/bin/pgsanity"] 7 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # PgSanity 2 | 3 | PgSanity checks the syntax of Postgresql SQL files. 4 | 5 | This Dockerfile is based on Alpine Linux and generates images that are approximately 110MB in size. 6 | 7 | ## Usage 8 | 9 | This is a simple example of checking files in the current directory: 10 | 11 | ```bash 12 | docker run --rm -it -v $PWD:/host -w /host pgsanity file1.sql file2.sql 13 | ``` 14 | -------------------------------------------------------------------------------- /pep8.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #E302 = 2 newlines before functions & classes 4 | find . -name '*.py' -exec pep8 --ignore=E302 --max-line-length=120 {} \; 5 | -------------------------------------------------------------------------------- /pgsanity/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markdrago/pgsanity/5fb3ce7ddfc62755d077657e2e58b62cff620b50/pgsanity/__init__.py -------------------------------------------------------------------------------- /pgsanity/ecpg.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import subprocess 3 | import re 4 | import os 5 | 6 | def check_syntax(string): 7 | """ Check syntax of a string of PostgreSQL-dialect SQL """ 8 | args = ["ecpg", "-o", "-", "-"] 9 | 10 | with open(os.devnull, "w") as devnull: 11 | try: 12 | proc = subprocess.Popen(args, shell=False, 13 | stdout=devnull, 14 | stdin=subprocess.PIPE, 15 | stderr=subprocess.PIPE, 16 | universal_newlines=True) 17 | _, err = proc.communicate(string) 18 | except OSError: 19 | msg = "Unable to execute 'ecpg', you likely need to install it.'" 20 | raise OSError(msg) 21 | if proc.returncode == 0: 22 | return (True, "") 23 | else: 24 | return (False, parse_error(err)) 25 | 26 | def parse_error(error): 27 | error = re.sub(r'^[^:]+:', 'line ', error, count=1) 28 | error = re.sub(r'\/\/', '--', error) 29 | return error.strip() 30 | -------------------------------------------------------------------------------- /pgsanity/pgsanity.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import print_function 4 | from __future__ import absolute_import 5 | import argparse 6 | import sys 7 | 8 | from pgsanity import sqlprep 9 | from pgsanity import ecpg 10 | 11 | def get_config(argv=sys.argv[1:]): 12 | parser = argparse.ArgumentParser(description='Check syntax of SQL for PostgreSQL') 13 | parser.add_argument('--add-semicolon', action='store_true') 14 | parser.add_argument('files', nargs='*', default=None) 15 | return parser.parse_args(argv) 16 | 17 | def check_file(filename=None, show_filename=False, add_semicolon=False): 18 | """ 19 | Check whether an input file is valid PostgreSQL. If no filename is 20 | passed, STDIN is checked. 21 | 22 | Returns a status code: 0 if the input is valid, 1 if invalid. 23 | """ 24 | # either work with sys.stdin or open the file 25 | if filename is not None: 26 | with open(filename, "r") as filelike: 27 | sql_string = filelike.read() 28 | else: 29 | with sys.stdin as filelike: 30 | sql_string = sys.stdin.read() 31 | 32 | success, msg = check_string(sql_string, add_semicolon=add_semicolon) 33 | 34 | # report results 35 | result = 0 36 | if not success: 37 | # possibly show the filename with the error message 38 | prefix = "" 39 | if show_filename and filename is not None: 40 | prefix = filename + ": " 41 | print(prefix + msg) 42 | result = 1 43 | 44 | return result 45 | 46 | def check_string(sql_string, add_semicolon=False): 47 | """ 48 | Check whether a string is valid PostgreSQL. Returns a boolean 49 | indicating validity and a message from ecpg, which will be an 50 | empty string if the input was valid, or a description of the 51 | problem otherwise. 52 | """ 53 | prepped_sql = sqlprep.prepare_sql(sql_string, add_semicolon=add_semicolon) 54 | success, msg = ecpg.check_syntax(prepped_sql) 55 | return success, msg 56 | 57 | def check_files(files, add_semicolon=False): 58 | if files is None or len(files) == 0: 59 | return check_file(add_semicolon=add_semicolon) 60 | else: 61 | # show filenames if > 1 file was passed as a parameter 62 | show_filenames = (len(files) > 1) 63 | 64 | accumulator = 0 65 | for filename in files: 66 | accumulator |= check_file(filename, show_filenames, add_semicolon=add_semicolon) 67 | return accumulator 68 | 69 | def main(): 70 | config = get_config() 71 | return check_files(config.files, add_semicolon=config.add_semicolon) 72 | 73 | if __name__ == '__main__': 74 | try: 75 | sys.exit(main()) 76 | except KeyboardInterrupt: 77 | pass 78 | -------------------------------------------------------------------------------- /pgsanity/sqlprep.py: -------------------------------------------------------------------------------- 1 | import re 2 | try: 3 | from cStringIO import StringIO 4 | except ImportError: 5 | from io import StringIO 6 | 7 | def prepare_sql(sql, add_semicolon=False): 8 | results = StringIO() 9 | 10 | in_statement = False 11 | in_line_comment = False 12 | in_block_comment = False 13 | for (start, end, contents) in split_sql(sql): 14 | precontents = None 15 | start_str = None 16 | 17 | # decide where we are 18 | if not in_statement and not in_line_comment and not in_block_comment: 19 | # not currently in any block 20 | if start != "--" and start != "/*" and len(contents.strip()) > 0: 21 | # not starting a comment and there is contents 22 | in_statement = True 23 | precontents = "EXEC SQL " 24 | 25 | if start == "/*": 26 | in_block_comment = True 27 | elif start == "--" and not in_block_comment: 28 | in_line_comment = True 29 | if not in_statement: 30 | start_str = "//" 31 | 32 | start_str = start_str or start or "" 33 | precontents = precontents or "" 34 | results.write(start_str + precontents + contents) 35 | 36 | if not in_line_comment and not in_block_comment and in_statement and end == ";": 37 | in_statement = False 38 | 39 | if in_block_comment and end == "*/": 40 | in_block_comment = False 41 | 42 | if in_line_comment and end == "\n": 43 | in_line_comment = False 44 | 45 | response = results.getvalue() 46 | results.close() 47 | if add_semicolon and in_statement and not in_block_comment: 48 | if in_line_comment: 49 | response = response + "\n" 50 | response = response + ';' 51 | return response 52 | 53 | def split_sql(sql): 54 | """generate hunks of SQL that are between the bookends 55 | return: tuple of beginning bookend, closing bookend, and contents 56 | note: beginning & end of string are returned as None""" 57 | bookends = ("\n", ";", "--", "/*", "*/") 58 | last_bookend_found = None 59 | start = 0 60 | 61 | while start <= len(sql): 62 | results = get_next_occurence(sql, start, bookends) 63 | if results is None: 64 | yield (last_bookend_found, None, sql[start:]) 65 | start = len(sql) + 1 66 | else: 67 | (end, bookend) = results 68 | yield (last_bookend_found, bookend, sql[start:end]) 69 | start = end + len(bookend) 70 | last_bookend_found = bookend 71 | 72 | def get_next_occurence(haystack, offset, needles): 73 | """find next occurence of one of the needles in the haystack 74 | return: tuple of (index, needle found) 75 | or: None if no needle was found""" 76 | # make map of first char to full needle (only works if all needles 77 | # have different first characters) 78 | firstcharmap = dict([(n[0], n) for n in needles]) 79 | firstchars = firstcharmap.keys() 80 | while offset < len(haystack): 81 | if haystack[offset] in firstchars: 82 | possible_needle = firstcharmap[haystack[offset]] 83 | if haystack[offset:offset + len(possible_needle)] == possible_needle: 84 | return (offset, possible_needle) 85 | offset += 1 86 | return None 87 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markdrago/pgsanity/5fb3ce7ddfc62755d077657e2e58b62cff620b50/setup.cfg -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='pgsanity', 5 | version='0.2.9', 6 | author='Mark Drago', 7 | author_email='markdrago@gmail.com', 8 | url='http://github.com/markdrago/pgsanity', 9 | download_url='http://pypi.python.org/pypi/pgsanity', 10 | description='Check syntax of sql for PostgreSQL', 11 | license='MIT', 12 | packages=['pgsanity'], 13 | entry_points={ 14 | 'console_scripts': [ 15 | 'pgsanity = pgsanity.pgsanity:main' 16 | ] 17 | }, 18 | test_suite='test', 19 | classifiers=[ 20 | 'Programming Language :: Python :: 2', 21 | 'Programming Language :: Python :: 3', 22 | 'License :: OSI Approved :: MIT License', 23 | 'Operating System :: POSIX', 24 | 'Development Status :: 4 - Beta', 25 | 'Intended Audience :: Developers', 26 | 'Intended Audience :: System Administrators', 27 | 'Intended Audience :: Information Technology', 28 | 'Topic :: Database', 29 | 'Topic :: Database :: Database Engines/Servers', 30 | 'Topic :: Software Development :: Quality Assurance', 31 | 'Topic :: Software Development :: Testing', 32 | 'Topic :: Utilities' 33 | ], 34 | long_description=""" 35 | **PgSanity checks the syntax of Postgresql SQL files.** 36 | 37 | It does this by leveraging the ecpg command which is traditionally 38 | used for preparing C files with embedded sql for compilation. 39 | However, as part of that preparation, ecpg checks the embedded SQL 40 | statements for syntax errors using the exact same parser that is 41 | in PostgreSQL. 42 | 43 | So the approach that PgSanity takes is to take a file that has a 44 | list of bare SQL in it, make that file look like a C file with 45 | embedded SQL, run it through ecpg and let ecpg report on the syntax 46 | errors of the SQL. 47 | """ 48 | ) 49 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | from test import test_sqlprep, test_ecpg, test_pgsanity 2 | -------------------------------------------------------------------------------- /test/test_ecpg.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pgsanity import ecpg 4 | 5 | class TestEcpg(unittest.TestCase): 6 | def test_simple_success(self): 7 | text = u"EXEC SQL select a from b;" 8 | (success, msg) = ecpg.check_syntax(text) 9 | self.assertTrue(success) 10 | 11 | def test_simple_failure(self): 12 | text = u"EXEC SQL garbage select a from b;" 13 | (success, msg) = ecpg.check_syntax(text) 14 | self.assertFalse(success) 15 | self.assertEqual('line 1: ERROR: unrecognized data type name "garbage"', msg) 16 | 17 | def test_parse_error_simple(self): 18 | error = '/tmp/tmpLBKZo5.pgc:1: ERROR: unrecognized data type name "garbage"' 19 | expected = 'line 1: ERROR: unrecognized data type name "garbage"' 20 | self.assertEqual(expected, ecpg.parse_error(error)) 21 | 22 | def test_parse_error_comments(self): 23 | error = '/tmp/tmpLBKZo5.pgc:5: ERROR: syntax error at or near "//"' 24 | expected = 'line 5: ERROR: syntax error at or near "--"' 25 | self.assertEqual(expected, ecpg.parse_error(error)) 26 | -------------------------------------------------------------------------------- /test/test_pgsanity.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import tempfile 3 | import os 4 | 5 | from pgsanity import pgsanity 6 | 7 | class TestPgSanity(unittest.TestCase): 8 | def test_args_parsed_one_filename(self): 9 | args = ["myfile.sql"] 10 | config = pgsanity.get_config(args) 11 | self.assertEqual(args, config.files) 12 | self.assertEqual(False, config.add_semicolon) 13 | 14 | def test_args_parsed_multiple_filenames(self): 15 | args = ["myfile.sql", "myotherfile.sql"] 16 | config = pgsanity.get_config(args) 17 | self.assertEqual(args, config.files) 18 | self.assertEqual(False, config.add_semicolon) 19 | 20 | def test_args_parsed_add_semicolon(self): 21 | args = ["--add-semicolon", "myfile.sql"] 22 | config = pgsanity.get_config(args) 23 | self.assertEqual(["myfile.sql"], config.files) 24 | self.assertEqual(True, config.add_semicolon) 25 | 26 | def test_check_valid_string(self): 27 | text = "select a from b;" 28 | (success, msg) = pgsanity.check_string(text) 29 | self.assertTrue(success) 30 | 31 | def test_check_invalid_string(self): 32 | text = "garbage select a from b;" 33 | (success, msg) = pgsanity.check_string(text) 34 | self.assertFalse(success) 35 | self.assertEqual('line 1: ERROR: unrecognized data type name "garbage"', msg) 36 | 37 | 38 | class TestPgSanityFiles(unittest.TestCase): 39 | def setUp(self): 40 | self.file = tempfile.NamedTemporaryFile(delete=False, suffix=".pgc") 41 | 42 | def tearDown(self): 43 | self.file.close() 44 | os.remove(self.file.name) 45 | 46 | def test_check_valid_file(self): 47 | text = "select a from b;" 48 | write_out(self.file, text.encode('utf-8')) 49 | status_code = pgsanity.check_file(self.file.name) 50 | self.assertEqual(status_code, 0) 51 | 52 | def test_check_invalid_file(self): 53 | text = "garbage select a from b;" 54 | write_out(self.file, text.encode('utf-8')) 55 | status_code = pgsanity.check_file(self.file.name) 56 | self.assertNotEqual(status_code, 0) 57 | 58 | def _write_missing_semi(self): 59 | text = "select a from b" 60 | write_out(self.file, text.encode('utf-8')) 61 | 62 | def test_check_missing_semi(self): 63 | self._write_missing_semi() 64 | status_code = pgsanity.check_file(self.file.name) 65 | self.assertNotEqual(status_code, 0) 66 | 67 | def test_check_missing_semi_ok(self): 68 | self._write_missing_semi() 69 | status_code = pgsanity.check_file(self.file.name, add_semicolon=True) 70 | self.assertEqual(status_code, 0) 71 | 72 | 73 | def write_out(f, text): 74 | f.write(text) 75 | f.flush() 76 | -------------------------------------------------------------------------------- /test/test_sqlprep.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pgsanity import sqlprep 4 | 5 | class TestSqlPrep(unittest.TestCase): 6 | def test_split_sql_nothing_interesting(self): 7 | text = "abcd123" 8 | expected = [(None, None, "abcd123")] 9 | self.assertEqual(expected, list(sqlprep.split_sql(text))) 10 | 11 | def test_split_sql_trailing_semicolon(self): 12 | text = "abcd123;" 13 | expected = [(None, ";", "abcd123"), (";", None, '')] 14 | self.assertEqual(expected, list(sqlprep.split_sql(text))) 15 | 16 | def test_split_sql_comment_between_statements(self): 17 | text = "select a from b;\n" 18 | text += "--comment here\n" 19 | text += "select a from b;" 20 | 21 | expected = [(None, ";", "select a from b"), 22 | (";", "\n", ''), 23 | ("\n", "--", ''), 24 | ("--", "\n", 'comment here'), 25 | ("\n", ";", 'select a from b'), 26 | (";", None, '')] 27 | self.assertEqual(expected, list(sqlprep.split_sql(text))) 28 | 29 | def test_split_sql_inline_comment(self): 30 | text = "select a from b; --comment here\n" 31 | text += "select a from b;" 32 | 33 | expected = [(None, ";", "select a from b"), 34 | (";", "--", ' '), 35 | ("--", "\n", 'comment here'), 36 | ("\n", ";", 'select a from b'), 37 | (";", None, '')] 38 | self.assertEqual(expected, list(sqlprep.split_sql(text))) 39 | 40 | def test_handles_first_column_comment_between_statements(self): 41 | text = "blah blah;\n" 42 | text += "--comment here\n" 43 | text += "blah blah;" 44 | 45 | expected = "EXEC SQL blah blah;\n" 46 | expected += "//comment here\n" 47 | expected += "EXEC SQL blah blah;" 48 | 49 | self.assertEqual(expected, sqlprep.prepare_sql(text)) 50 | 51 | def test_handles_inline_comment_between_statements(self): 52 | text = "blah blah; --comment here\n" 53 | text += "blah blah;" 54 | 55 | expected = "EXEC SQL blah blah; //comment here\n" 56 | expected += "EXEC SQL blah blah;" 57 | 58 | self.assertEqual(expected, sqlprep.prepare_sql(text)) 59 | 60 | def test_does_not_mangle_inline_comment_within_statement(self): 61 | text = "blah blah--comment here\n" 62 | text += "blah blah" 63 | 64 | expected = "EXEC SQL " + text 65 | 66 | self.assertEqual(expected, sqlprep.prepare_sql(text)) 67 | 68 | def test_does_not_mangle_first_column_comment_within_statement(self): 69 | text = "select a from b\n" 70 | text += "--comment here\n" 71 | text += "where c=3" 72 | 73 | expected = "EXEC SQL " + text 74 | 75 | self.assertEqual(expected, sqlprep.prepare_sql(text)) 76 | 77 | def test_prepend_exec_sql_to_simple_statements(self): 78 | text = "create table control.myfavoritetable (id bigint);" 79 | expected = "EXEC SQL " + text 80 | self.assertEqual(expected, sqlprep.prepare_sql(text)) 81 | 82 | def test_prepend_exec_sql_multiple_lines(self): 83 | text1 = "create table control.myfavoritetable (id bigint);\n" 84 | text2 = "create table control.myfavoritetable (id bigint);" 85 | expected = "EXEC SQL " + text1 + "EXEC SQL " + text2 86 | self.assertEqual(expected, sqlprep.prepare_sql(text1 + text2)) 87 | 88 | def test_prepend_exec_sql_wrapped_statement(self): 89 | text = "create table control.myfavoritetable (\n" 90 | text += " id bigint\n" 91 | text += ");" 92 | expected = "EXEC SQL " + text 93 | self.assertEqual(expected, sqlprep.prepare_sql(text)) 94 | 95 | def test_prepend_exec_sql_two_statements_one_line(self): 96 | text = "select a from b; select c from d;" 97 | expected = "EXEC SQL select a from b;EXEC SQL select c from d;" 98 | self.assertEqual(expected, sqlprep.prepare_sql(text)) 99 | 100 | def test_prepend_exec_sql_wrapped_statement_with_multiple_semis_on_last_line(self): 101 | text1 = "create table control.myfavoritetable (\n" 102 | text2 = " id bigint\n" 103 | text3 = ");" 104 | text4 = "select a from b;" 105 | expected = "EXEC SQL " + text1 + text2 + text3 + "EXEC SQL " + text4 106 | self.assertEqual(expected, sqlprep.prepare_sql(text1 + text2 + text3 + text4)) 107 | 108 | def test_prepend_exec_sql_wrapped_trailing_sql(self): 109 | text = "select a from b; select a\nfrom b;" 110 | expected = "EXEC SQL select a from b;EXEC SQL select a\nfrom b;" 111 | self.assertEqual(expected, sqlprep.prepare_sql(text)) 112 | 113 | def test_no_append_semi(self): 114 | text = "select a from b" 115 | expected = 'EXEC SQL ' + text 116 | self.assertEqual(expected, sqlprep.prepare_sql(text)) 117 | 118 | def test_append_semi(self): 119 | text = "select a from b" 120 | expected = 'EXEC SQL ' + text + ';' 121 | self.assertEqual(expected, sqlprep.prepare_sql(text, add_semicolon=True)) 122 | 123 | def test_append_semi_once(self): 124 | text = "select a from b;" 125 | expected = 'EXEC SQL ' + text 126 | self.assertEqual(expected, sqlprep.prepare_sql(text, add_semicolon=True)) 127 | 128 | def test_append_semi_line_comment(self): 129 | text = "select a from b\n-- looks done!" 130 | expected = 'EXEC SQL ' + text + "\n;" 131 | self.assertEqual(expected, sqlprep.prepare_sql(text, add_semicolon=True)) 132 | 133 | def test_append_semi_line_comment(self): 134 | text = "select a from b\n/* looks done!\n*" 135 | expected = 'EXEC SQL ' + text 136 | self.assertEqual(expected, sqlprep.prepare_sql(text, add_semicolon=True)) 137 | 138 | def test_comment_start_found_within_comment_within_statement(self): 139 | text = "select a from b --comment in comment --here\nwhere c=1;" 140 | expected = "EXEC SQL select a from b --comment in comment --here\nwhere c=1;" 141 | self.assertEqual(expected, sqlprep.prepare_sql(text)) 142 | 143 | def test_comment_start_found_within_comment_between_statements(self): 144 | text = "select a from b; --comment in comment --here\nselect c from d;" 145 | expected = "EXEC SQL select a from b; //comment in comment //here\nEXEC SQL select c from d;" 146 | self.assertEqual(expected, sqlprep.prepare_sql(text)) 147 | 148 | def test_double_semicolon(self): 149 | text = "select a from b;;" 150 | expected = "EXEC SQL select a from b;;" 151 | self.assertEqual(expected, sqlprep.prepare_sql(text)) 152 | 153 | def test_semi_found_in_comment_at_end_of_line(self): 154 | text = "select a\nfrom b --semi in comment;\nwhere c=1;" 155 | expected = "EXEC SQL select a\nfrom b --semi in comment;\nwhere c=1;" 156 | self.assertEqual(expected, sqlprep.prepare_sql(text)) 157 | 158 | def test_handles_first_line_comment(self): 159 | text = "--comment on line 1\nselect a from b;" 160 | expected = "//comment on line 1\nEXEC SQL select a from b;" 161 | self.assertEqual(expected, sqlprep.prepare_sql(text)) 162 | 163 | def test_handles_block_comment_on_last_line(self): 164 | text = "select a from b;\n/*\nselect c from d;\n*/" 165 | expected = "EXEC SQL select a from b;\n/*\nselect c from d;\n*/" 166 | self.assertEqual(expected, sqlprep.prepare_sql(text)) 167 | 168 | def test_semi_found_in_block_comment(self): 169 | text = "select a\n/*\n;\n*/from b;" 170 | expected = "EXEC SQL " + text 171 | self.assertEqual(expected, sqlprep.prepare_sql(text)) 172 | 173 | def test_line_comment_in_block_comment_is_undisturbed(self): 174 | text = "select a\n/*\n--hey\n*/\nfrom b;" 175 | expected = "EXEC SQL " + text 176 | self.assertEqual(expected, sqlprep.prepare_sql(text)) 177 | 178 | def test_opening_two_block_comments_only_requries_one_close(self): 179 | text = "select a\n/*\n/*\ncomment\n*/from b;select c from d;" 180 | expected = "EXEC SQL select a\n/*\n/*\ncomment\n*/from b;EXEC SQL select c from d;" 181 | self.assertEqual(expected, sqlprep.prepare_sql(text)) 182 | 183 | # TODO: 184 | # semicolon followed by only whitespace / comments 185 | # multiple semicolons in a row (legal?) 186 | # line starts with semi and then has a statement 187 | --------------------------------------------------------------------------------