1
+ import re
2
+ import uuid
3
+
1
4
import pyodbc # adapter for SQL Server
2
5
import psycopg2 # adapter for PostgreSQL
3
6
import configparser # used to read settings from file
4
7
import datetime # used to properly format dates and datetimes
5
8
import time # used to calculate time taken
6
9
10
+ # This script was created with global variables without initializing them here. Ideally, we should store a settings
11
+ # object rather than all of them separately but this works.
12
+ settings = None
13
+ EXCLUDED_COLUMNS = ["RowID" ]
14
+ delete_data = False
15
+ historical = False
16
+ demo_fix = False
17
+ migration_modules = []
18
+ old_connection = None
19
+ new_connection = None
20
+ today = datetime .date .today ()
21
+ now = datetime .datetime .now ()
22
+
7
23
8
24
# loads connection configuration and migration settings from a file.
9
25
# In future the settings file could be specified with a parameter.
@@ -29,16 +45,20 @@ def get_settings_from_file():
29
45
print (" Histrical data will be migrated to the new database." )
30
46
else :
31
47
print (" No historical data will be migrated." )
32
- global today
33
- global now
34
- today = datetime .date .today ()
35
- now = datetime .datetime .now ()
36
48
except KeyError as e :
37
49
print ("\x1b [0;31;48m" + "Error while trying to load settings. " + \
38
50
"Please make sure the settings.ini file exists in your working directory." + "\x1b [0m" )
39
51
exit (1 )
40
52
41
53
54
+ def is_uuid (value ):
55
+ try :
56
+ uuid .UUID (value , version = 4 )
57
+ return True
58
+ except ValueError :
59
+ return False
60
+
61
+
42
62
# tries to connect to both databases
43
63
def connect ():
44
64
print ("Setting up connection to the databases:" )
@@ -50,8 +70,9 @@ def connect():
50
70
global old_connection
51
71
old_connection = pyodbc .connect (old_connection_string )
52
72
except pyodbc .InterfaceError as exc :
53
- print ("\x1b [0;31;48m" +
54
- "ERROR: Could not connect to the SQL Server database. Make sure the server is running and check your settings." +
73
+ print ("\x1b [0;31;48m"
74
+ "ERROR: Could not connect to the SQL Server database. "
75
+ "Make sure the server is running and check your settings."
55
76
"\x1b [0m" )
56
77
print (exc )
57
78
exit (1 )
@@ -61,13 +82,15 @@ def connect():
61
82
new_db = settings ["NewDB" ]
62
83
new_connection_string = f'host={ new_db ["host" ]} port={ new_db ["port" ]} dbname={ new_db ["name" ]} ' \
63
84
f'user={ new_db ["user" ]} password={ new_db ["pwd" ]} '
64
- new_connection_string = f'postgres://{ new_db ["user" ]} @{ new_db ["host" ]} :{ new_db ["port" ]} /{ new_db ["name" ]} '
85
+ # new_connection_string = \
86
+ # f'postgres://{new_db["user"]}:{new_db["pwd"]}@{new_db["host"]}:{new_db["port"]}/{new_db["name"]}'
65
87
try :
66
88
global new_connection
67
89
new_connection = psycopg2 .connect (new_connection_string )
68
90
except psycopg2 .OperationalError as exc :
69
- print ("\x1b [0;31;48m" +
70
- "ERROR: Could not connect to the PostgreSQL database. Make sure the server is running and check your settings." +
91
+ print ("\x1b [0;31;48m"
92
+ "ERROR: Could not connect to the PostgreSQL database. "
93
+ "Make sure the server is running and check your settings."
71
94
"\x1b [0m" )
72
95
print (exc )
73
96
exit (1 )
@@ -108,43 +131,38 @@ def get_db_tables():
108
131
print ("Finding tables in both databases.\n " )
109
132
old_cursor .execute ("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE';" )
110
133
new_cursor .execute ("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';" )
111
- old_tables = list ()
112
- for x in old_cursor :
113
- # Remove special characters at the start and end of each item when adding it to the list.
114
- # This way the entries in the old and new list match
115
- old_tables .append (str (x )[2 :- 4 ])
116
- new_tables = list ()
117
- for x in new_cursor :
118
- # Remove special characters at the start and end of each item when adding it to the list.
119
- # This way the entries in the old and new list match
120
- new_tables .append (str (x )[2 :- 3 ])
134
+ old_tables = [x [0 ] for x in old_cursor ]
135
+ new_tables = [x [0 ] for x in new_cursor ]
121
136
return old_tables , new_tables
122
137
123
138
124
139
# This function puts the data from a SELECT statement into string and formats it correctly so that postgres can work
125
140
# with it.
126
141
def generate_insertion_string (row ):
127
- row_str = "("
142
+ row_list = []
128
143
for x in row :
129
144
# Strings must be enclosed in apostrophes, also escape singe quotes in a string by doubling them
130
145
if isinstance (x , str ):
131
- row_str = row_str + "'" + str (x ).replace ("'" , "''" ) + "', "
146
+ # The .NET webapp used to create uppercase UUIDs, so we try to detect it and lowercase it
147
+ if 32 <= len (x ) <= 36 and is_uuid (x ):
148
+ x = x .lower ()
149
+ row_list .append ("'" + str (x ).replace ("'" , "''" ) + "'" )
132
150
# Dates and datetimes must be enclosed in apostrophes
133
151
elif isinstance (x , datetime .datetime ) or isinstance (x , datetime .date ):
134
- row_str = row_str + "'" + str (x ) + "', "
135
- # If x is NoneType then str(x) get transtlated to "None", but sql wants "null"
152
+ row_list . append ( "'" + str (x ) + "'" )
153
+ # If x is NoneType then str(x) get translated to "None", but sql wants "null"
136
154
elif x is None :
137
- row_str = row_str + "null, "
155
+ row_list . append ( "null" )
138
156
# If x is bytes we need to make them nice (start with \x and append the data converted to hex):
139
157
elif isinstance (x , bytes ):
140
- row_str = row_str + "'\\ x" + str (x .hex ()) + "', "
158
+ row_list . append ( "'\\ x" + str (x .hex ()) + "'" )
141
159
else :
142
- row_str = row_str + str (x ) + ", "
143
- row_str = row_str [: - 2 ] + " )"
160
+ row_list . append ( str (x ))
161
+ row_str = f"( { ', ' . join ( row_list ) } )"
144
162
return row_str
145
163
146
164
147
- # When not migrating historical data, this function figures out what colums "ValidityTo" is so we can later check for
165
+ # When not migrating historical data, this function figures out what columns "ValidityTo" is so we can later check for
148
166
# each row if it is still valid or already historical
149
167
def get_validity_index (rows ):
150
168
vi = - 1
@@ -164,6 +182,8 @@ def get_validity_index(rows):
164
182
165
183
166
184
def get_validity (vi , row ):
185
+ global today
186
+ global now
167
187
if historical or ((not historical ) and vi == - 1 ):
168
188
return True
169
189
elif (not historical ) and vi != - 1 :
@@ -185,6 +205,17 @@ def get_validity(vi, row):
185
205
return True
186
206
187
207
208
+ def extract_sequence_name (column_default ):
209
+ if not column_default :
210
+ return None
211
+ pattern = r"nextval\('([^']*)"
212
+ match = re .search (pattern , column_default )
213
+ if match :
214
+ return match .group (1 )
215
+ else :
216
+ return None
217
+
218
+
188
219
def migrate ():
189
220
# This list collects all db tables that exist only in one of the databases but not the other.
190
221
lonely_tables = list ()
@@ -206,26 +237,36 @@ def migrate():
206
237
"\" FeedbackUUID\" , \" AuditUserID\" ) VALUES ('2000 01 01 00:00:00.000000', 0, 0, 0);" )
207
238
208
239
# Set up all the columns we're going to migrate.
209
- new_cursor .execute ("SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME = '" + table + "';" )
240
+ cursor = old_cursor .execute ("SELECT TOP 1 * FROM " + table + ";" )
241
+ old_columns_with_types = {column [0 ].lower (): column [1 ] for column in cursor .description }
242
+
243
+ new_cursor .execute ("SELECT COLUMN_NAME, COLUMN_DEFAULT "
244
+ "FROM information_schema.COLUMNS WHERE TABLE_NAME = '" + table + "';" )
210
245
rows = new_cursor .fetchall ()
211
246
# While we have the data ready: find out where dates are stored for historical data stuff. validity_index
212
247
# stores in which column the date (ValidityTo) is stored
213
248
validity_index = - 1
214
249
if not historical :
215
250
validity_index = get_validity_index (rows )
216
251
# Finally, set up the columns to migrate
217
- old_cols = ""
218
- new_cols = "("
252
+ sequence_columns = {}
253
+ old_cols_list = []
254
+ new_cols_list = []
219
255
for row in rows :
220
- old_cols = old_cols + str (row )[2 :- 3 ] + ", "
221
- new_cols = new_cols + "\" " + str (row )[2 :- 3 ] + "\" , "
222
- old_cols = old_cols [:- 2 ]
223
- new_cols = new_cols [:- 2 ] + ")"
256
+ if row [0 ] not in EXCLUDED_COLUMNS and row [0 ].lower () in old_columns_with_types :
257
+ col_default = extract_sequence_name (row [1 ])
258
+ if col_default :
259
+ sequence_columns [row [0 ]] = col_default
260
+ old_cols_list .append (row [0 ])
261
+ new_cols_list .append (f'"{ row [0 ]} "' )
262
+ old_cols = ", " .join (old_cols_list )
263
+ new_cols = "(" + ", " .join (new_cols_list ) + ")"
224
264
225
265
# Get the data from the old db with these column specifications
226
266
print (" Fetching data from old database." )
227
267
old_cursor .execute ("SELECT COUNT(*) FROM " + table + ";" )
228
- print (" Found " + str (old_cursor .fetchone ())[1 :- 3 ] + " entries." )
268
+ print (f" Found { old_cursor .fetchone ()[0 ]} entries." )
269
+ print (f" == old_cols: { old_cols } from { table } ==" )
229
270
old_cursor .execute ("SELECT " + old_cols + " FROM " + table + ";" )
230
271
231
272
# Set up the values for the insert statement and execute
@@ -247,14 +288,19 @@ def migrate():
247
288
# Not rolling back leads to an InFailedSqlTransaction exception.
248
289
new_connection .rollback ()
249
290
pass
250
-
291
+ except Exception as e :
292
+ print ("Failed: INSERT INTO \" " + table + "\" " + new_cols + " VALUES " + row_str + ";" )
293
+ raise
294
+ if sequence_columns :
295
+ print (" Data transferred, updating sequences." )
296
+ for column , sequence in sequence_columns .items ():
297
+ new_cursor .execute (f"select setval('{ sequence } ', max(\" { column } \" )) from \" { table } \" ;" )
251
298
print (" Table " + table + " has been migrated.\n " )
252
299
253
300
# Table doesn't exist
254
301
else :
255
- print ("\x1b [0;31;48m" + "WARNING: Table " + table + \
256
- " only exists in one of the databases (but not the other)! Is this correct?" + "\x1b [0m\n " )
257
- print ("" )
302
+ print (f"\x1b [0;31;48mWARNING: Table { table } only exists in one of the databases "
303
+ f"new: { table in new_tables } , old:{ table in old_tables } )! Is this correct?\x1b [0m\n " )
258
304
lonely_tables .append (table )
259
305
260
306
# Print all tables that have not been migrated due to missing schemas:
0 commit comments