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.
Files changed (42) hide show
  1. execsql/config.py +3 -0
  2. execsql/db/access.py +52 -51
  3. execsql/db/base.py +8 -8
  4. execsql/db/dsn.py +2 -1
  5. execsql/db/duckdb.py +1 -1
  6. execsql/db/firebird.py +56 -59
  7. execsql/db/mysql.py +99 -97
  8. execsql/db/oracle.py +63 -68
  9. execsql/db/postgres.py +141 -134
  10. execsql/db/sqlite.py +1 -1
  11. execsql/db/sqlserver.py +11 -12
  12. execsql/exceptions.py +3 -3
  13. execsql/exporters/html.py +5 -3
  14. execsql/exporters/json.py +7 -2
  15. execsql/importers/csv.py +2 -2
  16. execsql/metacommands/system.py +1 -2
  17. execsql/models.py +0 -1
  18. execsql/script/engine.py +2 -1
  19. execsql/script/variables.py +45 -1
  20. execsql/state.py +4 -1
  21. execsql/utils/fileio.py +3 -2
  22. {execsql2-2.15.5.dist-info → execsql2-2.15.7.dist-info}/METADATA +1 -1
  23. {execsql2-2.15.5.dist-info → execsql2-2.15.7.dist-info}/RECORD +42 -42
  24. {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/README.md +0 -0
  25. {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  26. {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  27. {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/execsql.conf +0 -0
  28. {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/make_config_db.sql +0 -0
  29. {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/md_compare.sql +0 -0
  30. {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/md_glossary.sql +0 -0
  31. {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/md_upsert.sql +0 -0
  32. {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/pg_compare.sql +0 -0
  33. {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  34. {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  35. {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/script_template.sql +0 -0
  36. {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/ss_compare.sql +0 -0
  37. {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  38. {execsql2-2.15.5.data → execsql2-2.15.7.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  39. {execsql2-2.15.5.dist-info → execsql2-2.15.7.dist-info}/WHEEL +0 -0
  40. {execsql2-2.15.5.dist-info → execsql2-2.15.7.dist-info}/entry_points.txt +0 -0
  41. {execsql2-2.15.5.dist-info → execsql2-2.15.7.dist-info}/licenses/LICENSE.txt +0 -0
  42. {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
- curs = self.cursor()
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)
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
- curs = self.cursor()
273
- curs.execute(sql)
274
- rows = curs.fetchall()
275
- return [d[0] for d in curs.description], rows
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
- curs = self.cursor()
312
- try:
313
- sql = "select Name from MSysObjects where Name=? And Type In (1,4,6);"
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()
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
- try:
340
- curs.execute(sql)
341
- except Exception:
342
- return False
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
- curs.execute(f"select top 1 * from {quoted_tbl};")
351
- return [d[0] for d in curs.description]
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
- curs = self.cursor()
357
- try:
358
- sql = "select Name from MSysObjects where Name=? And Type = 5;"
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()
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.cursor().execute(sql, (pyodbc.Binary(filedata),))
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
- 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()
582
- else:
583
- _import_loop()
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.cursor().execute(sql, (pyodbc.Binary(filedata),))
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.encode(self.encoding))
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
- try:
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
- 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()
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
- try:
168
- curs.execute(sql)
169
- except Exception:
170
- return False
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
- try:
179
- curs.execute(sql)
180
- except ErrInfo:
181
- raise
182
- except Exception as e:
183
- self.rollback()
184
- raise ErrInfo(
185
- type="db",
186
- command_text=sql,
187
- exception_msg=exception_desc(),
188
- other_msg=f"Failed to get column names for table {table_name} of {self.name()}",
189
- ) from e
190
- return [d[0] for d in curs.description]
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
- try:
197
- curs.execute(sql, (view_name,))
198
- except ErrInfo:
199
- raise
200
- except Exception as e:
201
- self.rollback()
202
- raise ErrInfo(
203
- type="db",
204
- command_text=sql,
205
- exception_msg=exception_desc(),
206
- other_msg=f"Failed test for existence of Firebird view {view_name}",
207
- ) from e
208
- rows = curs.fetchall()
209
- curs.close()
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
- curs = self.cursor()
219
- curs.execute(
220
- "SELECT DISTINCT USER FROM RDB$USER_PRIVILEGES WHERE USER = ? union "
221
- " SELECT DISTINCT RDB$ROLE_NAME FROM RDB$ROLES WHERE RDB$ROLE_NAME = ?;",
222
- (rolename, rolename),
223
- )
224
- rows = curs.fetchall()
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
- curs = self.cursor()
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()
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
- import_sql = f"load data local infile '{csv_file_obj.csvfname}' into table {sq_name}"
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
- import_sql = f"{import_sql} terminated by '{csv_file_obj.delimiter}'"
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
- import_sql = f"{import_sql} optionally enclosed by '{csv_file_obj.quotechar}'"
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
- while True:
235
- b: list = []
236
- for _j in range(_state.conf.import_row_buffer):
237
- try:
238
- line = next(f)
239
- except StopIteration:
240
- eof = True
241
- else:
242
- if len(line) > len(csv_file_cols):
243
- extra_err = True
244
- if _state.conf.del_empty_cols:
245
- any_non_empty = False
246
- for cno in range(len(csv_file_cols), len(line)):
247
- if not (
248
- line[cno] is None
249
- or (
250
- not _state.conf.empty_strings
251
- and isinstance(line[cno], _state.stringtypes)
252
- and len(line[cno].strip()) == 0
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
- if not _state.conf.empty_strings and line[i].strip() == "":
286
- line[i] = None
287
- # Pad short line with nulls
288
- line.extend([None] * (len(import_cols) - len(line)))
289
- linedata = [line[ix] for ix in data_indexes]
290
- add_line = True
291
- if not _state.conf.empty_rows:
292
- add_line = not all(c is None for c in linedata)
293
- if add_line:
294
- b.append(linedata)
295
- if len(b) > 0:
296
- try:
297
- curs.executemany(sql_template, b)
298
- except ErrInfo:
299
- raise
300
- except Exception as e:
301
- self.rollback()
302
- raise ErrInfo(
303
- type="db",
304
- command_text=sql_template,
305
- exception_msg=exception_desc(),
306
- other_msg=f"Import from file into table {sq_name}, line {{{line}}}",
307
- ) from e
308
- total_rows += len(b)
309
- interval = _state.conf.import_progress_interval
310
- if _state.exec_log and interval > 0 and total_rows % interval == 0:
311
- _state.exec_log.log_status_info(
312
- f"IMPORT into {sq_name}: {total_rows} rows imported so far.",
313
- )
314
- if eof:
315
- break
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.",