36

How can execute sql script stored in *.sql file using MySQLdb python driver. I was trying


cursor.execute(file(PATH_TO_FILE).read())

but this doesn't work because cursor.execute can run only one sql command at once. My sql script contains several sql statements instead. Also I was trying


cursor.execute('source %s'%PATH_TO_FILE)

but also with no success.

Mykola Kharechko
  • 3,104
  • 5
  • 31
  • 40

14 Answers14

37

From python, I start a mysql process to execute the file for me:

from subprocess import Popen, PIPE
process = Popen(['mysql', db, '-u', user, '-p', passwd],
                stdout=PIPE, stdin=PIPE)
output = process.communicate('source ' + filename)[0]
Jiangge Zhang
  • 4,298
  • 4
  • 25
  • 33
jdferreira
  • 651
  • 1
  • 6
  • 16
  • 2
    But if you need to use the output you have to parse it yourself, which is a major hassle. – augurar Oct 21 '14 at 18:14
  • 2
    That is correct. However, I never encountered a situation where I had to use the output of the SQL file. I use this approach mostly when I want to use a mysql-dump file into another database, which only has INSERT and CREATE and similar statements whose output is usually not used afterwards. – jdferreira Oct 22 '14 at 15:03
  • 1
    Hmm. Nice an easy, but I'm a bit wary of the security. You have to supply the password on the command line, which the MySQL client complains about not being safe when you do that from the terminal. I think you'd end up with that being in your process list, etc. Check out my answer, which is a more robust version based on this concept. – BuvinJ Feb 08 '18 at 15:15
  • If you have to process the results comprehensively, then you should more than likely be using MySQLdb (or some equivalent), not trying to parse this output. As @jdferreira said, it seems unlikely you need the output for such a purpose if you are executing a script like this. The script would be dynamic and open ended by its nature. How would you control for the input, that you wanted to parse the output from? If you want to parse it purely for error detection, you get that by virtue of what goes to stdout vs stderr. In my answer, I included some (unpleasant) parsing on the error messages. – BuvinJ Feb 09 '18 at 14:08
  • 2
    in python 3 you have to also encode str to bytes: `output = process.communicate(str.encode('source ' + filename))[0]` – Tomek C. Jan 22 '21 at 16:59
28

I also needed to execute a SQL file, but the catch was that there wasn't one statement per line, so the accepted answer didn't work for me.

The SQL file I wanted to execute looked like this:

-- SQL script to bootstrap the DB:
--
CREATE USER 'x'@'%' IDENTIFIED BY 'x';
GRANT ALL PRIVILEGES ON mystore.* TO 'x'@'%';
GRANT ALL ON `%`.* TO 'x'@`%`;
FLUSH PRIVILEGES;
--
--
CREATE DATABASE oozie;
GRANT ALL PRIVILEGES ON oozie.* TO 'oozie'@'localhost' IDENTIFIED BY 'oozie';
GRANT ALL PRIVILEGES ON oozie.* TO 'oozie'@'%' IDENTIFIED BY 'oozie';
FLUSH PRIVILEGES;
--
USE oozie;
--
CREATE TABLE `BUNDLE_ACTIONS` (
  `bundle_action_id` varchar(255) NOT NULL,
  `bundle_id` varchar(255) DEFAULT NULL,
  `coord_id` varchar(255) DEFAULT NULL,
  `coord_name` varchar(255) DEFAULT NULL,
  `critical` int(11) DEFAULT NULL,
  `last_modified_time` datetime DEFAULT NULL,
  `pending` int(11) DEFAULT NULL,
  `status` varchar(255) DEFAULT NULL,
  `bean_type` varchar(31) DEFAULT NULL,
  PRIMARY KEY (`bundle_action_id`),
  KEY `I_BNDLTNS_DTYPE` (`bean_type`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
--

Some statements in the above file lie on a single line and some statements also span multiple lines (like the CREATE TABLE at the end). There are also a few SQL inline comment lines that begin with "--".

As suggested by ThomasK, I had to write some simple rules to join lines into a statement. I ended up with a function to execute a sql file:

def exec_sql_file(cursor, sql_file):
    print "\n[INFO] Executing SQL script file: '%s'" % (sql_file)
    statement = ""

    for line in open(sql_file):
        if re.match(r'--', line):  # ignore sql comment lines
            continue
        if not re.search(r';$', line):  # keep appending lines that don't end in ';'
            statement = statement + line
        else:  # when you get a line ending in ';' then exec statement and reset for next statement
            statement = statement + line
            #print "\n\n[DEBUG] Executing SQL statement:\n%s" % (statement)
            try:
                cursor.execute(statement)
            except (OperationalError, ProgrammingError) as e:
                print "\n[WARN] MySQLError during execute statement \n\tArgs: '%s'" % (str(e.args))

            statement = ""

I'm sure there's scope for improvement, but for now it's working pretty well for me. Hope someone finds it useful.

Blafarus
  • 13
  • 5
nonbeing
  • 6,907
  • 6
  • 36
  • 46
  • 5
    Really good, it seems the only solution for large file. For me, I only changed the two re with line.strip().startswith('--') and line.strip().endswith(';'). And import the two errors from _mysql_exceptions. – cuble Jan 01 '17 at 08:43
  • 1
    This works well enough and is easily patched up to python 3. – cgp Dec 29 '20 at 00:12
  • 1
    does anyone know how to adapt it to support [multiline comments](https://stackoverflow.com/questions/9098655/how-can-i-add-comments-in-mysql)? `/* ... comment ... comment ... comment ... comment ... */` – abu Jun 01 '21 at 15:00
23

This worked for me:

with open('schema.sql') as f:
    cursor.execute(f.read().decode('utf-8'), multi=True)
madogan
  • 605
  • 7
  • 11
  • 2
    TypeError: execute() got an unexpected keyword argument 'multi' - without multi argument it worked for me. – blahy Feb 23 '19 at 19:25
  • Yeah, I remember both situations, but I don't know difference. Maybe it is related with version. – madogan Feb 24 '19 at 21:21
  • 5
    This is the only correct solution - no parsing, no separate mysql process; works with multi-line statements (though each statement must end with a semicolon). Notes: f.read() normally returns a string, so decode() is unnecessary (and will error). multi=True is needed as far as I can tell. This usage of execute is documented at https://dev.mysql.com/doc/connector-python/en/connector-python-api-mysqlcursor-execute.html – Burrito Apr 26 '19 at 03:25
  • Yeah, I got the `decode` error - removing it fixed the problem, this worked great, thanks. – skwidbreth Aug 28 '20 at 15:28
  • 2
    Doesn't work today. Per docs, execute() "returns an iterator that enables processing the result of each statement". Therefore you need something like for result in cur.execute(sql_script, multi=True): pass to make it work. – Rocky K Sep 05 '20 at 13:09
  • 1
    @skwidbreth No `.decode('utf-8')` bit on Python3 since `open` opens in text mode. If you want to explicitly use UTF8 (e.g. on Windows) use `open('schema.sql', encoding='utf8')`. – c z Oct 12 '20 at 11:21
  • @Burrito This has its limitations, namely ***the entire file is loaded into memory***. If your DB indeed fits into RAM I would reconsider the necessity of such a heavyweight dependency as MySql in the first place. – c z Oct 13 '20 at 11:41
17
for line in open(PATH_TO_FILE):
    cursor.execute(line)

This assumes you have one SQL statement per line in your file. Otherwise you'll need to write some rules to join lines together.

Thomas K
  • 39,200
  • 7
  • 84
  • 86
  • 2
    I encountered escaping problems with this approach... I had an sql file as output from a dump. When you load in the file via mysql command line I guess mysql handles the escaping. But python mysqldb is expecting the string you pass in as `line` to already be escaped. Alternatively it is expecting something like `execute('SOME SQL COMMAND blah WITH PARAMS %s', params)` and then it will escape the `params` correctly for you... but this doesn't work in the case I described. In which case maybe @jotmicron's answer is better. Fortunately I was able to use django fixtures instead. – Anentropic Aug 07 '12 at 09:38
11

Another solution that allows to leverage on the MySQL interpreter without any parsing is to use the os.system command to run a MySQL prompt command directly inside python:

from os import system
USERNAME = "root"
PASSWORD = "root"
DBNAME = "pablo"
HOST = "localhost"
PORT = 3306
FILE = "file.sql"
command = """mysql -u %s -p"%s" --host %s --port %s %s < %s""" %(USERNAME, PASSWORD, HOST, PORT, DBNAME, FILE)
system(command)

It avoids any parsing error when for example you would have a string variable with a smiley ;-) in it or if you check for the ; as the last character, if you have comments afterward like SELECT * FROM foo_table; # selecting data

Yohan Obadia
  • 2,552
  • 2
  • 24
  • 31
  • this is really great. Worked for me. How do you close the system(command) after it's done executing? – sharsart Sep 18 '19 at 16:10
  • @sharsart You don't need to close `system`, it's a blocking call. If you're feeling pedantic you can use `Popen` and `wait`. – c z Oct 13 '20 at 11:45
9

Many of the answers here have serious flaws...

First don't try to parse an open ended sql script yourself! If you think that is easily done, you aren't aware of how robust and complicated sql can be. Serious sql scripts certainly involve statements and procedure definitions spanning multiple lines. It is also common to explicitly declare and change delimiters the in middle of your scripts. You can also nest source commands within each other. For so many reasons, you want to run the script through the MySQL client and allow it to handle the heavy lifting. Trying to reinvent that is fraught peril and a huge waste of time. Maybe if you are the only one writing these scripts, and you are not writing anything sophisticated you could get away with that, but why limit yourself to such a degree? What about machine generated scripts, or those written by other developers?

The answer from @jdferreira is on the right track, but also has problems and weaknesses. The most significant is that a security hole is being opened up by sending the connection parameters to the process in that manner.

Here's a solution / example for your copy & paste pleasure. My extended discussion follows:

First, create a separate config file to save your user name and password.

db-creds.cfg

[client]
user     = XXXXXXX
password = YYYYYYY

Slap the right file system permissions on that, so the python process can read from it, but no one can view that who should not be able to.

Then, use this Python (in my example case the creds file is adjacent to the py script):

#!/usr/bin/python

import os
import sys
import MySQLdb
from subprocess import Popen, PIPE, STDOUT

__MYSQL_CLIENT_PATH = "mysql"

__THIS_DIR = os.path.dirname( os.path.realpath( sys.argv[0] ) )

__DB_CONFIG_PATH    = os.path.join( __THIS_DIR, "db-creds.cfg" )
__DB_CONFIG_SECTION = "client"

__DB_CONN_HOST = "localhost"
__DB_CONN_PORT = 3306

# ----------------------------------------------------------------

class MySqlScriptError( Exception ):

    def __init__( self, dbName, scriptPath, stdOut, stdErr ):
        Exception.__init__( self )
        self.dbName = dbName
        self.scriptPath = scriptPath
        self.priorOutput = stdOut
        self.errorMsg = stdErr                
        errNumParts = stdErr.split("(")        
        try : self.errorNum = long( errNumParts[0].replace("ERROR","").strip() )
        except: self.errorNum = None        
        try : self.sqlState = long( errNumParts[1].split(")")[0].strip() )
        except: self.sqlState = None

    def __str__( self ): 
        return ("--- MySqlScriptError ---\n" +
                "Script: %s\n" % (self.scriptPath,) +
                "Database: %s\n" % (self.dbName,) +
                self.errorMsg ) 

    def __repr__( self ): return self.__str__()

# ----------------------------------------------------------------

def databaseLoginParms() :        
    from ConfigParser import RawConfigParser
    parser = RawConfigParser()
    parser.read( __DB_CONFIG_PATH )   
    return ( parser.get( __DB_CONFIG_SECTION, "user" ).strip(), 
             parser.get( __DB_CONFIG_SECTION, "password" ).strip() )

def databaseConn( username, password, dbName ):        
    return MySQLdb.connect( host=__DB_CONN_HOST, port=__DB_CONN_PORT,
                            user=username, passwd=password, db=dbName )

def executeSqlScript( dbName, scriptPath, ignoreErrors=False ) :       
    scriptDirPath = os.path.dirname( os.path.realpath( scriptPath ) )
    sourceCmd = "SOURCE %s" % (scriptPath,)
    cmdList = [ __MYSQL_CLIENT_PATH,                
               "--defaults-extra-file=%s" % (__DB_CONFIG_PATH,) , 
               "--database", dbName,
               "--unbuffered" ] 
    if ignoreErrors : 
        cmdList.append( "--force" )
    else:
        cmdList.extend( ["--execute", sourceCmd ] )
    process = Popen( cmdList 
                   , cwd=scriptDirPath
                   , stdout=PIPE 
                   , stderr=(STDOUT if ignoreErrors else PIPE) 
                   , stdin=(PIPE if ignoreErrors else None) )
    stdOut, stdErr = process.communicate( sourceCmd if ignoreErrors else None )
    if stdErr is not None and len(stdErr) > 0 : 
        raise MySqlScriptError( dbName, scriptPath, stdOut, stdErr )
    return stdOut

If you want to test it out, add this:

if __name__ == "__main__": 

    ( username, password ) = databaseLoginParms()
    dbName = "ExampleDatabase"

    print "MySQLdb Test"
    print   
    conn = databaseConn( username, password, dbName )
    cursor = conn.cursor()
    cursor.execute( "show tables" )
    print cursor.fetchall()
    cursor.close()
    conn.close()
    print   

    print "-----------------"
    print "Execute Script with ignore errors"
    print   
    scriptPath = "test.sql"
    print executeSqlScript( dbName, scriptPath, 
                            ignoreErrors=True )
    print   

    print "-----------------"
    print "Execute Script WITHOUT ignore errors"                            
    print   
    try : print executeSqlScript( dbName, scriptPath )
    except MySqlScriptError as e :        
        print "dbName: %s" % (e.dbName,)
        print "scriptPath: %s" % (e.scriptPath,)
        print "errorNum: %s" % (str(e.errorNum),)
        print "sqlState: %s" % (str(e.sqlState),)
        print "priorOutput:"        
        print e.priorOutput
        print
        print "errorMsg:"
        print e.errorMsg           
        print
        print e
    print   

And for good measure, here's an example sql script to feed into it:

test.sql

show tables;
blow up;
show tables;

So, now for some discussion.

First, I illustrate how to use MySQLdb along with this external script execution, while storing the creds in one shared file you can use for both.

By using --defaults-extra-file on the command line you can SECURELY pass your connection parameters in.

The combination of either --force with stdin streaming the source command OR --execute running the command on the outside let's you dictate how the script will run. That is by ignoring errors and continuing to run, or stopping as soon as an error occurs.

The order in which the results comeback will also be preserved via --unbuffered. Without that, your stdout and stderr streams will be jumbled and undefined in their order, making it very hard to figure out what worked and what did not when comparing that to the input sql.

Using the Popen cwd=scriptDirPath let's you nest source commands within one another using relative paths. If your scripts will all be in the same directory (or a known path relative to it), doing this let's you reference those relative to where the top level script resides.

Finally, I threw in an exception class which carries all the info you could possibly want about what happened. If you are not using the ignoreErrors option, one of these exceptions will be thrown in your python when something goes wrong and script has stopped running upon that error.

BuvinJ
  • 10,221
  • 5
  • 83
  • 96
  • I took a look at my process list (linux mint 19), in the properties window you can see the command line parameters, but instead of the password you can only see "mysql -u root --password=x xxxxxx". So passing the password via the command line can be unsafe, but doesn't have to be. – Dwagner Jan 25 '19 at 11:41
  • Thanks for checking that. I have to assume this detail is dependent upon MySQL version. The MySQL client itself, always yells at you when you provide the password directly via `-p` from the terminal. I find it hard to imagine your OS hid the password in your process list. That must be some trick built into MySQL now. Perhaps they have that listed in a "what's new" doc? What version did you try this with? – BuvinJ Jan 25 '19 at 14:24
  • It seems I am using version "5.7.25" (I think the package is called mysql-server-5.7), I don't know if it's an OS specific thing. They might have changed it, but I just recently used MySQL for the first time, so I don't know. It is strange though, that the warning is still present. – Dwagner Jan 25 '19 at 15:06
  • Thanks. They've done at lot of work in recent releases. When Oracle bought the rights, they changed a lot. I think you have one of the last v5 releases. They jumped from that straight to v8 for whatever reason. It would be interesting to know if MariaDB works this way (that's a popular direct fork from the middle of the MySQL v5's)... – BuvinJ Jan 25 '19 at 17:26
8

At least MySQLdb 1.2.3 seems to allow this out of the box, you just have to call cursor.nextset() to cycle through the returned result sets.

db = conn.cursor()
db.execute('SELECT 1; SELECT 2;')

more = True
while more:
    print db.fetchall()
    more = db.nextset()

If you want to be absolutely sure the support for this is enabled, and/or disable the support, you can use something like this:

MYSQL_OPTION_MULTI_STATEMENTS_ON = 0
MYSQL_OPTION_MULTI_STATEMENTS_OFF = 1

conn.set_server_option(MYSQL_OPTION_MULTI_STATEMENTS_ON)
# Multiple statement execution here...
conn.set_server_option(MYSQL_OPTION_MULTI_STATEMENTS_OFF)
Aleksi Torhamo
  • 6,452
  • 2
  • 34
  • 44
  • 1
    This seems promising to keep it simple, however, do you know if SQL comments in the file will work? For example, db.execute('SELECT 1;\n --comment here\n SELECT 2;\n') or db.execute('SELECT 1; --comment here SELECT 2;'). Seems like turning on MYSQL_OPTION_MULTI_STATEMENTS_ON will be good if you know there are no comments in the file. – JacquelineIO Jan 11 '15 at 01:12
6

The accepted answer will encounter problems when your sql script contains empty lines and your query sentence spans multiple lines. Instead, using the following approach will solve the problem:

f = open(filename, 'r')
query = " ".join(f.readlines())
c.execute(query)
Yuhao
  • 1,570
  • 1
  • 21
  • 32
2

Are you able to use a different database driver?
If yes: what you want is possible with the MySQL Connector/Python driver by MySQL.

Its cursor.execute method supports executing multiple SQL statements at once by passing Multi=True.

Splitting the SQL statements in the file by semicolon is not necessary.

Simple example (mainly copy & paste from the second link, I just added reading the SQL from the file):

import mysql.connector

file = open('test.sql')
sql = file.read()

cnx = mysql.connector.connect(user='uuu', password='ppp', host='hhh', database='ddd')
cursor = cnx.cursor()

for result in cursor.execute(sql, multi=True):
  if result.with_rows:
    print("Rows produced by statement '{}':".format(
      result.statement))
    print(result.fetchall())
  else:
    print("Number of rows affected by statement '{}': {}".format(
      result.statement, result.rowcount))

cnx.close()

I'm using this to import MySQL dumps (created in phpMyAdmin by exporting the whole database to a SQL file) from the *.sql file back into a database.

Christian Specht
  • 35,843
  • 15
  • 128
  • 182
  • If this solution works, that is awesome. But can you catch errors when your script fails? Do you have to check the result for that, or catch an exception? – Dwagner Feb 01 '19 at 16:12
2

As mentioned in one of the comments, if you are sure that every command ends with a semi-colon, you can do this:

import mysql.connector
connection = mysql.connector.connect(
    host=host,
    user=user,
    password=password
)

cursor = connection.cursor()

with open(script, encoding="utf-8") as f:
    commands = f.read().split(';')

for command in commands:
    cursor.execute(command)
    print(command)

connection.close()
Webucator
  • 2,397
  • 24
  • 39
1

Load mysqldump file:

for line in open(PATH_TO_FILE).read().split(';\n'):
    cursor.execute(line)
  • It's actually just as inefficient as the answer of @Denzel The only problem is that the the file descriptor is never closed – Adam Kurkiewicz Dec 29 '17 at 19:16
  • 1
    `;\n` is not only possible delimiter. Like several answers here, this fails to account for the fact you can (and scripts often do) change delimiters themselves in the middle of the processing. Also, you can configure MySQL to use a different delimiter by default. When you define sql functions, procedures, triggers, etc. it is common to use `$$` as a delimiter and then use `;` inside those for instance. – BuvinJ Feb 09 '18 at 14:15
0

Here's a code snippet that will import a typical .sql that comes from an export. (I used it with exports from Sequel Pro successfully.) Deals with multi-line queries and comments (#).

  • Note 1: I used the initial lines from Thomas K's response but added more.
  • Note 2: For newbies, replace the DB_HOST, DB_PASS etc with your database connection info.

import MySQLdb
from configdb import DB_HOST, DB_PASS, DB_USER, DB_DATABASE_NAME

db = MySQLdb.connect(host=DB_HOST,    # your host, usually localhost
                     user=DB_USER,         # your username
                     passwd=DB_PASS,  # your password
                     db=DB_DATABASE_NAME)        # name of the data base

cur = db.cursor()

PATH_TO_FILE = "db-testcases.sql"

fullLine = ''

for line in open(PATH_TO_FILE):
  tempLine = line.strip()

  # Skip empty lines.
  # However, it seems "strip" doesn't remove every sort of whitespace.
  # So, we also catch the "Query was empty" error below.
  if len(tempLine) == 0:
    continue

  # Skip comments
  if tempLine[0] == '#':
    continue

  fullLine += line

  if not ';' in line:
    continue

  # You can remove this. It's for debugging purposes.
  print "[line] ", fullLine, "[/line]"

  try:
    cur.execute(fullLine)
  except MySQLdb.OperationalError as e:
    if e[1] == 'Query was empty':
      continue

    raise e

  fullLine = ''

db.close()
SilentSteel
  • 2,344
  • 1
  • 28
  • 28
0

How about using the pexpect library? The idea is, that you can start a process pexpect.spawn(...), and wait until the output of that process contains a certain pattern process.expect(pattern).

I actually used this to connect to the mysql client and execute some sql scripts.

Connecting:

import pexpect
process = pexpect.spawn("mysql", ["-u", user, "-p"])
process.expect("Enter password")
process.sendline(password)
process.expect("mysql>")

This way the password is not hardcoded into the command line parameter (removes security risk).

Executing even several sql scripts:

error = False
for script in sql_scripts:
    process.sendline("source {};".format(script))
    index = process.expect(["mysql>", "ERROR"])

    # Error occurred, interrupt
    if index == 1:
        error = True
        break

if not error:
    # commit changes of the scripts
    process.sendline("COMMIT;")
    process.expect("mysql>")

    print "Everything fine"
else:
    # don't commit + print error message
    print "Your scripts have errors"

Beware that you always call expect(pattern), and that it matches, otherwise you will get a timeout error. I needed this bit of code to execute several sql scripts and only commit their changes if no error occurred, but it is easily adaptable for use cases with only one script.

Dwagner
  • 237
  • 4
  • 11
-1

You can use something like this-

def write_data(schema_name: str, table_name: str, column_names: str, data: list):
    try:
        data_list_template = ','.join(['%s'] * len(data))
        insert_query = f"insert into {schema_name}.{table_name} ({column_names}) values {data_list_template}"
        db.execute(insert_query, data)
        conn_obj.commit()
    except Exception as e:
        db.execute("rollback")
        raise e
Tanmoy Datta
  • 1,604
  • 1
  • 17
  • 15