execsql2 2.1.2__py3-none-any.whl → 2.2.1__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 (75) hide show
  1. execsql/cli/__init__.py +436 -0
  2. execsql/cli/dsn.py +86 -0
  3. execsql/cli/help.py +140 -0
  4. execsql/{cli.py → cli/run.py} +14 -589
  5. execsql/config.py +13 -1
  6. execsql/db/access.py +16 -12
  7. execsql/db/base.py +158 -90
  8. execsql/db/dsn.py +6 -5
  9. execsql/db/duckdb.py +2 -2
  10. execsql/db/firebird.py +23 -19
  11. execsql/db/mysql.py +8 -7
  12. execsql/db/oracle.py +11 -11
  13. execsql/db/postgres.py +28 -16
  14. execsql/db/sqlite.py +12 -11
  15. execsql/db/sqlserver.py +5 -3
  16. execsql/exceptions.py +7 -7
  17. execsql/exporters/base.py +6 -1
  18. execsql/exporters/delimited.py +44 -35
  19. execsql/exporters/duckdb.py +2 -2
  20. execsql/exporters/feather.py +6 -6
  21. execsql/exporters/html.py +83 -69
  22. execsql/exporters/json.py +50 -42
  23. execsql/exporters/latex.py +33 -27
  24. execsql/exporters/ods.py +4 -4
  25. execsql/exporters/parquet.py +2 -2
  26. execsql/exporters/pretty.py +11 -9
  27. execsql/exporters/raw.py +17 -13
  28. execsql/exporters/sqlite.py +2 -2
  29. execsql/exporters/templates.py +23 -15
  30. execsql/exporters/values.py +22 -20
  31. execsql/exporters/xls.py +4 -4
  32. execsql/exporters/xml.py +28 -13
  33. execsql/importers/base.py +4 -4
  34. execsql/importers/csv.py +6 -6
  35. execsql/importers/feather.py +4 -4
  36. execsql/importers/ods.py +4 -4
  37. execsql/importers/xls.py +4 -4
  38. execsql/metacommands/__init__.py +518 -67
  39. execsql/metacommands/conditions.py +101 -27
  40. execsql/metacommands/control.py +8 -4
  41. execsql/metacommands/data.py +6 -6
  42. execsql/metacommands/debug.py +6 -2
  43. execsql/metacommands/io.py +67 -1310
  44. execsql/metacommands/io_export.py +442 -0
  45. execsql/metacommands/io_fileops.py +287 -0
  46. execsql/metacommands/io_import.py +398 -0
  47. execsql/metacommands/io_write.py +248 -0
  48. execsql/metacommands/prompt.py +22 -66
  49. execsql/metacommands/system.py +7 -2
  50. execsql/py.typed +0 -0
  51. execsql/script.py +49 -5
  52. execsql/types.py +20 -20
  53. execsql/utils/fileio.py +15 -8
  54. {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/METADATA +6 -6
  55. execsql2-2.2.1.dist-info/RECORD +104 -0
  56. execsql2-2.1.2.dist-info/RECORD +0 -96
  57. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/READ_ME.rst +0 -0
  58. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  59. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  60. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/execsql.conf +0 -0
  61. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
  62. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/md_compare.sql +0 -0
  63. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
  64. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
  65. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
  66. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  67. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  68. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/script_template.sql +0 -0
  69. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
  70. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  71. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  72. {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/WHEEL +0 -0
  73. {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/entry_points.txt +0 -0
  74. {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/licenses/LICENSE.txt +0 -0
  75. {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/licenses/NOTICE +0 -0
execsql/db/oracle.py CHANGED
@@ -24,7 +24,7 @@ class OracleDatabase(Database):
24
24
  db_name: str,
25
25
  user_name: str | None,
26
26
  need_passwd: bool = False,
27
- port: int | None = 5432,
27
+ port: int | None = 1521,
28
28
  encoding: str | None = "UTF8",
29
29
  password: str | None = None,
30
30
  ) -> None:
@@ -95,9 +95,9 @@ class OracleDatabase(Database):
95
95
  raise
96
96
  except ErrInfo:
97
97
  raise
98
- except Exception:
98
+ except Exception as e:
99
99
  msg = f"Failed to open Oracle database {self.db_name} on {self.server_name}"
100
- raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg)
100
+ raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg) from e
101
101
 
102
102
  def execute(self, sql: Any, paramlist: list | None = None) -> None:
103
103
  # Strip any semicolon off the end and pass to the parent method.
@@ -141,14 +141,14 @@ class OracleDatabase(Database):
141
141
  curs.execute(sql, params)
142
142
  except ErrInfo:
143
143
  raise
144
- except Exception:
144
+ except Exception as e:
145
145
  self.rollback()
146
146
  raise ErrInfo(
147
147
  type="db",
148
148
  command_text=sql,
149
149
  exception_msg=exception_desc(),
150
150
  other_msg=f"Failed test for existence of table {table_name} in {self.name()}",
151
- )
151
+ ) from e
152
152
  rows = curs.fetchall()
153
153
  curs.close()
154
154
  return len(rows) > 0
@@ -170,14 +170,14 @@ class OracleDatabase(Database):
170
170
  curs.execute(sql, params)
171
171
  except ErrInfo:
172
172
  raise
173
- except Exception:
173
+ except Exception as e:
174
174
  self.rollback()
175
175
  raise ErrInfo(
176
176
  type="db",
177
177
  command_text=sql,
178
178
  exception_msg=exception_desc(),
179
179
  other_msg=f"Failed test for existence of column {column_name} in table {table_name} of {self.name()}",
180
- )
180
+ ) from e
181
181
  rows = curs.fetchall()
182
182
  curs.close()
183
183
  return len(rows) > 0
@@ -194,14 +194,14 @@ class OracleDatabase(Database):
194
194
  curs.execute(sql, params)
195
195
  except ErrInfo:
196
196
  raise
197
- except Exception:
197
+ except Exception as e:
198
198
  self.rollback()
199
199
  raise ErrInfo(
200
200
  type="db",
201
201
  command_text=sql,
202
202
  exception_msg=exception_desc(),
203
203
  other_msg=f"Failed to get column names for table {table_name} of {self.name()}",
204
- )
204
+ ) from e
205
205
  rows = curs.fetchall()
206
206
  curs.close()
207
207
  return [row[0] for row in rows]
@@ -218,14 +218,14 @@ class OracleDatabase(Database):
218
218
  curs.execute(sql, params)
219
219
  except ErrInfo:
220
220
  raise
221
- except Exception:
221
+ except Exception as e:
222
222
  self.rollback()
223
223
  raise ErrInfo(
224
224
  type="db",
225
225
  command_text=sql,
226
226
  exception_msg=exception_desc(),
227
227
  other_msg=f"Failed test for existence of view {view_name} in {self.name()}",
228
- )
228
+ ) from e
229
229
  rows = curs.fetchall()
230
230
  curs.close()
231
231
  return len(rows) > 0
execsql/db/postgres.py CHANGED
@@ -87,18 +87,20 @@ class PostgresDatabase(Database):
87
87
  port=db.port,
88
88
  connect_timeout=db.connect_timeout,
89
89
  )
90
- except Exception:
90
+ except Exception as e:
91
91
  msg = (
92
92
  f"Failed to open PostgreSQL database {self.db_name} on {self.server_name}; "
93
93
  "check server and database name, and validity of credentials"
94
94
  )
95
- raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg)
95
+ raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg) from e
96
96
 
97
97
  def create_db(db: PostgresDatabase) -> None:
98
98
  conn = db_conn(db, "postgres")
99
99
  conn.autocommit = True
100
100
  curs = conn.cursor()
101
- curs.execute(f"create database {db.db_name} encoding '{db.encoding}';")
101
+ quoted_name = db.quote_identifier(db.db_name)
102
+ quoted_enc = db.quote_identifier(db.encoding)
103
+ curs.execute(f"create database {quoted_name} encoding {quoted_enc};")
102
104
  conn.close()
103
105
 
104
106
  if self.conn is None:
@@ -133,9 +135,9 @@ class PostgresDatabase(Database):
133
135
  raise
134
136
  except ErrInfo:
135
137
  raise
136
- except Exception:
138
+ except Exception as e:
137
139
  msg = f"Failed to open PostgreSQL database {self.db_name} on {self.server_name}"
138
- raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg)
140
+ raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg) from e
139
141
  # (Re)set the encoding to match the database.
140
142
  self.encoding = self.conn.encoding
141
143
 
@@ -177,14 +179,14 @@ class PostgresDatabase(Database):
177
179
  curs.execute(sql, params)
178
180
  except ErrInfo:
179
181
  raise
180
- except Exception:
182
+ except Exception as e:
181
183
  self.rollback()
182
184
  raise ErrInfo(
183
185
  type="db",
184
186
  command_text=sql,
185
187
  exception_msg=exception_desc(),
186
188
  other_msg=f"Failed test for existence of table {table_name} in {self.name()}",
187
- )
189
+ ) from e
188
190
  rows = curs.fetchall()
189
191
  curs.close()
190
192
  return len(rows) > 0
@@ -209,14 +211,14 @@ class PostgresDatabase(Database):
209
211
  curs.execute(sql, params)
210
212
  except ErrInfo:
211
213
  raise
212
- except Exception:
214
+ except Exception as e:
213
215
  self.rollback()
214
216
  raise ErrInfo(
215
217
  type="db",
216
218
  command_text=sql,
217
219
  exception_msg=exception_desc(),
218
220
  other_msg=f"Failed test for existence of view {view_name} in {self.name()}",
219
- )
221
+ ) from e
220
222
  rows = curs.fetchall()
221
223
  curs.close()
222
224
  return len(rows) > 0
@@ -304,9 +306,18 @@ class PostgresDatabase(Database):
304
306
  # ASCII unit separator, which, if it had been used for its intended purpose,
305
307
  # should have been identified as the delimiter, so presumably it has not been used.
306
308
  delim = csv_file_obj.delimiter if csv_file_obj.delimiter else chr(31)
307
- copy_cmd = f"copy {sq_name} ({input_col_list}) from stdin with (format csv, null '', delimiter '{delim}'"
309
+ if len(delim) != 1:
310
+ raise ErrInfo(
311
+ type="error",
312
+ other_msg=f"Invalid delimiter for COPY: expected single character, got {len(delim)} characters",
313
+ )
314
+ safe_delim = delim.replace("'", "''")
315
+ copy_cmd = (
316
+ f"copy {sq_name} ({input_col_list}) from stdin with (format csv, null '', delimiter '{safe_delim}'"
317
+ )
308
318
  if csv_file_obj.quotechar:
309
- copy_cmd = copy_cmd + f", quote '{csv_file_obj.quotechar}'"
319
+ safe_quote = csv_file_obj.quotechar.replace("'", "''")
320
+ copy_cmd = copy_cmd + f", quote '{safe_quote}'"
310
321
  copy_cmd = copy_cmd + ")"
311
322
  _state.exec_log.log_status_info(
312
323
  f"IMPORTing {csv_file_obj.csvfname} using Postgres' fast file reading routine",
@@ -315,13 +326,13 @@ class PostgresDatabase(Database):
315
326
  curs.copy_expert(copy_cmd, rf, _state.conf.import_buffer)
316
327
  except ErrInfo:
317
328
  raise
318
- except Exception:
329
+ except Exception as e:
319
330
  self.rollback()
320
331
  raise ErrInfo(
321
332
  type="exception",
322
333
  exception_msg=exception_desc(),
323
334
  other_msg=f"Can't import from file to table {sq_name}",
324
- )
335
+ ) from e
325
336
  else:
326
337
  data_indexes = [csv_file_cols_q.index(col) for col in import_cols]
327
338
  paramspec = ",".join(["%s"] * len(import_cols))
@@ -398,14 +409,14 @@ class PostgresDatabase(Database):
398
409
  curs.executemany(sql_template, b)
399
410
  except ErrInfo:
400
411
  raise
401
- except Exception:
412
+ except Exception as e:
402
413
  self.rollback()
403
414
  raise ErrInfo(
404
415
  type="db",
405
416
  command_text=sql_template,
406
417
  exception_msg=exception_desc(),
407
418
  other_msg=f"Can't load data into table {sq_name} of {self.name()} from line {{{line}}}",
408
- )
419
+ ) from e
409
420
  total_rows += len(b)
410
421
  interval = _state.conf.import_progress_interval
411
422
  if _state.exec_log and interval > 0 and total_rows % interval == 0:
@@ -431,5 +442,6 @@ class PostgresDatabase(Database):
431
442
  with open(file_name, "rb") as f:
432
443
  filedata = f.read()
433
444
  sq_name = self.schema_qualified_table_name(schema_name, table_name)
434
- sql = f"insert into {sq_name} ({column_name}) values ({self.paramsubs(1)});"
445
+ quoted_col = self.quote_identifier(column_name)
446
+ sql = f"insert into {sq_name} ({quoted_col}) values ({self.paramsubs(1)});"
435
447
  self.cursor().execute(sql, (psycopg2.Binary(filedata),))
execsql/db/sqlite.py CHANGED
@@ -54,12 +54,12 @@ class SQLiteDatabase(Database):
54
54
  self.conn = sqlite3.connect(self.db_name, timeout=self.timeout)
55
55
  except ErrInfo:
56
56
  raise
57
- except Exception:
57
+ except Exception as e:
58
58
  raise ErrInfo(
59
59
  type="exception",
60
60
  exception_msg=exception_desc(),
61
61
  other_msg=f"Can't open SQLite database {self.db_name}",
62
- )
62
+ ) from e
63
63
  pragma_cols, pragma_data = self.select_data("pragma encoding;")
64
64
  self.encoding = pragma_data[0][0]
65
65
 
@@ -82,14 +82,14 @@ class SQLiteDatabase(Database):
82
82
  curs.execute(sql, (table_name,))
83
83
  except ErrInfo:
84
84
  raise
85
- except Exception:
85
+ except Exception as e:
86
86
  self.rollback()
87
87
  raise ErrInfo(
88
88
  type="db",
89
89
  command_text=sql,
90
90
  exception_msg=exception_desc(),
91
91
  other_msg=f'Failed test for existence of SQLite table "{table_name}";',
92
- )
92
+ ) from e
93
93
  rows = curs.fetchall()
94
94
  return len(rows) > 0
95
95
 
@@ -110,14 +110,14 @@ class SQLiteDatabase(Database):
110
110
  curs.execute(sql)
111
111
  except ErrInfo:
112
112
  raise
113
- except Exception:
113
+ except Exception as e:
114
114
  self.rollback()
115
115
  raise ErrInfo(
116
116
  type="db",
117
117
  command_text=sql,
118
118
  exception_msg=exception_desc(),
119
119
  other_msg=f"Failed to get column names for table {table_name} of {self.name()}",
120
- )
120
+ ) from e
121
121
  return [d[0] for d in curs.description]
122
122
 
123
123
  def view_exists(self, view_name: str) -> bool:
@@ -127,14 +127,14 @@ class SQLiteDatabase(Database):
127
127
  curs.execute(sql, (view_name,))
128
128
  except ErrInfo:
129
129
  raise
130
- except Exception:
130
+ except Exception as e:
131
131
  self.rollback()
132
132
  raise ErrInfo(
133
133
  type="db",
134
134
  command_text=sql,
135
135
  exception_msg=exception_desc(),
136
136
  other_msg=f'Failed test for existence of SQLite view "{view_name}";',
137
- )
137
+ ) from e
138
138
  rows = curs.fetchall()
139
139
  return len(rows) > 0
140
140
 
@@ -204,14 +204,14 @@ class SQLiteDatabase(Database):
204
204
  curs.execute(sql, linedata)
205
205
  except ErrInfo:
206
206
  raise
207
- except Exception:
207
+ except Exception as e:
208
208
  self.rollback()
209
209
  raise ErrInfo(
210
210
  type="db",
211
211
  command_text=sql,
212
212
  exception_msg=exception_desc(),
213
213
  other_msg=f"Can't load data into table {sq_name} from line {{{line}}}",
214
- )
214
+ ) from e
215
215
  total_rows += 1
216
216
  interval = getattr(_state.conf, "import_progress_interval", 0)
217
217
  if _state.exec_log and interval > 0 and total_rows % interval == 0:
@@ -235,5 +235,6 @@ class SQLiteDatabase(Database):
235
235
  with open(file_name, "rb") as f:
236
236
  filedata = f.read()
237
237
  sq_name = self.schema_qualified_table_name(schema_name, table_name)
238
- sql = f"insert into {sq_name} ({column_name}) values ({self.paramsubs(1)});"
238
+ quoted_col = self.quote_identifier(column_name)
239
+ sql = f"insert into {sq_name} ({quoted_col}) values ({self.paramsubs(1)});"
239
240
  self.cursor().execute(sql, (sqlite3.Binary(filedata),))
execsql/db/sqlserver.py CHANGED
@@ -141,7 +141,7 @@ class SqlServerDatabase(Database):
141
141
 
142
142
  def schema_exists(self, schema_name: str) -> bool:
143
143
  curs = self.cursor()
144
- curs.execute(f"select * from sys.schemas where name = '{schema_name}';")
144
+ curs.execute("select * from sys.schemas where name = ?;", (schema_name,))
145
145
  rows = curs.fetchall()
146
146
  curs.close()
147
147
  return len(rows) > 0
@@ -149,7 +149,8 @@ class SqlServerDatabase(Database):
149
149
  def role_exists(self, rolename: str) -> bool:
150
150
  curs = self.cursor()
151
151
  curs.execute(
152
- f"select name from sys.database_principals where type in ('R', 'S') and name = '{rolename}';",
152
+ "select name from sys.database_principals where type in ('R', 'S') and name = ?;",
153
+ (rolename,),
153
154
  )
154
155
  rows = curs.fetchall()
155
156
  curs.close()
@@ -172,5 +173,6 @@ class SqlServerDatabase(Database):
172
173
  with open(file_name, "rb") as f:
173
174
  filedata = f.read()
174
175
  sq_name = self.schema_qualified_table_name(schema_name, table_name)
175
- sql = f"insert into {sq_name} ({column_name}) values ({self.paramsubs(1)});"
176
+ quoted_col = self.quote_identifier(column_name)
177
+ sql = f"insert into {sq_name} ({quoted_col}) values ({self.paramsubs(1)});"
176
178
  self.cursor().execute(sql, (pyodbc.Binary(filedata),))
execsql/exceptions.py CHANGED
@@ -58,7 +58,7 @@ class ExecSqlTimeoutError(ExecSqlError):
58
58
  super().__init__(errmsg)
59
59
 
60
60
 
61
- class ErrInfo(Exception):
61
+ class ErrInfo(ExecSqlError):
62
62
  """Rich exception and error-data carrier for execsql.
63
63
 
64
64
  ``str(e)`` returns the most informative available message (``other_msg``,
@@ -152,11 +152,11 @@ class ErrInfo(Exception):
152
152
  return self.eval_err()
153
153
 
154
154
 
155
- class DataTypeError(Exception):
155
+ class DataTypeError(ExecSqlError):
156
156
  def __init__(self, data_type_name: str, error_msg: str) -> None:
157
157
  self.data_type_name = data_type_name or "Unspecified data type"
158
158
  self.error_msg = error_msg or "Unspecified error"
159
- super().__init__(str(self))
159
+ Exception.__init__(self, str(self))
160
160
 
161
161
  def __repr__(self) -> str:
162
162
  return f"DataTypeError({self.data_type_name!r}, {self.error_msg!r})"
@@ -165,12 +165,12 @@ class DataTypeError(Exception):
165
165
  return f"{self.data_type_name}: {self.error_msg}"
166
166
 
167
167
 
168
- class DbTypeError(Exception):
168
+ class DbTypeError(ExecSqlError):
169
169
  def __init__(self, dbms_id: str, data_type: object, error_msg: str) -> None:
170
170
  self.dbms_id = dbms_id
171
171
  self.data_type = data_type
172
172
  self.error_msg = error_msg or "Unspecified error"
173
- super().__init__(str(self))
173
+ Exception.__init__(self, str(self))
174
174
 
175
175
  def __repr__(self) -> str:
176
176
  return f"DbTypeError({self.dbms_id!r}, {self.data_type!r}, {self.error_msg!r})"
@@ -190,11 +190,11 @@ class DataTableError(ExecSqlError):
190
190
  """Raised for DataTable-level errors."""
191
191
 
192
192
 
193
- class DatabaseNotImplementedError(Exception):
193
+ class DatabaseNotImplementedError(ExecSqlError):
194
194
  def __init__(self, db_name: str, method: str) -> None:
195
195
  self.db_name = db_name
196
196
  self.method = method
197
- super().__init__(str(self))
197
+ Exception.__init__(self, str(self))
198
198
 
199
199
  def __repr__(self) -> str:
200
200
  return f"DatabaseNotImplementedError({self.db_name!r}, {self.method!r})"
execsql/exporters/base.py CHANGED
@@ -143,7 +143,12 @@ class WriteSpec:
143
143
  if self.outfile:
144
144
  from execsql.utils.fileio import EncodedFile
145
145
 
146
- EncodedFile(self.outfile, conf.output_encoding).open("a").write(msg)
146
+ ef = EncodedFile(self.outfile, conf.output_encoding)
147
+ fh = ef.open("a")
148
+ try:
149
+ fh.write(msg)
150
+ finally:
151
+ fh.close()
147
152
  if (not self.outfile) or self.tee:
148
153
  try:
149
154
  _state.output.write(msg.encode(conf.output_encoding))
@@ -415,7 +415,11 @@ class CsvFile(EncodedFile):
415
415
  def evaluate_line_format(self) -> None:
416
416
  # Scans the file to determine the delimiter, quote character, and escapechar.
417
417
  if not self.lineformat_set:
418
- self.delimiter, self.quotechar, self.escapechar = self.diagnose_delim(self.openclean("rt"))
418
+ f = self.openclean("rt")
419
+ try:
420
+ self.delimiter, self.quotechar, self.escapechar = self.diagnose_delim(f)
421
+ finally:
422
+ f.close()
419
423
  self.lineformat_set = True
420
424
 
421
425
  def _record_format_error(self, pos_no: int, errmsg: str) -> None:
@@ -571,24 +575,26 @@ class CsvFile(EncodedFile):
571
575
  self.evaluate_line_format()
572
576
  f = self.openclean("rt")
573
577
  line_no = 0
574
- while True:
575
- line_no += 1
576
- try:
577
- elements = self.read_and_parse_line(f)
578
- except ErrInfo as e:
579
- raise ErrInfo("error", other_msg=f"{e.other} on line {line_no}.") from e
580
- except:
581
- raise
582
- if len(elements) > 0:
583
- if conf.del_empty_cols and len(self.blank_cols) > 0:
584
- blanks = copy.copy(self.blank_cols)
585
- while len(blanks) > 0:
586
- b = blanks.pop()
587
- del elements[b]
588
- yield elements
589
- else:
590
- break
591
- f.close()
578
+ try:
579
+ while True:
580
+ line_no += 1
581
+ try:
582
+ elements = self.read_and_parse_line(f)
583
+ except ErrInfo as e:
584
+ raise ErrInfo("error", other_msg=f"{e.other} on line {line_no}.") from e
585
+ except:
586
+ raise
587
+ if len(elements) > 0:
588
+ if conf.del_empty_cols and len(self.blank_cols) > 0:
589
+ blanks = copy.copy(self.blank_cols)
590
+ while len(blanks) > 0:
591
+ b = blanks.pop()
592
+ del elements[b]
593
+ yield elements
594
+ else:
595
+ break
596
+ finally:
597
+ f.close()
592
598
 
593
599
  def writer(self, append: bool = False) -> CsvWriter:
594
600
  return CsvWriter(self.filename, self.encoding, self.delimiter, self.quotechar, self.escapechar, append)
@@ -600,12 +606,12 @@ class CsvFile(EncodedFile):
600
606
  except ErrInfo as e:
601
607
  e.other = f"Can't read column header line from {self.filename}. {e.other or ''}"
602
608
  raise
603
- except Exception:
609
+ except Exception as e:
604
610
  raise ErrInfo(
605
611
  type="exception",
606
612
  exception_msg=exception_desc(),
607
613
  other_msg=f"Can't read column header line from {self.filename}",
608
- )
614
+ ) from e
609
615
  if any(x is None or len(x) == 0 for x in colnames):
610
616
  if conf.del_empty_cols:
611
617
  self.blank_cols = [
@@ -700,18 +706,21 @@ def write_delimited_file(
700
706
  filewriter_close(outfile)
701
707
  ofile = EncodedFile(outfile, file_encoding).open(mode=fmode)
702
708
  fdesc = outfile
703
- if not (filefmt.lower() == "plain" or (append and zipfile is None)):
704
- datarow = line_delimiter.delimited(column_headers)
705
- ofile.write(datarow)
706
- for rec in rowsource:
707
- try:
708
- datarow = line_delimiter.delimited(rec)
709
+ try:
710
+ if not (filefmt.lower() == "plain" or (append and zipfile is None)):
711
+ datarow = line_delimiter.delimited(column_headers)
709
712
  ofile.write(datarow)
710
- except ErrInfo:
711
- raise
712
- except Exception:
713
- raise ErrInfo(
714
- "exception",
715
- exception_msg=exception_desc(),
716
- other_msg=f"Can't write output to file {fdesc}.",
717
- )
713
+ for rec in rowsource:
714
+ try:
715
+ datarow = line_delimiter.delimited(rec)
716
+ ofile.write(datarow)
717
+ except ErrInfo:
718
+ raise
719
+ except Exception as e:
720
+ raise ErrInfo(
721
+ "exception",
722
+ exception_msg=exception_desc(),
723
+ other_msg=f"Can't write output to file {fdesc}.",
724
+ ) from e
725
+ finally:
726
+ ofile.close()
@@ -85,6 +85,6 @@ def write_query_to_duckdb(
85
85
  hdrs, rows = db.select_rowsource(select_stmt)
86
86
  except ErrInfo:
87
87
  raise
88
- except Exception:
89
- raise ErrInfo("db", select_stmt, exception_msg=exception_desc())
88
+ except Exception as e:
89
+ raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
90
90
  export_duckdb(outfile, hdrs, rows, append, tablename)
@@ -22,12 +22,12 @@ from execsql.utils.fileio import filewriter_close
22
22
  def write_query_to_feather(outfile: str, headers: list[str], rows: Any) -> None:
23
23
  try:
24
24
  import polars as pl
25
- except ImportError:
25
+ except ImportError as e:
26
26
  raise ErrInfo(
27
27
  "exception",
28
28
  exception_msg=exception_desc(),
29
29
  other_msg="The polars Python package must be installed to export data to the feather format.",
30
- )
30
+ ) from e
31
31
  rows_list = list(rows)
32
32
  if rows_list:
33
33
  df = pl.DataFrame(rows_list, schema=headers, orient="row")
@@ -47,18 +47,18 @@ def write_query_to_hdf5(
47
47
  ) -> None:
48
48
  try:
49
49
  import tables
50
- except ImportError:
50
+ except ImportError as e:
51
51
  raise ErrInfo(
52
52
  "exception",
53
53
  exception_msg=exception_desc(),
54
54
  other_msg="The tables Python library must be installed to export data to the HDF5 format.",
55
- )
55
+ ) from e
56
56
  try:
57
57
  hdrs, rows = db.select_rowsource(select_stmt)
58
58
  except ErrInfo:
59
59
  raise
60
- except Exception:
61
- raise ErrInfo("db", select_stmt, exception_msg=exception_desc())
60
+ except Exception as e:
61
+ raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
62
62
 
63
63
  def h5type(datatype, size):
64
64
  if datatype in (_state.DT_Varchar, _state.DT_Text):