Python
Mon but était de scripter le DDL d'un serveur SQL entier (tables, vues, procédures, sécurité…) pour le stocker dans une arborescence de répertoires sur le disque. Cela me permet de le conserver dans un outil de gestion de version (CVS, subversion, au pire Sourcesafe). Afin d'assurer une certaine compatibilité avec un groupe d'outils très puissants pour déployer des structures de bases à partir de projets Sourcesafe, et développé en Perl : AbaPerls, j'ai adopté une certaine convention de nommage des objets, séparant par exemple pour les tables la création de la table elle-même de ses contraintes d'intégrité référentielle et de ses indexes. Cela permet de générer une base sans se préoccuper de l'ordre de création des objets, puisque les tables peuvent être toutes générées d'abord, et toutes les contraintes DRI appliquées ensuite.
La première réécriture de mon script Perl original (moins sophistiqué) fut faite en Python. La réalisation est très simple, l'importation du DMO et des constantes de la bibliothèque sont aussi aisé que dans un langage M$. Le seul problème qui demeure à l'heure actuelle est un problème de jeu de caractères en scriptant des objets dont le code comporte des signes diacritiques (des accents dans les commentaires, principalement). Python génère une erreur. J'ai essayé de trouver un moyen de changer l'encodage, pour l'instant sans succès.
Une autre déception est venue lorsque, dans le code appelant ma classe, j'ai voulu utiliser un fichier de configuration XML. En Perl, j'utiliserais XML::simple et en un clin d'oeil ma structure XML serait disponible en tableaux associatifs. Apparemment le support d'XML est actuellement un point noir des bibliothèques Python. Pour y pallier, j'ai utilisé ConfigObj qui lit et écrit un fichier ini hiérarchique.
Ruby
Réécrire mon script Python en Ruby a pris très peu de temps : supprimer les : à la fin des structures de contrôle, finir le bloc avec un END, changer les appels de bibliothèques, quelques opérateurs, la syntaxe des expressions rationnelles. Le résultat donne un script très proche de son équivalent Python, à tel point qu'un diff devrait donner des différences minimales. Mais plus de double soulignements et d'indentation obligatoire. Ouf !
Une nuance toutefois : il semble qu'il n'y ait pas de destructeur en Ruby. Le principe est que de toute manière la classe et ses ressources vont être nettoyées par le garbage collector. J'avais en Python placé la déconnexion au serveur SQL dans un del. En Ruby j'ai créé une méthode disconnect. J'ai vu des propositions de méthodes détournées pour implémenter une façon de destructeur, mais je n'ai pas encore bien plongé dans le sujet.
Pour utiliser un fichier de configuration, je suis vite tombé sur yaml4r, une implémentation de YAML pour Ruby. Je ne connaissais pas YAML, dont le nom est encore un acronyme récursif (YAML Ain't a Markup Language) alors qu'il joue sur la construction habituelle du Yet Another… qui pourrait donner Yet Another Markup Language. C'est une initiative pour définir un langage très simple de description de contenu multiusage, à la fois descriptif et peu bavard (par opposition au XML). Il permet de structurer tout type d'information, aussi bien des tableaux, des listes, des logs qu'une sérialisation d'objets, sous une forme claire et très simple à comprendre. Ainsi on obtient par exemple un fichier de configuration hiérarchique aisé à maintenir et à lire, et parsable sans effort en Ruby. yaml4r fait maintenant partie des bibliothèques officielles livrées avec la distribution de Ruby. Vous n'aurez par besoin de les installer pour les utiliser.
Code Python
extract.py
# # requires ConfigObj : http://www.voidspace.org.uk/python/configobj.html # from win32com.client import constants, gencache import win32api import datetime, os, re, sys, locale from configobj import ConfigObj # avoid problems with diacritics. ascii is default encoding in Python #loc = locale.getdefaultlocale() #if loc[1]: # sys.setdefaultencoding(loc[1]) class extract: # == params == IncludeCollation = False def __init__(self): self.s = gencache.EnsureDispatch('SQLDMO.SQLServer') # we put it here to have COM enum (constants) available # == globals params == self.opToFile = constants.SQLDMOScript_ToFileOnly # 64 self.opScript = constants.SQLDMOScript_PrimaryObject | constants.SQLDMOScript_ObjectPermissions | constants.SQLDMOScript_OwnerQualify self.opTbl = constants.SQLDMOScript_Indexes | self.opScript self.op2 = constants.SQLDMOScript2_AnsiFile if not self.IncludeCollation: self.op2 = self.op2 | constants.SQLDMOScript2_NoCollation def __del__(self): self.s.DisConnect() def connect(self, server, integrated = True, login = None, pwd = None): self.s.LoginSecure = integrated self.s.Connect(server, login, pwd) # script all server objects def scriptServer(self, curdir='.'): serverdir = curdir + '\\' + self.s.Name + '\\' os.mkdir(serverdir) for db in self.s.Databases: print "scripting database " + db.Name if not db.SystemObject: dbdir = serverdir+'\\'+db.Name os.mkdir(dbdir) # -- tables -- os.mkdir(dbdir+'\\tbl') for tbl in db.Tables: if not tbl.SystemObject: script = tbl.Script(constants.SQLDMOScript_ToFileOnly | self.opTbl, dbdir+'\\tbl\\'+ tbl.Name +".tbl.sql", None, self.op2) # -- foreign keys -- fkeys = '' for fk in tbl.Keys: if fk.Type == 3: #SQLDMOKey_Foreign fkeys += fk.Script(self.opTbl, None, self.op2) if len(fkeys) > 0: fkeys = re.sub('\r', '', fkeys) # il fait des linefeeds... h = open(dbdir+'\\tbl\\'+ tbl.Name +".fkey.sql", 'w') h.write(fkeys) h.close() # -- indexes -- indexes = '' for idx in tbl.Indexes: indexes += idx.Script(self.opTbl, None, self.op2) if len(indexes) > 0: indexes = re.sub('\r', '', indexes) # il fait des linefeeds... h = open(dbdir+'\\tbl\\'+ tbl.Name +".ix.sql", 'w') h.write(indexes) h.close() # -- triggers -- triggers = '' for tri in tbl.Triggers: triggers += tri.Script(self.opTbl, None, self.op2) if len(triggers) > 0: triggers = re.sub('\r', '', triggers) # il fait des linefeeds... h = open(dbdir+'\\tbl\\'+ tbl.Name +".tri.sql", 'w') h.write(triggers) h.close() # -- views -- os.mkdir(dbdir+'\\view') for vw in db.Views: if not vw.SystemObject: script = vw.Script(self.opToFile | self.opTbl, dbdir+'\\view\\'+ vw.Name +".view.sql") # -- sp -- os.mkdir(dbdir+'\\sp') for sp in db.StoredProcedures: if not sp.SystemObject: script = sp.Script(self.opToFile | self.opScript, dbdir+'\\sp\\'+ sp.Name +".sp.sql") # -- udf -- os.mkdir(dbdir+'\\functions') for udf in db.UserDefinedFunctions: if not udf.SystemObject: script = udf.Script(self.opToFile | self.opScript, dbdir+'\\functions\\'+ udf.Name +".sqlfun.sql") # -- security -- os.mkdir(dbdir+'\\security') for user in db.Users: if not user.SystemObject: username = re.sub(r'\\', '_', user.Name) # attention au backslashs... script = user.Script(self.opToFile | self.opScript, dbdir+'\\security\\'+ username +".user.sql") for role in db.DatabaseRoles: if not role.IsFixedRole: script = role.Script(self.opToFile | self.opScript, dbdir+'\\security\\'+ role.Name +".role.sql") # -- datatypes -- os.mkdir(dbdir+'\\datatypes') for type in db.UserDefinedDatatypes: script = type.Script(self.opToFile | self.opScript, dbdir+'\\datatypes\\'+ type.Name +".datatype.sql") # generate INSERT statements for a table #def generateInserts(s, curdir='.', tableName, includeIdentity = false, top = 0):
Code Ruby
sqltools/extract.rb
require 'win32ole' module DMO end class Extract # == params == @@IncludeCollation = false def initialize() @s = WIN32OLE.new('SQLDMO.SQLServer') WIN32OLE.const_load(@s, DMO) # == globals params == @opToFile = DMO::SQLDMOScript_ToFileOnly # 64 @opScript = DMO::SQLDMOScript_PrimaryObject | DMO::SQLDMOScript_ObjectPermissions | DMO::SQLDMOScript_OwnerQualify @opTbl = DMO::SQLDMOScript_Indexes | @opScript @op2 = DMO::SQLDMOScript2_AnsiFile if not @IncludeCollation @op2 = @op2 | DMO::SQLDMOScript2_NoCollation end end #def __del__(self): # @s.DisConnect() def connect(server, integrated = true, login = nil, pwd = nil) @s.LoginSecure = integrated; @s.Connect(server, login, pwd); end def disconnect @s.DisConnect() end # script all server objects def scriptServer(curdir='.') $stdout.print "scripting server " + @s.Name + "\n\n" serverdir = curdir + '\\' + @s.Name + '\\' Dir.mkdir(serverdir) for db in @s.Databases if (not db.SystemObject) && (db.Status == DMO::SQLDMODBStat_Normal) $stdout.print "scripting database " + db.Name + "\n" dbdir = serverdir+'\\'+db.Name Dir.mkdir(dbdir) # -- tables -- Dir.mkdir(dbdir+'\\tbl') for tbl in db.Tables if not tbl.SystemObject script = tbl.Script(DMO::SQLDMOScript_ToFileOnly | @opTbl, dbdir+'\\tbl\\'+ tbl.Name() +'.tbl.sql', nil, @op2) # -- foreign keys -- fkeys = '' for fk in tbl.Keys if fk.Type == DMO::SQLDMOKey_Foreign fkeys += fk.Script(@opTbl, nil, @op2) end end if fkeys.length > 0 fkeys = fkeys.sub(/\r/,'') h = open(dbdir+'\\tbl\\'+ tbl.Name() +".fkey.sql", 'w') h.write(fkeys) h.close() end # -- indexes -- indexes = '' for idx in tbl.Indexes indexes += idx.Script(@opTbl, nil, @op2) end if indexes.length > 0 indexes = indexes.sub(/\r/, '') h = open(dbdir+'\\tbl\\'+ tbl.Name() +".ix.sql", 'w') h.write(indexes) h.close() end # -- triggers -- triggers = '' for tri in tbl.Triggers triggers += tri.Script(@opTbl, nil, @op2) end if triggers.length > 0 triggers = triggers.sub(/\r/, '') h = open(dbdir+'\\tbl\\'+ tbl.Name() +".tri.sql", 'w') h.write(triggers) h.close() end end end # -- views -- Dir.mkdir(dbdir+'\\view') for vw in db.Views if not vw.SystemObject script = vw.Script(@opToFile | @opTbl, dbdir+'\\view\\'+ vw.Name() +".view.sql") end end # -- sp -- Dir.mkdir(dbdir+'\\sp') for sp in db.StoredProcedures if not sp.SystemObject script = sp.Script(@opToFile | @opScript, dbdir+'\\sp\\'+ sp.Name() +".sp.sql") end end # -- udf -- Dir.mkdir(dbdir+'\\functions') for udf in db.UserDefinedFunctions if not udf.SystemObject script = udf.Script(@opToFile | @opScript, dbdir+'\\functions\\'+ udf.Name() +".sqlfun.sql") end end # -- security -- Dir.mkdir(dbdir+'\\security') for user in db.Users if not user.SystemObject username = user.Name() username = username.sub(/\\/, '_') script = user.Script(@opToFile | @opScript, dbdir+'\\security\\'+ username +".user.sql") end end for role in db.DatabaseRoles if not role.IsFixedRole script = role.Script(@opToFile | @opScript, dbdir+'\\security\\'+ role.Name() +".role.sql") end end # -- datatypes -- Dir.mkdir(dbdir+'\\datatypes') for type in db.UserDefinedDatatypes script = type.Script(@opToFile | @opScript, dbdir+'\\datatypes\\'+ type.Name() +".datatype.sql") end end end end end # generate INSERT statements for a table #def generateInserts(s, curdir='.', tableName, includeIdentity = false, top = 0):
Code d'appel de la classe Ruby
scriptDatabase.rb
# # I'm using a config file in YAML format : http://www.yaml.org/ # require "sqltools/extract" require 'yaml' conf = YAML::load( File.open( 'config.yaml' ) ) # create folder curdir = '.\\' + Time.now().strftime("%Y-%m-%d.%H%M") Dir.mkdir(curdir) e = Extract.new() for s in conf if conf[s[0]]['integrated'] == 1 e.connect(conf[s[0]]['host']) else e.connect(conf[s[0]]['host'], false, conf[s[0]]['login'], conf[s[0]]['pwd']) end e.scriptServer(curdir) e.disconnect() end
Exemple de fichier de configuration :
config.yaml
server1: host: 127.0.0.1 integrated: 0 login: mylogin pwd: mypassword server2: host: localhost integrated: 1
Le code est téléchargeable :



