Skip to content

Instantly share code, notes, and snippets.

@polaroidkidd
Forked from mrprompt/ExportSQLite.grt.lua
Created February 4, 2018 15:19
Show Gist options
  • Save polaroidkidd/b4d7bf10c0ec594aa8d5dfa4f5d7dd0d to your computer and use it in GitHub Desktop.
Save polaroidkidd/b4d7bf10c0ec594aa8d5dfa4f5d7dd0d to your computer and use it in GitHub Desktop.
MySQL WorkBench Plugin to export database to SQLite
-- 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