-
-
Save polaroidkidd/b4d7bf10c0ec594aa8d5dfa4f5d7dd0d to your computer and use it in GitHub Desktop.
MySQL WorkBench Plugin to export database to SQLite
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- ExportSQLite: SQLite export plugin for MySQL Workbench | |
-- Copyright (C) 2009 Thomas Henlich | |
-- | |
-- This program is free software: you can redistribute it and/or modify | |
-- it under the terms of the GNU General Public License as published by | |
-- the Free Software Foundation, either version 3 of the License, or | |
-- (at your option) any later version. | |
-- | |
-- This program is distributed in the hope that it will be useful, | |
-- but WITHOUT ANY WARRANTY; without even the implied warranty of | |
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
-- GNU General Public License for more details. | |
-- | |
-- You should have received a copy of the GNU General Public License | |
-- along with this program. If not, see <http://www.gnu.org/licenses/>. | |
-- | |
-- this function is called first by MySQL Workbench core to determine number of plugins in this module and basic plugin info | |
-- see the comments in the function body and adjust the parameters as appropriate | |
-- | |
function getModuleInfo() | |
return { | |
name= "ExportSQLite", | |
author= "Thomas Henlich", | |
version= "2009.12.02", | |
implements= "PluginInterface", | |
functions= { | |
"getPluginInfo:l<[email protected]>:", | |
"exportSQLite:i:[email protected]" | |
} | |
} | |
end | |
function objectPluginInput(type) | |
return grtV.newObj("app.PluginObjectInput", {objectStructName= type}) | |
end | |
function getPluginInfo() | |
local l | |
local plugin | |
-- create the list of plugins that this module exports | |
l = grtV.newList("object", "app.Plugin") | |
-- create a new app.Plugin object for every plugin | |
plugin = grtV.newObj("app.Plugin", { | |
name= "wb.catalog.util.exportSQLite", | |
caption= "Export SQLite CREATE script", | |
moduleName= "ExportSQLite", | |
pluginType= "normal", | |
moduleFunctionName= "exportSQLite", | |
inputValues= {objectPluginInput("db.Catalog")}, | |
rating= 100, | |
showProgress= 0, | |
groups= {"Catalog/Utilities", "Menu/Utilities"} | |
}) | |
-- fixup owner | |
plugin.inputValues[1].owner = plugin | |
-- add to the list of plugins | |
grtV.insert(l, plugin) | |
return l | |
end | |
-- check uniqueness of schema, table and index names | |
-- return 0 on success | |
-- otherwise return 1 (the export process should abort) | |
function validateForSQLiteExport(obj) | |
local id, i, j, errName, haveErrors, schema, tbl, column, index | |
id = {} | |
for i = 1, grtV.getn(obj.schemata) do | |
schema = obj.schemata[i] | |
if (id[schema.name]) then | |
haveErrors = 1 | |
if (Workbench:confirm("Name conflict", "Schemas " .. id[schema.name] .. " and " .. i .. " have the same name \"" .. schema.name .. "\". Please rename one of them.\nSearch for more such errors?") == 0) then | |
return 1 | |
end | |
else | |
id[schema.name] = i | |
end | |
end | |
-- do not continue looking for errors on schema name error | |
if (haveErrors) then | |
return 1 | |
end | |
for i = 1, grtV.getn(obj.schemata) do | |
schema = obj.schemata[i] | |
id = {} | |
for j = 1, grtV.getn(schema.tables) do | |
tbl = schema.tables[j] | |
if (tbl.name == "") then | |
haveErrors = 1 | |
if (Workbench:confirm("Name conflict", "Table " .. j .. " in schema \"" .. schema.name .. "\" has no name. Please rename.\nSearch for more such errors?") == 0) then | |
return 1 | |
end | |
end | |
if (id[tbl.name]) then | |
haveErrors = 1 | |
if (Workbench:confirm("Name conflict", "Tables " .. id[tbl.name] .. " and " .. j .. " in schema \"" .. schema.name .. "\" have the same name \"" .. tbl.name .. "\". Please rename one of them.\nSearch for more such errors?") == 0) then | |
return 1 | |
end | |
else | |
id[tbl.name] = j | |
end | |
end | |
end | |
if (haveErrors) then | |
return 1 | |
end | |
for i = 1, grtV.getn(obj.schemata) do | |
schema = obj.schemata[i] | |
for j = 1, grtV.getn(schema.tables) do | |
tbl = schema.tables[j] | |
id = {} | |
for k = 1, grtV.getn(tbl.columns) do | |
column = tbl.columns[k] | |
if (column.name == "") then | |
haveErrors = 1 | |
if (Workbench:confirm("Name conflict", "Column " .. k .. " in table \"" .. schema.name .. "\".\"" .. tbl.name .. "\" has no name. Please rename.\nSearch for more such errors?") == 0) then | |
return 1 | |
end | |
end | |
if (id[column.name]) then | |
haveErrors = 1 | |
if (Workbench:confirm("Name conflict", "Columns " .. id[column.name] .. " and " .. k .. " in table \"" .. schema.name .. "\".\"" .. tbl.name .. "\" have the same name \"" .. column.name .. "\". Please rename one of them.\nSearch for more such errors?") == 0) then | |
return 1 | |
end | |
else | |
id[column.name] = k | |
end | |
end | |
-- now check indices (except primary/unique) | |
id = {} | |
for k = 1, grtV.getn(tbl.indices) do | |
index = tbl.indices[k] | |
if (index.indexType == "INDEX") then | |
if (index.name == "") then | |
haveErrors = 1 | |
if (Workbench:confirm("Name conflict", "Index " .. k .. " in table \"" .. schema.name .. "\".\"" .. tbl.name .. "\" has no name. Please rename.\nSearch for more such errors?") == 0) then | |
return 1 | |
end | |
end | |
if (id[index.name]) then | |
haveErrors = 1 | |
if (Workbench:confirm("Name conflict", "Indices " .. id[index.name] .. " and " .. k .. " in table \"" .. schema.name .. "\".\"" .. tbl.name .. "\" have the same name \"" .. index.name .. "\". Please rename one of them.\nSearch for more such errors?") == 0) then | |
return 1 | |
end | |
else | |
id[index.name] = k | |
end | |
end | |
end | |
end | |
end | |
if (haveErrors) then | |
return 1 | |
end | |
return 0 | |
end | |
-- hack: if comment starts with "Defer..." we make it a deferred FK | |
-- could use member 'deferability' (WB has it), but there is no GUI for it | |
function isDeferred(fKey) | |
return (trim(fKey.comment):sub(1, 5):lower() == "defer") | |
end | |
function exportTable(file, dbName, schema, tbl) | |
local primaryKey, pKColumn, colComment | |
-- cannot create empty tables | |
if (grtV.getn(tbl.columns) > 0) then | |
file:write("CREATE TABLE " .. dbName .. dq(tbl.name) .. "(\n" .. sCommentFormat(tbl.comment)) | |
-- find the primary key | |
for k = 1, grtV.getn(tbl.indices) do | |
local index | |
index = tbl.indices[k] | |
if (index.isPrimary == 1) then | |
primaryKey = index | |
break | |
end | |
end | |
-- is primary key single-column? | |
if (primaryKey and (grtV.getn(primaryKey.columns) == 1)) then | |
pKColumn = primaryKey.columns[1].referencedColumn | |
end | |
colComment = "" | |
for k = 1, grtV.getn(tbl.columns) do | |
local column, sqLiteType, length, check, flags | |
check = "" | |
column = tbl.columns[k] | |
if (column.simpleType) then | |
sqLiteType = column.simpleType.name | |
flags = column.simpleType.flags | |
else | |
sqLiteType = column.userType.name | |
flags = column.flags | |
end | |
length = column.length | |
-- for INTEGER PRIMARY KEY column to become an alias for the rowid | |
-- the type needs to be "INTEGER" not "INT" | |
-- we fix it for other columns as well | |
if (sqLiteType:find("INT") or sqLiteType == "LONG") then | |
sqLiteType = "INTEGER" | |
length = -1 | |
-- check flags for "unsigned" | |
for f = 1, grtV.getn(column.flags) do | |
if (grtV.toLua(column.flags[f]) == "UNSIGNED") then | |
check = dq(column.name) .. ">=0" | |
break | |
end | |
end | |
end | |
-- we even implement ENUM (because we can) | |
if (sqLiteType == "ENUM") then | |
sqLiteType = "TEXT" | |
if (column.datatypeExplicitParams) then | |
check = dq(column.name) .. " IN" .. column.datatypeExplicitParams | |
end | |
end | |
if (k > 1) then | |
file:write("," .. commentFormat(colComment) .. "\n") | |
end | |
file:write(" " .. dq(column.name)) | |
-- type is optional in SQLite | |
if (sqLiteType ~= "") then | |
file:write(" " .. sqLiteType) | |
end | |
-- for [VAR]CHAR and such types specify length | |
-- even though this is not used in SQLite | |
if (length > 0) then | |
file:write("(" .. length .. ")") | |
end | |
-- Must specify single-column PKs as column-constraints | |
-- for AI/rowid behaviour | |
if (column == pKColumn) then | |
file:write(" PRIMARY KEY") | |
if (primaryKey.columns[1].descend == 1) then | |
file:write(" DESC") | |
end | |
-- only PK columns can be AI in SQLite | |
if (column.autoIncrement == 1) then | |
file:write(" AUTOINCREMENT") | |
end | |
end | |
-- check for NotNull | |
if (column.isNotNull == 1) then | |
file:write(" NOT NULL") | |
end | |
if (check ~= "") then | |
file:write(" CHECK(" .. check .. ")") | |
end | |
if (column.defaultValue ~= "") then | |
file:write(" DEFAULT " .. column.defaultValue) | |
end | |
colComment = column.comment | |
end | |
-- for multicolumn PKs | |
if (primaryKey and not pKColumn) then | |
file:write("," .. commentFormat(colComment) .. "\n PRIMARY KEY(" .. printIndexColumns(primaryKey) .. ")") | |
colComment = "" | |
end | |
-- put non-primary, UNIQUE Keys in CREATE TABLE as well (because we can) | |
for k = 1, grtV.getn(tbl.indices) do | |
local index | |
index = tbl.indices[k] | |
if (index ~= primaryKey and index.indexType == "UNIQUE") then | |
file:write("," .. commentFormat(colComment) .. "\n") | |
colComment = "" | |
if (index.name ~= "") then | |
file:write(" CONSTRAINT " .. dq(index.name) .. "\n ") | |
end | |
file:write(" UNIQUE(" .. printIndexColumns(index) .. ")") | |
end | |
end | |
for k = 1, grtV.getn(tbl.foreignKeys) do | |
local fKey | |
fKey = tbl.foreignKeys[k] | |
haveFKeys = 1 | |
file:write("," .. commentFormat(colComment) .. "\n") | |
colComment = "" | |
if (fKey.name ~= "") then | |
file:write(" CONSTRAINT " .. dq(fKey.name) .. "\n ") | |
end | |
file:write(" FOREIGN KEY(" .. printFKColumns(fKey.columns) .. ")\n"); | |
file:write(" REFERENCES " .. dq(fKey.referencedTable.name) .. "(" .. printFKColumns(fKey.referencedColumns) .. ")"); | |
if (fKey.deleteRule == "RESTRICT" or fKey.deleteRule == "CASCADE" or fKey.deleteRule == "SET NULL") then | |
file:write("\n ON DELETE " .. fKey.deleteRule) | |
end | |
if (fKey.updateRule == "RESTRICT" or fKey.updateRule == "CASCADE" or fKey.updateRule == "SET NULL") then | |
file:write("\n ON UPDATE " .. fKey.updateRule) | |
end | |
if (isDeferred(fKey)) then | |
file:write(" DEFERRABLE INITIALLY DEFERRED") | |
end | |
end | |
file:write(commentFormat(colComment) .. "\n);\n"); | |
-- CREATE INDEX statements for | |
-- all non-primary, non-unique, non-foreign indexes | |
for k = 1, grtV.getn(tbl.indices) do | |
local index, indexName | |
index = tbl.indices[k] | |
if (index.indexType == "INDEX") then | |
indexName = tbl.name .. "." .. index.name | |
if (index.name == "") then | |
indexName = tbl.name .. ".index" .. k | |
--uniqueId = uniqueId + 1 | |
end | |
file:write("CREATE INDEX " .. dbName .. dq(indexName) .. " ON " .. dq(tbl.name) .. "(") | |
file:write(printIndexColumns(index) .. ");\n") | |
end | |
end | |
-- write the INSERTS (currently always) | |
local tableInserts | |
if (type(tbl.inserts) == "string") then | |
-- old inserts, WB 5.1- | |
tableInserts = tbl.inserts | |
else | |
-- new inserts, WB 5.2.10+ | |
tableInserts = tbl:inserts() | |
end | |
for insert in string.gmatch(tableInserts, "[^\r\n]+") do | |
local columnsValues | |
-- WB 5.1- insert | |
local insertStart = "insert into `" .. tbl.name .. "` (" | |
if (insert:sub(1, insertStart:len()):lower() == insertStart) then | |
columnsValues = insert:sub(insertStart:len() + 1) | |
else | |
-- WB 5.2+ insert | |
insertStart = "insert into `" .. schema.name .. "`.`" .. tbl.name .. "` (" | |
if (insert:sub(1, insertStart:len()):lower() == insertStart) then | |
columnsValues = insert:sub(insertStart:len() + 1) | |
else | |
Workbench:confirm("Error", "Unrecognized command in insert") | |
return 1 | |
end | |
end | |
local lastColumn = 0 | |
for k = 1, grtV.getn(tbl.columns) do | |
columnName = "`" .. tbl.columns[k].name .. "`" | |
if (columnsValues:sub(1, columnName:len()) == columnName) then | |
columnsValues = columnsValues:sub(columnName:len() + 1) | |
if (columnsValues:sub(1, 1) == ")") then | |
columnsValues = columnsValues:sub(2) | |
lastColumn = k | |
break | |
else | |
if (columnsValues:sub(1, 2) == ", ") then | |
columnsValues = columnsValues:sub(3) | |
else | |
Workbench:confirm("Error", "Unrecognized character in column list") | |
end | |
end | |
else | |
Workbench:confirm("Error", "Unrecognized column in inserts") | |
return 1 | |
end | |
end | |
file:write("INSERT INTO " .. dq(tbl.name) .. "("); | |
for k = 1, lastColumn do | |
if (k > 1) then | |
file:write(",") | |
end | |
file:write(dq(tbl.columns[k].name)) | |
end | |
if (columnsValues:sub(1, 9):lower() ~= " values (") then | |
Workbench:confirm("Error", "Unrecognized SQL in insert") | |
return 1 | |
end | |
columnsValues = columnsValues:sub(10) | |
file:write(") VALUES(") | |
file:write(tostring(columnsValues:gsub("\\(.)", | |
function(c) | |
if (c == "'") then | |
return "''" | |
else | |
return c | |
end | |
end))) | |
file:write("\n") | |
end | |
end | |
return 0 | |
end | |
function orderTables(file, dbName, schema, unOrdered, respectDeferredness) | |
repeat | |
local haveOrdered = false | |
for j = 1, grtV.getn(schema.tables) do | |
local tbl = schema.tables[j] | |
if (unOrdered[tbl.name]) then | |
local hasForwardReference = false | |
for k = 1, grtV.getn(tbl.foreignKeys) do | |
local fKey | |
fKey = tbl.foreignKeys[k] | |
if (unOrdered[fKey.referencedTable.name] and fKey.referencedTable.name ~= tbl.name | |
and not(respectDeferredness and isDeferred(fKey))) then | |
hasForwardReference = true | |
break | |
end | |
end | |
if (not hasForwardReference) then | |
if (exportTable(file, dbName, schema, tbl) ~= 0) then | |
print("Error writing table " .. tbl.name .. "\n") | |
return 1 | |
end | |
unOrdered[tbl.name] = nil | |
haveOrdered = true | |
end | |
end | |
end | |
until (not haveOrdered) | |
return 0 | |
end | |
function exportSchema(file, schema, isMainSchema) | |
print("Schema " .. schema.name .. " has " .. grtV.getn(schema.tables) .. " tables\n") | |
if (grtV.getn(schema.tables) > 0) then | |
file:write("\n-- Schema: " .. schema.name .. "\n") | |
file:write(sCommentFormat(schema.comment)) | |
if (isMainSchema) then | |
dbName = "" | |
else | |
dbName = dq(schema.name) .. "." | |
file:write('ATTACH "' .. safeFileName(schema.name .. ".sdb") .. '" AS ' .. dq(schema.name) .. ";\n") | |
end | |
file:write("BEGIN;\n") | |
-- find a valid table order for inserts from FK constraints | |
local unOrdered = {} | |
for j = 1, grtV.getn(schema.tables) do | |
local tbl = schema.tables[j] | |
unOrdered[tbl.name] = tbl | |
end | |
-- try treating deferred keys like non-deferred keys first for ordering | |
if (orderTables(file, dbName, schema, unOrdered, false) ~= 0) then | |
print("Error ordering tables in schema " .. schema.name .. "\n") | |
return 1 | |
end | |
-- now try harder (leave out deferred keys from determining an order) | |
if (orderTables(file, dbName, schema, unOrdered, true) ~= 0) then | |
print("Error ordering tables in schema " .. schema.name .. "\n") | |
return 1 | |
end | |
-- loop through all remaining tables, if any. Have circular FK refs. How to handle? | |
for j = 1, grtV.getn(schema.tables) do | |
local tbl = schema.tables[j] | |
if (unOrdered[tbl.name]) then | |
if (exportTable(file, dbName, schema, tbl) ~= 0) then | |
print("Error writing table " .. tbl.name .. "\n") | |
return 1 | |
end | |
end | |
end | |
file:write("COMMIT;\n") | |
end | |
return 0 | |
end | |
-- function to go through all schemata in catalog and rename all FKs of table-objects | |
function exportSQLite(obj) | |
local i, j, k, f, schema, tbl, path, file, dbName, info, uniqueId, haveFKeys | |
local version, versionNumber | |
haveFKeys = 0 | |
version = grtV.getGlobal("/wb/info/version") | |
versionNumber = version.majorNumber .. "." .. version.minorNumber .. "." .. version.releaseNumber | |
if (validateForSQLiteExport(obj) ~= 0) then | |
return 1 | |
end | |
-- we don't have requestFileSave in <= 5.1 | |
if (Workbench.requestFileSave) then | |
path = Workbench:requestFileSave("Save as", "SQL Files (*.sql)|*.sql") | |
else | |
path = Workbench:input("Save as") | |
end | |
if (path == "") then | |
return 1 | |
end | |
file = io.open(path, "w+") | |
if (file == nil) then | |
Workbench:confirm("Error", "Cannot open file") | |
return 1 | |
end | |
-- if (not path:find("\.sql$")) then | |
-- truncate db file | |
-- file:close() | |
-- file = io.popen("sqlite3 -batch -bail " .. path, "w") | |
-- end | |
info = grtV.getGlobal("/wb/doc/info") | |
file:write(infoFormat("Creator", "MySQL Workbench " .. versionNumber .. "/ExportSQLite plugin " .. getModuleInfo().version)) | |
file:write(infoFormat("Author", info.author)) | |
file:write(infoFormat("Caption", info.caption)) | |
file:write(infoFormat("Project", info.project)) | |
file:write(infoFormat("Changed", info.dateChanged)) | |
file:write(infoFormat("Created", info.dateCreated)) | |
file:write(infoFormat("Description", info.description)) | |
file:write("PRAGMA foreign_keys = OFF;\n") | |
-- loop over all catalogs in schema, find main schema | |
-- main schema is first nonempty schema or nonempty schema named "main" | |
local iMain = -1 | |
for i = 1, grtV.getn(obj.schemata) do | |
local schema = obj.schemata[i] | |
if (grtV.getn(schema.tables) > 0) then | |
if (iMain < 0) then | |
iMain = i | |
end | |
if (schema.name == "main") then | |
iMain = i | |
break | |
end | |
end | |
schema = obj.schemata[i] | |
end | |
if (iMain > 0) then | |
if (exportSchema(file, obj.schemata[iMain], true) ~= 0) then | |
print("Error writing schema " .. obj.schemata[iMain].name .. "\n") | |
return 1 | |
end | |
end | |
for i = 1, grtV.getn(obj.schemata) do | |
uniqueId = 1 | |
if (i ~= iMain) then | |
if (exportSchema(file, obj.schemata[i], false) ~= 0) then | |
print("Error writing schema " .. obj.schemata[i].name .. "\n") | |
return 1 | |
end | |
end | |
end | |
file:close() | |
print("Export to " .. path .. " finished.\n") | |
return 0 | |
end | |
-- get comma separated column list of an index | |
function printIndexColumns(index) | |
local i, s | |
s = "" | |
for i = 1, grtV.getn(index.columns) do | |
local column, refColumn | |
column = index.columns[i] | |
if (i > 1) then | |
s = s .. "," | |
end | |
s = s .. dq(column.referencedColumn.name) | |
if (column.descend == 1) then | |
s = s .. " DESC" | |
end | |
end | |
return s | |
end | |
-- get comma separated column/reference list of a foreign key | |
function printFKColumns(columns) | |
local i, s | |
s = "" | |
for i = 1, grtV.getn(columns) do | |
if (i > 1) then | |
s = s .. "," | |
end | |
s = s .. dq(columns[i].name) | |
end | |
return s | |
end | |
-- get comma separated referenced column list of a foreign key | |
function printFKRefdColumns(fKey) | |
local i, s | |
s = "" | |
for i = 1, grtV.getn(fKey.columns) do | |
if (i > 1) then | |
s = s .. ", " | |
end | |
s = s .. dq(fKey.columns[i].referencedColumn.name) | |
end | |
return s | |
end | |
-- double quote identifer, replacing " by "" | |
function dq(id) | |
return '"' .. id:gsub('"', '""') .. '"' | |
end | |
-- create safe filename from identifer | |
function safeFileName(id) | |
return id:gsub( | |
'[/\\:%*%?"<>|%%]', | |
function(c) return string.format("%%%02x", string.byte(c)) end | |
) | |
end | |
-- remove trailing and leading whitespace from string. | |
function trim(s) | |
return (s:gsub("^%s*(.-)%s*$", "%1")) | |
end | |
-- format a info field as SQL comment | |
function infoFormat(header, body) | |
local strippedBody | |
strippedBody = trim(body) | |
if (strippedBody == "") then | |
return "" | |
elseif (strippedBody:find("\n")) then | |
-- multiline comment | |
return string.format("-- %s:\n-- %s\n", header, | |
strippedBody:gsub("\n","\n-- ")) | |
else | |
-- single line | |
return string.format("-- %-14s %s\n", header..":", strippedBody) | |
end | |
end | |
-- format a schema or table comment as SQL comment | |
-- table comments to be stored in SQLite schema | |
function sCommentFormat(body) | |
local strippedBody | |
strippedBody = trim(body) | |
if (strippedBody == "") then | |
return "" | |
else | |
-- multiline comment | |
return string.format("-- %s\n", | |
strippedBody:gsub("\n","\n-- ")) | |
end | |
end | |
-- format a column comment as SQL comment | |
-- to be stored in SQLite schema for user information | |
function commentFormat(body) | |
local strippedBody | |
strippedBody = trim(body) | |
if (strippedBody == "") then | |
return "" | |
elseif (strippedBody:find("\n")) then | |
-- multiline comment | |
return string.format("\n-- %s", | |
strippedBody:gsub("\n","\n-- ")) | |
else | |
-- single line | |
return string.format("-- %s", strippedBody) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment