execsql2 2.15.5__py3-none-any.whl → 2.15.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- execsql/config.py +3 -0
- execsql/db/access.py +52 -51
- execsql/db/base.py +8 -8
- execsql/db/dsn.py +2 -1
- execsql/db/duckdb.py +1 -1
- execsql/db/firebird.py +56 -59
- execsql/db/mysql.py +99 -97
- execsql/db/oracle.py +63 -68
- execsql/db/postgres.py +141 -134
- execsql/db/sqlite.py +1 -1
- execsql/db/sqlserver.py +11 -12
- execsql/exceptions.py +3 -3
- execsql/exporters/html.py +5 -3
- execsql/exporters/json.py +7 -2
- execsql/importers/csv.py +2 -2
- execsql/metacommands/system.py +1 -2
- execsql/models.py +0 -1
- execsql/script/engine.py +2 -1
- execsql/script/variables.py +45 -1
- execsql/state.py +4 -1
- execsql/utils/fileio.py +3 -2
- {execsql2-2.15.5.dist-info → execsql2-2.15.7.dist-info}/METADATA +1 -1
- {execsql2-2.15.5.dist-info → execsql2-2.15.7.dist-info}/RECORD +42 -42
- {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.15.5.dist-info → execsql2-2.15.7.dist-info}/WHEEL +0 -0
- {execsql2-2.15.5.dist-info → execsql2-2.15.7.dist-info}/entry_points.txt +0 -0
- {execsql2-2.15.5.dist-info → execsql2-2.15.7.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.15.5.dist-info → execsql2-2.15.7.dist-info}/licenses/NOTICE +0 -0
execsql/config.py
CHANGED
|
@@ -290,8 +290,11 @@ class ConfigData:
|
|
|
290
290
|
config_files = [sys_config_file, user_config_file, script_config_file, startdir_config_file]
|
|
291
291
|
else:
|
|
292
292
|
config_files = [sys_config_file, user_config_file, startdir_config_file]
|
|
293
|
+
_MAX_CONFIG_CHAIN = 20 # Guard against circular config_file references.
|
|
293
294
|
self.files_read: list = []
|
|
294
295
|
for ix, configfile in enumerate(config_files):
|
|
296
|
+
if len(self.files_read) >= _MAX_CONFIG_CHAIN:
|
|
297
|
+
break
|
|
295
298
|
if configfile not in self.files_read and Path(configfile).is_file():
|
|
296
299
|
self.files_read.append(configfile)
|
|
297
300
|
cp = ConfigParser()
|
execsql/db/access.py
CHANGED
|
@@ -243,16 +243,16 @@ class AccessDatabase(Database):
|
|
|
243
243
|
self.temp_query_names.append(qn)
|
|
244
244
|
else:
|
|
245
245
|
self.dao_flush_check()
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
246
|
+
with self._cursor() as curs:
|
|
247
|
+
if self.jet4:
|
|
248
|
+
encoded_sql = str(sql)
|
|
249
|
+
else:
|
|
250
|
+
encoded_sql = str(sql).encode(self.encoding)
|
|
251
|
+
if paramlist is None:
|
|
252
|
+
curs.execute(encoded_sql)
|
|
253
|
+
else:
|
|
254
|
+
curs.execute(encoded_sql, paramlist)
|
|
255
|
+
_state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
|
|
256
256
|
|
|
257
257
|
if type(sqlcmd) in (list, tuple):
|
|
258
258
|
for sql in sqlcmd:
|
|
@@ -269,10 +269,10 @@ class AccessDatabase(Database):
|
|
|
269
269
|
# Returns the results of the sql select statement.
|
|
270
270
|
# The Access driver returns data as unicode, so no decoding is necessary.
|
|
271
271
|
self.dao_flush_check()
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
272
|
+
with self._cursor() as curs:
|
|
273
|
+
curs.execute(sql)
|
|
274
|
+
rows = curs.fetchall()
|
|
275
|
+
return [d[0] for d in curs.description], rows
|
|
276
276
|
|
|
277
277
|
def select_rowsource(self, sql: str) -> tuple[list[str], Any]:
|
|
278
278
|
"""Return column names and an iterable that yields rows one at a time."""
|
|
@@ -308,20 +308,20 @@ class AccessDatabase(Database):
|
|
|
308
308
|
def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
|
|
309
309
|
"""Return True if the named table exists in the Access database."""
|
|
310
310
|
self.dao_flush_check()
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
311
|
+
sql = "select Name from MSysObjects where Name=? And Type In (1,4,6);"
|
|
312
|
+
with self._cursor() as curs:
|
|
313
|
+
try:
|
|
314
|
+
curs.execute(sql, (table_name,))
|
|
315
|
+
except ErrInfo:
|
|
316
|
+
raise
|
|
317
|
+
except Exception as e:
|
|
318
|
+
raise ErrInfo(
|
|
319
|
+
type="db",
|
|
320
|
+
command_text=sql,
|
|
321
|
+
exception_msg=exception_desc(),
|
|
322
|
+
other_msg=f"Failure on test for existence of Access table {table_name}",
|
|
323
|
+
) from e
|
|
324
|
+
rows = curs.fetchall()
|
|
325
325
|
return len(rows) > 0
|
|
326
326
|
|
|
327
327
|
def column_exists(
|
|
@@ -332,41 +332,41 @@ class AccessDatabase(Database):
|
|
|
332
332
|
) -> bool:
|
|
333
333
|
"""Return True if the named column exists in the given Access table."""
|
|
334
334
|
self.dao_flush_check()
|
|
335
|
-
curs = self.cursor()
|
|
336
335
|
quoted_col = self.quote_identifier(column_name)
|
|
337
336
|
quoted_tbl = self.quote_identifier(table_name)
|
|
338
337
|
sql = f"select top 1 {quoted_col} from {quoted_tbl};"
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
338
|
+
with self._cursor() as curs:
|
|
339
|
+
try:
|
|
340
|
+
curs.execute(sql)
|
|
341
|
+
except Exception:
|
|
342
|
+
return False
|
|
343
343
|
return True
|
|
344
344
|
|
|
345
345
|
def table_columns(self, table_name: str, schema_name: str | None = None) -> list[str]:
|
|
346
346
|
"""Return a list of column names for the given Access table."""
|
|
347
347
|
self.dao_flush_check()
|
|
348
|
-
curs = self.cursor()
|
|
349
348
|
quoted_tbl = self.quote_identifier(table_name)
|
|
350
|
-
|
|
351
|
-
|
|
349
|
+
with self._cursor() as curs:
|
|
350
|
+
curs.execute(f"select top 1 * from {quoted_tbl};")
|
|
351
|
+
return [d[0] for d in curs.description]
|
|
352
352
|
|
|
353
353
|
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
354
354
|
"""Return True if the named view or query exists in the Access database."""
|
|
355
355
|
self.dao_flush_check()
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
356
|
+
sql = "select Name from MSysObjects where Name=? And Type = 5;"
|
|
357
|
+
with self._cursor() as curs:
|
|
358
|
+
try:
|
|
359
|
+
curs.execute(sql, (view_name,))
|
|
360
|
+
except ErrInfo:
|
|
361
|
+
raise
|
|
362
|
+
except Exception as e:
|
|
363
|
+
raise ErrInfo(
|
|
364
|
+
type="db",
|
|
365
|
+
command_text=sql,
|
|
366
|
+
exception_msg=exception_desc(),
|
|
367
|
+
other_msg=f"Test for existence of Access view/query {view_name}",
|
|
368
|
+
) from e
|
|
369
|
+
rows = curs.fetchall()
|
|
370
370
|
return len(rows) > 0
|
|
371
371
|
|
|
372
372
|
def schema_exists(self, schema_name: str) -> bool:
|
|
@@ -447,4 +447,5 @@ class AccessDatabase(Database):
|
|
|
447
447
|
sq_name = self.schema_qualified_table_name(schema_name, table_name)
|
|
448
448
|
quoted_col = self.quote_identifier(column_name)
|
|
449
449
|
sql = f"insert into {sq_name} ({quoted_col}) values ({self.paramsubs(1)});"
|
|
450
|
-
self.
|
|
450
|
+
with self._cursor() as curs:
|
|
451
|
+
curs.execute(sql, (pyodbc.Binary(filedata),))
|
execsql/db/base.py
CHANGED
|
@@ -460,7 +460,6 @@ class Database(ABC):
|
|
|
460
460
|
paramspec = self.paramsubs(len(columns))
|
|
461
461
|
sql = f"insert into {sq_name} ({colspec}) values ({paramspec});"
|
|
462
462
|
rows = iter(rowsource)
|
|
463
|
-
curs = self.cursor()
|
|
464
463
|
eof = False
|
|
465
464
|
total_rows = 0
|
|
466
465
|
|
|
@@ -483,7 +482,7 @@ class Database(ABC):
|
|
|
483
482
|
except ImportError:
|
|
484
483
|
use_progress = False
|
|
485
484
|
|
|
486
|
-
def _import_loop() -> int:
|
|
485
|
+
def _import_loop(curs) -> int:
|
|
487
486
|
nonlocal eof, total_rows, task_id
|
|
488
487
|
while True:
|
|
489
488
|
b = []
|
|
@@ -575,12 +574,13 @@ class Database(ABC):
|
|
|
575
574
|
break
|
|
576
575
|
return total_rows
|
|
577
576
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
577
|
+
with self._cursor() as curs:
|
|
578
|
+
if use_progress and progress_ctx is not None:
|
|
579
|
+
with progress_ctx:
|
|
580
|
+
task_id = progress_ctx.add_task(sq_name, total=None)
|
|
581
|
+
_import_loop(curs)
|
|
582
|
+
else:
|
|
583
|
+
_import_loop(curs)
|
|
584
584
|
|
|
585
585
|
if _state.exec_log:
|
|
586
586
|
_state.exec_log.log_status_info(
|
execsql/db/dsn.py
CHANGED
|
@@ -145,4 +145,5 @@ class DsnDatabase(Database):
|
|
|
145
145
|
sq_name = self.schema_qualified_table_name(schema_name, table_name)
|
|
146
146
|
quoted_col = self.quote_identifier(column_name)
|
|
147
147
|
sql = f"insert into {sq_name} ({quoted_col}) values ({self.paramsubs(1)});"
|
|
148
|
-
self.
|
|
148
|
+
with self._cursor() as curs:
|
|
149
|
+
curs.execute(sql, (pyodbc.Binary(filedata),))
|
execsql/db/duckdb.py
CHANGED
|
@@ -67,7 +67,7 @@ class DuckDBDatabase(Database):
|
|
|
67
67
|
with self._cursor() as curs:
|
|
68
68
|
cmd = f"select * from {querycommand};"
|
|
69
69
|
try:
|
|
70
|
-
curs.execute(cmd
|
|
70
|
+
curs.execute(cmd)
|
|
71
71
|
_state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
|
|
72
72
|
except Exception:
|
|
73
73
|
self.rollback()
|
execsql/db/firebird.py
CHANGED
|
@@ -127,30 +127,29 @@ class FirebirdDatabase(Database):
|
|
|
127
127
|
|
|
128
128
|
def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
|
|
129
129
|
"""Return True if the named table exists in the Firebird database."""
|
|
130
|
-
curs = self.cursor()
|
|
131
130
|
sql = (
|
|
132
131
|
"SELECT RDB$RELATION_NAME FROM RDB$RELATIONS "
|
|
133
132
|
"WHERE RDB$SYSTEM_FLAG=0 AND RDB$VIEW_BLR IS NULL "
|
|
134
133
|
"AND RDB$RELATION_NAME=?;"
|
|
135
134
|
)
|
|
136
|
-
|
|
137
|
-
curs.execute(sql, (table_name.upper(),))
|
|
138
|
-
except ErrInfo:
|
|
139
|
-
raise
|
|
140
|
-
except Exception as e:
|
|
135
|
+
with self._cursor() as curs:
|
|
141
136
|
try:
|
|
142
|
-
|
|
143
|
-
except
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
137
|
+
curs.execute(sql, (table_name.upper(),))
|
|
138
|
+
except ErrInfo:
|
|
139
|
+
raise
|
|
140
|
+
except Exception as e:
|
|
141
|
+
try:
|
|
142
|
+
self.rollback()
|
|
143
|
+
except Exception:
|
|
144
|
+
pass # Rollback is best-effort after a failed query.
|
|
145
|
+
raise ErrInfo(
|
|
146
|
+
type="db",
|
|
147
|
+
command_text=sql,
|
|
148
|
+
exception_msg=exception_desc(),
|
|
149
|
+
other_msg=f"Failed test for existence of Firebird table {table_name}",
|
|
150
|
+
) from e
|
|
151
|
+
rows = curs.fetchall()
|
|
152
152
|
self.conn.commit()
|
|
153
|
-
curs.close()
|
|
154
153
|
return len(rows) > 0
|
|
155
154
|
|
|
156
155
|
def column_exists(
|
|
@@ -160,53 +159,52 @@ class FirebirdDatabase(Database):
|
|
|
160
159
|
schema_name: str | None = None,
|
|
161
160
|
) -> bool:
|
|
162
161
|
"""Return True if the named column exists in the given Firebird table."""
|
|
163
|
-
curs = self.cursor()
|
|
164
162
|
quoted_col = self.quote_identifier(column_name)
|
|
165
163
|
quoted_tbl = self.quote_identifier(table_name)
|
|
166
164
|
sql = f"select first 1 {quoted_col} from {quoted_tbl};"
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
165
|
+
with self._cursor() as curs:
|
|
166
|
+
try:
|
|
167
|
+
curs.execute(sql)
|
|
168
|
+
except Exception:
|
|
169
|
+
return False
|
|
171
170
|
return True
|
|
172
171
|
|
|
173
172
|
def table_columns(self, table_name: str, schema_name: str | None = None) -> list[str]:
|
|
174
173
|
"""Return a list of column names for the given Firebird table."""
|
|
175
|
-
curs = self.cursor()
|
|
176
174
|
quoted_tbl = self.quote_identifier(table_name)
|
|
177
175
|
sql = f"select first 1 * from {quoted_tbl};"
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
176
|
+
with self._cursor() as curs:
|
|
177
|
+
try:
|
|
178
|
+
curs.execute(sql)
|
|
179
|
+
except ErrInfo:
|
|
180
|
+
raise
|
|
181
|
+
except Exception as e:
|
|
182
|
+
self.rollback()
|
|
183
|
+
raise ErrInfo(
|
|
184
|
+
type="db",
|
|
185
|
+
command_text=sql,
|
|
186
|
+
exception_msg=exception_desc(),
|
|
187
|
+
other_msg=f"Failed to get column names for table {table_name} of {self.name()}",
|
|
188
|
+
) from e
|
|
189
|
+
return [d[0] for d in curs.description]
|
|
191
190
|
|
|
192
191
|
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
193
192
|
"""Return True if the named view exists in the Firebird database."""
|
|
194
|
-
curs = self.cursor()
|
|
195
193
|
sql = "select distinct rdb$view_name from rdb$view_relations where rdb$view_name = ?;"
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
194
|
+
with self._cursor() as curs:
|
|
195
|
+
try:
|
|
196
|
+
curs.execute(sql, (view_name,))
|
|
197
|
+
except ErrInfo:
|
|
198
|
+
raise
|
|
199
|
+
except Exception as e:
|
|
200
|
+
self.rollback()
|
|
201
|
+
raise ErrInfo(
|
|
202
|
+
type="db",
|
|
203
|
+
command_text=sql,
|
|
204
|
+
exception_msg=exception_desc(),
|
|
205
|
+
other_msg=f"Failed test for existence of Firebird view {view_name}",
|
|
206
|
+
) from e
|
|
207
|
+
rows = curs.fetchall()
|
|
210
208
|
return len(rows) > 0
|
|
211
209
|
|
|
212
210
|
def schema_exists(self, schema_name: str) -> bool:
|
|
@@ -215,14 +213,13 @@ class FirebirdDatabase(Database):
|
|
|
215
213
|
|
|
216
214
|
def role_exists(self, rolename: str) -> bool:
|
|
217
215
|
"""Return True if the named role or user exists in the Firebird database."""
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
curs.close()
|
|
216
|
+
with self._cursor() as curs:
|
|
217
|
+
curs.execute(
|
|
218
|
+
"SELECT DISTINCT USER FROM RDB$USER_PRIVILEGES WHERE USER = ? union "
|
|
219
|
+
" SELECT DISTINCT RDB$ROLE_NAME FROM RDB$ROLES WHERE RDB$ROLE_NAME = ?;",
|
|
220
|
+
(rolename, rolename),
|
|
221
|
+
)
|
|
222
|
+
rows = curs.fetchall()
|
|
226
223
|
return len(rows) > 0
|
|
227
224
|
|
|
228
225
|
def drop_table(self, tablename: str) -> None:
|
execsql/db/mysql.py
CHANGED
|
@@ -53,9 +53,9 @@ class MySQLDatabase(Database):
|
|
|
53
53
|
from execsql.types import dbt_mysql
|
|
54
54
|
|
|
55
55
|
self.type = dbt_mysql
|
|
56
|
-
self.server_name = str(server_name)
|
|
57
|
-
self.db_name = str(db_name)
|
|
58
|
-
self.user = str(user_name)
|
|
56
|
+
self.server_name = str(server_name) if server_name is not None else None
|
|
57
|
+
self.db_name = str(db_name) if db_name is not None else None
|
|
58
|
+
self.user = str(user_name) if user_name is not None else None
|
|
59
59
|
self.need_passwd = need_passwd
|
|
60
60
|
self.password = password
|
|
61
61
|
self.port = port if port else 3306
|
|
@@ -148,15 +148,14 @@ class MySQLDatabase(Database):
|
|
|
148
148
|
|
|
149
149
|
def role_exists(self, rolename: str) -> bool:
|
|
150
150
|
"""Return True if the named role or user exists in the MySQL server."""
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
curs.close()
|
|
151
|
+
with self._cursor() as curs:
|
|
152
|
+
curs.execute(
|
|
153
|
+
"select distinct user as role from mysql.user where user = %s"
|
|
154
|
+
" union select distinct role_name as role from information_schema.applicable_roles"
|
|
155
|
+
" where role_name = %s",
|
|
156
|
+
(rolename, rolename),
|
|
157
|
+
)
|
|
158
|
+
rows = curs.fetchall()
|
|
160
159
|
return len(rows) > 0
|
|
161
160
|
|
|
162
161
|
def import_tabular_file(
|
|
@@ -205,16 +204,19 @@ class MySQLDatabase(Database):
|
|
|
205
204
|
and not _state.conf.trim_strings
|
|
206
205
|
and not _state.conf.replace_newlines
|
|
207
206
|
):
|
|
208
|
-
|
|
207
|
+
safe_fname = csv_file_obj.csvfname.replace("'", "''")
|
|
208
|
+
import_sql = f"load data local infile '{safe_fname}' into table {sq_name}"
|
|
209
209
|
if csv_file_obj.encoding:
|
|
210
210
|
charset = _PYTHON_TO_MYSQL_CHARSET.get(csv_file_obj.encoding.lower(), csv_file_obj.encoding)
|
|
211
211
|
import_sql = f"{import_sql} character set {charset}"
|
|
212
212
|
if csv_file_obj.delimiter or csv_file_obj.quotechar:
|
|
213
213
|
import_sql = import_sql + " columns"
|
|
214
214
|
if csv_file_obj.delimiter:
|
|
215
|
-
|
|
215
|
+
safe_delim = csv_file_obj.delimiter.replace("'", "''")
|
|
216
|
+
import_sql = f"{import_sql} terminated by '{safe_delim}'"
|
|
216
217
|
if csv_file_obj.quotechar:
|
|
217
|
-
|
|
218
|
+
safe_quote = csv_file_obj.quotechar.replace("'", "''")
|
|
219
|
+
import_sql = f"{import_sql} optionally enclosed by '{safe_quote}'"
|
|
218
220
|
import_sql = f"{import_sql} ignore {1 + csv_file_obj.junk_header_lines} lines"
|
|
219
221
|
import_sql = f"{import_sql} ({input_col_list});"
|
|
220
222
|
_state.exec_log.log_status_info(
|
|
@@ -228,91 +230,91 @@ class MySQLDatabase(Database):
|
|
|
228
230
|
f = csv_file_obj.reader()
|
|
229
231
|
if skipheader:
|
|
230
232
|
next(f)
|
|
231
|
-
curs = self.cursor()
|
|
232
233
|
eof = False
|
|
233
234
|
total_rows = 0
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
and _state.conf.del_empty_cols
|
|
255
|
-
):
|
|
256
|
-
any_non_empty = True
|
|
257
|
-
break
|
|
258
|
-
extra_err = any_non_empty
|
|
259
|
-
if extra_err:
|
|
260
|
-
raise ErrInfo(
|
|
261
|
-
type="error",
|
|
262
|
-
other_msg=f"Too many data columns on line {{{line}}}",
|
|
263
|
-
)
|
|
264
|
-
else:
|
|
265
|
-
line = line[: len(csv_file_cols)]
|
|
266
|
-
if not (len(line) == 1 and line[0] is None):
|
|
267
|
-
if (
|
|
268
|
-
_state.conf.trim_strings
|
|
269
|
-
or _state.conf.replace_newlines
|
|
270
|
-
or not _state.conf.empty_strings
|
|
271
|
-
):
|
|
272
|
-
for i in range(len(line)):
|
|
273
|
-
if line[i] is not None and isinstance(
|
|
274
|
-
line[i],
|
|
275
|
-
_state.stringtypes,
|
|
276
|
-
):
|
|
277
|
-
if _state.conf.trim_strings:
|
|
278
|
-
line[i] = line[i].strip()
|
|
279
|
-
if _state.conf.replace_newlines:
|
|
280
|
-
line[i] = re.sub(
|
|
281
|
-
r"[\s\t]*[\r\n]+[\s\t]*",
|
|
282
|
-
" ",
|
|
283
|
-
line[i],
|
|
235
|
+
with self._cursor() as curs:
|
|
236
|
+
while True:
|
|
237
|
+
b: list = []
|
|
238
|
+
for _j in range(_state.conf.import_row_buffer):
|
|
239
|
+
try:
|
|
240
|
+
line = next(f)
|
|
241
|
+
except StopIteration:
|
|
242
|
+
eof = True
|
|
243
|
+
else:
|
|
244
|
+
if len(line) > len(csv_file_cols):
|
|
245
|
+
extra_err = True
|
|
246
|
+
if _state.conf.del_empty_cols:
|
|
247
|
+
any_non_empty = False
|
|
248
|
+
for cno in range(len(csv_file_cols), len(line)):
|
|
249
|
+
if not (
|
|
250
|
+
line[cno] is None
|
|
251
|
+
or (
|
|
252
|
+
not _state.conf.empty_strings
|
|
253
|
+
and isinstance(line[cno], _state.stringtypes)
|
|
254
|
+
and len(line[cno].strip()) == 0
|
|
284
255
|
)
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
256
|
+
and _state.conf.del_empty_cols
|
|
257
|
+
):
|
|
258
|
+
any_non_empty = True
|
|
259
|
+
break
|
|
260
|
+
extra_err = any_non_empty
|
|
261
|
+
if extra_err:
|
|
262
|
+
raise ErrInfo(
|
|
263
|
+
type="error",
|
|
264
|
+
other_msg=f"Too many data columns on line {{{line}}}",
|
|
265
|
+
)
|
|
266
|
+
else:
|
|
267
|
+
line = line[: len(csv_file_cols)]
|
|
268
|
+
if not (len(line) == 1 and line[0] is None):
|
|
269
|
+
if (
|
|
270
|
+
_state.conf.trim_strings
|
|
271
|
+
or _state.conf.replace_newlines
|
|
272
|
+
or not _state.conf.empty_strings
|
|
273
|
+
):
|
|
274
|
+
for i in range(len(line)):
|
|
275
|
+
if line[i] is not None and isinstance(
|
|
276
|
+
line[i],
|
|
277
|
+
_state.stringtypes,
|
|
278
|
+
):
|
|
279
|
+
if _state.conf.trim_strings:
|
|
280
|
+
line[i] = line[i].strip()
|
|
281
|
+
if _state.conf.replace_newlines:
|
|
282
|
+
line[i] = re.sub(
|
|
283
|
+
r"[\s\t]*[\r\n]+[\s\t]*",
|
|
284
|
+
" ",
|
|
285
|
+
line[i],
|
|
286
|
+
)
|
|
287
|
+
if not _state.conf.empty_strings and line[i].strip() == "":
|
|
288
|
+
line[i] = None
|
|
289
|
+
# Pad short line with nulls
|
|
290
|
+
line.extend([None] * (len(import_cols) - len(line)))
|
|
291
|
+
linedata = [line[ix] for ix in data_indexes]
|
|
292
|
+
add_line = True
|
|
293
|
+
if not _state.conf.empty_rows:
|
|
294
|
+
add_line = not all(c is None for c in linedata)
|
|
295
|
+
if add_line:
|
|
296
|
+
b.append(linedata)
|
|
297
|
+
if len(b) > 0:
|
|
298
|
+
try:
|
|
299
|
+
curs.executemany(sql_template, b)
|
|
300
|
+
except ErrInfo:
|
|
301
|
+
raise
|
|
302
|
+
except Exception as e:
|
|
303
|
+
self.rollback()
|
|
304
|
+
raise ErrInfo(
|
|
305
|
+
type="db",
|
|
306
|
+
command_text=sql_template,
|
|
307
|
+
exception_msg=exception_desc(),
|
|
308
|
+
other_msg=f"Import from file into table {sq_name}, line {{{line}}}",
|
|
309
|
+
) from e
|
|
310
|
+
total_rows += len(b)
|
|
311
|
+
interval = _state.conf.import_progress_interval
|
|
312
|
+
if _state.exec_log and interval > 0 and total_rows % interval == 0:
|
|
313
|
+
_state.exec_log.log_status_info(
|
|
314
|
+
f"IMPORT into {sq_name}: {total_rows} rows imported so far.",
|
|
315
|
+
)
|
|
316
|
+
if eof:
|
|
317
|
+
break
|
|
316
318
|
if _state.exec_log:
|
|
317
319
|
_state.exec_log.log_status_info(
|
|
318
320
|
f"IMPORT into {sq_name} complete: {total_rows} rows imported.",
|